mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 18:32:58 +08:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b332bada95 | ||
|
|
63a12c2ec8 | ||
|
|
743f242805 | ||
|
|
5bead0b27d | ||
|
|
73e3c7016b | ||
|
|
3829dcd0f9 | ||
|
|
b2047044fe | ||
|
|
47d1a13189 | ||
|
|
309909cbd7 | ||
|
|
b5cebb4cea | ||
|
|
b6dd2693cd | ||
|
|
5fdfe98f26 | ||
|
|
0c768aa1ca | ||
|
|
d493e6dc9e | ||
|
|
7e0c7d8891 | ||
|
|
3510c6600d | ||
|
|
32d91150bd | ||
|
|
bbf2d50e3f | ||
|
|
39725f9828 | ||
|
|
1e8c617a85 | ||
|
|
7f8573ec4c | ||
|
|
d8e629917e | ||
|
|
bdc0a15439 | ||
|
|
a25b97614f | ||
|
|
4e12c32566 | ||
|
|
ea9c0f1225 | ||
|
|
ff865f13a2 | ||
|
|
9875200912 | ||
|
|
9f218d004e | ||
|
|
ab727f66f4 | ||
|
|
efbc0302e4 | ||
|
|
ab2367d670 | ||
|
|
045e4f81d6 | ||
|
|
160cfee947 | ||
|
|
0e40b5ecce | ||
|
|
fcaddcee80 | ||
|
|
8d6295fd3b | ||
|
|
d0d51b3e6f | ||
|
|
b8d612f1d5 | ||
|
|
f7c49cde0c | ||
|
|
189f8fb3ba | ||
|
|
2a64bd28a8 | ||
|
|
8a733379a3 | ||
|
|
e5f854dfcd | ||
|
|
4e690bafed | ||
|
|
28b511e179 | ||
|
|
f010a13abd | ||
|
|
4d80ffafdd | ||
|
|
9aecd4d54b | ||
|
|
65091823eb | ||
|
|
d17218bfbd | ||
|
|
724c5d2632 | ||
|
|
6806c26028 | ||
|
|
dcd89174c9 | ||
|
|
9a8707b8cb | ||
|
|
28f1d05f06 | ||
|
|
54b044584e | ||
|
|
ed39449a20 | ||
|
|
2ff3f3a352 | ||
|
|
91e2e964a5 | ||
|
|
ca6cc68fed | ||
|
|
0962de7735 | ||
|
|
062b957fdb | ||
|
|
4efe4e5663 | ||
|
|
25eb6966c4 | ||
|
|
7843460020 | ||
|
|
1cbc6ba4a9 | ||
|
|
a43407bee8 | ||
|
|
05c4ec9af2 | ||
|
|
9236064293 | ||
|
|
e1955a371e | ||
|
|
58b56c4221 | ||
|
|
1e461e529f | ||
|
|
38ada1207c | ||
|
|
8bd1b34f46 | ||
|
|
4a513360e6 | ||
|
|
22da5c1c37 | ||
|
|
483582a8d1 | ||
|
|
f037cbfac0 | ||
|
|
343d11482d | ||
|
|
7ef81a0116 | ||
|
|
5df62d5d3e | ||
|
|
7db650d69f | ||
|
|
8d80d38d63 | ||
|
|
48f05d4cff | ||
|
|
9a1cf387c0 | ||
|
|
8b7efefbdb | ||
|
|
75f21db325 | ||
|
|
b094c9d4ff | ||
|
|
0da3c95759 | ||
|
|
fa79473ece | ||
|
|
86ccb5e0cc | ||
|
|
f385f4b277 | ||
|
|
3d0ef2a331 | ||
|
|
96999205a8 | ||
|
|
ee7f3871eb | ||
|
|
df2e9b0743 | ||
|
|
7964950149 | ||
|
|
e2d77fe881 | ||
|
|
f5783c8587 | ||
|
|
346044b1ba | ||
|
|
aa6ec8dd43 |
47
.github/workflows/linux-aarch64.yml
vendored
Normal file
47
.github/workflows/linux-aarch64.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Linux aarch64
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# download jdk
|
||||||
|
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-aarch64-b825.69.tar.gz
|
||||||
|
|
||||||
|
# appimagetool
|
||||||
|
- run: sudo apt install libfuse2
|
||||||
|
|
||||||
|
# install jdk
|
||||||
|
- name: Installing Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'jdkfile'
|
||||||
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
|
java-version: '21.0.6'
|
||||||
|
architecture: aarch64
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
# dist
|
||||||
|
- run: |
|
||||||
|
./gradlew dist --no-daemon
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-linux-aarch64
|
||||||
|
path: |
|
||||||
|
build/distributions/*.tar.gz
|
||||||
|
build/distributions/*.AppImage
|
||||||
22
.github/workflows/linux-x86-64.yml
vendored
22
.github/workflows/linux-x86-64.yml
vendored
@@ -4,14 +4,17 @@ on: [ push, pull_request ]
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# download jdk
|
# download jdk
|
||||||
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.5-linux-x64-b509.30.tar.gz
|
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-x64-b825.69.tar.gz
|
||||||
|
|
||||||
|
# appimagetool
|
||||||
|
- run: sudo apt install libfuse2
|
||||||
|
|
||||||
# install jdk
|
# install jdk
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
@@ -19,9 +22,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'jdkfile'
|
distribution: 'jdkfile'
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
java-version: '21.0.5'
|
java-version: '21.0.6'
|
||||||
architecture: x64
|
architecture: x64
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
# dist
|
# dist
|
||||||
- run: |
|
- run: |
|
||||||
./gradlew dist --no-daemon
|
./gradlew dist --no-daemon
|
||||||
@@ -30,4 +42,6 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: termora-linux-x86-64
|
name: termora-linux-x86-64
|
||||||
path: build/distributions/*.tar.gz
|
path: |
|
||||||
|
build/distributions/*.tar.gz
|
||||||
|
build/distributions/*.AppImage
|
||||||
|
|||||||
40
.github/workflows/osx-aarch64.yml
vendored
40
.github/workflows/osx-aarch64.yml
vendored
@@ -10,9 +10,31 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install the Apple certificate
|
||||||
|
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
||||||
|
env:
|
||||||
|
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||||
|
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
|
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# create variables
|
||||||
|
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
|
||||||
|
# import certificate from secrets
|
||||||
|
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
|
||||||
|
|
||||||
|
# create temporary keychain
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
# import certificate to keychain
|
||||||
|
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||||
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
|
||||||
# download jdk
|
# download jdk
|
||||||
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.5-osx-aarch64-b509.30.tar.gz
|
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b825.69.tar.gz
|
||||||
|
|
||||||
# install jdk
|
# install jdk
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
@@ -20,12 +42,24 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'jdkfile'
|
distribution: 'jdkfile'
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
java-version: '21.0.5'
|
java-version: '21.0.6'
|
||||||
architecture: aarch64
|
architecture: aarch64
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
# dist
|
# dist
|
||||||
- run: |
|
- name: Dist
|
||||||
|
env:
|
||||||
|
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' }}
|
||||||
|
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||||
|
run: |
|
||||||
./gradlew dist --no-daemon
|
./gradlew dist --no-daemon
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
|
|||||||
43
.github/workflows/osx-x86-64.yml
vendored
43
.github/workflows/osx-x86-64.yml
vendored
@@ -10,8 +10,31 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install the Apple certificate
|
||||||
|
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
||||||
|
env:
|
||||||
|
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||||
|
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
|
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# create variables
|
||||||
|
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
|
||||||
|
# import certificate from secrets
|
||||||
|
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
|
||||||
|
|
||||||
|
# create temporary keychain
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
# import certificate to keychain
|
||||||
|
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||||
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
|
||||||
# download jdk
|
# download jdk
|
||||||
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.5-osx-x64-b509.30.tar.gz
|
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b825.69.tar.gz
|
||||||
|
|
||||||
# install jdk
|
# install jdk
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
@@ -19,12 +42,26 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'jdkfile'
|
distribution: 'jdkfile'
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
java-version: '21.0.5'
|
java-version: '21.0.6'
|
||||||
architecture: x64
|
architecture: x64
|
||||||
|
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
|
||||||
# dist
|
# dist
|
||||||
- run: |
|
- name: Dist
|
||||||
|
env:
|
||||||
|
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' }}
|
||||||
|
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||||
|
run: |
|
||||||
./gradlew dist --no-daemon
|
./gradlew dist --no-daemon
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
|
|||||||
10
.github/workflows/windows-x86-64.yml
vendored
10
.github/workflows/windows-x86-64.yml
vendored
@@ -16,9 +16,19 @@ jobs:
|
|||||||
distribution: 'jetbrains'
|
distribution: 'jetbrains'
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
# dist
|
# dist
|
||||||
- run: |
|
- run: |
|
||||||
.\gradlew.bat dist --no-daemon
|
.\gradlew.bat dist --no-daemon
|
||||||
|
.\gradlew.bat --stop
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
13
.github/workflows/winget.yml
vendored
Normal file
13
.github/workflows/winget.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
name: Publish to WinGet
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [ released ]
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: vedantmgoyal9/winget-releaser@main
|
||||||
|
with:
|
||||||
|
identifier: TermoraDev.Termora
|
||||||
|
installers-regex: 'x86-64\.msi$' # Only x86-64.msi files
|
||||||
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
@@ -15,11 +15,13 @@
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- SSH and local terminal support
|
- SSH and local terminal support
|
||||||
- [SFTP](./docs/sftp.png?raw=1) file transfer support
|
- Serial port protocol support
|
||||||
|
- [SFTP](./docs/sftp.png?raw=1) & [Command](./docs/sftp-command.png?raw=1) file transfer support
|
||||||
- Compatible with Windows, macOS, and Linux
|
- Compatible with Windows, macOS, and Linux
|
||||||
- Zmodem protocol support
|
- Zmodem protocol support
|
||||||
- SSH port forwarding
|
- SSH port forwarding & Jump hosts
|
||||||
- Configuration synchronization via [Gist](https://gist.github.com)
|
- Terminal log
|
||||||
|
- Configuration synchronization via [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
||||||
- Macro support (record and replay scripts)
|
- Macro support (record and replay scripts)
|
||||||
- Keyword highlighting
|
- Keyword highlighting
|
||||||
- Key management
|
- Key management
|
||||||
@@ -32,6 +34,7 @@
|
|||||||
|
|
||||||
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||||
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
||||||
|
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,13 @@
|
|||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
- 支持 SSH 和本地终端
|
- 支持 SSH 和本地终端
|
||||||
- 支持 [SFTP](./docs/sftp-zh_CN.png?raw=1) 文件传输
|
- 支持串口协议
|
||||||
|
- 支持 [SFTP](./docs/sftp-zh_CN.png?raw=1) & [命令行](./docs/sftp-command.png?raw=1) 文件传输
|
||||||
- 支持 Windows、macOS、Linux 平台
|
- 支持 Windows、macOS、Linux 平台
|
||||||
- 支持 Zmodem 协议
|
- 支持 Zmodem 协议
|
||||||
- 支持 SSH 端口转发
|
- 支持 SSH 端口转发和跳板机
|
||||||
- 支持配置同步到 [Gist](https://gist.github.com)
|
- 终端日志记录
|
||||||
|
- 支持配置同步到 [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
||||||
- 支持宏(录制脚本并回放)
|
- 支持宏(录制脚本并回放)
|
||||||
- 支持关键词高亮
|
- 支持关键词高亮
|
||||||
- 支持密钥管理器
|
- 支持密钥管理器
|
||||||
@@ -28,6 +30,7 @@
|
|||||||
|
|
||||||
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||||
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
||||||
|
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
|
|||||||
@@ -240,4 +240,8 @@ https://github.com/mixpanel/mixpanel-java/blob/master/LICENSE
|
|||||||
|
|
||||||
json-20231013
|
json-20231013
|
||||||
Public Domain.
|
Public Domain.
|
||||||
https://github.com/stleary/JSON-java/blob/master/LICENSE
|
https://github.com/stleary/JSON-java/blob/master/LICENSE
|
||||||
|
|
||||||
|
jSerialComm 2.11.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/Fazecast/jSerialComm/blob/master/LICENSE-APACHE-2.0
|
||||||
130
build.gradle.kts
130
build.gradle.kts
@@ -1,5 +1,6 @@
|
|||||||
import org.gradle.internal.jvm.Jvm
|
import org.gradle.internal.jvm.Jvm
|
||||||
import org.gradle.kotlin.dsl.support.uppercaseFirstChar
|
import org.gradle.kotlin.dsl.support.uppercaseFirstChar
|
||||||
|
import org.gradle.nativeplatform.platform.internal.ArchitectureInternal
|
||||||
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||||
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
|
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
|
||||||
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
||||||
@@ -7,6 +8,7 @@ import java.nio.file.Files
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
|
idea
|
||||||
application
|
application
|
||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
alias(libs.plugins.kotlinx.serialization)
|
alias(libs.plugins.kotlinx.serialization)
|
||||||
@@ -14,10 +16,10 @@ plugins {
|
|||||||
|
|
||||||
|
|
||||||
group = "app.termora"
|
group = "app.termora"
|
||||||
version = "1.0.4"
|
version = "1.0.8"
|
||||||
|
|
||||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||||
val arch: Architecture = DefaultNativePlatform.getCurrentArchitecture()
|
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||||
|
|
||||||
// macOS 签名信息
|
// macOS 签名信息
|
||||||
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
||||||
@@ -37,7 +39,7 @@ repositories {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// 由于签名和公证,macOS 不携带 natives
|
// 由于签名和公证,macOS 不携带 natives
|
||||||
val useNoNativesFlatLaf = os.isMacOsX && macOSNotary && System.getenv("ENABLE_BUILD").toBoolean()
|
val useNoNativesFlatLaf = os.isMacOsX && System.getenv("ENABLE_BUILD").toBoolean()
|
||||||
|
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
testImplementation(libs.hutool)
|
testImplementation(libs.hutool)
|
||||||
@@ -104,6 +106,7 @@ dependencies {
|
|||||||
implementation(libs.bip39)
|
implementation(libs.bip39)
|
||||||
implementation(libs.colorpicker)
|
implementation(libs.colorpicker)
|
||||||
implementation(libs.mixpanel)
|
implementation(libs.mixpanel)
|
||||||
|
implementation(libs.jSerialComm)
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
@@ -114,7 +117,6 @@ application {
|
|||||||
"-XX:+ZUncommit",
|
"-XX:+ZUncommit",
|
||||||
"-XX:+ZGenerational",
|
"-XX:+ZGenerational",
|
||||||
"-XX:ZUncommitDelay=60",
|
"-XX:ZUncommitDelay=60",
|
||||||
"-XX:SoftMaxHeapSize=64m"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
@@ -148,6 +150,8 @@ tasks.register<Copy>("copy-dependencies") {
|
|||||||
val jna = libs.jna.asProvider().get()
|
val jna = libs.jna.asProvider().get()
|
||||||
val dylib = dir.get().dir("dylib").asFile
|
val dylib = dir.get().dir("dylib").asFile
|
||||||
val pty4j = libs.pty4j.get()
|
val pty4j = libs.pty4j.get()
|
||||||
|
val jSerialComm = libs.jSerialComm.get()
|
||||||
|
|
||||||
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
|
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
|
||||||
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
||||||
val targetDir = File(dylib, jna.name)
|
val targetDir = File(dylib, jna.name)
|
||||||
@@ -172,6 +176,21 @@ tasks.register<Copy>("copy-dependencies") {
|
|||||||
// @formatter:on
|
// @formatter:on
|
||||||
// 删除所有二进制类库
|
// 删除所有二进制类库
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
|
||||||
|
} else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) {
|
||||||
|
val archName = if (arch.isArm) "aarch64" else "x86_64"
|
||||||
|
val targetDir = FileUtils.getFile(dylib, jSerialComm.name, "OSX", archName)
|
||||||
|
FileUtils.forceMkdir(targetDir)
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "OSX/${archName}/*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
// 删除所有二进制类库
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "Android/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "FreeBSD/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "Linux/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "OpenBSD/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,22 +244,24 @@ tasks.register<Exec>("jpackage") {
|
|||||||
"-XX:+ZUncommit",
|
"-XX:+ZUncommit",
|
||||||
"-XX:+ZGenerational",
|
"-XX:+ZGenerational",
|
||||||
"-XX:ZUncommitDelay=60",
|
"-XX:ZUncommitDelay=60",
|
||||||
"-XX:SoftMaxHeapSize=64m",
|
|
||||||
"-XX:+HeapDumpOnOutOfMemoryError",
|
"-XX:+HeapDumpOnOutOfMemoryError",
|
||||||
"-Dlogger.console.level=off",
|
"-Dlogger.console.level=off",
|
||||||
"-Dkotlinx.coroutines.debug=off",
|
"-Dkotlinx.coroutines.debug=off",
|
||||||
"-Dapp-version=${project.version}",
|
"-Dapp-version=${project.version}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
options.add("-Dsun.java2d.metal=true")
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
options.add("-Dsun.java2d.metal=true")
|
|
||||||
options.add("-Dapple.awt.application.appearance=system")
|
options.add("-Dapple.awt.application.appearance=system")
|
||||||
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if (os.isLinux) {
|
||||||
options.add("-Dsun.java2d.opengl=true")
|
options.add("-Dsun.java2d.opengl=true")
|
||||||
}
|
}
|
||||||
|
|
||||||
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage", "--verbose")
|
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage")
|
||||||
arguments.addAll(listOf("--runtime-image", "${buildDir}/jlink"))
|
arguments.addAll(listOf("--runtime-image", "${buildDir}/jlink"))
|
||||||
arguments.addAll(listOf("--name", project.name.uppercaseFirstChar()))
|
arguments.addAll(listOf("--name", project.name.uppercaseFirstChar()))
|
||||||
arguments.addAll(listOf("--app-version", "${project.version}"))
|
arguments.addAll(listOf("--app-version", "${project.version}"))
|
||||||
@@ -252,7 +273,17 @@ tasks.register<Exec>("jpackage") {
|
|||||||
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE)))
|
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE)))
|
||||||
arguments.addAll(listOf("--vendor", "TermoraDev"))
|
arguments.addAll(listOf("--vendor", "TermoraDev"))
|
||||||
arguments.addAll(listOf("--copyright", "TermoraDev"))
|
arguments.addAll(listOf("--copyright", "TermoraDev"))
|
||||||
arguments.addAll(listOf("--description", "A terminal emulator and SSH client."))
|
|
||||||
|
if (os.isWindows) {
|
||||||
|
arguments.addAll(
|
||||||
|
listOf(
|
||||||
|
"--description",
|
||||||
|
"${project.name.uppercaseFirstChar()}: A terminal emulator and SSH client"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
arguments.addAll(listOf("--description", "A terminal emulator and SSH client."))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
@@ -270,6 +301,10 @@ tasks.register<Exec>("jpackage") {
|
|||||||
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.ico"))
|
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.ico"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (os.isLinux) {
|
||||||
|
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.png"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
arguments.add("--type")
|
arguments.add("--type")
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
@@ -366,6 +401,56 @@ tasks.register("dist") {
|
|||||||
throw GradleException("${os.name} is not supported")
|
throw GradleException("${os.name} is not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppImage
|
||||||
|
if (os.isLinux) {
|
||||||
|
|
||||||
|
// Download AppImageKit
|
||||||
|
val appimagetool = FileUtils.getFile(projectDir, ".gradle", "appimagetool")
|
||||||
|
if (!appimagetool.exists()) {
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"wget",
|
||||||
|
"-O", appimagetool.absolutePath,
|
||||||
|
"https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-${if (arch.isArm) "aarch64" else "x86_64"}.AppImage"
|
||||||
|
)
|
||||||
|
workingDir = distributionDir.asFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppImageKit chmod
|
||||||
|
exec { commandLine("chmod", "+x", appimagetool.absolutePath) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Desktop file
|
||||||
|
val termoraName = project.name.uppercaseFirstChar()
|
||||||
|
val desktopFile = distributionDir.file(termoraName + File.separator + termoraName + ".desktop").asFile
|
||||||
|
desktopFile.writeText(
|
||||||
|
"""[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=${termoraName}
|
||||||
|
Comment=Terminal emulator and SSH client
|
||||||
|
Icon=/lib/${termoraName}
|
||||||
|
Categories=Development;
|
||||||
|
Terminal=false
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppRun file
|
||||||
|
val appRun = File(desktopFile.parentFile, "AppRun")
|
||||||
|
val sb = StringBuilder()
|
||||||
|
sb.append("#!/bin/sh").appendLine()
|
||||||
|
sb.append("SELF=$(readlink -f \"$0\")").appendLine()
|
||||||
|
sb.append("HERE=\${SELF%/*}").appendLine()
|
||||||
|
sb.append("exec \"\${HERE}/bin/${termoraName}\" \"$@\"")
|
||||||
|
appRun.writeText(sb.toString())
|
||||||
|
appRun.setExecutable(true)
|
||||||
|
|
||||||
|
exec {
|
||||||
|
commandLine(appimagetool.absolutePath, termoraName, "${finalFilenameWithoutExtension}.AppImage")
|
||||||
|
workingDir = distributionDir.asFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// sign dmg
|
// sign dmg
|
||||||
if (os.isMacOsX && macOSSign) {
|
if (os.isMacOsX && macOSSign) {
|
||||||
@@ -383,6 +468,14 @@ tasks.register("dist") {
|
|||||||
"--wait",
|
"--wait",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 绑定公证信息
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"/usr/bin/xcrun",
|
||||||
|
"stapler", "staple", macOSFinalFilePath,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,6 +500,18 @@ tasks.register("check-license") {
|
|||||||
thirdParty[nameWithVersion.replace(StringUtils.SPACE, "-")] = license
|
thirdParty[nameWithVersion.replace(StringUtils.SPACE, "-")] = license
|
||||||
thirdPartyNames.add(nameWithVersion.split(StringUtils.SPACE).first())
|
thirdPartyNames.add(nameWithVersion.split(StringUtils.SPACE).first())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (file in configurations.runtimeClasspath.get()) {
|
||||||
|
val name = file.nameWithoutExtension
|
||||||
|
if (!thirdParty.containsKey(name)) {
|
||||||
|
if (logger.isWarnEnabled) {
|
||||||
|
logger.warn("$name does not exist in third-party")
|
||||||
|
}
|
||||||
|
if (!thirdPartyNames.contains(name)) {
|
||||||
|
throw GradleException("$name No license found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,4 +541,11 @@ kotlin {
|
|||||||
@Suppress("UnstableApiUsage")
|
@Suppress("UnstableApiUsage")
|
||||||
vendor = JvmVendorSpec.JETBRAINS
|
vendor = JvmVendorSpec.JETBRAINS
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idea {
|
||||||
|
module {
|
||||||
|
isDownloadJavadoc = true
|
||||||
|
isDownloadSources = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BIN
docs/sftp-command.png
Normal file
BIN
docs/sftp-command.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -41,6 +41,7 @@ rhino = "1.7.15"
|
|||||||
delight-rhino-sandbox = "0.0.17"
|
delight-rhino-sandbox = "0.0.17"
|
||||||
testcontainers = "1.20.4"
|
testcontainers = "1.20.4"
|
||||||
mixpanel = "1.5.3"
|
mixpanel = "1.5.3"
|
||||||
|
jSerialComm="2.11.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||||
@@ -97,6 +98,7 @@ rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
|||||||
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
|
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
|
||||||
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
|
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
|
||||||
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
|
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
|
||||||
|
jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "jSerialComm" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import org.slf4j.LoggerFactory
|
|||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import kotlin.math.ln
|
import kotlin.math.ln
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
@@ -60,6 +62,16 @@ object Application {
|
|||||||
return "/bin/bash"
|
return "/bin/bash"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getTemporaryDir(): File {
|
||||||
|
val temporaryDir = File(getBaseDataDir(), "temporary")
|
||||||
|
FileUtils.forceMkdir(temporaryDir)
|
||||||
|
return temporaryDir
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createSubTemporaryDir(prefix: String = getName()): Path {
|
||||||
|
return Files.createTempDirectory(getTemporaryDir().toPath(), prefix)
|
||||||
|
}
|
||||||
|
|
||||||
fun getBaseDataDir(): File {
|
fun getBaseDataDir(): File {
|
||||||
if (::baseDataDir.isInitialized) {
|
if (::baseDataDir.isInitialized) {
|
||||||
return baseDataDir
|
return baseDataDir
|
||||||
@@ -111,11 +123,18 @@ object Application {
|
|||||||
return "Termora"
|
return "Termora"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("OPT_IN_USAGE")
|
||||||
fun browse(uri: URI, async: Boolean = true) {
|
fun browse(uri: URI, async: Boolean = true) {
|
||||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
// https://github.com/TermoraDev/termora/issues/178
|
||||||
|
if (SystemInfo.isWindows && uri.scheme == "file") {
|
||||||
|
if (async) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
||||||
|
} else {
|
||||||
|
tryBrowse(uri)
|
||||||
|
}
|
||||||
|
} else if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||||
Desktop.getDesktop().browse(uri)
|
Desktop.getDesktop().browse(uri)
|
||||||
} else if (async) {
|
} else if (async) {
|
||||||
@Suppress("OPT_IN_USAGE")
|
|
||||||
GlobalScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
GlobalScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
||||||
} else {
|
} else {
|
||||||
tryBrowse(uri)
|
tryBrowse(uri)
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ import com.jthemedetecor.OsThemeDetector
|
|||||||
import com.mixpanel.mixpanelapi.ClientDelivery
|
import com.mixpanel.mixpanelapi.ClientDelivery
|
||||||
import com.mixpanel.mixpanelapi.MessageBuilder
|
import com.mixpanel.mixpanelapi.MessageBuilder
|
||||||
import com.mixpanel.mixpanelapi.MixpanelAPI
|
import com.mixpanel.mixpanelapi.MixpanelAPI
|
||||||
import com.sun.jna.platform.WindowUtils
|
|
||||||
import com.sun.jna.platform.win32.User32
|
|
||||||
import com.sun.jna.ptr.IntByReference
|
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -20,25 +17,27 @@ import kotlinx.coroutines.launch
|
|||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.apache.commons.lang3.LocaleUtils
|
import org.apache.commons.lang3.LocaleUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.apache.commons.lang3.math.NumberUtils
|
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.tinylog.configuration.Configuration
|
import org.tinylog.configuration.Configuration
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.RandomAccessFile
|
import java.nio.channels.FileChannel
|
||||||
import java.nio.channels.FileLock
|
import java.nio.channels.FileLock
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
class ApplicationRunner {
|
class ApplicationRunner {
|
||||||
|
private lateinit var singletonChannel: FileChannel
|
||||||
private lateinit var singletonLock: FileLock
|
private lateinit var singletonLock: FileLock
|
||||||
private val log by lazy {
|
private val log by lazy {
|
||||||
if (!::singletonLock.isInitialized) {
|
if (!::singletonLock.isInitialized) {
|
||||||
throw UnsupportedOperationException("Singleton lock is not initialized")
|
throw UnsupportedOperationException("Singleton lock is not initialized")
|
||||||
}
|
}
|
||||||
LoggerFactory.getLogger("Main")
|
LoggerFactory.getLogger(ApplicationRunner::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun run() {
|
fun run() {
|
||||||
@@ -74,6 +73,9 @@ class ApplicationRunner {
|
|||||||
// 解密数据
|
// 解密数据
|
||||||
val openDoor = measureTimeMillis { openDoor() }
|
val openDoor = measureTimeMillis { openDoor() }
|
||||||
|
|
||||||
|
// clear temporary
|
||||||
|
clearTemporary()
|
||||||
|
|
||||||
// 启动主窗口
|
// 启动主窗口
|
||||||
val startMainFrame = measureTimeMillis { startMainFrame() }
|
val startMainFrame = measureTimeMillis { startMainFrame() }
|
||||||
|
|
||||||
@@ -95,6 +97,22 @@ class ApplicationRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("OPT_IN_USAGE")
|
||||||
|
private fun clearTemporary() {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
|
||||||
|
// 启动时清除
|
||||||
|
FileUtils.cleanDirectory(Application.getTemporaryDir())
|
||||||
|
|
||||||
|
// 关闭时清除
|
||||||
|
Disposer.register(ApplicationScope.forApplicationScope(), object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
FileUtils.cleanDirectory(Application.getTemporaryDir())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private fun openDoor() {
|
private fun openDoor() {
|
||||||
if (Doorman.getInstance().isWorking()) {
|
if (Doorman.getInstance().isWorking()) {
|
||||||
@@ -224,36 +242,14 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
|
|
||||||
private fun checkSingleton() {
|
private fun checkSingleton() {
|
||||||
val file = File(Application.getBaseDataDir(), "lock")
|
singletonChannel = FileChannel.open(
|
||||||
val pidFile = File(Application.getBaseDataDir(), "pid")
|
Paths.get(Application.getBaseDataDir().absolutePath, "lock"),
|
||||||
|
StandardOpenOption.CREATE,
|
||||||
|
StandardOpenOption.WRITE,
|
||||||
val raf = RandomAccessFile(file, "rw")
|
)
|
||||||
val lock = raf.channel.tryLock()
|
|
||||||
|
|
||||||
if (lock != null) {
|
|
||||||
pidFile.writeText(ProcessHandle.current().pid().toString())
|
|
||||||
pidFile.deleteOnExit()
|
|
||||||
file.deleteOnExit()
|
|
||||||
} else {
|
|
||||||
if (SystemInfo.isWindows && pidFile.exists()) {
|
|
||||||
val pid = NumberUtils.toLong(pidFile.readText())
|
|
||||||
for (window in WindowUtils.getAllWindows(false)) {
|
|
||||||
if (pid > 0) {
|
|
||||||
val processId = IntByReference()
|
|
||||||
User32.INSTANCE.GetWindowThreadProcessId(window.hwnd, processId)
|
|
||||||
if (processId.value.toLong() != pid) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else if (window.title != Application.getName() || window.filePath.endsWith("explorer.exe")) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
User32.INSTANCE.ShowWindow(window.hwnd, User32.SW_SHOWNOACTIVATE)
|
|
||||||
User32.INSTANCE.SetForegroundWindow(window.hwnd)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
val lock = singletonChannel.tryLock()
|
||||||
|
if (lock == null) {
|
||||||
System.err.println("Program is already running")
|
System.err.println("Program is already running")
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,6 @@ class ChannelShellPtyConnector(
|
|||||||
output.flush()
|
output.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun write(buffer: String) {
|
|
||||||
write(buffer.toByteArray(charset))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun resize(rows: Int, cols: Int) {
|
override fun resize(rows: Int, cols: Int) {
|
||||||
channel.sendWindowChange(cols, rows)
|
channel.sendWindowChange(cols, rows)
|
||||||
}
|
}
|
||||||
@@ -38,4 +34,8 @@ class ChannelShellPtyConnector(
|
|||||||
override fun close() {
|
override fun close() {
|
||||||
channel.close(true)
|
channel.close(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getCharset(): Charset {
|
||||||
|
return charset
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -14,12 +14,10 @@ import kotlinx.coroutines.GlobalScope
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.component1
|
|
||||||
import kotlin.collections.component2
|
|
||||||
import kotlin.collections.set
|
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
@@ -55,6 +53,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
val safetyProperties by lazy { SafetyProperties("Setting.SafetyProperties") }
|
val safetyProperties by lazy { SafetyProperties("Setting.SafetyProperties") }
|
||||||
val terminal by lazy { Terminal() }
|
val terminal by lazy { Terminal() }
|
||||||
val appearance by lazy { Appearance() }
|
val appearance by lazy { Appearance() }
|
||||||
|
val sftp by lazy { SFTP() }
|
||||||
val sync by lazy { Sync() }
|
val sync by lazy { Sync() }
|
||||||
|
|
||||||
private val doorman get() = Doorman.getInstance()
|
private val doorman get() = Doorman.getInstance()
|
||||||
@@ -454,6 +453,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
*/
|
*/
|
||||||
var debug by BooleanPropertyDelegate(false)
|
var debug by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 蜂鸣声
|
||||||
|
*/
|
||||||
|
var beep by BooleanPropertyDelegate(true)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 选中复制
|
* 选中复制
|
||||||
*/
|
*/
|
||||||
@@ -463,6 +467,16 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
* 光标样式
|
* 光标样式
|
||||||
*/
|
*/
|
||||||
var cursor by CursorStylePropertyDelegate(CursorStyle.Block)
|
var cursor by CursorStylePropertyDelegate(CursorStyle.Block)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 终端断开连接时自动关闭Tab
|
||||||
|
*/
|
||||||
|
var autoCloseTabWhenDisconnected by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否显示悬浮工具栏
|
||||||
|
*/
|
||||||
|
var floatingToolbar by BooleanPropertyDelegate(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -563,6 +577,19 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SFTP
|
||||||
|
*/
|
||||||
|
inner class SFTP : Property("Setting.SFTP") {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑命令
|
||||||
|
*/
|
||||||
|
var editCommand by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步配置
|
* 同步配置
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jumpHostsOption.filter = { it.id != host.id }
|
jumpHostsOption.filter = { it.id != host.id }
|
||||||
|
|
||||||
|
val serialComm = host.options.serialComm
|
||||||
|
if (serialComm.port.isNotBlank()) {
|
||||||
|
serialCommOption.serialPortComboBox.selectedItem = serialComm.port
|
||||||
|
}
|
||||||
|
serialCommOption.baudRateComboBox.selectedItem = serialComm.baudRate
|
||||||
|
serialCommOption.dataBitsComboBox.selectedItem = serialComm.dataBits
|
||||||
|
serialCommOption.parityComboBox.selectedItem = serialComm.parity
|
||||||
|
serialCommOption.stopBitsComboBox.selectedItem = serialComm.stopBits
|
||||||
|
serialCommOption.flowControlComboBox.selectedItem = serialComm.flowControl
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getHost(): Host {
|
override fun getHost(): Host {
|
||||||
|
|||||||
@@ -5,6 +5,17 @@ import org.apache.commons.lang3.StringUtils
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
fun Map<*, *>.toPropertiesString(): String {
|
||||||
|
val env = StringBuilder()
|
||||||
|
for ((i, e) in entries.withIndex()) {
|
||||||
|
env.append(e.key).append('=').append(e.value)
|
||||||
|
if (i != size - 1) {
|
||||||
|
env.appendLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return env.toString()
|
||||||
|
}
|
||||||
|
|
||||||
fun UUID.toSimpleString(): String {
|
fun UUID.toSimpleString(): String {
|
||||||
return toString().replace("-", StringUtils.EMPTY)
|
return toString().replace("-", StringUtils.EMPTY)
|
||||||
}
|
}
|
||||||
@@ -13,6 +24,13 @@ enum class Protocol {
|
|||||||
Folder,
|
Folder,
|
||||||
SSH,
|
SSH,
|
||||||
Local,
|
Local,
|
||||||
|
Serial,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交互式的 SFTP,此协议只在系统内部交互不应该暴露给用户也不应该持久化
|
||||||
|
*/
|
||||||
|
@Transient
|
||||||
|
SFTPPty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -39,6 +57,53 @@ data class Authentication(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class SerialCommParity {
|
||||||
|
None,
|
||||||
|
Even,
|
||||||
|
Odd,
|
||||||
|
Mark,
|
||||||
|
Space
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SerialCommFlowControl {
|
||||||
|
None,
|
||||||
|
RTS_CTS,
|
||||||
|
XON_XOFF,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SerialComm(
|
||||||
|
/**
|
||||||
|
* 串口
|
||||||
|
*/
|
||||||
|
val port: String = StringUtils.EMPTY,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 波特率
|
||||||
|
*/
|
||||||
|
val baudRate: Int = 9600,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据位:5、6、7、8
|
||||||
|
*/
|
||||||
|
val dataBits: Int = 8,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止位: 1、1.5、2
|
||||||
|
*/
|
||||||
|
val stopBits: String = "1",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验位
|
||||||
|
*/
|
||||||
|
val parity: SerialCommParity = SerialCommParity.None,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流控
|
||||||
|
*/
|
||||||
|
val flowControl: SerialCommFlowControl = SerialCommFlowControl.None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Options(
|
data class Options(
|
||||||
@@ -61,7 +126,12 @@ data class Options(
|
|||||||
/**
|
/**
|
||||||
* SSH 心跳间隔
|
* SSH 心跳间隔
|
||||||
*/
|
*/
|
||||||
val heartbeatInterval: Int = 30
|
val heartbeatInterval: Int = 30,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 串口配置
|
||||||
|
*/
|
||||||
|
val serialComm: SerialComm = SerialComm(),
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val Default = Options()
|
val Default = Options()
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package app.termora
|
|||||||
|
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.swing.Swing
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import org.apache.sshd.client.SshClient
|
import org.apache.sshd.client.SshClient
|
||||||
import org.apache.sshd.client.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
@@ -47,38 +50,70 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
putValue(NAME, "${I18n.getString("termora.new-host.test-connection")}...")
|
putValue(NAME, "${I18n.getString("termora.new-host.test-connection")}...")
|
||||||
SwingUtilities.invokeLater {
|
isEnabled = false
|
||||||
testConnection(pane.getHost())
|
|
||||||
putValue(NAME, I18n.getString("termora.new-host.test-connection"))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
testConnection(pane.getHost())
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
putValue(NAME, I18n.getString("termora.new-host.test-connection"))
|
||||||
|
isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun testConnection(host: Host) {
|
private suspend fun testConnection(host: Host) {
|
||||||
if (host.protocol != Protocol.SSH) {
|
val owner = this
|
||||||
OptionPane.showMessageDialog(this, I18n.getString("termora.new-host.test-connection-successful"))
|
if (host.protocol == Protocol.Local) {
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
OptionPane.showMessageDialog(owner, I18n.getString("termora.new-host.test-connection-successful"))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (host.protocol == Protocol.SSH) {
|
||||||
|
testSSH(host)
|
||||||
|
} else if (host.protocol == Protocol.Serial) {
|
||||||
|
testSerial(host)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, ExceptionUtils.getMessage(e),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
I18n.getString("termora.new-host.test-connection-successful")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testSSH(host: Host) {
|
||||||
var client: SshClient? = null
|
var client: SshClient? = null
|
||||||
var session: ClientSession? = null
|
var session: ClientSession? = null
|
||||||
try {
|
try {
|
||||||
client = SshClients.openClient(host)
|
client = SshClients.openClient(host)
|
||||||
|
client.userInteraction = TerminalUserInteraction(owner)
|
||||||
session = SshClients.openSession(host, client)
|
session = SshClients.openSession(host, client)
|
||||||
OptionPane.showMessageDialog(this, I18n.getString("termora.new-host.test-connection-successful"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
OptionPane.showMessageDialog(
|
|
||||||
this, ExceptionUtils.getRootCauseMessage(e),
|
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
|
||||||
)
|
|
||||||
} finally {
|
} finally {
|
||||||
session?.close()
|
session?.close()
|
||||||
client?.close()
|
client?.close()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testSerial(host: Host) {
|
||||||
|
Serials.openPort(host).closePort()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun doOKAction() {
|
override fun doOKAction() {
|
||||||
|
|||||||
@@ -2,11 +2,17 @@ package app.termora
|
|||||||
|
|
||||||
import app.termora.keymgr.KeyManager
|
import app.termora.keymgr.KeyManager
|
||||||
import app.termora.keymgr.KeyManagerDialog
|
import app.termora.keymgr.KeyManagerDialog
|
||||||
|
import com.fazecast.jSerialComm.SerialPort
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
import java.awt.event.*
|
import java.awt.event.*
|
||||||
@@ -22,6 +28,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
protected val proxyOption = ProxyOption()
|
protected val proxyOption = ProxyOption()
|
||||||
protected val terminalOption = TerminalOption()
|
protected val terminalOption = TerminalOption()
|
||||||
protected val jumpHostsOption = JumpHostsOption()
|
protected val jumpHostsOption = JumpHostsOption()
|
||||||
|
protected val serialCommOption = SerialCommOption()
|
||||||
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -30,6 +37,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
addOption(tunnelingOption)
|
addOption(tunnelingOption)
|
||||||
addOption(jumpHostsOption)
|
addOption(jumpHostsOption)
|
||||||
addOption(terminalOption)
|
addOption(terminalOption)
|
||||||
|
addOption(serialCommOption)
|
||||||
|
|
||||||
setContentBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
setContentBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
||||||
}
|
}
|
||||||
@@ -43,6 +51,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
var authentication = Authentication.No
|
var authentication = Authentication.No
|
||||||
var proxy = Proxy.No
|
var proxy = Proxy.No
|
||||||
|
|
||||||
|
|
||||||
if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.Password) {
|
if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.Password) {
|
||||||
authentication = authentication.copy(
|
authentication = authentication.copy(
|
||||||
type = AuthenticationType.Password,
|
type = AuthenticationType.Password,
|
||||||
@@ -66,12 +75,23 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val serialComm = SerialComm(
|
||||||
|
port = serialCommOption.serialPortComboBox.selectedItem?.toString() ?: StringUtils.EMPTY,
|
||||||
|
baudRate = serialCommOption.baudRateComboBox.selectedItem?.toString()?.toIntOrNull() ?: 9600,
|
||||||
|
dataBits = serialCommOption.dataBitsComboBox.selectedItem as Int? ?: 8,
|
||||||
|
stopBits = serialCommOption.stopBitsComboBox.selectedItem as String? ?: "1",
|
||||||
|
parity = serialCommOption.parityComboBox.selectedItem as SerialCommParity,
|
||||||
|
flowControl = serialCommOption.flowControlComboBox.selectedItem as SerialCommFlowControl
|
||||||
|
)
|
||||||
|
|
||||||
val options = Options.Default.copy(
|
val options = Options.Default.copy(
|
||||||
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
||||||
env = terminalOption.environmentTextArea.text,
|
env = terminalOption.environmentTextArea.text,
|
||||||
startupCommand = terminalOption.startupCommandTextField.text,
|
startupCommand = terminalOption.startupCommandTextField.text,
|
||||||
heartbeatInterval = (terminalOption.heartbeatIntervalTextField.value ?: 30) as Int,
|
heartbeatInterval = (terminalOption.heartbeatIntervalTextField.value ?: 30) as Int,
|
||||||
jumpHosts = jumpHostsOption.jumpHosts.map { it.id }
|
jumpHosts = jumpHostsOption.jumpHosts.map { it.id },
|
||||||
|
serialComm = serialComm
|
||||||
)
|
)
|
||||||
|
|
||||||
return Host(
|
return Host(
|
||||||
@@ -103,6 +123,12 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
if (validateField(generalOption.usernameTextField)) {
|
if (validateField(generalOption.usernameTextField)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
} else if (host.protocol == Protocol.Serial) {
|
||||||
|
if (validateField(serialCommOption.serialPortComboBox)
|
||||||
|
|| validateField(serialCommOption.baudRateComboBox)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (host.authentication.type == AuthenticationType.Password) {
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
@@ -152,7 +178,8 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
* 返回 true 表示有错误
|
* 返回 true 表示有错误
|
||||||
*/
|
*/
|
||||||
private fun validateField(comboBox: JComboBox<*>): Boolean {
|
private fun validateField(comboBox: JComboBox<*>): Boolean {
|
||||||
if (comboBox.isEnabled && comboBox.selectedItem == null) {
|
val selectedItem = comboBox.selectedItem
|
||||||
|
if (comboBox.isEnabled && (selectedItem == null || (selectedItem is String && selectedItem.isBlank()))) {
|
||||||
selectOptionJComponent(comboBox)
|
selectOptionJComponent(comboBox)
|
||||||
comboBox.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
comboBox.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
comboBox.requestFocusInWindow()
|
comboBox.requestFocusInWindow()
|
||||||
@@ -259,6 +286,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
protocolTypeComboBox.addItem(Protocol.SSH)
|
protocolTypeComboBox.addItem(Protocol.SSH)
|
||||||
protocolTypeComboBox.addItem(Protocol.Local)
|
protocolTypeComboBox.addItem(Protocol.Local)
|
||||||
|
protocolTypeComboBox.addItem(Protocol.Serial)
|
||||||
|
|
||||||
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||||
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||||
@@ -328,7 +356,9 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
passwordTextField.isEnabled = true
|
passwordTextField.isEnabled = true
|
||||||
chooseKeyBtn.isEnabled = true
|
chooseKeyBtn.isEnabled = true
|
||||||
|
|
||||||
if (protocolTypeComboBox.selectedItem == Protocol.Local) {
|
if (protocolTypeComboBox.selectedItem == Protocol.Local
|
||||||
|
|| protocolTypeComboBox.selectedItem == Protocol.Serial
|
||||||
|
) {
|
||||||
hostTextField.isEnabled = false
|
hostTextField.isEnabled = false
|
||||||
portTextField.isEnabled = false
|
portTextField.isEnabled = false
|
||||||
usernameTextField.isEnabled = false
|
usernameTextField.isEnabled = false
|
||||||
@@ -901,6 +931,127 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected inner class SerialCommOption : JPanel(BorderLayout()), Option {
|
||||||
|
val serialPortComboBox = OutlineComboBox<String>()
|
||||||
|
val baudRateComboBox = OutlineComboBox<Int>()
|
||||||
|
val dataBitsComboBox = OutlineComboBox<Int>()
|
||||||
|
val parityComboBox = OutlineComboBox<SerialCommParity>()
|
||||||
|
val stopBitsComboBox = OutlineComboBox<String>()
|
||||||
|
val flowControlComboBox = OutlineComboBox<SerialCommFlowControl>()
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
|
||||||
|
serialPortComboBox.isEditable = true
|
||||||
|
|
||||||
|
baudRateComboBox.isEditable = true
|
||||||
|
baudRateComboBox.addItem(9600)
|
||||||
|
baudRateComboBox.addItem(19200)
|
||||||
|
baudRateComboBox.addItem(38400)
|
||||||
|
baudRateComboBox.addItem(57600)
|
||||||
|
baudRateComboBox.addItem(115200)
|
||||||
|
|
||||||
|
dataBitsComboBox.addItem(5)
|
||||||
|
dataBitsComboBox.addItem(6)
|
||||||
|
dataBitsComboBox.addItem(7)
|
||||||
|
dataBitsComboBox.addItem(8)
|
||||||
|
dataBitsComboBox.selectedItem = 8
|
||||||
|
|
||||||
|
parityComboBox.addItem(SerialCommParity.None)
|
||||||
|
parityComboBox.addItem(SerialCommParity.Even)
|
||||||
|
parityComboBox.addItem(SerialCommParity.Odd)
|
||||||
|
parityComboBox.addItem(SerialCommParity.Mark)
|
||||||
|
parityComboBox.addItem(SerialCommParity.Space)
|
||||||
|
|
||||||
|
stopBitsComboBox.addItem("1")
|
||||||
|
stopBitsComboBox.addItem("1.5")
|
||||||
|
stopBitsComboBox.addItem("2")
|
||||||
|
stopBitsComboBox.selectedItem = "1"
|
||||||
|
|
||||||
|
flowControlComboBox.addItem(SerialCommFlowControl.None)
|
||||||
|
flowControlComboBox.addItem(SerialCommFlowControl.RTS_CTS)
|
||||||
|
flowControlComboBox.addItem(SerialCommFlowControl.XON_XOFF)
|
||||||
|
|
||||||
|
flowControlComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
val text = value?.toString() ?: StringUtils.EMPTY
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
text.replace('_', '/'),
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
addComponentListener(object : ComponentAdapter() {
|
||||||
|
override fun componentShown(e: ComponentEvent) {
|
||||||
|
removeComponentListener(this)
|
||||||
|
@Suppress("OPT_IN_USAGE")
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
for (commPort in SerialPort.getCommPorts()) {
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
serialPortComboBox.addItem(commPort.systemPortName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.new-host.serial")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $formMargin, default:grow, $formMargin",
|
||||||
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.serial.port")}:").xy(1, rows)
|
||||||
|
.add(serialPortComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.serial.baud-rate")}:").xy(1, rows)
|
||||||
|
.add(baudRateComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.serial.data-bits")}:").xy(1, rows)
|
||||||
|
.add(dataBitsComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.serial.parity")}:").xy(1, rows)
|
||||||
|
.add(parityComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.serial.stop-bits")}:").xy(1, rows)
|
||||||
|
.add(stopBitsComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.serial.flow-control")}:").xy(1, rows)
|
||||||
|
.add(flowControlComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.build()
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected inner class JumpHostsOption : JPanel(BorderLayout()), Option {
|
protected inner class JumpHostsOption : JPanel(BorderLayout()), Option {
|
||||||
val jumpHosts = mutableListOf<Host>()
|
val jumpHosts = mutableListOf<Host>()
|
||||||
@@ -1006,7 +1157,8 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
val rows = table.selectedRows.sortedDescending()
|
val rows = table.selectedRows.sortedDescending()
|
||||||
if (rows.isEmpty()) return
|
if (rows.isEmpty()) return
|
||||||
for (row in rows) {
|
for (row in rows) {
|
||||||
model.removeRow(row)
|
jumpHosts.removeAt(row)
|
||||||
|
model.fireTableRowsDeleted(row, row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.DataProvider
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -12,7 +14,7 @@ abstract class HostTerminalTab(
|
|||||||
val windowScope: WindowScope,
|
val windowScope: WindowScope,
|
||||||
val host: Host,
|
val host: Host,
|
||||||
protected val terminal: Terminal = TerminalFactory.getInstance(windowScope).createTerminal()
|
protected val terminal: Terminal = TerminalFactory.getInstance(windowScope).createTerminal()
|
||||||
) : PropertyTerminalTab() {
|
) : PropertyTerminalTab(), DataProvider {
|
||||||
companion object {
|
companion object {
|
||||||
val Host = DataKey(app.termora.Host::class)
|
val Host = DataKey(app.termora.Host::class)
|
||||||
}
|
}
|
||||||
@@ -69,4 +71,11 @@ abstract class HostTerminalTab(
|
|||||||
unread = false
|
unread = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
|
if (dataKey == DataProviders.Terminal) {
|
||||||
|
return terminal as T?
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.actions.NewHostAction
|
import app.termora.actions.NewHostAction
|
||||||
import app.termora.actions.OpenHostAction
|
import app.termora.actions.OpenHostAction
|
||||||
|
import app.termora.transport.SFTPAction
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
||||||
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
|
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
@@ -54,6 +57,7 @@ class HostTree : JTree(), Disposable {
|
|||||||
editor.preferredSize = Dimension(220, 0)
|
editor.preferredSize = Dimension(220, 0)
|
||||||
|
|
||||||
setCellRenderer(object : DefaultXTreeCellRenderer() {
|
setCellRenderer(object : DefaultXTreeCellRenderer() {
|
||||||
|
private val properties get() = Database.getDatabase().properties
|
||||||
override fun getTreeCellRendererComponent(
|
override fun getTreeCellRendererComponent(
|
||||||
tree: JTree,
|
tree: JTree,
|
||||||
value: Any,
|
value: Any,
|
||||||
@@ -64,11 +68,41 @@ class HostTree : JTree(), Disposable {
|
|||||||
hasFocus: Boolean
|
hasFocus: Boolean
|
||||||
): Component {
|
): Component {
|
||||||
val host = value as Host
|
val host = value as Host
|
||||||
val c = super.getTreeCellRendererComponent(tree, host, sel, expanded, leaf, row, hasFocus)
|
var text = host.name
|
||||||
if (host.protocol == Protocol.Folder) {
|
|
||||||
icon = if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
// 是否显示更多信息
|
||||||
} else if (host.protocol == Protocol.SSH || host.protocol == Protocol.Local) {
|
if (properties.getString("HostTree.showMoreInfo", "false").toBoolean()) {
|
||||||
icon = if (sel && this@HostTree.hasFocus()) Icons.terminal.dark else Icons.terminal
|
val color = if (sel) {
|
||||||
|
if (this@HostTree.hasFocus()) {
|
||||||
|
UIManager.getColor("textHighlightText")
|
||||||
|
} else {
|
||||||
|
this.foreground
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
UIManager.getColor("textInactiveText")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.protocol == Protocol.SSH) {
|
||||||
|
text = """
|
||||||
|
<html>${host.name}
|
||||||
|
|
||||||
|
<font color=rgb(${color.red},${color.green},${color.blue})>${host.username}@${host.host}</font></html>
|
||||||
|
""".trimIndent()
|
||||||
|
} else if (host.protocol == Protocol.Serial) {
|
||||||
|
text = """
|
||||||
|
<html>${host.name}
|
||||||
|
|
||||||
|
<font color=rgb(${color.red},${color.green},${color.blue})>${host.options.serialComm.port}</font></html>
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus)
|
||||||
|
|
||||||
|
icon = when (host.protocol) {
|
||||||
|
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||||
|
Protocol.Serial -> if (sel && this@HostTree.hasFocus()) Icons.plugin.dark else Icons.plugin
|
||||||
|
else -> if (sel && this@HostTree.hasFocus()) Icons.terminal.dark else Icons.terminal
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@@ -312,12 +346,17 @@ class HostTree : JTree(), Disposable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val properties = Database.getDatabase().properties
|
||||||
val popupMenu = FlatPopupMenu()
|
val popupMenu = FlatPopupMenu()
|
||||||
val newMenu = JMenu(I18n.getString("termora.welcome.contextmenu.new"))
|
val newMenu = JMenu(I18n.getString("termora.welcome.contextmenu.new"))
|
||||||
val newFolder = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.folder"))
|
val newFolder = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.folder"))
|
||||||
val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host"))
|
val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host"))
|
||||||
|
|
||||||
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open"))
|
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect"))
|
||||||
|
val openWith = popupMenu.add(JMenu(I18n.getString("termora.welcome.contextmenu.connect-with"))) as JMenu
|
||||||
|
val openWithSFTP = openWith.add("SFTP")
|
||||||
|
val openWithSFTPCommand = openWith.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
|
||||||
|
val openInNewWindow = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open-in-new-window"))
|
||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
val copy = popupMenu.add(I18n.getString("termora.welcome.contextmenu.copy"))
|
val copy = popupMenu.add(I18n.getString("termora.welcome.contextmenu.copy"))
|
||||||
val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove"))
|
val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove"))
|
||||||
@@ -328,17 +367,28 @@ class HostTree : JTree(), Disposable {
|
|||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
popupMenu.add(newMenu)
|
popupMenu.add(newMenu)
|
||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
|
|
||||||
|
val showMoreInfo = JCheckBoxMenuItem(I18n.getString("termora.welcome.contextmenu.show-more-info"))
|
||||||
|
showMoreInfo.isSelected = properties.getString("HostTree.showMoreInfo", "false").toBoolean()
|
||||||
|
showMoreInfo.addActionListener {
|
||||||
|
properties.putString(
|
||||||
|
"HostTree.showMoreInfo",
|
||||||
|
showMoreInfo.isSelected.toString()
|
||||||
|
)
|
||||||
|
SwingUtilities.updateComponentTreeUI(this)
|
||||||
|
}
|
||||||
|
popupMenu.add(showMoreInfo)
|
||||||
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
|
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
|
||||||
|
|
||||||
open.addActionListener { evt ->
|
open.addActionListener { openHosts(it, false) }
|
||||||
getSelectionNodes()
|
openWithSFTP.addActionListener { openWithSFTP(it) }
|
||||||
.filter { it.protocol != Protocol.Folder }
|
openWithSFTPCommand.addActionListener { openWithSFTPCommand(it) }
|
||||||
.forEach {
|
openInNewWindow.addActionListener { openHosts(it, true) }
|
||||||
ActionManager.getInstance()
|
|
||||||
.getAction(OpenHostAction.OPEN_HOST)
|
// 如果选中了 SSH 服务器,那么才启用
|
||||||
?.actionPerformed(OpenHostActionEvent(evt.source, it, evt))
|
openWithSFTP.isEnabled = getSelectionNodes().any { it.protocol == Protocol.SSH }
|
||||||
}
|
openWithSFTPCommand.isEnabled = openWithSFTP.isEnabled
|
||||||
}
|
openWith.isEnabled = openWith.menuComponents.any { it is JMenuItem && it.isEnabled }
|
||||||
|
|
||||||
rename.addActionListener {
|
rename.addActionListener {
|
||||||
startEditingAtPath(TreePath(model.getPathToRoot(lastHost)))
|
startEditingAtPath(TreePath(model.getPathToRoot(lastHost)))
|
||||||
@@ -422,6 +472,7 @@ class HostTree : JTree(), Disposable {
|
|||||||
property.addActionListener(object : AbstractAction() {
|
property.addActionListener(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
val dialog = HostDialog(SwingUtilities.getWindowAncestor(this@HostTree), lastHost)
|
val dialog = HostDialog(SwingUtilities.getWindowAncestor(this@HostTree), lastHost)
|
||||||
|
dialog.title = lastHost.name
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
val host = dialog.host ?: return
|
val host = dialog.host ?: return
|
||||||
runCatchingHost(host)
|
runCatchingHost(host)
|
||||||
@@ -454,6 +505,37 @@ class HostTree : JTree(), Disposable {
|
|||||||
popupMenu.show(this, event.x, event.y)
|
popupMenu.show(this, event.x, event.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openHosts(evt: EventObject, openInNewWindow: Boolean) {
|
||||||
|
assertEventDispatchThread()
|
||||||
|
val nodes = getSelectionNodes().filter { it.protocol != Protocol.Folder }
|
||||||
|
if (nodes.isEmpty()) return
|
||||||
|
val openHostAction = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST) ?: return
|
||||||
|
val source = if (openInNewWindow)
|
||||||
|
TermoraFrameManager.getInstance().createWindow().apply { isVisible = true }
|
||||||
|
else evt.source
|
||||||
|
|
||||||
|
nodes.forEach { openHostAction.actionPerformed(OpenHostActionEvent(source, it, evt)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openWithSFTP(evt: EventObject) {
|
||||||
|
val nodes = getSelectionNodes().filter { it.protocol == Protocol.SSH }
|
||||||
|
if (nodes.isEmpty()) return
|
||||||
|
|
||||||
|
val sftpAction = ActionManager.getInstance().getAction(Actions.SFTP) as SFTPAction? ?: return
|
||||||
|
val tab = sftpAction.openOrCreateSFTPTerminalTab(AnActionEvent(this, StringUtils.EMPTY, evt)) ?: return
|
||||||
|
for (node in nodes) {
|
||||||
|
sftpAction.connectHost(node, tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openWithSFTPCommand(evt: EventObject) {
|
||||||
|
val nodes = getSelectionNodes().filter { it.protocol == Protocol.SSH }
|
||||||
|
if (nodes.isEmpty()) return
|
||||||
|
val action = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST) ?: return
|
||||||
|
for (host in nodes) {
|
||||||
|
action.actionPerformed(OpenHostActionEvent(this, host.copy(protocol = Protocol.SFTPPty), evt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun expandNode(node: Host, including: Boolean = false) {
|
fun expandNode(node: Host, including: Boolean = false) {
|
||||||
expandPath(TreePath(model.getPathToRoot(node)))
|
expandPath(TreePath(model.getPathToRoot(node)))
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package app.termora
|
|||||||
object Icons {
|
object Icons {
|
||||||
val bulletList by lazy { DynamicIcon("icons/bulletList.svg", "icons/bulletList_dark.svg") }
|
val bulletList by lazy { DynamicIcon("icons/bulletList.svg", "icons/bulletList_dark.svg") }
|
||||||
val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") }
|
val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") }
|
||||||
|
val closeSmall by lazy { DynamicIcon("icons/closeSmall.svg", "icons/closeSmall_dark.svg") }
|
||||||
|
val closeSmallHovered by lazy { DynamicIcon("icons/closeSmallHovered.svg", "icons/closeSmallHovered_dark.svg") }
|
||||||
|
val plugin by lazy { DynamicIcon("icons/plugin.svg", "icons/plugin_dark.svg") }
|
||||||
val moveUp by lazy { DynamicIcon("icons/moveUp.svg", "icons/moveUp_dark.svg") }
|
val moveUp by lazy { DynamicIcon("icons/moveUp.svg", "icons/moveUp_dark.svg") }
|
||||||
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
|
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
|
||||||
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
|
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
|
||||||
@@ -47,6 +50,7 @@ object Icons {
|
|||||||
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") }
|
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") }
|
||||||
val export by lazy { DynamicIcon("icons/export.svg", "icons/export_dark.svg") }
|
val export by lazy { DynamicIcon("icons/export.svg", "icons/export_dark.svg") }
|
||||||
val terminal by lazy { DynamicIcon("icons/terminal.svg", "icons/terminal_dark.svg") }
|
val terminal by lazy { DynamicIcon("icons/terminal.svg", "icons/terminal_dark.svg") }
|
||||||
|
val fileFormat by lazy { DynamicIcon("icons/fileFormat.svg", "icons/fileFormat_dark.svg") }
|
||||||
val azure by lazy { DynamicIcon("icons/azure.svg", "icons/azure_dark.svg") }
|
val azure by lazy { DynamicIcon("icons/azure.svg", "icons/azure_dark.svg") }
|
||||||
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
|
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
|
||||||
val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") }
|
val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") }
|
||||||
@@ -67,6 +71,7 @@ object Icons {
|
|||||||
val network by lazy { DynamicIcon("icons/network.svg", "icons/network_dark.svg") }
|
val network by lazy { DynamicIcon("icons/network.svg", "icons/network_dark.svg") }
|
||||||
val server by lazy { DynamicIcon("icons/server.svg", "icons/server_dark.svg") }
|
val server by lazy { DynamicIcon("icons/server.svg", "icons/server_dark.svg") }
|
||||||
val runAnything by lazy { DynamicIcon("icons/runAnything.svg", "icons/runAnything_dark.svg") }
|
val runAnything by lazy { DynamicIcon("icons/runAnything.svg", "icons/runAnything_dark.svg") }
|
||||||
|
val run by lazy { DynamicIcon("icons/run.svg", "icons/run_dark.svg") }
|
||||||
val uiForm by lazy { DynamicIcon("icons/uiForm.svg", "icons/uiForm_dark.svg") }
|
val uiForm by lazy { DynamicIcon("icons/uiForm.svg", "icons/uiForm_dark.svg") }
|
||||||
val cloud by lazy { DynamicIcon("icons/cloud.svg", "icons/cloud_dark.svg") }
|
val cloud by lazy { DynamicIcon("icons/cloud.svg", "icons/cloud_dark.svg") }
|
||||||
val externalLink by lazy { DynamicIcon("icons/externalLink.svg", "icons/externalLink_dark.svg") }
|
val externalLink by lazy { DynamicIcon("icons/externalLink.svg", "icons/externalLink_dark.svg") }
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import app.termora.terminal.PtyConnector
|
|||||||
import org.apache.commons.io.Charsets
|
import org.apache.commons.io.Charsets
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
class LocalTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
|
class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
||||||
|
PtyHostTerminalTab(windowScope, host) {
|
||||||
|
|
||||||
override suspend fun openPtyConnector(): PtyConnector {
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
val winSize = terminalPanel.winSize()
|
val winSize = terminalPanel.winSize()
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ fun main() {
|
|||||||
setupNativeLibraries()
|
setupNativeLibraries()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||||
|
System.setProperty("apple.awt.application.name", Application.getName())
|
||||||
|
}
|
||||||
|
|
||||||
ApplicationRunner().run()
|
ApplicationRunner().run()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,4 +45,9 @@ private fun setupNativeLibraries() {
|
|||||||
if (pty4j.exists()) {
|
if (pty4j.exists()) {
|
||||||
System.setProperty(PtyUtil.PREFERRED_NATIVE_FOLDER_KEY, pty4j.absolutePath)
|
System.setProperty(PtyUtil.PREFERRED_NATIVE_FOLDER_KEY, pty4j.absolutePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val jSerialComm = FileUtils.getFile(dylib, "jSerialComm")
|
||||||
|
if (jSerialComm.exists()) {
|
||||||
|
System.setProperty("jSerialComm.library.path", jSerialComm.absolutePath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import app.termora.actions.ActionManager
|
|||||||
import app.termora.terminal.Terminal
|
import app.termora.terminal.Terminal
|
||||||
import app.termora.terminal.TerminalColor
|
import app.termora.terminal.TerminalColor
|
||||||
import app.termora.terminal.TextStyle
|
import app.termora.terminal.TextStyle
|
||||||
|
import app.termora.terminal.panel.FloatingToolbarPanel
|
||||||
import app.termora.terminal.panel.TerminalDisplay
|
import app.termora.terminal.panel.TerminalDisplay
|
||||||
import app.termora.terminal.panel.TerminalPaintListener
|
import app.termora.terminal.panel.TerminalPaintListener
|
||||||
import app.termora.terminal.panel.TerminalPanel
|
import app.termora.terminal.panel.TerminalPanel
|
||||||
@@ -32,13 +33,25 @@ class MultipleTerminalListener : TerminalPaintListener {
|
|||||||
// 正在搜索那么需要下移
|
// 正在搜索那么需要下移
|
||||||
val finding = terminal.getTerminalModel().getData(TerminalPanel.Finding, false)
|
val finding = terminal.getTerminalModel().getData(TerminalPanel.Finding, false)
|
||||||
|
|
||||||
|
// 如果悬浮窗正在显示,那么需要下移
|
||||||
|
val floatingToolBar = terminalPanel.getData(FloatingToolbarPanel.FloatingToolbar)?.isVisible == true
|
||||||
|
|
||||||
|
var y = g.fontMetrics.ascent
|
||||||
|
if (finding) {
|
||||||
|
y += g.fontMetrics.height + g.fontMetrics.ascent / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if (floatingToolBar) {
|
||||||
|
y += g.fontMetrics.height + g.fontMetrics.ascent / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
g.font = font
|
g.font = font
|
||||||
g.color = Color(colorPalette.getColor(TerminalColor.Normal.RED))
|
g.color = Color(colorPalette.getColor(TerminalColor.Normal.RED))
|
||||||
g.drawString(
|
g.drawString(
|
||||||
text,
|
text,
|
||||||
terminalDisplay.width - width - terminalPanel.getAverageCharWidth() / 2,
|
terminalDisplay.width - width - terminalPanel.getAverageCharWidth() / 2,
|
||||||
g.fontMetrics.ascent + if (finding)
|
y
|
||||||
g.fontMetrics.height + g.fontMetrics.ascent / 2 else 0
|
|
||||||
)
|
)
|
||||||
g.font = oldFont
|
g.font = oldFont
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,266 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.*
|
||||||
|
import java.awt.event.*
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.*
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
class MyTabbedPane : FlatTabbedPane() {
|
class MyTabbedPane : FlatTabbedPane() {
|
||||||
|
|
||||||
|
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
private val dragMouseAdaptor = DragMouseAdaptor()
|
||||||
|
private val terminalTabbedManager
|
||||||
|
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||||
|
.getData(DataProviders.TerminalTabbedManager)
|
||||||
|
|
||||||
|
init {
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateUI() {
|
||||||
|
styleMap = mapOf(
|
||||||
|
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground"),
|
||||||
|
"hoverColor" to UIManager.getColor("TabbedPane.background"),
|
||||||
|
)
|
||||||
|
super.updateUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
addMouseListener(dragMouseAdaptor)
|
||||||
|
addMouseMotionListener(dragMouseAdaptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun processMouseEvent(e: MouseEvent) {
|
||||||
|
// Shift + Click ===> close tab
|
||||||
|
if (e.id == MouseEvent.MOUSE_CLICKED && SwingUtilities.isLeftMouseButton(e) && isShiftPressedOnly(e.modifiersEx)) {
|
||||||
|
val index = indexAtLocation(e.x, e.y)
|
||||||
|
if (index >= 0) {
|
||||||
|
tabCloseCallback?.accept(this, index)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (e.id == MouseEvent.MOUSE_PRESSED && isShiftPressedOnly(e.modifiersEx)) {
|
||||||
|
val index = indexAtLocation(e.x, e.y)
|
||||||
|
if (index >= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.processMouseEvent(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isShiftPressedOnly(modifiersEx: Int): Boolean {
|
||||||
|
return (modifiersEx and InputEvent.ALT_DOWN_MASK) == 0
|
||||||
|
&& (modifiersEx and InputEvent.ALT_GRAPH_DOWN_MASK) == 0
|
||||||
|
&& (modifiersEx and InputEvent.CTRL_DOWN_MASK) == 0
|
||||||
|
&& (modifiersEx and InputEvent.SHIFT_DOWN_MASK) != 0
|
||||||
|
}
|
||||||
|
|
||||||
override fun setSelectedIndex(index: Int) {
|
override fun setSelectedIndex(index: Int) {
|
||||||
val oldIndex = selectedIndex
|
val oldIndex = selectedIndex
|
||||||
super.setSelectedIndex(index)
|
super.setSelectedIndex(index)
|
||||||
firePropertyChange("selectedIndex", oldIndex, index)
|
firePropertyChange("selectedIndex", oldIndex, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class DragMouseAdaptor : MouseAdapter(), KeyEventDispatcher {
|
||||||
|
private var mousePressedPoint = Point()
|
||||||
|
private var tabIndex = 0 - 1
|
||||||
|
private var cancelled = false
|
||||||
|
private var window: Window? = null
|
||||||
|
private var terminalTab: TerminalTab? = null
|
||||||
|
private var isDragging = false
|
||||||
|
private var lastVisitTabIndex = -1
|
||||||
|
private var releasedPoint = Point()
|
||||||
|
|
||||||
|
override fun mousePressed(e: MouseEvent) {
|
||||||
|
val index = indexAtLocation(e.x, e.y)
|
||||||
|
if (index < 0 || !isTabClosable(index)) {
|
||||||
|
tabIndex = -1
|
||||||
|
mousePressedPoint = Point()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tabIndex = index
|
||||||
|
mousePressedPoint = e.point
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseDragged(e: MouseEvent) {
|
||||||
|
// 如果正在拖拽中,那么修改 Window 的位置
|
||||||
|
if (isDragging) {
|
||||||
|
window?.location = e.locationOnScreen
|
||||||
|
lastVisitTabIndex = indexAtLocation(e.x, e.y)
|
||||||
|
} else if (tabIndex >= 0) { // 这里之所以判断是确保在 mousePressed 时已经确定了 Tab
|
||||||
|
// 有的时候会太灵敏,这里容错一下
|
||||||
|
val diff = 5
|
||||||
|
if (abs(mousePressedPoint.y - e.y) >= diff || abs(mousePressedPoint.x - e.x) >= diff) {
|
||||||
|
startDrag(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startDrag(e: MouseEvent) {
|
||||||
|
if (isDragging) return
|
||||||
|
val terminalTabbedManager = terminalTabbedManager ?: return
|
||||||
|
val window = JDialog(owner).also { this.window = it }
|
||||||
|
window.isUndecorated = true
|
||||||
|
val image = createTabImage(tabIndex)
|
||||||
|
window.size = Dimension(image.width, image.height)
|
||||||
|
window.add(JLabel(ImageIcon(image)))
|
||||||
|
window.location = e.locationOnScreen
|
||||||
|
window.addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowClosed(e: WindowEvent) {
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.removeKeyEventDispatcher(this@DragMouseAdaptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun windowOpened(e: WindowEvent) {
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.addKeyEventDispatcher(this@DragMouseAdaptor)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 暂时关闭 Tab
|
||||||
|
terminalTabbedManager.closeTerminalTab(terminalTabbedManager.getTerminalTabs()[tabIndex].also {
|
||||||
|
terminalTab = it
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
window.isVisible = true
|
||||||
|
|
||||||
|
isDragging = true
|
||||||
|
cancelled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopDrag() {
|
||||||
|
if (!isDragging) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是取消,那么不需要移动到其它窗口
|
||||||
|
val c = if (cancelled) owner else getTopMostWindowUnderMouse()
|
||||||
|
|
||||||
|
// 如果等于 null 表示在空地方释放,那么单独一个窗口
|
||||||
|
if (c == null) {
|
||||||
|
val window = TermoraFrameManager.getInstance().createWindow()
|
||||||
|
dragToAnotherWindow(window)
|
||||||
|
window.location = releasedPoint
|
||||||
|
window.isVisible = true
|
||||||
|
} else if (c != owner && c is TermoraFrame) { // 如果在某个窗口内释放,那么就移动到某个窗口内
|
||||||
|
dragToAnotherWindow(c)
|
||||||
|
} else {
|
||||||
|
val tab = this.terminalTab
|
||||||
|
val terminalTabbedManager = terminalTabbedManager
|
||||||
|
if (tab != null && terminalTabbedManager != null) {
|
||||||
|
moveTab(
|
||||||
|
terminalTabbedManager,
|
||||||
|
tab,
|
||||||
|
lastVisitTabIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset
|
||||||
|
window?.dispose()
|
||||||
|
isDragging = false
|
||||||
|
tabIndex = -1
|
||||||
|
cancelled = false
|
||||||
|
lastVisitTabIndex = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseReleased(e: MouseEvent) {
|
||||||
|
releasedPoint = e.point
|
||||||
|
stopDrag()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTabImage(index: Int): BufferedImage {
|
||||||
|
val tabBounds = getBoundsAt(index)
|
||||||
|
val image = BufferedImage(tabBounds.width, tabBounds.height, BufferedImage.TYPE_INT_ARGB)
|
||||||
|
val g2 = image.createGraphics()
|
||||||
|
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||||
|
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
|
||||||
|
g2.translate(-tabBounds.x, -tabBounds.y)
|
||||||
|
paint(g2)
|
||||||
|
g2.dispose()
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
|
||||||
|
if (e.keyCode == KeyEvent.VK_ESCAPE) {
|
||||||
|
cancelled = true
|
||||||
|
stopDrag()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTopMostWindowUnderMouse(): Window? {
|
||||||
|
val mouseLocation = MouseInfo.getPointerInfo().location
|
||||||
|
val owner = owner
|
||||||
|
if (owner.isVisible && owner.bounds.contains(mouseLocation)) {
|
||||||
|
return owner
|
||||||
|
}
|
||||||
|
|
||||||
|
val windows = Window.getWindows()
|
||||||
|
// 倒序遍历,最上层的窗口优先匹配
|
||||||
|
for (i in windows.indices.reversed()) {
|
||||||
|
val window = windows[i]
|
||||||
|
if (window !is TermoraFrame) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (window.isVisible && window.bounds.contains(mouseLocation)) {
|
||||||
|
val topComponent = SwingUtilities.getDeepestComponentAt(
|
||||||
|
window,
|
||||||
|
mouseLocation.x - window.x, mouseLocation.y - window.y
|
||||||
|
)
|
||||||
|
if (topComponent != null) {
|
||||||
|
return SwingUtilities.getWindowAncestor(topComponent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun dragToAnotherWindow(frame: TermoraFrame) {
|
||||||
|
val tab = this.terminalTab ?: return
|
||||||
|
val tabbedManager = frame.getData(DataProviders.TerminalTabbed) ?: return
|
||||||
|
val tabbedPane = frame.getData(DataProviders.TabbedPane) ?: return
|
||||||
|
val location = Point(MouseInfo.getPointerInfo().location)
|
||||||
|
SwingUtilities.convertPointFromScreen(location, tabbedPane)
|
||||||
|
val index = tabbedPane.indexAtLocation(location.x, location.y)
|
||||||
|
|
||||||
|
moveTab(
|
||||||
|
tabbedManager,
|
||||||
|
tab,
|
||||||
|
index
|
||||||
|
)
|
||||||
|
|
||||||
|
if (frame.hasFocus()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
frame.requestFocus()
|
||||||
|
tabbedPane.selectedComponent?.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun moveTab(terminalTabbedManager: TerminalTabbedManager, tab: TerminalTab, lastVisitTabIndex: Int) {
|
||||||
|
// 如果是手动取消
|
||||||
|
if (cancelled) {
|
||||||
|
terminalTabbedManager.addTerminalTab(tabIndex, tab)
|
||||||
|
} else if (lastVisitTabIndex > 0) {
|
||||||
|
terminalTabbedManager.addTerminalTab(lastVisitTabIndex, tab)
|
||||||
|
} else if (lastVisitTabIndex == 0) {
|
||||||
|
terminalTabbedManager.addTerminalTab(1, tab)
|
||||||
|
} else {
|
||||||
|
terminalTabbedManager.addTerminalTab(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -6,8 +6,6 @@ import com.formdev.flatlaf.util.SystemInfo
|
|||||||
import com.jetbrains.JBR
|
import com.jetbrains.JBR
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.jdesktop.swingx.JXLabel
|
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
@@ -57,6 +55,7 @@ object OptionPane {
|
|||||||
pane.selectInitialValue()
|
pane.selectInitialValue()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
dialog.setLocationRelativeTo(parentComponent)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
dialog.dispose()
|
dialog.dispose()
|
||||||
val selectedValue = pane.value
|
val selectedValue = pane.value
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.terminal.panel.FloatingToolbarPanel
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.beans.PropertyChangeEvent
|
import java.beans.PropertyChangeEvent
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
abstract class PropertyTerminalTab : TerminalTab {
|
abstract class PropertyTerminalTab : TerminalTab {
|
||||||
protected val listeners = mutableListOf<PropertyChangeListener>()
|
protected val listeners = mutableListOf<PropertyChangeListener>()
|
||||||
@@ -26,6 +30,10 @@ abstract class PropertyTerminalTab : TerminalTab {
|
|||||||
|
|
||||||
override fun onLostFocus() {
|
override fun onLostFocus() {
|
||||||
hasFocus = false
|
hasFocus = false
|
||||||
|
|
||||||
|
// 切换标签时,尝试隐藏悬浮工具栏
|
||||||
|
val evt = AnActionEvent(getJComponent(), StringUtils.EMPTY, EventObject(getJComponent()))
|
||||||
|
evt.getData(FloatingToolbarPanel.FloatingToolbar)?.triggerHide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,20 @@ class PtyConnectorFactory : Disposable {
|
|||||||
rows: Int = 24, cols: Int = 80,
|
rows: Int = 24, cols: Int = 80,
|
||||||
env: Map<String, String> = emptyMap(),
|
env: Map<String, String> = emptyMap(),
|
||||||
charset: Charset = StandardCharsets.UTF_8
|
charset: Charset = StandardCharsets.UTF_8
|
||||||
|
): PtyConnector {
|
||||||
|
val command = database.terminal.localShell
|
||||||
|
val commands = mutableListOf(command)
|
||||||
|
if (SystemUtils.IS_OS_UNIX) {
|
||||||
|
commands.add("-l")
|
||||||
|
}
|
||||||
|
return createPtyConnector(commands.toTypedArray(), rows, cols, env, charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createPtyConnector(
|
||||||
|
commands: Array<String>,
|
||||||
|
rows: Int = 24, cols: Int = 80,
|
||||||
|
env: Map<String, String> = emptyMap(),
|
||||||
|
charset: Charset = StandardCharsets.UTF_8
|
||||||
): PtyConnector {
|
): PtyConnector {
|
||||||
val envs = mutableMapOf<String, String>()
|
val envs = mutableMapOf<String, String>()
|
||||||
envs.putAll(System.getenv())
|
envs.putAll(System.getenv())
|
||||||
@@ -38,21 +52,17 @@ class PtyConnectorFactory : Disposable {
|
|||||||
val locale = Locale.getDefault()
|
val locale = Locale.getDefault()
|
||||||
if (StringUtils.isNoneBlank(locale.language, locale.country)) {
|
if (StringUtils.isNoneBlank(locale.language, locale.country)) {
|
||||||
envs["LANG"] = "${locale.language}_${locale.country}.${Charset.defaultCharset().name()}"
|
envs["LANG"] = "${locale.language}_${locale.country}.${Charset.defaultCharset().name()}"
|
||||||
|
} else {
|
||||||
|
envs["LANG"] = "en_US.UTF-8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val command = database.terminal.localShell
|
|
||||||
val commands = mutableListOf(command)
|
|
||||||
if (SystemUtils.IS_OS_UNIX) {
|
|
||||||
commands.add("-l")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("command: {} , envs: {}", commands.joinToString(" "), envs)
|
log.debug("command: {} , envs: {}", commands.joinToString(" "), envs)
|
||||||
}
|
}
|
||||||
|
|
||||||
val ptyProcess = PtyProcessBuilder(commands.toTypedArray())
|
val ptyProcess = PtyProcessBuilder(commands)
|
||||||
.setEnvironment(envs)
|
.setEnvironment(envs)
|
||||||
.setInitialRows(rows)
|
.setInitialRows(rows)
|
||||||
.setInitialColumns(cols)
|
.setInitialColumns(cols)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
@@ -23,8 +24,9 @@ abstract class PtyHostTerminalTab(
|
|||||||
private var readerJob: Job? = null
|
private var readerJob: Job? = null
|
||||||
private val ptyConnectorDelegate = PtyConnectorDelegate()
|
private val ptyConnectorDelegate = PtyConnectorDelegate()
|
||||||
|
|
||||||
protected val terminalPanel =
|
private val terminalPanelFactory = TerminalPanelFactory.getInstance(windowScope)
|
||||||
TerminalPanelFactory.getInstance(windowScope).createTerminalPanel(terminal, ptyConnectorDelegate)
|
protected val terminalPanel = terminalPanelFactory.createTerminalPanel(terminal, ptyConnectorDelegate)
|
||||||
|
.apply { Disposer.register(this@PtyHostTerminalTab, this) }
|
||||||
protected val ptyConnectorFactory get() = PtyConnectorFactory.getInstance(windowScope)
|
protected val ptyConnectorFactory get() = PtyConnectorFactory.getInstance(windowScope)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -49,12 +51,16 @@ abstract class PtyHostTerminalTab(
|
|||||||
startPtyConnectorReader()
|
startPtyConnectorReader()
|
||||||
|
|
||||||
// 启动命令
|
// 启动命令
|
||||||
if (host.options.startupCommand.isNotBlank()) {
|
if (host.options.startupCommand.isNotBlank() && host.protocol != Protocol.SFTPPty) {
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
delay(250.milliseconds)
|
delay(250.milliseconds)
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
ptyConnector.write(host.options.startupCommand)
|
val charset = ptyConnector.getCharset()
|
||||||
ptyConnector.write(terminal.getKeyEncoder().encode(TerminalKeyEvent(KeyEvent.VK_ENTER)))
|
ptyConnector.write(host.options.startupCommand.toByteArray(charset))
|
||||||
|
ptyConnector.write(
|
||||||
|
terminal.getKeyEncoder().encode(TerminalKeyEvent(KeyEvent.VK_ENTER))
|
||||||
|
.toByteArray(charset)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,6 +122,7 @@ abstract class PtyHostTerminalTab(
|
|||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
stop()
|
stop()
|
||||||
|
terminalPanel
|
||||||
super.dispose()
|
super.dispose()
|
||||||
|
|
||||||
|
|
||||||
@@ -129,4 +136,12 @@ abstract class PtyHostTerminalTab(
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract suspend fun openPtyConnector(): PtyConnector
|
abstract suspend fun openPtyConnector(): PtyConnector
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
|
if (dataKey == DataProviders.TerminalPanel) {
|
||||||
|
return terminalPanel as T?
|
||||||
|
}
|
||||||
|
return super.getData(dataKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
146
src/main/kotlin/app/termora/RequestAuthenticationDialog.kt
Normal file
146
src/main/kotlin/app/termora/RequestAuthenticationDialog.kt
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.keymgr.OhKeyPair
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.event.ItemEvent
|
||||||
|
import javax.swing.*
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
class RequestAuthenticationDialog(owner: Window) : DialogWrapper(owner) {
|
||||||
|
|
||||||
|
private val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||||
|
private val rememberCheckBox = JCheckBox("Remember")
|
||||||
|
private val passwordPanel = JPanel(BorderLayout())
|
||||||
|
private val passwordPasswordField = OutlinePasswordField()
|
||||||
|
private val publicKeyComboBox = OutlineComboBox<OhKeyPair>()
|
||||||
|
private val keyManager get() = KeyManager.getInstance()
|
||||||
|
private var authentication = Authentication.No
|
||||||
|
|
||||||
|
init {
|
||||||
|
isModal = true
|
||||||
|
title = "SSH User Authentication"
|
||||||
|
controlsVisible = false
|
||||||
|
|
||||||
|
init()
|
||||||
|
|
||||||
|
pack()
|
||||||
|
|
||||||
|
size = Dimension(max(380, size.width), size.height)
|
||||||
|
|
||||||
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
|
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
if (value is OhKeyPair) value.name else value,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (keyPair in keyManager.getOhKeyPairs()) {
|
||||||
|
publicKeyComboBox.addItem(keyPair)
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationTypeComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
switchPasswordComponent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.PublicKey)
|
||||||
|
val formMargin = "7dlu"
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $formMargin, default:grow",
|
||||||
|
"pref, $formMargin, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
switchPasswordComponent()
|
||||||
|
|
||||||
|
return FormBuilder.create().padding("$formMargin, $formMargin, $formMargin, $formMargin")
|
||||||
|
.layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, 1)
|
||||||
|
.add(authenticationTypeComboBox).xy(3, 1)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, 3)
|
||||||
|
.add(passwordPanel).xy(3, 3)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun switchPasswordComponent() {
|
||||||
|
passwordPanel.removeAll()
|
||||||
|
if (authenticationTypeComboBox.selectedItem == AuthenticationType.Password) {
|
||||||
|
passwordPanel.add(passwordPasswordField, BorderLayout.CENTER)
|
||||||
|
} else if (authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
|
||||||
|
passwordPanel.add(publicKeyComboBox, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
passwordPanel.revalidate()
|
||||||
|
passwordPanel.repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createSouthPanel(): JComponent? {
|
||||||
|
val box = super.createSouthPanel() ?: return null
|
||||||
|
rememberCheckBox.isFocusable = false
|
||||||
|
box.add(rememberCheckBox, 0)
|
||||||
|
return box
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doCancelAction() {
|
||||||
|
authentication = Authentication.No
|
||||||
|
super.doCancelAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doOKAction() {
|
||||||
|
val type = authenticationTypeComboBox.selectedItem as AuthenticationType
|
||||||
|
|
||||||
|
if (type == AuthenticationType.Password) {
|
||||||
|
if (passwordPasswordField.password.isEmpty()) {
|
||||||
|
passwordPasswordField.requestFocusInWindow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (type == AuthenticationType.PublicKey) {
|
||||||
|
if (publicKeyComboBox.selectedItem == null) {
|
||||||
|
publicKeyComboBox.requestFocusInWindow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authentication = authentication.copy(
|
||||||
|
type = type,
|
||||||
|
password = if (type == AuthenticationType.Password) String(passwordPasswordField.password)
|
||||||
|
else (publicKeyComboBox.selectedItem as OhKeyPair).id
|
||||||
|
)
|
||||||
|
super.doOKAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAuthentication(): Authentication {
|
||||||
|
isModal = true
|
||||||
|
SwingUtilities.invokeLater { passwordPasswordField.requestFocusInWindow() }
|
||||||
|
isVisible = true
|
||||||
|
return authentication
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isRemembered(): Boolean {
|
||||||
|
return rememberCheckBox.isSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
196
src/main/kotlin/app/termora/SFTPPtyTerminalTab.kt
Normal file
196
src/main/kotlin/app/termora/SFTPPtyTerminalTab.kt
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||||
|
import app.termora.terminal.*
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import org.apache.commons.io.Charsets
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.sshd.client.ClientBuilder
|
||||||
|
import org.apache.sshd.client.SshClient
|
||||||
|
import org.apache.sshd.client.session.ClientSession
|
||||||
|
import org.apache.sshd.common.config.keys.writer.openssh.OpenSSHKeyPairResourceWriter
|
||||||
|
import org.apache.sshd.common.util.net.SshdSocketAddress
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import javax.swing.Icon
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
|
||||||
|
private val keyManager by lazy { KeyManager.getInstance() }
|
||||||
|
private val tempFiles = mutableListOf<Path>()
|
||||||
|
private var sshClient: SshClient? = null
|
||||||
|
private var sshSession: ClientSession? = null
|
||||||
|
private var lastPasswordReporterDataListener: PasswordReporterDataListener? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val canSupports by lazy {
|
||||||
|
val process = if (SystemInfo.isWindows) {
|
||||||
|
ProcessBuilder("cmd.exe", "/c", "where", "sftp").start()
|
||||||
|
} else {
|
||||||
|
ProcessBuilder("which", "sftp").start()
|
||||||
|
}
|
||||||
|
process.waitFor()
|
||||||
|
return@lazy process.exitValue() == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
|
|
||||||
|
|
||||||
|
val useJumpHosts = host.options.jumpHosts.isNotEmpty() || host.proxy.type != ProxyType.No
|
||||||
|
val commands = mutableListOf("sftp")
|
||||||
|
var host = this.host
|
||||||
|
|
||||||
|
// 如果配置了跳板机或者代理,那么通过 SSH 的端口转发到本地
|
||||||
|
if (useJumpHosts) {
|
||||||
|
host = host.copy(
|
||||||
|
tunnelings = listOf(
|
||||||
|
Tunneling(
|
||||||
|
type = TunnelingType.Local,
|
||||||
|
sourceHost = SshdSocketAddress.LOCALHOST_NAME,
|
||||||
|
destinationHost = SshdSocketAddress.LOCALHOST_NAME,
|
||||||
|
destinationPort = host.port,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val sshClient = SshClients.openClient(host).apply { sshClient = this }
|
||||||
|
val sshSession = SshClients.openSession(host, sshClient).apply { sshSession = this }
|
||||||
|
|
||||||
|
// 打开通道
|
||||||
|
for (tunneling in host.tunnelings) {
|
||||||
|
val address = SshClients.openTunneling(sshSession, host, tunneling)
|
||||||
|
host = host.copy(host = address.hostName, port = address.port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (useJumpHosts) {
|
||||||
|
// 打开通道后忽略 key 检查
|
||||||
|
commands.add("-o")
|
||||||
|
commands.add("StrictHostKeyChecking=no")
|
||||||
|
|
||||||
|
// 不保存 known_hosts
|
||||||
|
commands.add("-o")
|
||||||
|
commands.add("UserKnownHostsFile=${if (SystemInfo.isWindows) "NUL" else "/dev/null"}")
|
||||||
|
} else {
|
||||||
|
// known_hosts
|
||||||
|
commands.add("-o")
|
||||||
|
commands.add("UserKnownHostsFile=${File(Application.getBaseDataDir(), "known_hosts").absolutePath}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compression
|
||||||
|
commands.add("-o")
|
||||||
|
commands.add("Compression=yes")
|
||||||
|
|
||||||
|
// HostKeyAlgorithms 让 SFTP 命令的顺序和 sshd 的一致 这样可以避免 known_hosts 文件不一致问题
|
||||||
|
val hostKeyAlgorithms = ClientBuilder.setUpDefaultSignatureFactories(true).joinToString(",") { it.name }
|
||||||
|
commands.add("-o")
|
||||||
|
commands.add("HostKeyAlgorithms=${hostKeyAlgorithms}")
|
||||||
|
|
||||||
|
// 不使用配置文件
|
||||||
|
commands.add("-F")
|
||||||
|
commands.add("/dev/null")
|
||||||
|
|
||||||
|
// port
|
||||||
|
commands.add("-P")
|
||||||
|
commands.add(host.port.toString())
|
||||||
|
|
||||||
|
// 设置认证信息
|
||||||
|
setAuthentication(commands, host)
|
||||||
|
|
||||||
|
|
||||||
|
val envs = host.options.envs()
|
||||||
|
if (envs.containsKey("CurrentDir")) {
|
||||||
|
val currentDir = envs.getValue("CurrentDir")
|
||||||
|
commands.add("${host.username}@${host.host}:${currentDir}")
|
||||||
|
} else {
|
||||||
|
commands.add("${host.username}@${host.host}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val winSize = terminalPanel.winSize()
|
||||||
|
val ptyConnector = ptyConnectorFactory.createPtyConnector(
|
||||||
|
commands.toTypedArray(),
|
||||||
|
winSize.rows, winSize.cols,
|
||||||
|
host.options.envs(),
|
||||||
|
Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8),
|
||||||
|
)
|
||||||
|
|
||||||
|
return ptyConnector
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAuthentication(commands: MutableList<String>, host: Host) {
|
||||||
|
// 如果通过公钥连接
|
||||||
|
if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||||
|
val keyPair = keyManager.getOhKeyPair(host.authentication.password)
|
||||||
|
if (keyPair != null) {
|
||||||
|
val keyPair = OhKeyPairKeyPairProvider.generateKeyPair(keyPair)
|
||||||
|
val privateKeyPath = Application.createSubTemporaryDir()
|
||||||
|
val privateKeyFile = Files.createTempFile(privateKeyPath, Application.getName(), StringUtils.EMPTY)
|
||||||
|
Files.newOutputStream(privateKeyFile)
|
||||||
|
.use { OpenSSHKeyPairResourceWriter.INSTANCE.writePrivateKey(keyPair, null, null, it) }
|
||||||
|
commands.add("-i")
|
||||||
|
commands.add(privateKeyFile.toFile().absolutePath)
|
||||||
|
tempFiles.add(privateKeyPath)
|
||||||
|
}
|
||||||
|
} else if (host.authentication.type == AuthenticationType.Password) {
|
||||||
|
terminal.getTerminalModel().addDataListener(PasswordReporterDataListener(host).apply {
|
||||||
|
lastPasswordReporterDataListener = this
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
// 删除密码监听
|
||||||
|
lastPasswordReporterDataListener?.let { listener ->
|
||||||
|
SwingUtilities.invokeLater { terminal.getTerminalModel().removeDataListener(listener) }
|
||||||
|
}
|
||||||
|
|
||||||
|
IOUtils.closeQuietly(sshSession)
|
||||||
|
IOUtils.closeQuietly(sshClient)
|
||||||
|
|
||||||
|
tempFiles.removeIf {
|
||||||
|
FileUtils.deleteQuietly(it.toFile())
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
super.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(): Icon {
|
||||||
|
return Icons.fileFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class PasswordReporterDataListener(private val host: Host) : DataListener {
|
||||||
|
override fun onChanged(key: DataKey<*>, data: Any) {
|
||||||
|
if (key == VisualTerminal.Written && data is String) {
|
||||||
|
|
||||||
|
// 要求输入密码
|
||||||
|
val line = terminal.getDocument().getScreenLine(terminal.getCursorModel().getPosition().y)
|
||||||
|
if (line.getText().trim().trimIndent().startsWith("${host.username}@${host.host}'s password:")) {
|
||||||
|
|
||||||
|
// 删除密码监听
|
||||||
|
terminal.getTerminalModel().removeDataListener(this)
|
||||||
|
|
||||||
|
val ptyConnector = getPtyConnector()
|
||||||
|
|
||||||
|
// password
|
||||||
|
ptyConnector.write(host.authentication.password.toByteArray(ptyConnector.getCharset()))
|
||||||
|
|
||||||
|
// enter
|
||||||
|
ptyConnector.write(
|
||||||
|
terminal.getKeyEncoder().encode(TerminalKeyEvent(KeyEvent.VK_ENTER))
|
||||||
|
.toByteArray(ptyConnector.getCharset())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.DataProvider
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.transport.TransportDataProviders
|
import app.termora.transport.TransportDataProviders
|
||||||
import app.termora.transport.TransportPanel
|
import app.termora.transport.TransportPanel
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
@@ -8,7 +10,7 @@ import javax.swing.JComponent
|
|||||||
import javax.swing.JOptionPane
|
import javax.swing.JOptionPane
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
class SFTPTerminalTab : Disposable, TerminalTab {
|
class SFTPTerminalTab : Disposable, TerminalTab, DataProvider {
|
||||||
|
|
||||||
private val transportPanel by lazy {
|
private val transportPanel by lazy {
|
||||||
TransportPanel().apply {
|
TransportPanel().apply {
|
||||||
@@ -54,4 +56,12 @@ class SFTPTerminalTab : Disposable, TerminalTab {
|
|||||||
) == JOptionPane.OK_OPTION
|
) == JOptionPane.OK_OPTION
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
|
if (dataKey == TransportDataProviders.TransportPanel) {
|
||||||
|
return transportPanel as T
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.actions.TabReconnectAction
|
import app.termora.actions.TabReconnectAction
|
||||||
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
||||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
@@ -24,14 +26,15 @@ import org.apache.sshd.common.channel.ChannelListener
|
|||||||
import org.apache.sshd.common.session.Session
|
import org.apache.sshd.common.session.Session
|
||||||
import org.apache.sshd.common.session.SessionListener
|
import org.apache.sshd.common.session.SessionListener
|
||||||
import org.apache.sshd.common.session.SessionListener.Event
|
import org.apache.sshd.common.session.SessionListener.Event
|
||||||
import org.apache.sshd.common.util.net.SshdSocketAddress
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.util.*
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
|
||||||
class SSHTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
|
class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
||||||
|
PtyHostTerminalTab(windowScope, host) {
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
|
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
|
||||||
}
|
}
|
||||||
@@ -41,6 +44,9 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(
|
|||||||
private var sshClient: SshClient? = null
|
private var sshClient: SshClient? = null
|
||||||
private var sshSession: ClientSession? = null
|
private var sshSession: ClientSession? = null
|
||||||
private var sshChannelShell: ChannelShell? = null
|
private var sshChannelShell: ChannelShell? = null
|
||||||
|
private val terminalTabbedManager
|
||||||
|
get() = AnActionEvent(getJComponent(), StringUtils.EMPTY, EventObject(getJComponent()))
|
||||||
|
.getData(DataProviders.TerminalTabbedManager)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
terminalPanel.dropFiles = false
|
terminalPanel.dropFiles = false
|
||||||
@@ -80,9 +86,24 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(
|
|||||||
terminal.write("SSH client is opening...\r\n")
|
terminal.write("SSH client is opening...\r\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var host = this.host.copy(authentication = this.host.authentication.copy())
|
||||||
|
val owner = SwingUtilities.getWindowAncestor(terminalPanel)
|
||||||
val client = SshClients.openClient(host).also { sshClient = it }
|
val client = SshClients.openClient(host).also { sshClient = it }
|
||||||
|
client.serverKeyVerifier = DialogServerKeyVerifier(owner)
|
||||||
// keyboard interactive
|
// keyboard interactive
|
||||||
client.userInteraction = TerminalUserInteraction(SwingUtilities.getWindowAncestor(terminalPanel))
|
client.userInteraction = TerminalUserInteraction(owner)
|
||||||
|
|
||||||
|
if (host.authentication.type == AuthenticationType.No) {
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
val dialog = RequestAuthenticationDialog(owner)
|
||||||
|
val authentication = dialog.getAuthentication()
|
||||||
|
host = host.copy(authentication = authentication)
|
||||||
|
// save
|
||||||
|
if (dialog.isRemembered()) {
|
||||||
|
HostManager.getInstance().addHost(this@SSHTerminalTab.host.copy(authentication = authentication))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val sessionListener = MySessionListener()
|
val sessionListener = MySessionListener()
|
||||||
val channelListener = MyChannelListener()
|
val channelListener = MyChannelListener()
|
||||||
@@ -119,13 +140,25 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(
|
|||||||
override fun channelClosed(channel: Channel, reason: Throwable?) {
|
override fun channelClosed(channel: Channel, reason: Throwable?) {
|
||||||
coroutineScope.launch(Dispatchers.Swing) {
|
coroutineScope.launch(Dispatchers.Swing) {
|
||||||
terminal.write("\r\n\r\n${ControlCharacters.ESC}[31m")
|
terminal.write("\r\n\r\n${ControlCharacters.ESC}[31m")
|
||||||
terminal.write("Channel has been disconnected.")
|
terminal.write(I18n.getString("termora.terminal.channel-disconnected"))
|
||||||
if (reconnectShortcut is KeyShortcut) {
|
if (reconnectShortcut is KeyShortcut) {
|
||||||
terminal.write(" Type $reconnectShortcut to reconnect.")
|
terminal.write(
|
||||||
|
I18n.getString(
|
||||||
|
"termora.terminal.channel-reconnect",
|
||||||
|
reconnectShortcut.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
terminal.write("\r\n")
|
terminal.write("\r\n")
|
||||||
terminal.write("${ControlCharacters.ESC}[0m")
|
terminal.write("${ControlCharacters.ESC}[0m")
|
||||||
terminalModel.setData(DataKey.ShowCursor, false)
|
terminalModel.setData(DataKey.ShowCursor, false)
|
||||||
|
if (Database.getDatabase().terminal.autoCloseTabWhenDisconnected) {
|
||||||
|
terminalTabbedManager?.let { manager ->
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
manager.closeTerminalTab(this@SSHTerminalTab, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -159,28 +192,8 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (tunneling in host.tunnelings) {
|
for (tunneling in host.tunnelings) {
|
||||||
if (tunneling.type == TunnelingType.Local) {
|
|
||||||
session.startLocalPortForwarding(
|
|
||||||
SshdSocketAddress(tunneling.sourceHost, tunneling.sourcePort),
|
|
||||||
SshdSocketAddress(tunneling.destinationHost, tunneling.destinationPort)
|
|
||||||
)
|
|
||||||
} else if (tunneling.type == TunnelingType.Remote) {
|
|
||||||
session.startRemotePortForwarding(
|
|
||||||
SshdSocketAddress(tunneling.sourceHost, tunneling.sourcePort),
|
|
||||||
SshdSocketAddress(tunneling.destinationHost, tunneling.destinationPort),
|
|
||||||
)
|
|
||||||
} else if (tunneling.type == TunnelingType.Dynamic) {
|
|
||||||
session.startDynamicPortForwarding(
|
|
||||||
SshdSocketAddress(
|
|
||||||
tunneling.sourceHost,
|
|
||||||
tunneling.sourcePort
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log.isInfoEnabled) {
|
SshClients.openTunneling(session, host, tunneling)
|
||||||
log.info("SSH [{}] started {} port forwarding.", host.name, tunneling.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
terminal.write("Start [${tunneling.name}] port forwarding successfully.\r\n")
|
terminal.write("Start [${tunneling.name}] port forwarding successfully.\r\n")
|
||||||
|
|||||||
61
src/main/kotlin/app/termora/SerialPortPtyConnector.kt
Normal file
61
src/main/kotlin/app/termora/SerialPortPtyConnector.kt
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.terminal.PtyConnector
|
||||||
|
import com.fazecast.jSerialComm.SerialPort
|
||||||
|
import com.fazecast.jSerialComm.SerialPortDataListener
|
||||||
|
import com.fazecast.jSerialComm.SerialPortEvent
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class SerialPortPtyConnector(
|
||||||
|
private val serialPort: SerialPort,
|
||||||
|
private val charset: Charset = Charsets.UTF_8
|
||||||
|
) : PtyConnector, SerialPortDataListener {
|
||||||
|
|
||||||
|
private val queue = LinkedBlockingQueue<Char>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
serialPort.addDataListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(buffer: CharArray): Int {
|
||||||
|
buffer[0] = queue.poll(1, TimeUnit.SECONDS) ?: return 0
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(buffer: ByteArray, offset: Int, len: Int) {
|
||||||
|
serialPort.writeBytes(buffer, len, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resize(rows: Int, cols: Int) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun waitFor(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
queue.clear()
|
||||||
|
serialPort.closePort()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getListeningEvents(): Int {
|
||||||
|
return SerialPort.LISTENING_EVENT_DATA_RECEIVED
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialEvent(event: SerialPortEvent) {
|
||||||
|
if (event.eventType == SerialPort.LISTENING_EVENT_DATA_RECEIVED) {
|
||||||
|
val data = event.receivedData
|
||||||
|
if (data.isEmpty()) return
|
||||||
|
for (c in String(data, charset).toCharArray()) {
|
||||||
|
queue.add(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCharset(): Charset {
|
||||||
|
return charset
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/main/kotlin/app/termora/SerialTerminalTab.kt
Normal file
21
src/main/kotlin/app/termora/SerialTerminalTab.kt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.terminal.PtyConnector
|
||||||
|
import org.apache.commons.io.Charsets
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import javax.swing.Icon
|
||||||
|
|
||||||
|
class SerialTerminalTab(windowScope: WindowScope, host: Host) :
|
||||||
|
PtyHostTerminalTab(windowScope, host) {
|
||||||
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
|
val serialPort = Serials.openPort(host)
|
||||||
|
return SerialPortPtyConnector(
|
||||||
|
serialPort,
|
||||||
|
Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(): Icon {
|
||||||
|
return Icons.plugin
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/main/kotlin/app/termora/Serials.kt
Normal file
38
src/main/kotlin/app/termora/Serials.kt
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.fazecast.jSerialComm.SerialPort
|
||||||
|
|
||||||
|
object Serials {
|
||||||
|
fun openPort(host: Host): SerialPort {
|
||||||
|
val serialComm = host.options.serialComm
|
||||||
|
val serialPort = SerialPort.getCommPort(serialComm.port)
|
||||||
|
serialPort.setBaudRate(serialComm.baudRate)
|
||||||
|
serialPort.setNumDataBits(serialComm.dataBits)
|
||||||
|
|
||||||
|
when (serialComm.parity) {
|
||||||
|
SerialCommParity.None -> serialPort.setParity(SerialPort.NO_PARITY)
|
||||||
|
SerialCommParity.Mark -> serialPort.setParity(SerialPort.MARK_PARITY)
|
||||||
|
SerialCommParity.Even -> serialPort.setParity(SerialPort.EVEN_PARITY)
|
||||||
|
SerialCommParity.Odd -> serialPort.setParity(SerialPort.ODD_PARITY)
|
||||||
|
SerialCommParity.Space -> serialPort.setParity(SerialPort.SPACE_PARITY)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (serialComm.stopBits) {
|
||||||
|
"1" -> serialPort.setNumStopBits(SerialPort.ONE_STOP_BIT)
|
||||||
|
"1.5" -> serialPort.setNumStopBits(SerialPort.ONE_POINT_FIVE_STOP_BITS)
|
||||||
|
"2" -> serialPort.setNumStopBits(SerialPort.TWO_STOP_BITS)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (serialComm.flowControl) {
|
||||||
|
SerialCommFlowControl.None -> serialPort.setFlowControl(SerialPort.FLOW_CONTROL_DISABLED)
|
||||||
|
SerialCommFlowControl.RTS_CTS -> serialPort.setFlowControl(SerialPort.FLOW_CONTROL_RTS_ENABLED or SerialPort.FLOW_CONTROL_CTS_ENABLED)
|
||||||
|
SerialCommFlowControl.XON_XOFF -> serialPort.setFlowControl(SerialPort.FLOW_CONTROL_XONXOFF_IN_ENABLED or SerialPort.FLOW_CONTROL_XONXOFF_OUT_ENABLED)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serialPort.openPort()) {
|
||||||
|
throw IllegalStateException("Open serial port [${serialComm.port}] failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return serialPort
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,14 @@ import app.termora.AES.encodeBase64String
|
|||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.highlight.KeywordHighlight
|
||||||
import app.termora.highlight.KeywordHighlightManager
|
import app.termora.highlight.KeywordHighlightManager
|
||||||
|
import app.termora.keymap.Keymap
|
||||||
|
import app.termora.keymap.KeymapManager
|
||||||
import app.termora.keymap.KeymapPanel
|
import app.termora.keymap.KeymapPanel
|
||||||
import app.termora.keymgr.KeyManager
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.keymgr.OhKeyPair
|
||||||
|
import app.termora.macro.Macro
|
||||||
import app.termora.macro.MacroManager
|
import app.termora.macro.MacroManager
|
||||||
import app.termora.native.FileChooser
|
import app.termora.native.FileChooser
|
||||||
import app.termora.sync.SyncConfig
|
import app.termora.sync.SyncConfig
|
||||||
@@ -15,10 +20,13 @@ import app.termora.sync.SyncType
|
|||||||
import app.termora.sync.SyncerProvider
|
import app.termora.sync.SyncerProvider
|
||||||
import app.termora.terminal.CursorStyle
|
import app.termora.terminal.CursorStyle
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
|
import app.termora.terminal.panel.FloatingToolbarPanel
|
||||||
import app.termora.terminal.panel.TerminalPanel
|
import app.termora.terminal.panel.TerminalPanel
|
||||||
import cash.z.ecc.android.bip39.Mnemonics
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
import com.formdev.flatlaf.extras.components.*
|
import com.formdev.flatlaf.extras.components.*
|
||||||
|
import com.formdev.flatlaf.util.FontUtils
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
@@ -27,17 +35,19 @@ import com.sun.jna.LastErrorException
|
|||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.*
|
||||||
import kotlinx.serialization.json.encodeToJsonElement
|
import org.apache.commons.codec.binary.Base64
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import org.apache.commons.lang3.time.DateFormatUtils
|
import org.apache.commons.lang3.time.DateFormatUtils
|
||||||
import org.jdesktop.swingx.JXEditorPane
|
import org.jdesktop.swingx.JXEditorPane
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Toolkit
|
||||||
import java.awt.datatransfer.StringSelection
|
import java.awt.datatransfer.StringSelection
|
||||||
import java.awt.event.ItemEvent
|
import java.awt.event.ItemEvent
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -54,6 +64,11 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||||||
class SettingsOptionsPane : OptionsPane() {
|
class SettingsOptionsPane : OptionsPane() {
|
||||||
private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane)
|
private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane)
|
||||||
private val database get() = Database.getDatabase()
|
private val database get() = Database.getDatabase()
|
||||||
|
private val hostManager get() = HostManager.getInstance()
|
||||||
|
private val keymapManager get() = KeymapManager.getInstance()
|
||||||
|
private val macroManager get() = MacroManager.getInstance()
|
||||||
|
private val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
||||||
|
private val keyManager get() = KeyManager.getInstance()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java)
|
private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java)
|
||||||
@@ -97,6 +112,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
addOption(AppearanceOption())
|
addOption(AppearanceOption())
|
||||||
addOption(TerminalOption())
|
addOption(TerminalOption())
|
||||||
addOption(KeyShortcutsOption())
|
addOption(KeyShortcutsOption())
|
||||||
|
addOption(SFTPOption())
|
||||||
addOption(CloudSyncOption())
|
addOption(CloudSyncOption())
|
||||||
addOption(DoormanOption())
|
addOption(DoormanOption())
|
||||||
addOption(AboutOption())
|
addOption(AboutOption())
|
||||||
@@ -288,12 +304,15 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private inner class TerminalOption : JPanel(BorderLayout()), Option {
|
private inner class TerminalOption : JPanel(BorderLayout()), Option {
|
||||||
private val cursorStyleComboBox = FlatComboBox<CursorStyle>()
|
private val cursorStyleComboBox = FlatComboBox<CursorStyle>()
|
||||||
private val debugComboBox = YesOrNoComboBox()
|
private val debugComboBox = YesOrNoComboBox()
|
||||||
|
private val beepComboBox = YesOrNoComboBox()
|
||||||
private val fontComboBox = FlatComboBox<String>()
|
private val fontComboBox = FlatComboBox<String>()
|
||||||
private val shellComboBox = FlatComboBox<String>()
|
private val shellComboBox = FlatComboBox<String>()
|
||||||
private val maxRowsTextField = IntSpinner(0, 0)
|
private val maxRowsTextField = IntSpinner(0, 0)
|
||||||
private val fontSizeTextField = IntSpinner(0, 9, 99)
|
private val fontSizeTextField = IntSpinner(0, 9, 99)
|
||||||
private val terminalSetting get() = Database.getDatabase().terminal
|
private val terminalSetting get() = Database.getDatabase().terminal
|
||||||
private val selectCopyComboBox = YesOrNoComboBox()
|
private val selectCopyComboBox = YesOrNoComboBox()
|
||||||
|
private val autoCloseTabComboBox = YesOrNoComboBox()
|
||||||
|
private val floatingToolbarComboBox = YesOrNoComboBox()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -309,6 +328,26 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
autoCloseTabComboBox.addItemListener { e ->
|
||||||
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
|
terminalSetting.autoCloseTabWhenDisconnected = autoCloseTabComboBox.selectedItem as Boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
autoCloseTabComboBox.toolTipText = I18n.getString("termora.settings.terminal.auto-close-tab-description")
|
||||||
|
|
||||||
|
floatingToolbarComboBox.addItemListener { e ->
|
||||||
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
|
terminalSetting.floatingToolbar = floatingToolbarComboBox.selectedItem as Boolean
|
||||||
|
TerminalPanelFactory.getAllTerminalPanel().forEach { tp ->
|
||||||
|
if (terminalSetting.floatingToolbar && FloatingToolbarPanel.isPined) {
|
||||||
|
tp.getData(FloatingToolbarPanel.FloatingToolbar)?.triggerShow()
|
||||||
|
} else {
|
||||||
|
tp.getData(FloatingToolbarPanel.FloatingToolbar)?.triggerHide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
selectCopyComboBox.addItemListener { e ->
|
selectCopyComboBox.addItemListener { e ->
|
||||||
if (e.stateChange == ItemEvent.SELECTED) {
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
terminalSetting.selectCopy = selectCopyComboBox.selectedItem as Boolean
|
terminalSetting.selectCopy = selectCopyComboBox.selectedItem as Boolean
|
||||||
@@ -345,6 +384,13 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
beepComboBox.addItemListener { e ->
|
||||||
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
|
terminalSetting.beep = beepComboBox.selectedItem as Boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
shellComboBox.addItemListener {
|
shellComboBox.addItemListener {
|
||||||
if (it.stateChange == ItemEvent.SELECTED) {
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
terminalSetting.localShell = shellComboBox.selectedItem as String
|
terminalSetting.localShell = shellComboBox.selectedItem as String
|
||||||
@@ -379,6 +425,33 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fontComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
init {
|
||||||
|
preferredSize = Dimension(preferredSize.width, fontComboBox.preferredSize.height - 2)
|
||||||
|
maximumSize = Dimension(preferredSize.width, preferredSize.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
if (value is String) {
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
"<html><font face='$value'>$value</font></html>",
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fontComboBox.maximumSize = fontComboBox.preferredSize
|
||||||
|
|
||||||
cursorStyleComboBox.addItem(CursorStyle.Block)
|
cursorStyleComboBox.addItem(CursorStyle.Block)
|
||||||
cursorStyleComboBox.addItem(CursorStyle.Bar)
|
cursorStyleComboBox.addItem(CursorStyle.Bar)
|
||||||
cursorStyleComboBox.addItem(CursorStyle.Underline)
|
cursorStyleComboBox.addItem(CursorStyle.Underline)
|
||||||
@@ -391,13 +464,24 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
shellComboBox.selectedItem = terminalSetting.localShell
|
shellComboBox.selectedItem = terminalSetting.localShell
|
||||||
|
|
||||||
fontComboBox.addItem("JetBrains Mono")
|
val fonts = linkedSetOf<String>("JetBrains Mono", "Source Code Pro", "Monospaced")
|
||||||
fontComboBox.addItem("Source Code Pro")
|
FontUtils.getAllFonts().forEach {
|
||||||
|
if (!fonts.contains(it.family)) {
|
||||||
|
fonts.addLast(it.family)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (font in fonts) {
|
||||||
|
fontComboBox.addItem(font)
|
||||||
|
}
|
||||||
|
|
||||||
fontComboBox.selectedItem = terminalSetting.font
|
fontComboBox.selectedItem = terminalSetting.font
|
||||||
debugComboBox.selectedItem = terminalSetting.debug
|
debugComboBox.selectedItem = terminalSetting.debug
|
||||||
|
beepComboBox.selectedItem = terminalSetting.beep
|
||||||
cursorStyleComboBox.selectedItem = terminalSetting.cursor
|
cursorStyleComboBox.selectedItem = terminalSetting.cursor
|
||||||
selectCopyComboBox.selectedItem = terminalSetting.selectCopy
|
selectCopyComboBox.selectedItem = terminalSetting.selectCopy
|
||||||
|
autoCloseTabComboBox.selectedItem = terminalSetting.autoCloseTabWhenDisconnected
|
||||||
|
floatingToolbarComboBox.selectedItem = terminalSetting.floatingToolbar
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(isSelected: Boolean): Icon {
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
@@ -415,9 +499,14 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private fun getCenterComponent(): JComponent {
|
private fun getCenterComponent(): JComponent {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow",
|
"left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow",
|
||||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val beepBtn = JButton(Icons.run)
|
||||||
|
beepBtn.isFocusable = false
|
||||||
|
beepBtn.putClientProperty(FlatClientProperties.BUTTON_TYPE, FlatClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON)
|
||||||
|
beepBtn.addActionListener { Toolkit.getDefaultToolkit().beep() }
|
||||||
|
|
||||||
var rows = 1
|
var rows = 1
|
||||||
val step = 2
|
val step = 2
|
||||||
val panel = FormBuilder.create().layout(layout)
|
val panel = FormBuilder.create().layout(layout)
|
||||||
@@ -430,10 +519,17 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
.add(maxRowsTextField).xy(3, rows).apply { rows += step }
|
.add(maxRowsTextField).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.debug")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.debug")}:").xy(1, rows)
|
||||||
.add(debugComboBox).xy(3, rows).apply { rows += step }
|
.add(debugComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.settings.terminal.beep")}:").xy(1, rows)
|
||||||
|
.add(beepComboBox).xy(3, rows)
|
||||||
|
.add(beepBtn).xy(5, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.select-copy")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.select-copy")}:").xy(1, rows)
|
||||||
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
|
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
|
||||||
.add(cursorStyleComboBox).xy(3, rows).apply { rows += step }
|
.add(cursorStyleComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.settings.terminal.floating-toolbar")}:").xy(1, rows)
|
||||||
|
.add(floatingToolbarComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.settings.terminal.auto-close-tab")}:").xy(1, rows)
|
||||||
|
.add(autoCloseTabComboBox).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.local-shell")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.local-shell")}:").xy(1, rows)
|
||||||
.add(shellComboBox).xyw(3, rows, 5)
|
.add(shellComboBox).xyw(3, rows, 5)
|
||||||
.build()
|
.build()
|
||||||
@@ -451,6 +547,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
val domainTextField = OutlineTextField(255)
|
val domainTextField = OutlineTextField(255)
|
||||||
val uploadConfigButton = JButton(I18n.getString("termora.settings.sync.push"), Icons.upload)
|
val uploadConfigButton = JButton(I18n.getString("termora.settings.sync.push"), Icons.upload)
|
||||||
val exportConfigButton = JButton(I18n.getString("termora.settings.sync.export"), Icons.export)
|
val exportConfigButton = JButton(I18n.getString("termora.settings.sync.export"), Icons.export)
|
||||||
|
val importConfigButton = JButton(I18n.getString("termora.settings.sync.import"), Icons.import)
|
||||||
val downloadConfigButton = JButton(I18n.getString("termora.settings.sync.pull"), Icons.download)
|
val downloadConfigButton = JButton(I18n.getString("termora.settings.sync.pull"), Icons.download)
|
||||||
val lastSyncTimeLabel = JLabel()
|
val lastSyncTimeLabel = JLabel()
|
||||||
val sync get() = database.sync
|
val sync get() = database.sync
|
||||||
@@ -492,12 +589,6 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeComboBox.selectedItem == SyncType.Gitee) {
|
|
||||||
gistTextField.trailingComponent = null
|
|
||||||
} else {
|
|
||||||
gistTextField.trailingComponent = visitGistBtn
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAll()
|
removeAll()
|
||||||
add(getCenterComponent(), BorderLayout.CENTER)
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
revalidate()
|
revalidate()
|
||||||
@@ -562,6 +653,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exportConfigButton.addActionListener { export() }
|
exportConfigButton.addActionListener { export() }
|
||||||
|
importConfigButton.addActionListener { import() }
|
||||||
|
|
||||||
keysCheckBox.addActionListener { refreshButtons() }
|
keysCheckBox.addActionListener { refreshButtons() }
|
||||||
hostsCheckBox.addActionListener { refreshButtons() }
|
hostsCheckBox.addActionListener { refreshButtons() }
|
||||||
@@ -578,24 +670,228 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|| keywordHighlightsCheckBox.isSelected
|
|| keywordHighlightsCheckBox.isSelected
|
||||||
uploadConfigButton.isEnabled = downloadConfigButton.isEnabled
|
uploadConfigButton.isEnabled = downloadConfigButton.isEnabled
|
||||||
exportConfigButton.isEnabled = downloadConfigButton.isEnabled
|
exportConfigButton.isEnabled = downloadConfigButton.isEnabled
|
||||||
|
importConfigButton.isEnabled = downloadConfigButton.isEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun export() {
|
private fun export() {
|
||||||
|
|
||||||
|
assertEventDispatchThread()
|
||||||
|
|
||||||
|
val passwordField = OutlinePasswordField()
|
||||||
|
val panel = object : JPanel(BorderLayout()) {
|
||||||
|
override fun requestFocusInWindow(): Boolean {
|
||||||
|
return passwordField.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val label = JLabel(I18n.getString("termora.settings.sync.export-encrypt") + StringUtils.SPACE.repeat(25))
|
||||||
|
label.border = BorderFactory.createEmptyBorder(0, 0, 8, 0)
|
||||||
|
panel.add(label, BorderLayout.NORTH)
|
||||||
|
panel.add(passwordField, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
var password = StringUtils.EMPTY
|
||||||
|
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
owner,
|
||||||
|
panel,
|
||||||
|
optionType = JOptionPane.YES_NO_OPTION,
|
||||||
|
initialValue = passwordField
|
||||||
|
) == JOptionPane.YES_OPTION
|
||||||
|
) {
|
||||||
|
password = String(passwordField.password).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val fileChooser = FileChooser()
|
val fileChooser = FileChooser()
|
||||||
fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
||||||
fileChooser.win32Filters.add(Pair("All Files", listOf("*")))
|
fileChooser.win32Filters.add(Pair("All Files", listOf("*")))
|
||||||
fileChooser.win32Filters.add(Pair("JSON files", listOf("json")))
|
fileChooser.win32Filters.add(Pair("JSON files", listOf("json")))
|
||||||
fileChooser.showSaveDialog(owner, "${Application.getName()}.json").thenAccept { file ->
|
fileChooser.showSaveDialog(owner, "${Application.getName()}.json").thenAccept { file ->
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
SwingUtilities.invokeLater { exportText(file) }
|
SwingUtilities.invokeLater { exportText(file, password) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportText(file: File) {
|
private fun import() {
|
||||||
|
val fileChooser = FileChooser()
|
||||||
|
fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
||||||
|
fileChooser.osxAllowedFileTypes = listOf("json")
|
||||||
|
fileChooser.win32Filters.add(Pair("JSON files", listOf("json")))
|
||||||
|
fileChooser.showOpenDialog(owner).thenAccept { files ->
|
||||||
|
if (files.isNotEmpty()) {
|
||||||
|
SwingUtilities.invokeLater { importFromFile(files.first()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DuplicatedCode")
|
||||||
|
private fun importFromFile(file: File) {
|
||||||
|
if (!file.exists()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val ranges = getSyncConfig().ranges
|
||||||
|
if (ranges.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最大 100MB
|
||||||
|
if (file.length() >= 1024 * 1024 * 100) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, I18n.getString("termora.settings.sync.import.file-too-large"),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val text = file.readText()
|
||||||
|
val jsonResult = ohMyJson.runCatching { decodeFromString<JsonObject>(text) }
|
||||||
|
if (jsonResult.isFailure) {
|
||||||
|
val e = jsonResult.exceptionOrNull() ?: return
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, ExceptionUtils.getRootCauseMessage(e),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = jsonResult.getOrNull() ?: return
|
||||||
|
|
||||||
|
// 如果加密了 则解密数据
|
||||||
|
if (json["encryption"]?.jsonPrimitive?.booleanOrNull == true) {
|
||||||
|
val data = json["data"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
|
||||||
|
if (data.isBlank()) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, "Data file corruption",
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val passwordField = OutlinePasswordField()
|
||||||
|
val panel = object : JPanel(BorderLayout()) {
|
||||||
|
override fun requestFocusInWindow(): Boolean {
|
||||||
|
return passwordField.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val label = JLabel("Please enter the password" + StringUtils.SPACE.repeat(25))
|
||||||
|
label.border = BorderFactory.createEmptyBorder(0, 0, 8, 0)
|
||||||
|
panel.add(label, BorderLayout.NORTH)
|
||||||
|
panel.add(passwordField, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
owner,
|
||||||
|
panel,
|
||||||
|
optionType = JOptionPane.YES_NO_OPTION,
|
||||||
|
initialValue = passwordField
|
||||||
|
) != JOptionPane.YES_OPTION
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passwordField.password.isEmpty()) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, I18n.getString("termora.doorman.unlock-data"),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val password = String(passwordField.password)
|
||||||
|
val key = PBKDF2.generateSecret(
|
||||||
|
password.toCharArray(),
|
||||||
|
password.toByteArray(), keyLength = 128
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val dataText = AES.ECB.decrypt(key, Base64.decodeBase64(data)).toString(Charsets.UTF_8)
|
||||||
|
val dataJsonResult = ohMyJson.runCatching { decodeFromString<JsonObject>(dataText) }
|
||||||
|
if (dataJsonResult.isFailure) {
|
||||||
|
val e = dataJsonResult.exceptionOrNull() ?: return
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, ExceptionUtils.getRootCauseMessage(e),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
json = dataJsonResult.getOrNull() ?: return
|
||||||
|
break
|
||||||
|
} catch (_: Exception) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, I18n.getString("termora.doorman.password-wrong"),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ranges.contains(SyncRange.Hosts)) {
|
||||||
|
val hosts = json["hosts"]
|
||||||
|
if (hosts is JsonArray) {
|
||||||
|
ohMyJson.runCatching { decodeFromJsonElement<List<Host>>(hosts.jsonArray) }.onSuccess {
|
||||||
|
for (host in it) {
|
||||||
|
hostManager.addHost(host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ranges.contains(SyncRange.KeyPairs)) {
|
||||||
|
val keyPairs = json["keyPairs"]
|
||||||
|
if (keyPairs is JsonArray) {
|
||||||
|
ohMyJson.runCatching { decodeFromJsonElement<List<OhKeyPair>>(keyPairs.jsonArray) }.onSuccess {
|
||||||
|
for (keyPair in it) {
|
||||||
|
keyManager.addOhKeyPair(keyPair)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ranges.contains(SyncRange.KeywordHighlights)) {
|
||||||
|
val keywordHighlights = json["keywordHighlights"]
|
||||||
|
if (keywordHighlights is JsonArray) {
|
||||||
|
ohMyJson.runCatching { decodeFromJsonElement<List<KeywordHighlight>>(keywordHighlights.jsonArray) }
|
||||||
|
.onSuccess {
|
||||||
|
for (keyPair in it) {
|
||||||
|
keywordHighlightManager.addKeywordHighlight(keyPair)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ranges.contains(SyncRange.Macros)) {
|
||||||
|
val macros = json["macros"]
|
||||||
|
if (macros is JsonArray) {
|
||||||
|
ohMyJson.runCatching { decodeFromJsonElement<List<Macro>>(macros.jsonArray) }.onSuccess {
|
||||||
|
for (macro in it) {
|
||||||
|
macroManager.addMacro(macro)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ranges.contains(SyncRange.Keymap)) {
|
||||||
|
val keymaps = json["keymaps"]
|
||||||
|
if (keymaps is JsonArray) {
|
||||||
|
for (keymap in keymaps.jsonArray.mapNotNull { Keymap.fromJSON(it.jsonObject) }) {
|
||||||
|
keymapManager.addKeymap(keymap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, I18n.getString("termora.settings.sync.import.successful"),
|
||||||
|
messageType = JOptionPane.INFORMATION_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun exportText(file: File, password: String) {
|
||||||
val syncConfig = getSyncConfig()
|
val syncConfig = getSyncConfig()
|
||||||
val text = ohMyJson.encodeToString(buildJsonObject {
|
var text = ohMyJson.encodeToString(buildJsonObject {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
put("exporter", SystemUtils.USER_NAME)
|
put("exporter", SystemUtils.USER_NAME)
|
||||||
put("version", Application.getVersion())
|
put("version", Application.getVersion())
|
||||||
@@ -603,21 +899,29 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
put("os", SystemUtils.OS_NAME)
|
put("os", SystemUtils.OS_NAME)
|
||||||
put("exportDateHuman", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(Date(now)))
|
put("exportDateHuman", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(Date(now)))
|
||||||
if (syncConfig.ranges.contains(SyncRange.Hosts)) {
|
if (syncConfig.ranges.contains(SyncRange.Hosts)) {
|
||||||
put("hosts", ohMyJson.encodeToJsonElement(HostManager.getInstance().hosts()))
|
put("hosts", ohMyJson.encodeToJsonElement(hostManager.hosts()))
|
||||||
}
|
}
|
||||||
if (syncConfig.ranges.contains(SyncRange.KeyPairs)) {
|
if (syncConfig.ranges.contains(SyncRange.KeyPairs)) {
|
||||||
put("keyPairs", ohMyJson.encodeToJsonElement(KeyManager.getInstance().getOhKeyPairs()))
|
put("keyPairs", ohMyJson.encodeToJsonElement(keyManager.getOhKeyPairs()))
|
||||||
}
|
}
|
||||||
if (syncConfig.ranges.contains(SyncRange.KeywordHighlights)) {
|
if (syncConfig.ranges.contains(SyncRange.KeywordHighlights)) {
|
||||||
put(
|
put(
|
||||||
"keywordHighlights",
|
"keywordHighlights",
|
||||||
ohMyJson.encodeToJsonElement(KeywordHighlightManager.getInstance().getKeywordHighlights())
|
ohMyJson.encodeToJsonElement(keywordHighlightManager.getKeywordHighlights())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (syncConfig.ranges.contains(SyncRange.Macros)) {
|
if (syncConfig.ranges.contains(SyncRange.Macros)) {
|
||||||
put(
|
put(
|
||||||
"macros",
|
"macros",
|
||||||
ohMyJson.encodeToJsonElement(MacroManager.getInstance().getMacros())
|
ohMyJson.encodeToJsonElement(macroManager.getMacros())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (syncConfig.ranges.contains(SyncRange.Keymap)) {
|
||||||
|
val keymaps = keymapManager.getKeymaps().filter { !it.isReadonly }
|
||||||
|
.map { it.toJSONObject() }
|
||||||
|
put(
|
||||||
|
"keymaps",
|
||||||
|
ohMyJson.encodeToJsonElement(keymaps)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
put("settings", buildJsonObject {
|
put("settings", buildJsonObject {
|
||||||
@@ -626,6 +930,19 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
put("terminal", ohMyJson.encodeToJsonElement(database.terminal.getProperties()))
|
put("terminal", ohMyJson.encodeToJsonElement(database.terminal.getProperties()))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (password.isNotBlank()) {
|
||||||
|
val key = PBKDF2.generateSecret(
|
||||||
|
password.toCharArray(),
|
||||||
|
password.toByteArray(), keyLength = 128
|
||||||
|
)
|
||||||
|
|
||||||
|
text = ohMyJson.encodeToString(buildJsonObject {
|
||||||
|
put("encryption", true)
|
||||||
|
put("data", AES.ECB.encrypt(key, text.toByteArray(Charsets.UTF_8)).encodeBase64String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
file.outputStream().use {
|
file.outputStream().use {
|
||||||
IOUtils.write(text, it, StandardCharsets.UTF_8)
|
IOUtils.write(text, it, StandardCharsets.UTF_8)
|
||||||
OptionPane.openFileInFolder(
|
OptionPane.openFileInFolder(
|
||||||
@@ -662,6 +979,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DuplicatedCode")
|
||||||
private suspend fun pushOrPull(push: Boolean) {
|
private suspend fun pushOrPull(push: Boolean) {
|
||||||
|
|
||||||
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
||||||
@@ -717,6 +1035,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
exportConfigButton.isEnabled = false
|
exportConfigButton.isEnabled = false
|
||||||
|
importConfigButton.isEnabled = false
|
||||||
downloadConfigButton.isEnabled = false
|
downloadConfigButton.isEnabled = false
|
||||||
uploadConfigButton.isEnabled = false
|
uploadConfigButton.isEnabled = false
|
||||||
typeComboBox.isEnabled = false
|
typeComboBox.isEnabled = false
|
||||||
@@ -752,6 +1071,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
downloadConfigButton.isEnabled = true
|
downloadConfigButton.isEnabled = true
|
||||||
exportConfigButton.isEnabled = true
|
exportConfigButton.isEnabled = true
|
||||||
|
importConfigButton.isEnabled = true
|
||||||
uploadConfigButton.isEnabled = true
|
uploadConfigButton.isEnabled = true
|
||||||
keysCheckBox.isEnabled = true
|
keysCheckBox.isEnabled = true
|
||||||
hostsCheckBox.isEnabled = true
|
hostsCheckBox.isEnabled = true
|
||||||
@@ -813,6 +1133,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
typeComboBox.addItem(SyncType.GitHub)
|
typeComboBox.addItem(SyncType.GitHub)
|
||||||
typeComboBox.addItem(SyncType.GitLab)
|
typeComboBox.addItem(SyncType.GitLab)
|
||||||
typeComboBox.addItem(SyncType.Gitee)
|
typeComboBox.addItem(SyncType.Gitee)
|
||||||
|
typeComboBox.addItem(SyncType.WebDAV)
|
||||||
|
|
||||||
hostsCheckBox.isFocusable = false
|
hostsCheckBox.isFocusable = false
|
||||||
keysCheckBox.isFocusable = false
|
keysCheckBox.isFocusable = false
|
||||||
@@ -831,7 +1152,31 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
tokenTextField.text = sync.token
|
tokenTextField.text = sync.token
|
||||||
domainTextField.trailingComponent = JButton(Icons.externalLink).apply {
|
domainTextField.trailingComponent = JButton(Icons.externalLink).apply {
|
||||||
addActionListener {
|
addActionListener {
|
||||||
Application.browse(URI.create("https://docs.gitlab.com/ee/api/snippets.html"))
|
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
||||||
|
Application.browse(URI.create("https://docs.gitlab.com/ee/api/snippets.html"))
|
||||||
|
|
||||||
|
} else if (typeComboBox.selectedItem == SyncType.WebDAV) {
|
||||||
|
val url = domainTextField.text
|
||||||
|
if (url.isNullOrBlank()) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
I18n.getString("termora.settings.sync.webdav.help")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val uri = URI.create(url)
|
||||||
|
val sb = StringBuilder()
|
||||||
|
sb.append(uri.scheme).append("://")
|
||||||
|
if (tokenTextField.password.isNotEmpty() && gistTextField.text.isNotBlank()) {
|
||||||
|
sb.append(String(tokenTextField.password)).append(":").append(gistTextField.text)
|
||||||
|
sb.append('@')
|
||||||
|
}
|
||||||
|
sb.append(uri.authority).append(uri.path)
|
||||||
|
if (!uri.query.isNullOrBlank()) {
|
||||||
|
sb.append('?').append(uri.query)
|
||||||
|
}
|
||||||
|
Application.browse(URI.create(sb.toString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -841,12 +1186,15 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
tokenTextField.trailingComponent = if (tokenTextField.password.isEmpty()) getTokenBtn else null
|
tokenTextField.trailingComponent = if (tokenTextField.password.isEmpty()) getTokenBtn else null
|
||||||
|
|
||||||
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
if (domainTextField.text.isBlank()) {
|
||||||
if (domainTextField.text.isBlank()) {
|
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
||||||
domainTextField.text = StringUtils.defaultIfBlank(sync.domain, "https://gitlab.com/api")
|
domainTextField.text = StringUtils.defaultIfBlank(sync.domain, "https://gitlab.com/api")
|
||||||
|
} else if (typeComboBox.selectedItem == SyncType.WebDAV) {
|
||||||
|
domainTextField.text = sync.domain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val lastSyncTime = sync.lastSyncTime
|
val lastSyncTime = sync.lastSyncTime
|
||||||
lastSyncTimeLabel.text = "${I18n.getString("termora.settings.sync.last-sync-time")}: ${
|
lastSyncTimeLabel.text = "${I18n.getString("termora.settings.sync.last-sync-time")}: ${
|
||||||
if (lastSyncTime > 0) DateFormatUtils.format(
|
if (lastSyncTime > 0) DateFormatUtils.format(
|
||||||
@@ -892,29 +1240,50 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
var rows = 1
|
var rows = 1
|
||||||
val step = 2
|
val step = 2
|
||||||
val builder = FormBuilder.create().layout(layout).debug(false);
|
val builder = FormBuilder.create().layout(layout).debug(false)
|
||||||
val box = Box.createHorizontalBox()
|
val box = Box.createHorizontalBox()
|
||||||
box.add(typeComboBox)
|
box.add(typeComboBox)
|
||||||
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
if (typeComboBox.selectedItem == SyncType.GitLab || typeComboBox.selectedItem == SyncType.WebDAV) {
|
||||||
box.add(Box.createHorizontalStrut(4))
|
box.add(Box.createHorizontalStrut(4))
|
||||||
box.add(domainTextField)
|
box.add(domainTextField)
|
||||||
}
|
}
|
||||||
builder.add("${I18n.getString("termora.settings.sync.type")}:").xy(1, rows)
|
builder.add("${I18n.getString("termora.settings.sync.type")}:").xy(1, rows)
|
||||||
.add(box).xy(3, rows).apply { rows += step }
|
.add(box).xy(3, rows).apply { rows += step }
|
||||||
|
|
||||||
builder.add("${I18n.getString("termora.settings.sync.token")}:").xy(1, rows)
|
val isWebDAV = typeComboBox.selectedItem == SyncType.WebDAV
|
||||||
.add(tokenTextField).xy(3, rows).apply { rows += step }
|
|
||||||
.add("${I18n.getString("termora.settings.sync.gist")}:").xy(1, rows)
|
val tokenText = if (isWebDAV) {
|
||||||
.add(gistTextField).xy(3, rows).apply { rows += step }
|
I18n.getString("termora.new-host.general.username")
|
||||||
|
} else {
|
||||||
|
I18n.getString("termora.settings.sync.token")
|
||||||
|
}
|
||||||
|
|
||||||
|
val gistText = if (isWebDAV) {
|
||||||
|
I18n.getString("termora.new-host.general.password")
|
||||||
|
} else {
|
||||||
|
I18n.getString("termora.settings.sync.gist")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeComboBox.selectedItem == SyncType.Gitee || isWebDAV) {
|
||||||
|
gistTextField.trailingComponent = null
|
||||||
|
} else {
|
||||||
|
gistTextField.trailingComponent = visitGistBtn
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.add("${tokenText}:").xy(1, rows)
|
||||||
|
.add(if (isWebDAV) gistTextField else tokenTextField).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${gistText}:").xy(1, rows)
|
||||||
|
.add(if (isWebDAV) tokenTextField else gistTextField).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.sync.range")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.sync.range")}:").xy(1, rows)
|
||||||
.add(rangeBox).xy(3, rows).apply { rows += step }
|
.add(rangeBox).xy(3, rows).apply { rows += step }
|
||||||
// Sync buttons
|
// Sync buttons
|
||||||
.add(
|
.add(
|
||||||
FormBuilder.create()
|
FormBuilder.create()
|
||||||
.layout(FormLayout("left:pref, $formMargin, left:pref, $formMargin, left:pref", "pref"))
|
.layout(FormLayout("pref, 2dlu, pref, 2dlu, pref, 2dlu, pref", "pref"))
|
||||||
.add(uploadConfigButton).xy(1, 1)
|
.add(uploadConfigButton).xy(1, 1)
|
||||||
.add(downloadConfigButton).xy(3, 1)
|
.add(downloadConfigButton).xy(3, 1)
|
||||||
.add(exportConfigButton).xy(5, 1)
|
.add(exportConfigButton).xy(5, 1)
|
||||||
|
.add(importConfigButton).xy(7, 1)
|
||||||
.build()
|
.build()
|
||||||
).xy(3, rows, "center, fill").apply { rows += step }
|
).xy(3, rows, "center, fill").apply { rows += step }
|
||||||
.add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step }
|
.add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step }
|
||||||
@@ -925,6 +1294,63 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||||
|
|
||||||
|
val editCommandField = OutlineTextField(255)
|
||||||
|
private val sftp get() = database.sftp
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
editCommandField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
|
sftp.editCommand = editCommandField.text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
editCommandField.placeholderText = "notepad {0}"
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
editCommandField.placeholderText = "open -a TextEdit {0}"
|
||||||
|
}
|
||||||
|
|
||||||
|
editCommandField.text = sftp.editCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.folder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return "SFTP"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $formMargin, default:grow, 30dlu",
|
||||||
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
val builder = FormBuilder.create().layout(layout).debug(false)
|
||||||
|
builder.add("${I18n.getString("termora.settings.sftp.edit-command")}:").xy(1, 1)
|
||||||
|
builder.add(editCommandField).xy(3, 1)
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private inner class AboutOption : JPanel(BorderLayout()), Option {
|
private inner class AboutOption : JPanel(BorderLayout()), Option {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -1009,8 +1435,6 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private val tip = FlatLabel()
|
private val tip = FlatLabel()
|
||||||
private val safeBtn = FlatButton()
|
private val safeBtn = FlatButton()
|
||||||
private val doorman get() = Doorman.getInstance()
|
private val doorman get() = Doorman.getInstance()
|
||||||
private val hostManager get() = HostManager.getInstance()
|
|
||||||
private val keyManager get() = KeyManager.getInstance()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
|
|||||||
@@ -2,26 +2,45 @@ package app.termora
|
|||||||
|
|
||||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||||
import app.termora.terminal.TerminalSize
|
import app.termora.terminal.TerminalSize
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.sshd.client.ClientBuilder
|
import org.apache.sshd.client.ClientBuilder
|
||||||
import org.apache.sshd.client.SshClient
|
import org.apache.sshd.client.SshClient
|
||||||
import org.apache.sshd.client.channel.ChannelShell
|
import org.apache.sshd.client.channel.ChannelShell
|
||||||
|
import org.apache.sshd.client.config.hosts.HostConfigEntry
|
||||||
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver
|
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver
|
||||||
|
import org.apache.sshd.client.config.hosts.KnownHostEntry
|
||||||
|
import org.apache.sshd.client.kex.DHGClient
|
||||||
|
import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier
|
||||||
|
import org.apache.sshd.client.keyverifier.ModifiedServerKeyAcceptor
|
||||||
|
import org.apache.sshd.client.keyverifier.ServerKeyVerifier
|
||||||
import org.apache.sshd.client.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
import org.apache.sshd.common.SshException
|
import org.apache.sshd.common.SshException
|
||||||
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
||||||
|
import org.apache.sshd.common.config.keys.KeyUtils
|
||||||
import org.apache.sshd.common.global.KeepAliveHandler
|
import org.apache.sshd.common.global.KeepAliveHandler
|
||||||
|
import org.apache.sshd.common.kex.BuiltinDHFactories
|
||||||
|
import org.apache.sshd.common.keyprovider.KeyIdentityProvider
|
||||||
import org.apache.sshd.common.util.net.SshdSocketAddress
|
import org.apache.sshd.common.util.net.SshdSocketAddress
|
||||||
import org.apache.sshd.core.CoreModuleProperties
|
import org.apache.sshd.core.CoreModuleProperties
|
||||||
import org.apache.sshd.server.forward.AcceptAllForwardingFilter
|
import org.apache.sshd.server.forward.AcceptAllForwardingFilter
|
||||||
import org.apache.sshd.server.forward.RejectAllForwardingFilter
|
import org.apache.sshd.server.forward.RejectAllForwardingFilter
|
||||||
|
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient
|
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient
|
||||||
import org.eclipse.jgit.transport.CredentialsProvider
|
import org.eclipse.jgit.transport.CredentialsProvider
|
||||||
import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
|
import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
|
||||||
import org.eclipse.jgit.transport.sshd.ProxyData
|
import org.eclipse.jgit.transport.sshd.ProxyData
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Window
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Proxy
|
import java.net.Proxy
|
||||||
|
import java.net.SocketAddress
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.security.PublicKey
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
object SshClients {
|
object SshClients {
|
||||||
@@ -86,7 +105,7 @@ object SshClients {
|
|||||||
val sessions = mutableListOf<ClientSession>()
|
val sessions = mutableListOf<ClientSession>()
|
||||||
for (i in 0 until jumpHosts.size) {
|
for (i in 0 until jumpHosts.size) {
|
||||||
val currentHost = jumpHosts[i]
|
val currentHost = jumpHosts[i]
|
||||||
sessions.add(doOpenSession(currentHost, client))
|
sessions.add(doOpenSession(currentHost, client, i != 0))
|
||||||
|
|
||||||
// 如果有下一跳
|
// 如果有下一跳
|
||||||
if (i < jumpHosts.size - 1) {
|
if (i < jumpHosts.size - 1) {
|
||||||
@@ -107,8 +126,27 @@ object SshClients {
|
|||||||
return sessions.last()
|
return sessions.last()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doOpenSession(host: Host, client: SshClient): ClientSession {
|
fun isMiddleware(session: ClientSession): Boolean {
|
||||||
val session = client.connect(host.username, host.host, host.port)
|
if (session is JGitClientSession) {
|
||||||
|
if (session.hostConfigEntry.properties["Middleware"]?.toBoolean() == true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param middleware 如果为 true 表示是跳板
|
||||||
|
*/
|
||||||
|
private fun doOpenSession(host: Host, client: SshClient, middleware: Boolean = false): ClientSession {
|
||||||
|
val entry = HostConfigEntry()
|
||||||
|
entry.port = host.port
|
||||||
|
entry.username = host.username
|
||||||
|
entry.hostName = host.host
|
||||||
|
entry.setProperty("Middleware", middleware.toString())
|
||||||
|
|
||||||
|
val session = client.connect(entry)
|
||||||
.verify(timeout).session
|
.verify(timeout).session
|
||||||
if (host.authentication.type == AuthenticationType.Password) {
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
session.addPasswordIdentity(host.authentication.password)
|
session.addPasswordIdentity(host.authentication.password)
|
||||||
@@ -124,6 +162,41 @@ object SshClients {
|
|||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openTunneling(session: ClientSession, host: Host, tunneling: Tunneling): SshdSocketAddress {
|
||||||
|
|
||||||
|
val sshdSocketAddress = if (tunneling.type == TunnelingType.Local) {
|
||||||
|
session.startLocalPortForwarding(
|
||||||
|
SshdSocketAddress(tunneling.sourceHost, tunneling.sourcePort),
|
||||||
|
SshdSocketAddress(tunneling.destinationHost, tunneling.destinationPort)
|
||||||
|
)
|
||||||
|
} else if (tunneling.type == TunnelingType.Remote) {
|
||||||
|
session.startRemotePortForwarding(
|
||||||
|
SshdSocketAddress(tunneling.sourceHost, tunneling.sourcePort),
|
||||||
|
SshdSocketAddress(tunneling.destinationHost, tunneling.destinationPort),
|
||||||
|
)
|
||||||
|
} else if (tunneling.type == TunnelingType.Dynamic) {
|
||||||
|
session.startDynamicPortForwarding(
|
||||||
|
SshdSocketAddress(
|
||||||
|
tunneling.sourceHost,
|
||||||
|
tunneling.sourcePort
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SshdSocketAddress.LOCALHOST_ADDRESS
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info(
|
||||||
|
"SSH [{}] started {} port forwarding. host: {} , port: {}",
|
||||||
|
host.name,
|
||||||
|
tunneling.name,
|
||||||
|
sshdSocketAddress.hostName,
|
||||||
|
sshdSocketAddress.port
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sshdSocketAddress
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开一个客户端
|
* 打开一个客户端
|
||||||
@@ -133,6 +206,18 @@ object SshClients {
|
|||||||
builder.globalRequestHandlers(listOf(KeepAliveHandler.INSTANCE))
|
builder.globalRequestHandlers(listOf(KeepAliveHandler.INSTANCE))
|
||||||
.factory { JGitSshClient() }
|
.factory { JGitSshClient() }
|
||||||
|
|
||||||
|
val keyExchangeFactories = ClientBuilder.setUpDefaultKeyExchanges(true).toMutableList()
|
||||||
|
|
||||||
|
// https://github.com/TermoraDev/termora/issues/123
|
||||||
|
keyExchangeFactories.addAll(
|
||||||
|
listOf(
|
||||||
|
DHGClient.newFactory(BuiltinDHFactories.dhg1),
|
||||||
|
DHGClient.newFactory(BuiltinDHFactories.dhg14),
|
||||||
|
DHGClient.newFactory(BuiltinDHFactories.dhgex),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
builder.keyExchangeFactories(keyExchangeFactories)
|
||||||
|
|
||||||
if (host.tunnelings.isEmpty() && host.options.jumpHosts.isEmpty()) {
|
if (host.tunnelings.isEmpty() && host.options.jumpHosts.isEmpty()) {
|
||||||
builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE)
|
builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE)
|
||||||
} else {
|
} else {
|
||||||
@@ -142,8 +227,15 @@ object SshClients {
|
|||||||
builder.hostConfigEntryResolver(HostConfigEntryResolver.EMPTY)
|
builder.hostConfigEntryResolver(HostConfigEntryResolver.EMPTY)
|
||||||
|
|
||||||
val sshClient = builder.build() as JGitSshClient
|
val sshClient = builder.build() as JGitSshClient
|
||||||
|
|
||||||
|
// https://github.com/TermoraDev/termora/issues/180
|
||||||
|
// JGit 会尝试读取本地的私钥或缓存的私钥
|
||||||
|
sshClient.keyIdentityProvider = KeyIdentityProvider { mutableListOf() }
|
||||||
|
|
||||||
val heartbeatInterval = max(host.options.heartbeatInterval, 3)
|
val heartbeatInterval = max(host.options.heartbeatInterval, 3)
|
||||||
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
|
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
|
||||||
|
CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true)
|
||||||
|
|
||||||
sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) }
|
sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) }
|
||||||
|
|
||||||
if (host.proxy.type != ProxyType.No) {
|
if (host.proxy.type != ProxyType.No) {
|
||||||
@@ -169,4 +261,94 @@ object SshClients {
|
|||||||
sshClient.start()
|
sshClient.start()
|
||||||
return sshClient
|
return sshClient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class MyDialogServerKeyVerifier(private val owner: Window) : ServerKeyVerifier, ModifiedServerKeyAcceptor {
|
||||||
|
override fun verifyServerKey(
|
||||||
|
clientSession: ClientSession,
|
||||||
|
remoteAddress: SocketAddress,
|
||||||
|
serverKey: PublicKey
|
||||||
|
): Boolean {
|
||||||
|
|
||||||
|
if (SshClients.isMiddleware(clientSession)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = AtomicBoolean(false)
|
||||||
|
|
||||||
|
SwingUtilities.invokeAndWait {
|
||||||
|
result.set(
|
||||||
|
OptionPane.showConfirmDialog(
|
||||||
|
parentComponent = owner,
|
||||||
|
message = I18n.getString(
|
||||||
|
"termora.host.verify-server-key",
|
||||||
|
remoteAddress.toString().replace("/", StringUtils.EMPTY),
|
||||||
|
KeyUtils.getKeyType(serverKey),
|
||||||
|
KeyUtils.getFingerPrint(serverKey)
|
||||||
|
),
|
||||||
|
optionType = JOptionPane.OK_CANCEL_OPTION,
|
||||||
|
messageType = JOptionPane.WARNING_MESSAGE,
|
||||||
|
) == JOptionPane.OK_OPTION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun acceptModifiedServerKey(
|
||||||
|
clientSession: ClientSession?,
|
||||||
|
remoteAddress: SocketAddress?,
|
||||||
|
entry: KnownHostEntry?,
|
||||||
|
expected: PublicKey?,
|
||||||
|
actual: PublicKey?
|
||||||
|
): Boolean {
|
||||||
|
val result = AtomicBoolean(false)
|
||||||
|
|
||||||
|
SwingUtilities.invokeAndWait {
|
||||||
|
result.set(
|
||||||
|
OptionPane.showConfirmDialog(
|
||||||
|
parentComponent = owner,
|
||||||
|
message = I18n.getString(
|
||||||
|
"termora.host.modified-server-key",
|
||||||
|
remoteAddress.toString().replace("/", StringUtils.EMPTY),
|
||||||
|
KeyUtils.getKeyType(expected),
|
||||||
|
KeyUtils.getFingerPrint(expected),
|
||||||
|
KeyUtils.getKeyType(actual),
|
||||||
|
KeyUtils.getFingerPrint(actual),
|
||||||
|
),
|
||||||
|
optionType = JOptionPane.OK_CANCEL_OPTION,
|
||||||
|
messageType = JOptionPane.WARNING_MESSAGE,
|
||||||
|
) == JOptionPane.OK_OPTION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DialogServerKeyVerifier(
|
||||||
|
owner: Window,
|
||||||
|
) : KnownHostsServerKeyVerifier(
|
||||||
|
MyDialogServerKeyVerifier(owner),
|
||||||
|
Paths.get(Application.getBaseDataDir().absolutePath, "known_hosts")
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
modifiedServerKeyAcceptor = delegateVerifier as ModifiedServerKeyAcceptor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateKnownHostsFile(
|
||||||
|
clientSession: ClientSession?,
|
||||||
|
remoteAddress: SocketAddress?,
|
||||||
|
serverKey: PublicKey?,
|
||||||
|
file: Path?,
|
||||||
|
knownHosts: Collection<HostEntryPair?>?
|
||||||
|
): KnownHostEntry? {
|
||||||
|
if (clientSession is JGitClientSession) {
|
||||||
|
if (SshClients.isMiddleware(clientSession)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.updateKnownHostsFile(clientSession, remoteAddress, serverKey, file, knownHosts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ class TerminalFactory private constructor() : Disposable {
|
|||||||
|
|
||||||
// terminal logger listener
|
// terminal logger listener
|
||||||
terminal.getTerminalModel().addDataListener(TerminalLoggerDataListener(terminal))
|
terminal.getTerminalModel().addDataListener(TerminalLoggerDataListener(terminal))
|
||||||
|
terminal.addTerminalListener(object : TerminalListener {
|
||||||
|
override fun onClose(terminal: Terminal) {
|
||||||
|
terminals.remove(terminal)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
terminals.add(terminal)
|
terminals.add(terminal)
|
||||||
return terminal
|
return terminal
|
||||||
@@ -51,6 +56,11 @@ class TerminalFactory private constructor() : Disposable {
|
|||||||
return colorPalette
|
return colorPalette
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun bell() {
|
||||||
|
if (config.beep) {
|
||||||
|
super.bell()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : Any> getData(key: DataKey<T>): T {
|
override fun <T : Any> getData(key: DataKey<T>): T {
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ class TerminalPanelFactory {
|
|||||||
fun getInstance(scope: Scope): TerminalPanelFactory {
|
fun getInstance(scope: Scope): TerminalPanelFactory {
|
||||||
return scope.getOrCreate(TerminalPanelFactory::class) { TerminalPanelFactory() }
|
return scope.getOrCreate(TerminalPanelFactory::class) { TerminalPanelFactory() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAllTerminalPanel(): List<TerminalPanel> {
|
||||||
|
return ApplicationScope.forApplicationScope().windowScopes()
|
||||||
|
.map { getInstance(it) }
|
||||||
|
.flatMap { it.getTerminalPanels() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
||||||
@@ -23,6 +29,11 @@ class TerminalPanelFactory {
|
|||||||
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
||||||
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
||||||
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
||||||
|
Disposer.register(terminalPanel, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
terminalPanels.remove(terminalPanel)
|
||||||
|
}
|
||||||
|
})
|
||||||
terminalPanels.add(terminalPanel)
|
terminalPanels.add(terminalPanel)
|
||||||
return terminalPanel
|
return terminalPanel
|
||||||
}
|
}
|
||||||
@@ -47,4 +58,8 @@ class TerminalPanelFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeTerminalPanel(terminalPanel: TerminalPanel) {
|
||||||
|
terminalPanels.remove(terminalPanel)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,12 +10,14 @@ import app.termora.transport.TransportPanel
|
|||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
import java.awt.event.AWTEventListener
|
import java.awt.event.AWTEventListener
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
|
import java.util.*
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -30,7 +32,7 @@ class TerminalTabbed(
|
|||||||
private val toolbar = termoraToolBar.getJToolBar()
|
private val toolbar = termoraToolBar.getJToolBar()
|
||||||
private val actionManager = ActionManager.getInstance()
|
private val actionManager = ActionManager.getInstance()
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
|
private val titleProperty = UUID.randomUUID().toSimpleString()
|
||||||
private val iconListener = PropertyChangeListener { e ->
|
private val iconListener = PropertyChangeListener { e ->
|
||||||
val source = e.source
|
val source = e.source
|
||||||
if (e.propertyName == "icon" && source is TerminalTab) {
|
if (e.propertyName == "icon" && source is TerminalTab) {
|
||||||
@@ -52,9 +54,6 @@ class TerminalTabbed(
|
|||||||
tabbedPane.isTabsClosable = true
|
tabbedPane.isTabsClosable = true
|
||||||
tabbedPane.tabType = FlatTabbedPane.TabType.card
|
tabbedPane.tabType = FlatTabbedPane.TabType.card
|
||||||
|
|
||||||
tabbedPane.styleMap = mapOf(
|
|
||||||
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground")
|
|
||||||
)
|
|
||||||
tabbedPane.trailingComponent = toolbar
|
tabbedPane.trailingComponent = toolbar
|
||||||
|
|
||||||
add(tabbedPane, BorderLayout.CENTER)
|
add(tabbedPane, BorderLayout.CENTER)
|
||||||
@@ -190,16 +189,16 @@ class TerminalTabbed(
|
|||||||
// 修改名称
|
// 修改名称
|
||||||
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
|
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
|
||||||
rename.addActionListener {
|
rename.addActionListener {
|
||||||
val index = tabbedPane.selectedIndex
|
if (tabIndex > 0) {
|
||||||
if (index > 0) {
|
|
||||||
val dialog = InputDialog(
|
val dialog = InputDialog(
|
||||||
SwingUtilities.getWindowAncestor(this),
|
SwingUtilities.getWindowAncestor(this),
|
||||||
title = rename.text,
|
title = rename.text,
|
||||||
text = tabbedPane.getTitleAt(index),
|
text = tabbedPane.getTitleAt(tabIndex),
|
||||||
)
|
)
|
||||||
val text = dialog.getText()
|
val text = dialog.getText()
|
||||||
if (!text.isNullOrBlank()) {
|
if (!text.isNullOrBlank()) {
|
||||||
tabbedPane.setTitleAt(index, text)
|
tabbedPane.setTitleAt(tabIndex, text)
|
||||||
|
c.putClientProperty(titleProperty, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +235,17 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (tab is HostTerminalTab) {
|
||||||
|
val openHostAction = actionManager.getAction(OpenHostAction.OPEN_HOST)
|
||||||
|
if (openHostAction != null) {
|
||||||
|
if (tab.host.protocol == Protocol.SSH || tab.host.protocol == Protocol.SFTPPty) {
|
||||||
|
popupMenu.addSeparator()
|
||||||
|
val sftpCommand = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
|
||||||
|
sftpCommand.addActionListener { openSFTPPtyTab(tab, openHostAction, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
|
|
||||||
// 关闭
|
// 关闭
|
||||||
@@ -276,9 +286,8 @@ class TerminalTabbed(
|
|||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
|
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
|
||||||
reconnect.addActionListener {
|
reconnect.addActionListener {
|
||||||
val index = tabbedPane.selectedIndex
|
if (tabIndex > 0) {
|
||||||
if (index > 0) {
|
tabs[tabIndex].reconnect()
|
||||||
tabs[index].reconnect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,21 +298,57 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun addTab(tab: TerminalTab) {
|
private fun addTab(index: Int, tab: TerminalTab) {
|
||||||
tabbedPane.addTab(
|
val c = tab.getJComponent()
|
||||||
tab.getTitle(),
|
val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
|
||||||
|
|
||||||
|
tabbedPane.insertTab(
|
||||||
|
title,
|
||||||
tab.getIcon(),
|
tab.getIcon(),
|
||||||
tab.getJComponent()
|
c,
|
||||||
|
StringUtils.EMPTY,
|
||||||
|
index
|
||||||
)
|
)
|
||||||
|
c.putClientProperty(titleProperty, title)
|
||||||
|
|
||||||
// 监听 icons 变化
|
// 监听 icons 变化
|
||||||
tab.addPropertyChangeListener(iconListener)
|
tab.addPropertyChangeListener(iconListener)
|
||||||
|
|
||||||
tabs.add(tab)
|
tabs.add(index, tab)
|
||||||
tabbedPane.selectedIndex = tabbedPane.tabCount - 1
|
tabbedPane.selectedIndex = index
|
||||||
Disposer.register(this, tab)
|
Disposer.register(this, tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openSFTPPtyTab(tab: HostTerminalTab, openHostAction: Action, evt: EventObject) {
|
||||||
|
if (!SFTPPtyTerminalTab.canSupports) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
SwingUtilities.getWindowAncestor(this),
|
||||||
|
I18n.getString("termora.tabbed.contextmenu.sftp-not-install"),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var host = tab.host
|
||||||
|
|
||||||
|
if (host.protocol == Protocol.SSH) {
|
||||||
|
val envs = tab.host.options.envs().toMutableMap()
|
||||||
|
val currentDir = tab.getData(DataProviders.Terminal)?.getTerminalModel()
|
||||||
|
?.getData(DataKey.CurrentDir, StringUtils.EMPTY) ?: StringUtils.EMPTY
|
||||||
|
|
||||||
|
if (currentDir.isNotBlank()) {
|
||||||
|
envs["CurrentDir"] = currentDir
|
||||||
|
}
|
||||||
|
|
||||||
|
host = host.copy(
|
||||||
|
protocol = Protocol.SFTPPty,
|
||||||
|
options = host.options.copy(env = envs.toPropertiesString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
openHostAction.actionPerformed(OpenHostActionEvent(this, host, evt))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对着 ToolBar 右键
|
* 对着 ToolBar 右键
|
||||||
*/
|
*/
|
||||||
@@ -393,7 +438,11 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun addTerminalTab(tab: TerminalTab) {
|
override fun addTerminalTab(tab: TerminalTab) {
|
||||||
addTab(tab)
|
addTab(tabs.size, tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addTerminalTab(index: Int, tab: TerminalTab) {
|
||||||
|
addTab(index, tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSelectedTerminalTab(): TerminalTab? {
|
override fun getSelectedTerminalTab(): TerminalTab? {
|
||||||
@@ -418,10 +467,10 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun closeTerminalTab(tab: TerminalTab) {
|
override fun closeTerminalTab(tab: TerminalTab, disposable: Boolean) {
|
||||||
for (i in 0 until tabs.size) {
|
for (i in 0 until tabs.size) {
|
||||||
if (tabs[i] == tab) {
|
if (tabs[i] == tab) {
|
||||||
removeTabAt(i, true)
|
removeTabAt(i, disposable)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package app.termora
|
|||||||
|
|
||||||
interface TerminalTabbedManager {
|
interface TerminalTabbedManager {
|
||||||
fun addTerminalTab(tab: TerminalTab)
|
fun addTerminalTab(tab: TerminalTab)
|
||||||
|
fun addTerminalTab(index: Int, tab: TerminalTab)
|
||||||
fun getSelectedTerminalTab(): TerminalTab?
|
fun getSelectedTerminalTab(): TerminalTab?
|
||||||
fun getTerminalTabs(): List<TerminalTab>
|
fun getTerminalTabs(): List<TerminalTab>
|
||||||
fun setSelectedTerminalTab(tab: TerminalTab)
|
fun setSelectedTerminalTab(tab: TerminalTab)
|
||||||
fun closeTerminalTab(tab: TerminalTab)
|
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true)
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
minimumSize = Dimension(640, 400)
|
minimumSize = Dimension(640, 400)
|
||||||
terminalTabbed.addTab(welcomePanel)
|
terminalTabbed.addTerminalTab(welcomePanel)
|
||||||
|
|
||||||
// macOS 要避开左边的控制栏
|
// macOS 要避开左边的控制栏
|
||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isMacOS) {
|
||||||
@@ -116,6 +116,7 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
Disposer.register(windowScope, terminalTabbed)
|
Disposer.register(windowScope, terminalTabbed)
|
||||||
add(terminalTabbed)
|
add(terminalTabbed)
|
||||||
|
|
||||||
|
dataProviderSupport.addData(DataProviders.TabbedPane, tabbedPane)
|
||||||
dataProviderSupport.addData(DataProviders.TermoraFrame, this)
|
dataProviderSupport.addData(DataProviders.TermoraFrame, this)
|
||||||
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
|
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import com.formdev.flatlaf.util.SystemInfo
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.event.WindowAdapter
|
import java.awt.event.WindowAdapter
|
||||||
import java.awt.event.WindowEvent
|
import java.awt.event.WindowEvent
|
||||||
import javax.swing.WindowConstants.DISPOSE_ON_CLOSE
|
import javax.swing.JOptionPane
|
||||||
|
import javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class TermoraFrameManager {
|
class TermoraFrameManager {
|
||||||
@@ -22,7 +23,7 @@ class TermoraFrameManager {
|
|||||||
val frame = TermoraFrame()
|
val frame = TermoraFrame()
|
||||||
registerCloseCallback(frame)
|
registerCloseCallback(frame)
|
||||||
frame.title = if (SystemInfo.isLinux) null else Application.getName()
|
frame.title = if (SystemInfo.isLinux) null else Application.getName()
|
||||||
frame.defaultCloseOperation = DISPOSE_ON_CLOSE
|
frame.defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
||||||
frame.setSize(1280, 800)
|
frame.setSize(1280, 800)
|
||||||
frame.setLocationRelativeTo(null)
|
frame.setLocationRelativeTo(null)
|
||||||
return frame
|
return frame
|
||||||
@@ -43,6 +44,21 @@ class TermoraFrameManager {
|
|||||||
this@TermoraFrameManager.dispose()
|
this@TermoraFrameManager.dispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun windowClosing(e: WindowEvent) {
|
||||||
|
if (ApplicationScope.windowScopes().size == 1) {
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
window,
|
||||||
|
I18n.getString("termora.quit-confirm", Application.getName()),
|
||||||
|
optionType = JOptionPane.YES_NO_OPTION,
|
||||||
|
) == JOptionPane.YES_OPTION
|
||||||
|
) {
|
||||||
|
window.dispose()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,10 @@ class TermoraToolBar(
|
|||||||
|
|
||||||
toolbar.add(Box.createHorizontalGlue())
|
toolbar.add(Box.createHorizontalGlue())
|
||||||
|
|
||||||
|
if (SystemInfo.isLinux || SystemInfo.isWindows) {
|
||||||
|
toolbar.add(Box.createHorizontalStrut(16))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// update btn
|
// update btn
|
||||||
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
|
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ class OutlinePasswordField(
|
|||||||
styleMap = mapOf(
|
styleMap = mapOf(
|
||||||
"showRevealButton" to true
|
"showRevealButton" to true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
putClientProperty("JPasswordField.cutCopyAllowed", true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import app.termora.Application.ohMyJson
|
|||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.time.DateFormatUtils
|
||||||
import org.commonmark.node.BulletList
|
import org.commonmark.node.BulletList
|
||||||
import org.commonmark.node.Heading
|
import org.commonmark.node.Heading
|
||||||
import org.commonmark.node.Paragraph
|
import org.commonmark.node.Paragraph
|
||||||
@@ -97,7 +98,14 @@ class UpdaterManager private constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val parser = Parser.builder().build()
|
val parser = Parser.builder().build()
|
||||||
val document = parser.parse("# ${name.trim()}\n${body.trim()}")
|
val document = parser.parse(
|
||||||
|
"# 🎉 ${name.trim()} (${
|
||||||
|
DateFormatUtils.format(
|
||||||
|
publishedDate,
|
||||||
|
"yyyy-MM-dd"
|
||||||
|
)
|
||||||
|
}) \n${body.trim()}"
|
||||||
|
)
|
||||||
val renderer = HtmlRenderer.builder()
|
val renderer = HtmlRenderer.builder()
|
||||||
.attributeProviderFactory {
|
.attributeProviderFactory {
|
||||||
AttributeProvider { node, _, attributes ->
|
AttributeProvider { node, _, attributes ->
|
||||||
@@ -106,7 +114,7 @@ class UpdaterManager private constructor() {
|
|||||||
attributes["style"] = "margin: 5px 0;"
|
attributes["style"] = "margin: 5px 0;"
|
||||||
} else if (node is BulletList) {
|
} else if (node is BulletList) {
|
||||||
attributes["style"] = "margin: 0 20px;"
|
attributes["style"] = "margin: 0 20px;"
|
||||||
}else if(node is Paragraph){
|
} else if (node is Paragraph) {
|
||||||
attributes["style"] = "margin: 0;"
|
attributes["style"] = "margin: 0;"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ object DataProviders {
|
|||||||
val Terminal = DataKey(app.termora.terminal.Terminal::class)
|
val Terminal = DataKey(app.termora.terminal.Terminal::class)
|
||||||
val PtyConnector = DataKey(app.termora.terminal.PtyConnector::class)
|
val PtyConnector = DataKey(app.termora.terminal.PtyConnector::class)
|
||||||
|
|
||||||
|
val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
|
||||||
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
|
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
|
||||||
val TerminalTab = DataKey(app.termora.TerminalTab::class)
|
val TerminalTab = DataKey(app.termora.TerminalTab::class)
|
||||||
val TerminalTabbedManager = DataKey(app.termora.TerminalTabbedManager::class)
|
val TerminalTabbedManager = DataKey(app.termora.TerminalTabbedManager::class)
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
import app.termora.LocalTerminalTab
|
import app.termora.*
|
||||||
import app.termora.OpenHostActionEvent
|
|
||||||
import app.termora.Protocol
|
|
||||||
import app.termora.SSHTerminalTab
|
|
||||||
|
|
||||||
class OpenHostAction : AnAction() {
|
class OpenHostAction : AnAction() {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -18,9 +15,19 @@ class OpenHostAction : AnAction() {
|
|||||||
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||||
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
|
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
|
||||||
|
|
||||||
val tab = if (evt.host.protocol == Protocol.SSH)
|
// 如果不支持 SFTP 那么不处理这个响应
|
||||||
SSHTerminalTab(windowScope, evt.host)
|
if (evt.host.protocol == Protocol.SFTPPty) {
|
||||||
else LocalTerminalTab(windowScope, evt.host)
|
if (!SFTPPtyTerminalTab.canSupports) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val tab = when (evt.host.protocol) {
|
||||||
|
Protocol.SSH -> SSHTerminalTab(windowScope, evt.host)
|
||||||
|
Protocol.Serial -> SerialTerminalTab(windowScope, evt.host)
|
||||||
|
Protocol.SFTPPty -> SFTPPtyTerminalTab(windowScope, evt.host)
|
||||||
|
else -> LocalTerminalTab(windowScope, evt.host)
|
||||||
|
}
|
||||||
|
|
||||||
terminalTabbedManager.addTerminalTab(tab)
|
terminalTabbedManager.addTerminalTab(tab)
|
||||||
tab.start()
|
tab.start()
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
|
||||||
class TerminalClearScreenAction : AnAction() {
|
class TerminalClearScreenAction : AnAction() {
|
||||||
companion object {
|
companion object {
|
||||||
const val CLEAR_SCREEN = "ClearScreen"
|
const val CLEAR_SCREEN = "ClearScreen"
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
putValue(SHORT_DESCRIPTION, "Clear Terminal Buffer")
|
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.clear-screen"))
|
||||||
putValue(ACTION_COMMAND_KEY, CLEAR_SCREEN)
|
putValue(ACTION_COMMAND_KEY, CLEAR_SCREEN)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class TerminalUserInteraction(
|
|||||||
prompt[i],
|
prompt[i],
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
dialog.setLocationRelativeTo(owner)
|
||||||
dialog.title = instruction ?: name ?: StringUtils.EMPTY
|
dialog.title = instruction ?: name ?: StringUtils.EMPTY
|
||||||
passwords[i] = dialog.getText()
|
passwords[i] = dialog.getText()
|
||||||
if (passwords[i].isBlank()) {
|
if (passwords[i].isBlank()) {
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
package app.termora.keymap
|
package app.termora.keymap
|
||||||
|
|
||||||
import app.termora.ApplicationScope
|
import app.termora.*
|
||||||
import app.termora.Database
|
|
||||||
import app.termora.DialogWrapper
|
|
||||||
import app.termora.Disposable
|
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.actions.DataProviders
|
|
||||||
import app.termora.findeverywhere.FindEverywhereAction
|
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Container
|
||||||
import java.awt.KeyEventDispatcher
|
import java.awt.KeyEventDispatcher
|
||||||
import java.awt.KeyEventPostProcessor
|
|
||||||
import java.awt.KeyboardFocusManager
|
import java.awt.KeyboardFocusManager
|
||||||
import java.awt.event.KeyEvent
|
import java.awt.event.KeyEvent
|
||||||
|
import javax.swing.JComponent
|
||||||
import javax.swing.JDialog
|
import javax.swing.JDialog
|
||||||
|
import javax.swing.JPopupMenu
|
||||||
import javax.swing.KeyStroke
|
import javax.swing.KeyStroke
|
||||||
|
|
||||||
class KeymapManager private constructor() : Disposable {
|
class KeymapManager private constructor() : Disposable {
|
||||||
@@ -23,24 +20,21 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(KeymapManager::class.java)
|
private val log = LoggerFactory.getLogger(KeymapManager::class.java)
|
||||||
|
|
||||||
const val PROCESS_GLOBAL_KEYMAP = "PROCESS_GLOBAL_KEYMAP"
|
|
||||||
|
|
||||||
fun getInstance(): KeymapManager {
|
fun getInstance(): KeymapManager {
|
||||||
return ApplicationScope.forApplicationScope()
|
return ApplicationScope.forApplicationScope()
|
||||||
.getOrCreate(KeymapManager::class) { KeymapManager() }
|
.getOrCreate(KeymapManager::class) { KeymapManager() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val myKeyEventPostProcessor = MyKeyEventPostProcessor()
|
private val keymapKeyEventDispatcher = KeymapKeyEventDispatcher()
|
||||||
private val myKeyEventDispatcher = MyKeyEventDispatcher()
|
|
||||||
private val database get() = Database.getDatabase()
|
private val database get() = Database.getDatabase()
|
||||||
|
private val properties get() = database.properties
|
||||||
private val keymaps = linkedMapOf<String, Keymap>()
|
private val keymaps = linkedMapOf<String, Keymap>()
|
||||||
private val activeKeymap get() = database.properties.getString("Keymap.Active")
|
private val activeKeymap get() = properties.getString("Keymap.Active")
|
||||||
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
|
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
keyboardFocusManager.addKeyEventPostProcessor(myKeyEventPostProcessor)
|
keyboardFocusManager.addKeyEventDispatcher(keymapKeyEventDispatcher)
|
||||||
keyboardFocusManager.addKeyEventDispatcher(myKeyEventDispatcher)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (keymap in database.getKeymaps()) {
|
for (keymap in database.getKeymaps()) {
|
||||||
@@ -97,73 +91,69 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
database.removeKeymap(name)
|
database.removeKeymap(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class MyKeyEventPostProcessor : KeyEventPostProcessor {
|
private inner class KeymapKeyEventDispatcher : KeyEventDispatcher {
|
||||||
override fun postProcessKeyEvent(e: KeyEvent): Boolean {
|
|
||||||
// 只处理 PRESSED 和 带有 modifiers 键的事件
|
|
||||||
if (!e.isConsumed && e.id == KeyEvent.KEY_PRESSED && e.modifiersEx != 0) {
|
|
||||||
val shortcuts = getActiveKeymap()
|
|
||||||
val actionIds = shortcuts.getActionIds(KeyShortcut(KeyStroke.getKeyStrokeForEvent(e)))
|
|
||||||
if (actionIds.isEmpty()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val focusedWindow = keyboardFocusManager.focusedWindow
|
|
||||||
if (focusedWindow is DialogWrapper) {
|
|
||||||
if (!focusedWindow.processGlobalKeymap) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else if (focusedWindow is JDialog) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val evt = AnActionEvent(e.source, StringUtils.EMPTY, e)
|
|
||||||
for (actionId in actionIds) {
|
|
||||||
val action = ActionManager.getInstance().getAction(actionId) ?: continue
|
|
||||||
if (!action.isEnabled) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
action.actionPerformed(evt)
|
|
||||||
if (evt.isConsumed) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class MyKeyEventDispatcher : KeyEventDispatcher {
|
|
||||||
// double shift
|
|
||||||
private var lastTime = -1L
|
|
||||||
|
|
||||||
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
|
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
|
||||||
if (e.keyCode == KeyEvent.VK_SHIFT && e.id == KeyEvent.KEY_PRESSED) {
|
if (e.isConsumed || e.id != KeyEvent.KEY_PRESSED || e.modifiersEx == 0) {
|
||||||
val owner = AnActionEvent(e.source, StringUtils.EMPTY, e).getData(DataProviders.TermoraFrame)
|
return false
|
||||||
?: return false
|
|
||||||
if (keyboardFocusManager.focusedWindow == owner) {
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
if (now - 250 < lastTime) {
|
|
||||||
app.termora.actions.ActionManager.getInstance()
|
|
||||||
.getAction(FindEverywhereAction.FIND_EVERYWHERE)
|
|
||||||
?.actionPerformed(AnActionEvent(e.source, StringUtils.EMPTY, e))
|
|
||||||
}
|
|
||||||
lastTime = now
|
|
||||||
}
|
|
||||||
} else if (e.keyCode != KeyEvent.VK_SHIFT) { // 如果不是 Shift 键,那么就阻断了连续性,重置时间
|
|
||||||
lastTime = -1
|
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
|
|
||||||
|
val keyStroke = KeyStroke.getKeyStrokeForEvent(e)
|
||||||
|
val component = e.source
|
||||||
|
|
||||||
|
if (component is JComponent) {
|
||||||
|
// 如果这个键已经被组件注册了,那么忽略
|
||||||
|
if (component.getConditionForKeyStroke(keyStroke) != JComponent.UNDEFINED_CONDITION) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val shortcuts = getActiveKeymap()
|
||||||
|
val actionIds = shortcuts.getActionIds(KeyShortcut(keyStroke))
|
||||||
|
if (actionIds.isEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val focusedWindow = keyboardFocusManager.focusedWindow
|
||||||
|
if (focusedWindow is DialogWrapper) {
|
||||||
|
if (!focusedWindow.processGlobalKeymap) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (focusedWindow is JDialog) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果当前有 Popup ,那么不派发事件
|
||||||
|
val c = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||||
|
if (c is Container) {
|
||||||
|
val popups: List<JPopupMenu> = SwingUtils.getDescendantsOfType(
|
||||||
|
JPopupMenu::class.java,
|
||||||
|
c, true
|
||||||
|
)
|
||||||
|
if (popups.isNotEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val evt = AnActionEvent(e.source, StringUtils.EMPTY, e)
|
||||||
|
for (actionId in actionIds) {
|
||||||
|
val action = ActionManager.getInstance().getAction(actionId) ?: continue
|
||||||
|
if (!action.isEnabled) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
action.actionPerformed(evt)
|
||||||
|
if (evt.isConsumed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
keyboardFocusManager.removeKeyEventPostProcessor(myKeyEventPostProcessor)
|
keyboardFocusManager.removeKeyEventDispatcher(keymapKeyEventDispatcher)
|
||||||
keyboardFocusManager.removeKeyEventDispatcher(myKeyEventDispatcher)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,7 @@ class KeymapTableModel : DefaultTableModel() {
|
|||||||
TerminalZoomOutAction.ZOOM_OUT,
|
TerminalZoomOutAction.ZOOM_OUT,
|
||||||
TerminalZoomResetAction.ZOOM_RESET,
|
TerminalZoomResetAction.ZOOM_RESET,
|
||||||
OpenLocalTerminalAction.LOCAL_TERMINAL,
|
OpenLocalTerminalAction.LOCAL_TERMINAL,
|
||||||
|
TerminalClearScreenAction.CLEAR_SCREEN,
|
||||||
FindEverywhereAction.FIND_EVERYWHERE,
|
FindEverywhereAction.FIND_EVERYWHERE,
|
||||||
NewWindowAction.NEW_WINDOW,
|
NewWindowAction.NEW_WINDOW,
|
||||||
TabReconnectAction.RECONNECT_TAB,
|
TabReconnectAction.RECONNECT_TAB,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class KeyManager private constructor() {
|
|||||||
if (keyPair == OhKeyPair.empty) {
|
if (keyPair == OhKeyPair.empty) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
keyPairs.remove(keyPair)
|
||||||
keyPairs.add(keyPair)
|
keyPairs.add(keyPair)
|
||||||
database.addKeyPair(keyPair)
|
database.addKeyPair(keyPair)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package app.termora.keymgr
|
|||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.AES.decodeBase64
|
import app.termora.AES.decodeBase64
|
||||||
|
import app.termora.actions.AnAction
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.native.FileChooser
|
import app.termora.native.FileChooser
|
||||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
import com.formdev.flatlaf.extras.components.FlatTable
|
import com.formdev.flatlaf.extras.components.FlatTable
|
||||||
@@ -48,6 +51,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
private val exportBtn = JButton(I18n.getString("termora.keymgr.export"))
|
private val exportBtn = JButton(I18n.getString("termora.keymgr.export"))
|
||||||
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
|
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
|
||||||
private val deleteBtn = JButton(I18n.getString("termora.remove"))
|
private val deleteBtn = JButton(I18n.getString("termora.remove"))
|
||||||
|
private val sshCopyIdBtn = JButton("ssh-copy-id")
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -59,6 +63,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
|
|
||||||
exportBtn.isEnabled = false
|
exportBtn.isEnabled = false
|
||||||
editBtn.isEnabled = false
|
editBtn.isEnabled = false
|
||||||
|
sshCopyIdBtn.isEnabled = false
|
||||||
deleteBtn.isEnabled = false
|
deleteBtn.isEnabled = false
|
||||||
|
|
||||||
keyPairTableModel.addColumn(I18n.getString("termora.keymgr.table.name"))
|
keyPairTableModel.addColumn(I18n.getString("termora.keymgr.table.name"))
|
||||||
@@ -75,7 +80,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
val formMargin = "4dlu"
|
val formMargin = "4dlu"
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"default:grow",
|
"default:grow",
|
||||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, 16dlu, pref"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rows = 1
|
var rows = 1
|
||||||
@@ -91,6 +96,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
.add(importBtn).xy(1, rows).apply { rows += step }
|
.add(importBtn).xy(1, rows).apply { rows += step }
|
||||||
.add(exportBtn).xy(1, rows).apply { rows += step }
|
.add(exportBtn).xy(1, rows).apply { rows += step }
|
||||||
.add(deleteBtn).xy(1, rows).apply { rows += step }
|
.add(deleteBtn).xy(1, rows).apply { rows += step }
|
||||||
|
.add(sshCopyIdBtn).xy(1, rows).apply { rows += step }
|
||||||
.build(), BorderLayout.EAST)
|
.build(), BorderLayout.EAST)
|
||||||
border = BorderFactory.createEmptyBorder(if (SystemInfo.isWindows || SystemInfo.isLinux) 6 else 0, 12, 12, 12)
|
border = BorderFactory.createEmptyBorder(if (SystemInfo.isWindows || SystemInfo.isLinux) 6 else 0, 12, 12, 12)
|
||||||
|
|
||||||
@@ -175,13 +181,48 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
sshCopyIdBtn.addActionListener(object : AnAction() {
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
sshCopyId(evt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
keyPairTable.selectionModel.addListSelectionListener {
|
keyPairTable.selectionModel.addListSelectionListener {
|
||||||
exportBtn.isEnabled = keyPairTable.selectedRowCount > 0
|
exportBtn.isEnabled = keyPairTable.selectedRowCount > 0
|
||||||
editBtn.isEnabled = exportBtn.isEnabled
|
editBtn.isEnabled = exportBtn.isEnabled
|
||||||
deleteBtn.isEnabled = exportBtn.isEnabled
|
deleteBtn.isEnabled = exportBtn.isEnabled
|
||||||
|
sshCopyIdBtn.isEnabled = exportBtn.isEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sshCopyId(evt: AnActionEvent) {
|
||||||
|
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
|
||||||
|
val keyPairs = keyPairTable.selectedRows.map { keyPairTableModel.getOhKeyPair(it) }
|
||||||
|
val publicKeys = mutableListOf<Pair<String, String>>()
|
||||||
|
for (keyPair in keyPairs) {
|
||||||
|
val publicKey = OhKeyPairKeyPairProvider.generateKeyPair(keyPair).public
|
||||||
|
val baos = ByteArrayOutputStream()
|
||||||
|
OpenSSHKeyPairResourceWriter.INSTANCE.writePublicKey(publicKey, keyPair.name, baos)
|
||||||
|
publicKeys.add(Pair(keyPair.name, baos.toString(Charsets.UTF_8)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publicKeys.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val owner = SwingUtilities.getWindowAncestor(this) ?: return
|
||||||
|
val hostTreeDialog = HostTreeDialog(owner) {
|
||||||
|
it.protocol == Protocol.SSH
|
||||||
|
}
|
||||||
|
hostTreeDialog.isVisible = true
|
||||||
|
val hosts = hostTreeDialog.hosts
|
||||||
|
if (hosts.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SSHCopyIdDialog(owner, windowScope, hosts, publicKeys).start()
|
||||||
|
}
|
||||||
|
|
||||||
private fun exportKeyPairs(file: File, keyPairs: List<OhKeyPair>) {
|
private fun exportKeyPairs(file: File, keyPairs: List<OhKeyPair>) {
|
||||||
file.outputStream().use { fis ->
|
file.outputStream().use { fis ->
|
||||||
val names = mutableMapOf<String, Int>()
|
val names = mutableMapOf<String, Int>()
|
||||||
|
|||||||
197
src/main/kotlin/app/termora/keymgr/SSHCopyIdDialog.kt
Normal file
197
src/main/kotlin/app/termora/keymgr/SSHCopyIdDialog.kt
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
package app.termora.keymgr
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
|
import app.termora.terminal.ControlCharacters
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
|
import app.termora.terminal.PtyConnectorDelegate
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.sshd.client.SshClient
|
||||||
|
import org.apache.sshd.client.channel.ClientChannelEvent
|
||||||
|
import org.apache.sshd.client.session.ClientSession
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.AbstractAction
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.UIManager
|
||||||
|
|
||||||
|
class SSHCopyIdDialog(
|
||||||
|
owner: Window,
|
||||||
|
private val windowScope: WindowScope,
|
||||||
|
private val hosts: List<Host>,
|
||||||
|
// key: name , value: public key
|
||||||
|
private val publicKeys: List<Pair<String, String>>,
|
||||||
|
) : DialogWrapper(owner) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(SSHCopyIdDialog::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val terminalPanelFactory = TerminalPanelFactory.getInstance(windowScope)
|
||||||
|
private val terminal by lazy {
|
||||||
|
TerminalFactory.getInstance(windowScope).createTerminal().apply {
|
||||||
|
getTerminalModel().setData(DataKey.ShowCursor, false)
|
||||||
|
getTerminalModel().setData(DataKey.AutoNewline, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val terminalPanel by lazy {
|
||||||
|
terminalPanelFactory.createTerminalPanel(terminal, PtyConnectorDelegate())
|
||||||
|
}
|
||||||
|
private val coroutineScope = CoroutineScope(Job() + Dispatchers.IO)
|
||||||
|
|
||||||
|
init {
|
||||||
|
size = Dimension(UIManager.getInt("Dialog.width") - 100, UIManager.getInt("Dialog.height") - 100)
|
||||||
|
isModal = true
|
||||||
|
title = "SSH Copy ID"
|
||||||
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
|
Disposer.register(disposable, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
coroutineScope.cancel()
|
||||||
|
terminal.close()
|
||||||
|
Disposer.dispose(terminalPanel)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
return terminalPanel
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
coroutineScope.launch {
|
||||||
|
try {
|
||||||
|
doStart()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun createActions(): List<AbstractAction> {
|
||||||
|
return listOf(CancelAction())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun magenta(text: Any): String {
|
||||||
|
return "${ControlCharacters.ESC}[35m${text}${ControlCharacters.ESC}[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cyan(text: Any): String {
|
||||||
|
return "${ControlCharacters.ESC}[36m${text}${ControlCharacters.ESC}[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun red(text: Any): String {
|
||||||
|
return "${ControlCharacters.ESC}[31m${text}${ControlCharacters.ESC}[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun green(text: Any): String {
|
||||||
|
return "${ControlCharacters.ESC}[32m${text}${ControlCharacters.ESC}[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun doStart() {
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
terminal.write(
|
||||||
|
I18n.getString(
|
||||||
|
"termora.keymgr.ssh-copy-id.number",
|
||||||
|
magenta(hosts.size),
|
||||||
|
magenta(publicKeys.size)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
terminal.getDocument().newline()
|
||||||
|
terminal.getDocument().newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
var myClient: SshClient? = null
|
||||||
|
var mySession: ClientSession? = null
|
||||||
|
val timeout = Duration.ofMinutes(1)
|
||||||
|
|
||||||
|
// 获取公钥名称最长的
|
||||||
|
val publicKeyNameLength = publicKeys.maxOfOrNull { it.first.length } ?: 0
|
||||||
|
|
||||||
|
for (index in hosts.indices) {
|
||||||
|
if (!coroutineScope.isActive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val host = hosts[index]
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
terminal.write("[${cyan(index + 1)}/${cyan(hosts.size)}] ${host.name}")
|
||||||
|
terminal.getDocument().newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((j, e) in publicKeys.withIndex()) {
|
||||||
|
if (!coroutineScope.isActive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val publicKeyName = e.first.padEnd(publicKeyNameLength, ' ')
|
||||||
|
val publicKey = e.second
|
||||||
|
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
// @formatter:off
|
||||||
|
terminal.write("\t[${cyan(j + 1)}/${cyan(publicKeys.size)}] $publicKeyName ${I18n.getString("termora.transport.sftp.connecting")}")
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val client = SshClients.openClient(host).apply { myClient = this }
|
||||||
|
client.userInteraction = TerminalUserInteraction(owner)
|
||||||
|
val session = SshClients.openSession(host, client).apply { mySession = this }
|
||||||
|
val channel =
|
||||||
|
session.createExecChannel("mkdir -p ~/.ssh && grep -qxF \"$publicKey\" ~/.ssh/authorized_keys || echo \"$publicKey\" >> ~/.ssh/authorized_keys")
|
||||||
|
val baos = ByteArrayOutputStream()
|
||||||
|
channel.out = baos
|
||||||
|
if (channel.open().verify(timeout).await(timeout)) {
|
||||||
|
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), timeout);
|
||||||
|
}
|
||||||
|
if (channel.exitStatus != 0) {
|
||||||
|
throw IllegalStateException("Server response: ${channel.exitStatus}")
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
terminal.getDocument().eraseInLine(2)
|
||||||
|
// @formatter:off
|
||||||
|
terminal.write("\r\t[${cyan(j + 1)}/${cyan(publicKeys.size)}] $publicKeyName ${green(I18n.getString("termora.keymgr.ssh-copy-id.successful"))}")
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
terminal.getDocument().eraseInLine(2)
|
||||||
|
// @formatter:off
|
||||||
|
terminal.write("\r\t[${cyan(j + 1)}/${cyan(publicKeys.size)}] $publicKeyName ${red("${I18n.getString("termora.keymgr.ssh-copy-id.failed")}: ${e.message}")}")
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
IOUtils.closeQuietly(mySession)
|
||||||
|
IOUtils.closeQuietly(myClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
terminal.getDocument().newline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
terminal.getDocument().newline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
terminal.write(I18n.getString("termora.keymgr.ssh-copy-id.end"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -17,6 +17,10 @@ class FileChooser {
|
|||||||
var allowsOtherFileTypes = true
|
var allowsOtherFileTypes = true
|
||||||
var canCreateDirectories = true
|
var canCreateDirectories = true
|
||||||
var win32Filters = mutableListOf<Pair<String, List<String>>>()
|
var win32Filters = mutableListOf<Pair<String, List<String>>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* e.g. listOf("json")
|
||||||
|
*/
|
||||||
var osxAllowedFileTypes = emptyList<String>()
|
var osxAllowedFileTypes = emptyList<String>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,42 +1,19 @@
|
|||||||
package app.termora.sync
|
package app.termora.sync
|
||||||
|
|
||||||
import app.termora.*
|
|
||||||
import app.termora.AES.CBC.aesCBCDecrypt
|
|
||||||
import app.termora.AES.CBC.aesCBCEncrypt
|
|
||||||
import app.termora.AES.decodeBase64
|
|
||||||
import app.termora.AES.encodeBase64String
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.highlight.KeywordHighlight
|
import app.termora.ResponseException
|
||||||
import app.termora.highlight.KeywordHighlightManager
|
|
||||||
import app.termora.keymap.Keymap
|
|
||||||
import app.termora.keymap.KeymapManager
|
|
||||||
import app.termora.keymgr.KeyManager
|
|
||||||
import app.termora.keymgr.OhKeyPair
|
|
||||||
import app.termora.macro.Macro
|
|
||||||
import app.termora.macro.MacroManager
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.apache.commons.lang3.ArrayUtils
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import javax.swing.SwingUtilities
|
|
||||||
|
|
||||||
abstract class GitSyncer : Syncer {
|
abstract class GitSyncer : SafetySyncer() {
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(GitSyncer::class.java)
|
private val log = LoggerFactory.getLogger(GitSyncer::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected val description = "${Application.getName()} config"
|
|
||||||
protected val httpClient get() = Application.httpClient
|
|
||||||
protected val hostManager get() = HostManager.getInstance()
|
|
||||||
protected val keyManager get() = KeyManager.getInstance()
|
|
||||||
protected val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
|
||||||
protected val macroManager get() = MacroManager.getInstance()
|
|
||||||
protected val keymapManager get() = KeymapManager.getInstance()
|
|
||||||
|
|
||||||
override fun pull(config: SyncConfig): GistResponse {
|
override fun pull(config: SyncConfig): GistResponse {
|
||||||
|
|
||||||
if (log.isInfoEnabled) {
|
if (log.isInfoEnabled) {
|
||||||
@@ -92,174 +69,6 @@ abstract class GitSyncer : Syncer {
|
|||||||
return gistResponse
|
return gistResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeHosts(text: String, config: SyncConfig) {
|
|
||||||
// aes key
|
|
||||||
val key = getKey(config)
|
|
||||||
val encryptedHosts = ohMyJson.decodeFromString<List<EncryptedHost>>(text)
|
|
||||||
val hosts = hostManager.hosts().associateBy { it.id }
|
|
||||||
|
|
||||||
for (encryptedHost in encryptedHosts) {
|
|
||||||
val oldHost = hosts[encryptedHost.id]
|
|
||||||
|
|
||||||
// 如果一样,则无需配置
|
|
||||||
if (oldHost != null) {
|
|
||||||
if (oldHost.updateDate == encryptedHost.updateDate) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// aes iv
|
|
||||||
val iv = getIv(encryptedHost.id)
|
|
||||||
val host = Host(
|
|
||||||
id = encryptedHost.id,
|
|
||||||
name = encryptedHost.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
protocol = Protocol.valueOf(
|
|
||||||
encryptedHost.protocol.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
|
||||||
),
|
|
||||||
host = encryptedHost.host.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
port = encryptedHost.port.decodeBase64().aesCBCDecrypt(key, iv)
|
|
||||||
.decodeToString().toIntOrNull() ?: 0,
|
|
||||||
username = encryptedHost.username.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
remark = encryptedHost.remark.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
authentication = ohMyJson.decodeFromString(
|
|
||||||
encryptedHost.authentication.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
|
||||||
),
|
|
||||||
proxy = ohMyJson.decodeFromString(
|
|
||||||
encryptedHost.proxy.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
|
||||||
),
|
|
||||||
options = ohMyJson.decodeFromString(
|
|
||||||
encryptedHost.options.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
|
||||||
),
|
|
||||||
tunnelings = ohMyJson.decodeFromString(
|
|
||||||
encryptedHost.tunnelings.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
|
||||||
),
|
|
||||||
sort = encryptedHost.sort,
|
|
||||||
parentId = encryptedHost.parentId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
ownerId = encryptedHost.ownerId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
creatorId = encryptedHost.creatorId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
createDate = encryptedHost.createDate,
|
|
||||||
updateDate = encryptedHost.updateDate,
|
|
||||||
deleted = encryptedHost.deleted
|
|
||||||
)
|
|
||||||
SwingUtilities.invokeLater { hostManager.addHost(host) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (log.isWarnEnabled) {
|
|
||||||
log.warn("Decode host: ${encryptedHost.id} failed. error: {}", e.message, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Decode hosts: {}", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeKeys(text: String, config: SyncConfig) {
|
|
||||||
// aes key
|
|
||||||
val key = getKey(config)
|
|
||||||
val encryptedKeys = ohMyJson.decodeFromString<List<OhKeyPair>>(text)
|
|
||||||
|
|
||||||
for (encryptedKey in encryptedKeys) {
|
|
||||||
try {
|
|
||||||
// aes iv
|
|
||||||
val iv = getIv(encryptedKey.id)
|
|
||||||
val keyPair = OhKeyPair(
|
|
||||||
id = encryptedKey.id,
|
|
||||||
publicKey = encryptedKey.publicKey.decodeBase64().aesCBCDecrypt(key, iv).encodeBase64String(),
|
|
||||||
privateKey = encryptedKey.privateKey.decodeBase64().aesCBCDecrypt(key, iv).encodeBase64String(),
|
|
||||||
type = encryptedKey.type.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
name = encryptedKey.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
remark = encryptedKey.remark.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
length = encryptedKey.length,
|
|
||||||
sort = encryptedKey.sort
|
|
||||||
)
|
|
||||||
SwingUtilities.invokeLater { keyManager.addOhKeyPair(keyPair) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (log.isWarnEnabled) {
|
|
||||||
log.warn("Decode key: ${encryptedKey.id} failed. error: {}", e.message, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Decode keys: {}", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeKeywordHighlights(text: String, config: SyncConfig) {
|
|
||||||
// aes key
|
|
||||||
val key = getKey(config)
|
|
||||||
val encryptedKeywordHighlights = ohMyJson.decodeFromString<List<KeywordHighlight>>(text)
|
|
||||||
|
|
||||||
for (e in encryptedKeywordHighlights) {
|
|
||||||
try {
|
|
||||||
// aes iv
|
|
||||||
val iv = getIv(e.id)
|
|
||||||
keywordHighlightManager.addKeywordHighlight(
|
|
||||||
e.copy(
|
|
||||||
keyword = e.keyword.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
description = e.description.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
if (log.isWarnEnabled) {
|
|
||||||
log.warn("Decode KeywordHighlight: ${e.id} failed. error: {}", ex.message, ex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Decode KeywordHighlight: {}", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeMacros(text: String, config: SyncConfig) {
|
|
||||||
// aes key
|
|
||||||
val key = getKey(config)
|
|
||||||
val encryptedMacros = ohMyJson.decodeFromString<List<Macro>>(text)
|
|
||||||
|
|
||||||
for (e in encryptedMacros) {
|
|
||||||
try {
|
|
||||||
// aes iv
|
|
||||||
val iv = getIv(e.id)
|
|
||||||
macroManager.addMacro(
|
|
||||||
e.copy(
|
|
||||||
name = e.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
macro = e.macro.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
if (log.isWarnEnabled) {
|
|
||||||
log.warn("Decode Macro: ${e.id} failed. error: {}", ex.message, ex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Decode Macros: {}", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeKeymaps(text: String, config: SyncConfig) {
|
|
||||||
|
|
||||||
for (keymap in ohMyJson.decodeFromString<List<JsonObject>>(text).mapNotNull { Keymap.fromJSON(it) }) {
|
|
||||||
keymapManager.addKeymap(keymap)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Decode Keymaps: {}", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getKey(config: SyncConfig): ByteArray {
|
|
||||||
return ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getIv(id: String): ByteArray {
|
|
||||||
return ArrayUtils.subarray(id.padEnd(16, '0').toByteArray(), 0, 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun push(config: SyncConfig): GistResponse {
|
override fun push(config: SyncConfig): GistResponse {
|
||||||
val gistFiles = mutableListOf<GistFile>()
|
val gistFiles = mutableListOf<GistFile>()
|
||||||
@@ -268,62 +77,16 @@ abstract class GitSyncer : Syncer {
|
|||||||
|
|
||||||
// Hosts
|
// Hosts
|
||||||
if (config.ranges.contains(SyncRange.Hosts)) {
|
if (config.ranges.contains(SyncRange.Hosts)) {
|
||||||
val encryptedHosts = mutableListOf<EncryptedHost>()
|
val hostsContent = encodeHosts(key)
|
||||||
for (host in hostManager.hosts()) {
|
|
||||||
// aes iv
|
|
||||||
val iv = ArrayUtils.subarray(host.id.padEnd(16, '0').toByteArray(), 0, 16)
|
|
||||||
val encryptedHost = EncryptedHost()
|
|
||||||
encryptedHost.id = host.id
|
|
||||||
encryptedHost.name = host.name.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.protocol = host.protocol.name.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.host = host.host.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.port = "${host.port}".aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.username = host.username.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.remark = host.remark.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.authentication = ohMyJson.encodeToString(host.authentication)
|
|
||||||
.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.proxy = ohMyJson.encodeToString(host.proxy).aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.options =
|
|
||||||
ohMyJson.encodeToString(host.options).aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.tunnelings =
|
|
||||||
ohMyJson.encodeToString(host.tunnelings).aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.sort = host.sort
|
|
||||||
encryptedHost.deleted = host.deleted
|
|
||||||
encryptedHost.parentId = host.parentId.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.ownerId = host.ownerId.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.creatorId = host.creatorId.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.createDate = host.createDate
|
|
||||||
encryptedHost.updateDate = host.updateDate
|
|
||||||
encryptedHosts.add(encryptedHost)
|
|
||||||
}
|
|
||||||
|
|
||||||
val hostsContent = ohMyJson.encodeToString(encryptedHosts)
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Push encryptedHosts: {}", hostsContent)
|
log.debug("Push encryptedHosts: {}", hostsContent)
|
||||||
}
|
}
|
||||||
gistFiles.add(GistFile("Hosts", hostsContent))
|
gistFiles.add(GistFile("Hosts", hostsContent))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyPairs
|
// KeyPairs
|
||||||
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
||||||
val encryptedKeys = mutableListOf<OhKeyPair>()
|
val keysContent = encodeKeys(key)
|
||||||
for (keyPair in keyManager.getOhKeyPairs()) {
|
|
||||||
// aes iv
|
|
||||||
val iv = ArrayUtils.subarray(keyPair.id.padEnd(16, '0').toByteArray(), 0, 16)
|
|
||||||
val encryptedKeyPair = OhKeyPair(
|
|
||||||
id = keyPair.id,
|
|
||||||
publicKey = keyPair.publicKey.decodeBase64().aesCBCEncrypt(key, iv).encodeBase64String(),
|
|
||||||
privateKey = keyPair.privateKey.decodeBase64().aesCBCEncrypt(key, iv).encodeBase64String(),
|
|
||||||
type = keyPair.type.aesCBCEncrypt(key, iv).encodeBase64String(),
|
|
||||||
name = keyPair.name.aesCBCEncrypt(key, iv).encodeBase64String(),
|
|
||||||
remark = keyPair.remark.aesCBCEncrypt(key, iv).encodeBase64String(),
|
|
||||||
length = keyPair.length,
|
|
||||||
sort = keyPair.sort
|
|
||||||
)
|
|
||||||
encryptedKeys.add(encryptedKeyPair)
|
|
||||||
}
|
|
||||||
val keysContent = ohMyJson.encodeToString(encryptedKeys)
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Push encryptedKeys: {}", keysContent)
|
log.debug("Push encryptedKeys: {}", keysContent)
|
||||||
}
|
}
|
||||||
@@ -332,17 +95,7 @@ abstract class GitSyncer : Syncer {
|
|||||||
|
|
||||||
// Highlights
|
// Highlights
|
||||||
if (config.ranges.contains(SyncRange.KeywordHighlights)) {
|
if (config.ranges.contains(SyncRange.KeywordHighlights)) {
|
||||||
val keywordHighlights = mutableListOf<KeywordHighlight>()
|
val keywordHighlightsContent = encodeKeywordHighlights(key)
|
||||||
for (keywordHighlight in keywordHighlightManager.getKeywordHighlights()) {
|
|
||||||
// aes iv
|
|
||||||
val iv = getIv(keywordHighlight.id)
|
|
||||||
val encryptedKeyPair = keywordHighlight.copy(
|
|
||||||
keyword = keywordHighlight.keyword.aesCBCEncrypt(key, iv).encodeBase64String(),
|
|
||||||
description = keywordHighlight.description.aesCBCEncrypt(key, iv).encodeBase64String(),
|
|
||||||
)
|
|
||||||
keywordHighlights.add(encryptedKeyPair)
|
|
||||||
}
|
|
||||||
val keywordHighlightsContent = ohMyJson.encodeToString(keywordHighlights)
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Push keywordHighlights: {}", keywordHighlightsContent)
|
log.debug("Push keywordHighlights: {}", keywordHighlightsContent)
|
||||||
}
|
}
|
||||||
@@ -351,17 +104,7 @@ abstract class GitSyncer : Syncer {
|
|||||||
|
|
||||||
// Macros
|
// Macros
|
||||||
if (config.ranges.contains(SyncRange.Macros)) {
|
if (config.ranges.contains(SyncRange.Macros)) {
|
||||||
val macros = mutableListOf<Macro>()
|
val macrosContent = encodeMacros(key)
|
||||||
for (macro in macroManager.getMacros()) {
|
|
||||||
val iv = getIv(macro.id)
|
|
||||||
macros.add(
|
|
||||||
macro.copy(
|
|
||||||
name = macro.name.aesCBCEncrypt(key, iv).encodeBase64String(),
|
|
||||||
macro = macro.macro.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val macrosContent = ohMyJson.encodeToString(macros)
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Push macros: {}", macrosContent)
|
log.debug("Push macros: {}", macrosContent)
|
||||||
}
|
}
|
||||||
@@ -370,22 +113,11 @@ abstract class GitSyncer : Syncer {
|
|||||||
|
|
||||||
// Keymap
|
// Keymap
|
||||||
if (config.ranges.contains(SyncRange.Keymap)) {
|
if (config.ranges.contains(SyncRange.Keymap)) {
|
||||||
val keymaps = mutableListOf<JsonObject>()
|
val keymapsContent = encodeKeymaps()
|
||||||
for (keymap in keymapManager.getKeymaps()) {
|
if (log.isDebugEnabled) {
|
||||||
// 只读的是内置的
|
log.debug("Push keymaps: {}", keymapsContent)
|
||||||
if (keymap.isReadonly) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
keymaps.add(keymap.toJSONObject())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keymaps.isNotEmpty()) {
|
|
||||||
val keymapsContent = ohMyJson.encodeToString(keymaps)
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Push keymaps: {}", keymapsContent)
|
|
||||||
}
|
|
||||||
gistFiles.add(GistFile("Keymaps", keymapsContent))
|
|
||||||
}
|
}
|
||||||
|
gistFiles.add(GistFile("Keymaps", keymapsContent))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gistFiles.isEmpty()) {
|
if (gistFiles.isEmpty()) {
|
||||||
|
|||||||
299
src/main/kotlin/app/termora/sync/SafetySyncer.kt
Normal file
299
src/main/kotlin/app/termora/sync/SafetySyncer.kt
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
package app.termora.sync
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.AES.CBC.aesCBCDecrypt
|
||||||
|
import app.termora.AES.CBC.aesCBCEncrypt
|
||||||
|
import app.termora.AES.decodeBase64
|
||||||
|
import app.termora.AES.encodeBase64String
|
||||||
|
import app.termora.Application.ohMyJson
|
||||||
|
import app.termora.highlight.KeywordHighlight
|
||||||
|
import app.termora.highlight.KeywordHighlightManager
|
||||||
|
import app.termora.keymap.Keymap
|
||||||
|
import app.termora.keymap.KeymapManager
|
||||||
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.keymgr.OhKeyPair
|
||||||
|
import app.termora.macro.Macro
|
||||||
|
import app.termora.macro.MacroManager
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
abstract class SafetySyncer : Syncer {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(SafetySyncer::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val description = "${Application.getName()} config"
|
||||||
|
protected val httpClient get() = Application.httpClient
|
||||||
|
protected val hostManager get() = HostManager.getInstance()
|
||||||
|
protected val keyManager get() = KeyManager.getInstance()
|
||||||
|
protected val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
||||||
|
protected val macroManager get() = MacroManager.getInstance()
|
||||||
|
protected val keymapManager get() = KeymapManager.getInstance()
|
||||||
|
|
||||||
|
protected fun decodeHosts(text: String, config: SyncConfig) {
|
||||||
|
// aes key
|
||||||
|
val key = getKey(config)
|
||||||
|
val encryptedHosts = ohMyJson.decodeFromString<List<EncryptedHost>>(text)
|
||||||
|
val hosts = hostManager.hosts().associateBy { it.id }
|
||||||
|
|
||||||
|
for (encryptedHost in encryptedHosts) {
|
||||||
|
val oldHost = hosts[encryptedHost.id]
|
||||||
|
|
||||||
|
// 如果一样,则无需配置
|
||||||
|
if (oldHost != null) {
|
||||||
|
if (oldHost.updateDate == encryptedHost.updateDate) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// aes iv
|
||||||
|
val iv = getIv(encryptedHost.id)
|
||||||
|
val host = Host(
|
||||||
|
id = encryptedHost.id,
|
||||||
|
name = encryptedHost.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
protocol = Protocol.valueOf(
|
||||||
|
encryptedHost.protocol.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
||||||
|
),
|
||||||
|
host = encryptedHost.host.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
port = encryptedHost.port.decodeBase64().aesCBCDecrypt(key, iv)
|
||||||
|
.decodeToString().toIntOrNull() ?: 0,
|
||||||
|
username = encryptedHost.username.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
remark = encryptedHost.remark.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
authentication = ohMyJson.decodeFromString(
|
||||||
|
encryptedHost.authentication.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
||||||
|
),
|
||||||
|
proxy = ohMyJson.decodeFromString(
|
||||||
|
encryptedHost.proxy.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
||||||
|
),
|
||||||
|
options = ohMyJson.decodeFromString(
|
||||||
|
encryptedHost.options.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
||||||
|
),
|
||||||
|
tunnelings = ohMyJson.decodeFromString(
|
||||||
|
encryptedHost.tunnelings.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
||||||
|
),
|
||||||
|
sort = encryptedHost.sort,
|
||||||
|
parentId = encryptedHost.parentId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
ownerId = encryptedHost.ownerId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
creatorId = encryptedHost.creatorId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
createDate = encryptedHost.createDate,
|
||||||
|
updateDate = encryptedHost.updateDate,
|
||||||
|
deleted = encryptedHost.deleted
|
||||||
|
)
|
||||||
|
SwingUtilities.invokeLater { hostManager.addHost(host) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn("Decode host: ${encryptedHost.id} failed. error: {}", e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Decode hosts: {}", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun encodeHosts(key: ByteArray): String {
|
||||||
|
val encryptedHosts = mutableListOf<EncryptedHost>()
|
||||||
|
for (host in hostManager.hosts()) {
|
||||||
|
// aes iv
|
||||||
|
val iv = ArrayUtils.subarray(host.id.padEnd(16, '0').toByteArray(), 0, 16)
|
||||||
|
val encryptedHost = EncryptedHost()
|
||||||
|
encryptedHost.id = host.id
|
||||||
|
encryptedHost.name = host.name.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.protocol = host.protocol.name.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.host = host.host.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.port = "${host.port}".aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.username = host.username.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.remark = host.remark.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.authentication = ohMyJson.encodeToString(host.authentication)
|
||||||
|
.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.proxy = ohMyJson.encodeToString(host.proxy).aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.options =
|
||||||
|
ohMyJson.encodeToString(host.options).aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.tunnelings =
|
||||||
|
ohMyJson.encodeToString(host.tunnelings).aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.sort = host.sort
|
||||||
|
encryptedHost.deleted = host.deleted
|
||||||
|
encryptedHost.parentId = host.parentId.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.ownerId = host.ownerId.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.creatorId = host.creatorId.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.createDate = host.createDate
|
||||||
|
encryptedHost.updateDate = host.updateDate
|
||||||
|
encryptedHosts.add(encryptedHost)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ohMyJson.encodeToString(encryptedHosts)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun decodeKeys(text: String, config: SyncConfig) {
|
||||||
|
// aes key
|
||||||
|
val key = getKey(config)
|
||||||
|
val encryptedKeys = ohMyJson.decodeFromString<List<OhKeyPair>>(text)
|
||||||
|
|
||||||
|
for (encryptedKey in encryptedKeys) {
|
||||||
|
try {
|
||||||
|
// aes iv
|
||||||
|
val iv = getIv(encryptedKey.id)
|
||||||
|
val keyPair = OhKeyPair(
|
||||||
|
id = encryptedKey.id,
|
||||||
|
publicKey = encryptedKey.publicKey.decodeBase64().aesCBCDecrypt(key, iv).encodeBase64String(),
|
||||||
|
privateKey = encryptedKey.privateKey.decodeBase64().aesCBCDecrypt(key, iv).encodeBase64String(),
|
||||||
|
type = encryptedKey.type.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
name = encryptedKey.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
remark = encryptedKey.remark.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
length = encryptedKey.length,
|
||||||
|
sort = encryptedKey.sort
|
||||||
|
)
|
||||||
|
SwingUtilities.invokeLater { keyManager.addOhKeyPair(keyPair) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn("Decode key: ${encryptedKey.id} failed. error: {}", e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Decode keys: {}", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun encodeKeys(key: ByteArray): String {
|
||||||
|
val encryptedKeys = mutableListOf<OhKeyPair>()
|
||||||
|
for (keyPair in keyManager.getOhKeyPairs()) {
|
||||||
|
// aes iv
|
||||||
|
val iv = ArrayUtils.subarray(keyPair.id.padEnd(16, '0').toByteArray(), 0, 16)
|
||||||
|
val encryptedKeyPair = OhKeyPair(
|
||||||
|
id = keyPair.id,
|
||||||
|
publicKey = keyPair.publicKey.decodeBase64().aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||||
|
privateKey = keyPair.privateKey.decodeBase64().aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||||
|
type = keyPair.type.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||||
|
name = keyPair.name.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||||
|
remark = keyPair.remark.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||||
|
length = keyPair.length,
|
||||||
|
sort = keyPair.sort
|
||||||
|
)
|
||||||
|
encryptedKeys.add(encryptedKeyPair)
|
||||||
|
}
|
||||||
|
return ohMyJson.encodeToString(encryptedKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun decodeKeywordHighlights(text: String, config: SyncConfig) {
|
||||||
|
// aes key
|
||||||
|
val key = getKey(config)
|
||||||
|
val encryptedKeywordHighlights = ohMyJson.decodeFromString<List<KeywordHighlight>>(text)
|
||||||
|
|
||||||
|
for (e in encryptedKeywordHighlights) {
|
||||||
|
try {
|
||||||
|
// aes iv
|
||||||
|
val iv = getIv(e.id)
|
||||||
|
keywordHighlightManager.addKeywordHighlight(
|
||||||
|
e.copy(
|
||||||
|
keyword = e.keyword.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
description = e.description.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn("Decode KeywordHighlight: ${e.id} failed. error: {}", ex.message, ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Decode KeywordHighlight: {}", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun encodeKeywordHighlights(key: ByteArray): String {
|
||||||
|
val keywordHighlights = mutableListOf<KeywordHighlight>()
|
||||||
|
for (keywordHighlight in keywordHighlightManager.getKeywordHighlights()) {
|
||||||
|
// aes iv
|
||||||
|
val iv = getIv(keywordHighlight.id)
|
||||||
|
val encryptedKeyPair = keywordHighlight.copy(
|
||||||
|
keyword = keywordHighlight.keyword.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||||
|
description = keywordHighlight.description.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||||
|
)
|
||||||
|
keywordHighlights.add(encryptedKeyPair)
|
||||||
|
}
|
||||||
|
return ohMyJson.encodeToString(keywordHighlights)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun decodeMacros(text: String, config: SyncConfig) {
|
||||||
|
// aes key
|
||||||
|
val key = getKey(config)
|
||||||
|
val encryptedMacros = ohMyJson.decodeFromString<List<Macro>>(text)
|
||||||
|
|
||||||
|
for (e in encryptedMacros) {
|
||||||
|
try {
|
||||||
|
// aes iv
|
||||||
|
val iv = getIv(e.id)
|
||||||
|
macroManager.addMacro(
|
||||||
|
e.copy(
|
||||||
|
name = e.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
macro = e.macro.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn("Decode Macro: ${e.id} failed. error: {}", ex.message, ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Decode Macros: {}", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun encodeMacros(key: ByteArray): String {
|
||||||
|
val macros = mutableListOf<Macro>()
|
||||||
|
for (macro in macroManager.getMacros()) {
|
||||||
|
val iv = getIv(macro.id)
|
||||||
|
macros.add(
|
||||||
|
macro.copy(
|
||||||
|
name = macro.name.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||||
|
macro = macro.macro.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return ohMyJson.encodeToString(macros)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun decodeKeymaps(text: String, config: SyncConfig) {
|
||||||
|
|
||||||
|
for (keymap in ohMyJson.decodeFromString<List<JsonObject>>(text).mapNotNull { Keymap.fromJSON(it) }) {
|
||||||
|
keymapManager.addKeymap(keymap)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Decode Keymaps: {}", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun encodeKeymaps(): String {
|
||||||
|
val keymaps = mutableListOf<JsonObject>()
|
||||||
|
for (keymap in keymapManager.getKeymaps()) {
|
||||||
|
// 只读的是内置的
|
||||||
|
if (keymap.isReadonly) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keymaps.add(keymap.toJSONObject())
|
||||||
|
}
|
||||||
|
|
||||||
|
return ohMyJson.encodeToString(keymaps)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun getKey(config: SyncConfig): ByteArray {
|
||||||
|
return ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun getIv(id: String): ByteArray {
|
||||||
|
return ArrayUtils.subarray(id.padEnd(16, '0').toByteArray(), 0, 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ enum class SyncType {
|
|||||||
GitLab,
|
GitLab,
|
||||||
GitHub,
|
GitHub,
|
||||||
Gitee,
|
Gitee,
|
||||||
|
WebDAV,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class SyncRange {
|
enum class SyncRange {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class SyncerProvider private constructor() {
|
|||||||
SyncType.GitHub -> GitHubSyncer.getInstance()
|
SyncType.GitHub -> GitHubSyncer.getInstance()
|
||||||
SyncType.Gitee -> GiteeSyncer.getInstance()
|
SyncType.Gitee -> GiteeSyncer.getInstance()
|
||||||
SyncType.GitLab -> GitLabSyncer.getInstance()
|
SyncType.GitLab -> GitLabSyncer.getInstance()
|
||||||
|
SyncType.WebDAV -> WebDAVSyncer.getInstance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
152
src/main/kotlin/app/termora/sync/WebDAVSyncer.kt
Normal file
152
src/main/kotlin/app/termora/sync/WebDAVSyncer.kt
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package app.termora.sync
|
||||||
|
|
||||||
|
import app.termora.Application.ohMyJson
|
||||||
|
import app.termora.ApplicationScope
|
||||||
|
import app.termora.PBKDF2
|
||||||
|
import app.termora.ResponseException
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import okhttp3.Credentials
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
class WebDAVSyncer private constructor() : SafetySyncer() {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(WebDAVSyncer::class.java)
|
||||||
|
|
||||||
|
fun getInstance(): WebDAVSyncer {
|
||||||
|
return ApplicationScope.forApplicationScope().getOrCreate(WebDAVSyncer::class) { WebDAVSyncer() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun pull(config: SyncConfig): GistResponse {
|
||||||
|
val response = httpClient.newCall(newRequestBuilder(config).get().build()).execute()
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw ResponseException(response.code, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
val text = response.use { resp -> resp.body?.use { it.string() } }
|
||||||
|
?: throw ResponseException(response.code, response)
|
||||||
|
|
||||||
|
val json = ohMyJson.decodeFromString<JsonObject>(text)
|
||||||
|
|
||||||
|
// decode hosts
|
||||||
|
json["Hosts"]?.jsonPrimitive?.content?.let {
|
||||||
|
decodeHosts(it, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode KeyPairs
|
||||||
|
json["KeyPairs"]?.jsonPrimitive?.content?.let {
|
||||||
|
decodeKeys(it, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode Highlights
|
||||||
|
json["KeywordHighlights"]?.jsonPrimitive?.content?.let {
|
||||||
|
decodeKeywordHighlights(it, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode Macros
|
||||||
|
json["Macros"]?.jsonPrimitive?.content?.let {
|
||||||
|
decodeMacros(it, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode Keymaps
|
||||||
|
json["Keymaps"]?.jsonPrimitive?.content?.let {
|
||||||
|
decodeKeymaps(it, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
return GistResponse(config, emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun push(config: SyncConfig): GistResponse {
|
||||||
|
// aes key
|
||||||
|
val key = getKey(config)
|
||||||
|
val json = buildJsonObject {
|
||||||
|
// Hosts
|
||||||
|
if (config.ranges.contains(SyncRange.Hosts)) {
|
||||||
|
val hostsContent = encodeHosts(key)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Push encryptedHosts: {}", hostsContent)
|
||||||
|
}
|
||||||
|
put("Hosts", hostsContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyPairs
|
||||||
|
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
||||||
|
val keysContent = encodeKeys(key)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Push encryptedKeys: {}", keysContent)
|
||||||
|
}
|
||||||
|
put("KeyPairs", keysContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlights
|
||||||
|
if (config.ranges.contains(SyncRange.KeywordHighlights)) {
|
||||||
|
val keywordHighlightsContent = encodeKeywordHighlights(key)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Push keywordHighlights: {}", keywordHighlightsContent)
|
||||||
|
}
|
||||||
|
put("KeywordHighlights", keywordHighlightsContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Macros
|
||||||
|
if (config.ranges.contains(SyncRange.Macros)) {
|
||||||
|
val macrosContent = encodeMacros(key)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Push macros: {}", macrosContent)
|
||||||
|
}
|
||||||
|
put("Macros", macrosContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keymap
|
||||||
|
if (config.ranges.contains(SyncRange.Keymap)) {
|
||||||
|
val keymapsContent = encodeKeymaps()
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Push keymaps: {}", keymapsContent)
|
||||||
|
}
|
||||||
|
put("Keymaps", keymapsContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = httpClient.newCall(
|
||||||
|
newRequestBuilder(config).put(
|
||||||
|
ohMyJson.encodeToString(json)
|
||||||
|
.toRequestBody("application/json".toMediaType())
|
||||||
|
).build()
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw ResponseException(response.code, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
return GistResponse(
|
||||||
|
config = config,
|
||||||
|
gists = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun getWebDavFileUrl(config: SyncConfig): String {
|
||||||
|
return config.options["domain"] ?: throw IllegalStateException("domain is not defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getKey(config: SyncConfig): ByteArray {
|
||||||
|
return PBKDF2.generateSecret(
|
||||||
|
config.gistId.toCharArray(),
|
||||||
|
config.token.toByteArray(),
|
||||||
|
10000, 128
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newRequestBuilder(config: SyncConfig): Request.Builder {
|
||||||
|
return Request.Builder()
|
||||||
|
.header("Authorization", Credentials.basic(config.gistId, config.token, Charsets.UTF_8))
|
||||||
|
.url(getWebDavFileUrl(config))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -485,9 +485,11 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
|||||||
val m = args.first()
|
val m = args.first()
|
||||||
if (m == '6') {
|
if (m == '6') {
|
||||||
val position = terminal.getCursorModel().getPosition()
|
val position = terminal.getCursorModel().getPosition()
|
||||||
ptyConnector.write("${ControlCharacters.ESC}[${position.y};${position.x}R")
|
val bytes = "${ControlCharacters.ESC}[${position.y};${position.x}R".toByteArray(ptyConnector.getCharset())
|
||||||
|
ptyConnector.write(bytes)
|
||||||
} else if (m == '5') {
|
} else if (m == '5') {
|
||||||
ptyConnector.write("${ControlCharacters.ESC}[0n")
|
val bytes = "${ControlCharacters.ESC}[0n".toByteArray(ptyConnector.getCharset())
|
||||||
|
ptyConnector.write(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -689,6 +691,13 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
|||||||
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
|
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
|
||||||
1049 -> {
|
1049 -> {
|
||||||
|
|
||||||
|
// Save cursor
|
||||||
|
if (enable) {
|
||||||
|
CursorStoreStores.store(terminal)
|
||||||
|
} else {
|
||||||
|
CursorStoreStores.restore(terminal)
|
||||||
|
}
|
||||||
|
|
||||||
// 如果是关闭 清屏
|
// 如果是关闭 清屏
|
||||||
if (!enable) {
|
if (!enable) {
|
||||||
terminal.getDocument().eraseInDisplay(2)
|
terminal.getDocument().eraseInDisplay(2)
|
||||||
@@ -922,7 +931,7 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (log.isWarnEnabled) {
|
if (log.isWarnEnabled) {
|
||||||
log.warn("xterm-256 foreground color, code: $code")
|
log.warn("xterm-256 background color, code: $code")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ data class CursorStore(
|
|||||||
*/
|
*/
|
||||||
val textStyle: TextStyle,
|
val textStyle: TextStyle,
|
||||||
/**
|
/**
|
||||||
|
* 如果为 null 表示没有设置
|
||||||
|
*
|
||||||
* @see [DataKey.AutoWrapMode]
|
* @see [DataKey.AutoWrapMode]
|
||||||
*/
|
*/
|
||||||
val autoWarpMode: Boolean,
|
val autoWarpMode: Boolean?,
|
||||||
/**
|
/**
|
||||||
* @see [DataKey.OriginMode]
|
* @see [DataKey.OriginMode]
|
||||||
*/
|
*/
|
||||||
|
|||||||
68
src/main/kotlin/app/termora/terminal/CursorStoreStores.kt
Normal file
68
src/main/kotlin/app/termora/terminal/CursorStoreStores.kt
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package app.termora.terminal
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
|
||||||
|
object CursorStoreStores {
|
||||||
|
private val log = LoggerFactory.getLogger(CursorStoreStores::class.java)
|
||||||
|
|
||||||
|
fun restore(terminal: Terminal) {
|
||||||
|
val terminalModel = terminal.getTerminalModel()
|
||||||
|
val cursorStore = if (terminalModel.hasData(DataKey.SaveCursor)) {
|
||||||
|
terminalModel.getData(DataKey.SaveCursor)
|
||||||
|
} else {
|
||||||
|
CursorStore(
|
||||||
|
position = Position(1, 1),
|
||||||
|
textStyle = TextStyle.Default,
|
||||||
|
autoWarpMode = false,
|
||||||
|
originMode = false,
|
||||||
|
graphicCharacterSet = GraphicCharacterSet()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
terminalModel.setData(DataKey.OriginMode, cursorStore.originMode)
|
||||||
|
terminalModel.setData(DataKey.TextStyle, cursorStore.textStyle)
|
||||||
|
if (cursorStore.autoWarpMode != null) {
|
||||||
|
terminalModel.setData(DataKey.AutoWrapMode, cursorStore.autoWarpMode)
|
||||||
|
}
|
||||||
|
terminalModel.setData(DataKey.GraphicCharacterSet, cursorStore.graphicCharacterSet)
|
||||||
|
|
||||||
|
val region = if (terminalModel.isOriginMode()) terminalModel.getScrollingRegion()
|
||||||
|
else ScrollingRegion(top = 1, bottom = terminalModel.getRows())
|
||||||
|
var y = cursorStore.position.y
|
||||||
|
if (y < region.top) {
|
||||||
|
y = 1
|
||||||
|
} else if (y > region.bottom) {
|
||||||
|
y = region.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal.getCursorModel().move(row = y, col = cursorStore.position.x)
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Restore Cursor (DECRC). $cursorStore")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun store(terminal: Terminal) {
|
||||||
|
val terminalModel = terminal.getTerminalModel()
|
||||||
|
|
||||||
|
val graphicCharacterSet = terminalModel.getData(DataKey.GraphicCharacterSet)
|
||||||
|
// 避免引用
|
||||||
|
val characterSets = mutableMapOf<Graphic, CharacterSet>()
|
||||||
|
characterSets.putAll(graphicCharacterSet.characterSets)
|
||||||
|
|
||||||
|
val cursorStore = CursorStore(
|
||||||
|
position = terminal.getCursorModel().getPosition(),
|
||||||
|
textStyle = terminalModel.getData(DataKey.TextStyle),
|
||||||
|
autoWarpMode = if (terminalModel.hasData(DataKey.AutoWrapMode)) terminalModel.getData(DataKey.AutoWrapMode) else null,
|
||||||
|
originMode = terminalModel.isOriginMode(),
|
||||||
|
graphicCharacterSet = graphicCharacterSet.copy(characterSets = characterSets),
|
||||||
|
)
|
||||||
|
|
||||||
|
terminalModel.setData(DataKey.SaveCursor, cursorStore)
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Save Cursor (DECSC). $cursorStore")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,6 +74,13 @@ class DataKey<T : Any>(val clazz: KClass<T>) {
|
|||||||
*/
|
*/
|
||||||
val Workdir = DataKey(String::class)
|
val Workdir = DataKey(String::class)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OSC 1337 CurrentDir
|
||||||
|
*
|
||||||
|
* https://iterm2.com/documentation-escape-codes.html
|
||||||
|
*/
|
||||||
|
val CurrentDir = DataKey(String::class)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* true: alternate keypad.
|
* true: alternate keypad.
|
||||||
* false: Normal Keypad (DECKPNM)
|
* false: Normal Keypad (DECKPNM)
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package app.termora.terminal
|
|
||||||
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
class DeviceControlProcessor(private val terminal: Terminal) : Processor {
|
|
||||||
private val args = StringBuilder()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val log = LoggerFactory.getLogger(DeviceControlProcessor::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun process(ch: Char): ProcessorState {
|
|
||||||
val state = when (ch) {
|
|
||||||
ControlCharacters.ST -> {
|
|
||||||
if (log.isWarnEnabled) {
|
|
||||||
log.warn("Ignore DCS: {}", args)
|
|
||||||
}
|
|
||||||
TerminalState.READY
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
args.append(ch)
|
|
||||||
TerminalState.DCS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == TerminalState.READY) {
|
|
||||||
args.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
return state
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package app.termora.terminal
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
class DeviceControlStringProcessor(terminal: Terminal, reader: TerminalReader) : AbstractProcessor(terminal, reader) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(DeviceControlStringProcessor::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val systemCommandSequence = SystemCommandSequence()
|
||||||
|
|
||||||
|
override fun process(ch: Char): ProcessorState {
|
||||||
|
// 回退回去,然后重新读取出来
|
||||||
|
reader.addFirst(ch)
|
||||||
|
|
||||||
|
do {
|
||||||
|
|
||||||
|
if (systemCommandSequence.process(reader.read())) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有检测到结束,那么退出重新来
|
||||||
|
if (reader.isEmpty()) {
|
||||||
|
return TerminalState.DCS
|
||||||
|
}
|
||||||
|
|
||||||
|
} while (reader.isNotEmpty())
|
||||||
|
|
||||||
|
processCommand(systemCommandSequence.getCommand())
|
||||||
|
|
||||||
|
systemCommandSequence.reset()
|
||||||
|
|
||||||
|
return TerminalState.READY
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun processCommand(command: String) {
|
||||||
|
if (command.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn("Cannot process command: {}", command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -128,9 +128,9 @@ class EscapeSequenceProcessor(terminal: Terminal, reader: TerminalReader) : Abst
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Device Control String (DCS is 0x90).
|
// Device Control String (DCS is 0x90).
|
||||||
'P' -> {
|
'P' -> {
|
||||||
|
state = TerminalState.DCS
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start of Guarded Area (SPA is 0x96).
|
// Start of Guarded Area (SPA is 0x96).
|
||||||
@@ -333,59 +333,12 @@ class EscapeSequenceProcessor(terminal: Terminal, reader: TerminalReader) : Abst
|
|||||||
|
|
||||||
// ESC 7 Save Cursor (DECSC), VT100.
|
// ESC 7 Save Cursor (DECSC), VT100.
|
||||||
'7' -> {
|
'7' -> {
|
||||||
val graphicCharacterSet = terminalModel.getData(DataKey.GraphicCharacterSet)
|
CursorStoreStores.store(terminal)
|
||||||
// 避免引用
|
|
||||||
val characterSets = mutableMapOf<Graphic, CharacterSet>()
|
|
||||||
characterSets.putAll(graphicCharacterSet.characterSets)
|
|
||||||
|
|
||||||
val cursorStore = CursorStore(
|
|
||||||
position = terminal.getCursorModel().getPosition(),
|
|
||||||
textStyle = terminalModel.getData(DataKey.TextStyle),
|
|
||||||
autoWarpMode = terminalModel.getData(DataKey.AutoWrapMode, false),
|
|
||||||
originMode = terminalModel.isOriginMode(),
|
|
||||||
graphicCharacterSet = graphicCharacterSet.copy(characterSets = characterSets),
|
|
||||||
)
|
|
||||||
|
|
||||||
terminalModel.setData(DataKey.SaveCursor, cursorStore)
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Save Cursor (DECSC). $cursorStore")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore Cursor (DECRC), VT100.
|
// Restore Cursor (DECRC), VT100.
|
||||||
'8' -> {
|
'8' -> {
|
||||||
val cursorStore = if (terminalModel.hasData(DataKey.SaveCursor)) {
|
CursorStoreStores.restore(terminal)
|
||||||
terminalModel.getData(DataKey.SaveCursor)
|
|
||||||
} else {
|
|
||||||
CursorStore(
|
|
||||||
position = Position(1, 1),
|
|
||||||
textStyle = TextStyle.Default,
|
|
||||||
autoWarpMode = false,
|
|
||||||
originMode = false,
|
|
||||||
graphicCharacterSet = GraphicCharacterSet()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
terminalModel.setData(DataKey.OriginMode, cursorStore.originMode)
|
|
||||||
terminalModel.setData(DataKey.TextStyle, cursorStore.textStyle)
|
|
||||||
terminalModel.setData(DataKey.AutoWrapMode, cursorStore.autoWarpMode)
|
|
||||||
terminalModel.setData(DataKey.GraphicCharacterSet, cursorStore.graphicCharacterSet)
|
|
||||||
|
|
||||||
val region = if (terminalModel.isOriginMode()) terminalModel.getScrollingRegion()
|
|
||||||
else ScrollingRegion(top = 1, bottom = terminalModel.getRows())
|
|
||||||
var y = cursorStore.position.y
|
|
||||||
if (y < region.top) {
|
|
||||||
y = 1
|
|
||||||
} else if (y > region.bottom) {
|
|
||||||
y = region.bottom
|
|
||||||
}
|
|
||||||
|
|
||||||
terminal.getCursorModel().move(row = y, col = cursorStore.position.x)
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Restore Cursor (DECRC). $cursorStore")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package app.termora.terminal
|
package app.termora.terminal
|
||||||
|
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.event.KeyEvent
|
import java.awt.event.KeyEvent
|
||||||
|
|
||||||
@Suppress("MemberVisibilityCanBePrivate")
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataListener {
|
open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataListener {
|
||||||
|
|
||||||
private val mapping = mutableMapOf<TerminalKeyEvent, String>()
|
private val mapping = mutableMapOf<TerminalKeyEvent, String>()
|
||||||
private val nothing = String()
|
private val nothing = StringUtils.EMPTY
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
|||||||
|
|
||||||
configureLeftRight()
|
configureLeftRight()
|
||||||
|
|
||||||
|
// Ctrl + C
|
||||||
putCode(TerminalKeyEvent(keyCode = 8), String(byteArrayOf(127)))
|
putCode(TerminalKeyEvent(keyCode = 8), String(byteArrayOf(127)))
|
||||||
|
|
||||||
// Enter
|
// Enter
|
||||||
@@ -38,15 +40,15 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
|||||||
|
|
||||||
|
|
||||||
// Page Up
|
// Page Up
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x21), encode = "${ControlCharacters.ESC}[5~")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_PAGE_UP), encode = "${ControlCharacters.ESC}[5~")
|
||||||
// Page Down
|
// Page Down
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x22), encode = "${ControlCharacters.ESC}[6~")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_PAGE_DOWN), encode = "${ControlCharacters.ESC}[6~")
|
||||||
|
|
||||||
|
|
||||||
// Insert
|
// Insert
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x9B), encode = "${ControlCharacters.ESC}[2~")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_INSERT), encode = "${ControlCharacters.ESC}[2~")
|
||||||
// Delete
|
// Delete
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x7F), encode = "${ControlCharacters.ESC}[3~")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_DELETE), encode = "${ControlCharacters.ESC}[3~")
|
||||||
|
|
||||||
// Function Keys
|
// Function Keys
|
||||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F1), encode = "${ControlCharacters.ESC}OP")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F1), encode = "${ControlCharacters.ESC}OP")
|
||||||
@@ -84,26 +86,29 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
|||||||
|
|
||||||
fun arrowKeysApplicationSequences() {
|
fun arrowKeysApplicationSequences() {
|
||||||
// Up
|
// Up
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x26), encode = "${ControlCharacters.ESC}OA")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_UP), encode = "${ControlCharacters.ESC}OA")
|
||||||
// Down
|
// Down
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x28), encode = "${ControlCharacters.ESC}OB")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_DOWN), encode = "${ControlCharacters.ESC}OB")
|
||||||
// Left
|
// Left
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x25), encode = "${ControlCharacters.ESC}OD")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_LEFT), encode = "${ControlCharacters.ESC}OD")
|
||||||
// Right
|
// Right
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x27), encode = "${ControlCharacters.ESC}OC")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_RIGHT), encode = "${ControlCharacters.ESC}OC")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun arrowKeysAnsiCursorSequences() {
|
fun arrowKeysAnsiCursorSequences() {
|
||||||
// Up
|
// Up
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x26), encode = "${ControlCharacters.ESC}[A")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_UP), encode = "${ControlCharacters.ESC}[A")
|
||||||
// Down
|
// Down
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x28), encode = "${ControlCharacters.ESC}[B")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_DOWN), encode = "${ControlCharacters.ESC}[B")
|
||||||
// Left
|
// Left
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x25), encode = "${ControlCharacters.ESC}[D")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_LEFT), encode = "${ControlCharacters.ESC}[D")
|
||||||
// Right
|
// Right
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x27), encode = "${ControlCharacters.ESC}[C")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_RIGHT), encode = "${ControlCharacters.ESC}[C")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alt + Left/Right
|
||||||
|
*/
|
||||||
fun configureLeftRight() {
|
fun configureLeftRight() {
|
||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isMacOS) {
|
||||||
putCode(
|
putCode(
|
||||||
@@ -141,32 +146,32 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
|||||||
|
|
||||||
fun keypadApplicationSequences() {
|
fun keypadApplicationSequences() {
|
||||||
// Up
|
// Up
|
||||||
putCode(TerminalKeyEvent(keyCode = 0xE0), encode = "${ControlCharacters.ESC}OA")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_UP), encode = "${ControlCharacters.ESC}OA")
|
||||||
// Down
|
// Down
|
||||||
putCode(TerminalKeyEvent(keyCode = 0xE1), encode = "${ControlCharacters.ESC}OB")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_DOWN), encode = "${ControlCharacters.ESC}OB")
|
||||||
// Left
|
// Left
|
||||||
putCode(TerminalKeyEvent(keyCode = 0xE2), encode = "${ControlCharacters.ESC}OD")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_LEFT), encode = "${ControlCharacters.ESC}OD")
|
||||||
// Right
|
// Right
|
||||||
putCode(TerminalKeyEvent(keyCode = 0xE3), encode = "${ControlCharacters.ESC}OC")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_RIGHT), encode = "${ControlCharacters.ESC}OC")
|
||||||
// Home
|
// Home
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x24), encode = "${ControlCharacters.ESC}OH")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_HOME), encode = "${ControlCharacters.ESC}OH")
|
||||||
// End
|
// End
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x23), encode = "${ControlCharacters.ESC}OF")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_END), encode = "${ControlCharacters.ESC}OF")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun keypadAnsiSequences() {
|
fun keypadAnsiSequences() {
|
||||||
// Up
|
// Up
|
||||||
putCode(TerminalKeyEvent(keyCode = 0xE0), encode = "${ControlCharacters.ESC}[A")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_UP), encode = "${ControlCharacters.ESC}[A")
|
||||||
// Down
|
// Down
|
||||||
putCode(TerminalKeyEvent(keyCode = 0xE1), encode = "${ControlCharacters.ESC}[B")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_DOWN), encode = "${ControlCharacters.ESC}[B")
|
||||||
// Left
|
// Left
|
||||||
putCode(TerminalKeyEvent(keyCode = 0xE2), encode = "${ControlCharacters.ESC}[D")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_LEFT), encode = "${ControlCharacters.ESC}[D")
|
||||||
// Right
|
// Right
|
||||||
putCode(TerminalKeyEvent(keyCode = 0xE3), encode = "${ControlCharacters.ESC}[C")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_RIGHT), encode = "${ControlCharacters.ESC}[C")
|
||||||
// Home
|
// Home
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x24), encode = "${ControlCharacters.ESC}[H")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_HOME), encode = "${ControlCharacters.ESC}[H")
|
||||||
// End
|
// End
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x23), encode = "${ControlCharacters.ESC}[F")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_END), encode = "${ControlCharacters.ESC}[F")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onChanged(key: DataKey<*>, data: Any) {
|
override fun onChanged(key: DataKey<*>, data: Any) {
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import org.apache.commons.codec.binary.Base64
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.Toolkit
|
import java.awt.Toolkit
|
||||||
import java.awt.datatransfer.StringSelection
|
import java.awt.datatransfer.StringSelection
|
||||||
|
import java.io.StringReader
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class OperatingSystemCommandProcessor(terminal: Terminal, reader: TerminalReader) :
|
class OperatingSystemCommandProcessor(terminal: Terminal, reader: TerminalReader) :
|
||||||
AbstractProcessor(terminal, reader) {
|
AbstractProcessor(terminal, reader) {
|
||||||
private val args = StringBuilder()
|
private val systemCommandSequence = SystemCommandSequence()
|
||||||
private val colorPalette get() = terminal.getTerminalModel().getColorPalette()
|
private val colorPalette get() = terminal.getTerminalModel().getColorPalette()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -20,14 +22,7 @@ class OperatingSystemCommandProcessor(terminal: Terminal, reader: TerminalReader
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
|
|
||||||
val c = reader.read()
|
if (systemCommandSequence.process(reader.read())) {
|
||||||
args.append(c)
|
|
||||||
if (c == ControlCharacters.BEL || c == ControlCharacters.ST) {
|
|
||||||
args.deleteAt(args.lastIndex)
|
|
||||||
break
|
|
||||||
} else if (c == '\\' && args.length >= 2 && args[args.length - 2] == ControlCharacters.ESC) {
|
|
||||||
args.deleteAt(args.lastIndex)
|
|
||||||
args.deleteAt(args.lastIndex)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +37,7 @@ class OperatingSystemCommandProcessor(terminal: Terminal, reader: TerminalReader
|
|||||||
// process osc
|
// process osc
|
||||||
processOperatingSystemCommandProcessor()
|
processOperatingSystemCommandProcessor()
|
||||||
|
|
||||||
args.clear()
|
systemCommandSequence.reset()
|
||||||
|
|
||||||
return TerminalState.READY
|
return TerminalState.READY
|
||||||
}
|
}
|
||||||
@@ -52,6 +47,7 @@ class OperatingSystemCommandProcessor(terminal: Terminal, reader: TerminalReader
|
|||||||
* https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
* https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
||||||
*/
|
*/
|
||||||
private fun processOperatingSystemCommandProcessor() {
|
private fun processOperatingSystemCommandProcessor() {
|
||||||
|
val args = systemCommandSequence.getCommand()
|
||||||
val idx = args.indexOfFirst { it == ';' }
|
val idx = args.indexOfFirst { it == ';' }
|
||||||
if (idx == -1) {
|
if (idx == -1) {
|
||||||
return
|
return
|
||||||
@@ -91,6 +87,19 @@ class OperatingSystemCommandProcessor(terminal: Terminal, reader: TerminalReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://iterm2.com/documentation-escape-codes.html
|
||||||
|
1337 -> {
|
||||||
|
val properties = Properties()
|
||||||
|
properties.load(StringReader(suffix))
|
||||||
|
if (properties.containsKey("CurrentDir")) {
|
||||||
|
val currentDir = properties.getProperty("CurrentDir")
|
||||||
|
terminal.getTerminalModel().setData(DataKey.CurrentDir, currentDir)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("CurrentDir: $currentDir")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 11: background color
|
// 11: background color
|
||||||
// 10: foreground color
|
// 10: foreground color
|
||||||
11, 10 -> {
|
11, 10 -> {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.termora.terminal
|
package app.termora.terminal
|
||||||
|
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
|
|
||||||
interface PtyConnector {
|
interface PtyConnector {
|
||||||
@@ -15,15 +16,18 @@ interface PtyConnector {
|
|||||||
*/
|
*/
|
||||||
fun write(buffer: ByteArray, offset: Int, len: Int)
|
fun write(buffer: ByteArray, offset: Int, len: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入数组。
|
||||||
|
*
|
||||||
|
* 如果要写入 String 字符串,请通过 [getCharset] 编码。
|
||||||
|
*/
|
||||||
fun write(buffer: ByteArray) {
|
fun write(buffer: ByteArray) {
|
||||||
write(buffer, 0, buffer.size)
|
write(buffer, 0, buffer.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun write(buffer: String) {
|
/**
|
||||||
if (buffer.isEmpty()) return
|
* 写入单个 Int
|
||||||
write(buffer.toByteArray())
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
fun write(buffer: Int) {
|
fun write(buffer: Int) {
|
||||||
write(ByteBuffer.allocate(Integer.BYTES).putInt(buffer).flip().array())
|
write(ByteBuffer.allocate(Integer.BYTES).putInt(buffer).flip().array())
|
||||||
}
|
}
|
||||||
@@ -43,4 +47,8 @@ interface PtyConnector {
|
|||||||
*/
|
*/
|
||||||
fun close()
|
fun close()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编码
|
||||||
|
*/
|
||||||
|
fun getCharset(): Charset = Charsets.UTF_8
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.termora.terminal
|
package app.termora.terminal
|
||||||
|
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
open class PtyConnectorDelegate(
|
open class PtyConnectorDelegate(
|
||||||
@Volatile var ptyConnector: PtyConnector? = null
|
@Volatile var ptyConnector: PtyConnector? = null
|
||||||
) : PtyConnector {
|
) : PtyConnector {
|
||||||
@@ -26,5 +28,7 @@ open class PtyConnectorDelegate(
|
|||||||
ptyConnector = null
|
ptyConnector = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getCharset(): Charset {
|
||||||
|
return ptyConnector?.getCharset() ?: super.getCharset()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -20,9 +20,6 @@ class PtyProcessConnector(private val process: PtyProcess, private val charset:
|
|||||||
output.flush()
|
output.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun write(buffer: String) {
|
|
||||||
write(buffer.toByteArray(charset))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun resize(rows: Int, cols: Int) {
|
override fun resize(rows: Int, cols: Int) {
|
||||||
process.winSize = WinSize(cols, rows)
|
process.winSize = WinSize(cols, rows)
|
||||||
@@ -38,5 +35,7 @@ class PtyProcessConnector(private val process: PtyProcess, private val charset:
|
|||||||
process.destroyForcibly()
|
process.destroyForcibly()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getCharset(): Charset {
|
||||||
|
return charset
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package app.termora.terminal
|
||||||
|
|
||||||
|
class SystemCommandSequence {
|
||||||
|
private var isTerminated = false
|
||||||
|
private val command = StringBuilder()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return 返回 true 表示处理完毕
|
||||||
|
*/
|
||||||
|
fun process(c: Char): Boolean {
|
||||||
|
|
||||||
|
if (isTerminated) {
|
||||||
|
throw UnsupportedOperationException("Cannot be processed, call the reset method")
|
||||||
|
}
|
||||||
|
|
||||||
|
command.append(c)
|
||||||
|
if (c == ControlCharacters.BEL || c == ControlCharacters.ST) {
|
||||||
|
command.deleteAt(command.lastIndex)
|
||||||
|
isTerminated = true
|
||||||
|
} else if (c == '\\' && command.length >= 2 && command[command.length - 2] == ControlCharacters.ESC) {
|
||||||
|
command.deleteAt(command.lastIndex)
|
||||||
|
command.deleteAt(command.lastIndex)
|
||||||
|
isTerminated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return isTerminated
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCommand(): String {
|
||||||
|
return command.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
isTerminated = false
|
||||||
|
command.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.terminal
|
package app.termora.terminal
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.Toolkit
|
import java.awt.Toolkit
|
||||||
import kotlin.reflect.cast
|
import kotlin.reflect.cast
|
||||||
@@ -8,7 +9,7 @@ open class TerminalModelImpl(private val terminal: Terminal) : TerminalModel {
|
|||||||
private var rows: Int = 27
|
private var rows: Int = 27
|
||||||
private var cols: Int = 80
|
private var cols: Int = 80
|
||||||
private val data = mutableMapOf<DataKey<*>, Any>()
|
private val data = mutableMapOf<DataKey<*>, Any>()
|
||||||
private val listeners = mutableListOf<DataListener>()
|
private var listeners = emptyArray<DataListener>()
|
||||||
private val colorPalette = ColorPaletteImpl(terminal)
|
private val colorPalette = ColorPaletteImpl(terminal)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -92,11 +93,11 @@ open class TerminalModelImpl(private val terminal: Terminal) : TerminalModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun addDataListener(listener: DataListener) {
|
override fun addDataListener(listener: DataListener) {
|
||||||
listeners.add(listener)
|
listeners = ArrayUtils.add(listeners, listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeDataListener(listener: DataListener) {
|
override fun removeDataListener(listener: DataListener) {
|
||||||
listeners.remove(listener)
|
listeners = ArrayUtils.removeElement(listeners, listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bell() {
|
override fun bell() {
|
||||||
@@ -129,9 +130,8 @@ open class TerminalModelImpl(private val terminal: Terminal) : TerminalModel {
|
|||||||
|
|
||||||
@Suppress("MemberVisibilityCanBePrivate")
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
protected fun <T : Any> fireDataChanged(key: DataKey<T>, data: T) {
|
protected fun <T : Any> fireDataChanged(key: DataKey<T>, data: T) {
|
||||||
val size = listeners.size
|
for (listener in listeners) {
|
||||||
for (i in 0 until size) {
|
listener.onChanged(key, data)
|
||||||
listeners.getOrNull(i)?.onChanged(key, data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ private class MyProcessor(private val terminal: Terminal, reader: TerminalReader
|
|||||||
TerminalState.CSI to ControlSequenceIntroducerProcessor(terminal, reader),
|
TerminalState.CSI to ControlSequenceIntroducerProcessor(terminal, reader),
|
||||||
TerminalState.OSC to OperatingSystemCommandProcessor(terminal, reader),
|
TerminalState.OSC to OperatingSystemCommandProcessor(terminal, reader),
|
||||||
TerminalState.ESC_LPAREN to EscapeDesignateCharacterSetProcessor(terminal, reader),
|
TerminalState.ESC_LPAREN to EscapeDesignateCharacterSetProcessor(terminal, reader),
|
||||||
TerminalState.DCS to DeviceControlProcessor(terminal),
|
TerminalState.DCS to DeviceControlStringProcessor(terminal, reader),
|
||||||
TerminalState.Text to TextProcessor(terminal, reader),
|
TerminalState.Text to TextProcessor(terminal, reader),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package app.termora.terminal.panel
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.actions.AnAction
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
|
import com.formdev.flatlaf.ui.FlatRoundBorder
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.event.ActionListener
|
||||||
|
import javax.swing.JButton
|
||||||
|
|
||||||
|
class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||||
|
private val floatingToolbarEnable get() = Database.getDatabase().terminal.floatingToolbar
|
||||||
|
private var closed = false
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val FloatingToolbar = DataKey(FloatingToolbarPanel::class)
|
||||||
|
val isPined get() = pinAction.isSelected
|
||||||
|
|
||||||
|
private val pinAction by lazy {
|
||||||
|
object : AnAction() {
|
||||||
|
private val properties get() = Database.getDatabase().properties
|
||||||
|
private val key = "FloatingToolbar.pined"
|
||||||
|
|
||||||
|
init {
|
||||||
|
setStateAction()
|
||||||
|
isSelected = properties.getString(key, StringUtils.EMPTY).toBoolean()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
isSelected = !isSelected
|
||||||
|
properties.putString(key, isSelected.toString())
|
||||||
|
actionListeners.forEach { it.actionPerformed(evt) }
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
TerminalPanelFactory.getAllTerminalPanel().forEach {
|
||||||
|
it.getData(FloatingToolbar)?.triggerShow()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 触发者的不隐藏
|
||||||
|
val c = evt.getData(FloatingToolbar)
|
||||||
|
TerminalPanelFactory.getAllTerminalPanel().forEach {
|
||||||
|
val e = it.getData(FloatingToolbar)
|
||||||
|
if (c != e) {
|
||||||
|
e?.triggerHide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
border = FlatRoundBorder()
|
||||||
|
isOpaque = false
|
||||||
|
isFocusable = false
|
||||||
|
isFloatable = false
|
||||||
|
isVisible = false
|
||||||
|
|
||||||
|
if (floatingToolbarEnable) {
|
||||||
|
if (pinAction.isSelected) {
|
||||||
|
isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun triggerShow() {
|
||||||
|
if (!floatingToolbarEnable || closed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVisible == false) {
|
||||||
|
isVisible = true
|
||||||
|
firePropertyChange("visible", false, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun triggerHide() {
|
||||||
|
if (floatingToolbarEnable && !closed) {
|
||||||
|
if (pinAction.isSelected) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVisible == true) {
|
||||||
|
isVisible = false
|
||||||
|
firePropertyChange("visible", true, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initActions() {
|
||||||
|
// Pin
|
||||||
|
add(initPinActionButton())
|
||||||
|
|
||||||
|
// 重连
|
||||||
|
add(initReconnectActionButton())
|
||||||
|
|
||||||
|
// 关闭
|
||||||
|
add(initCloseActionButton())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initPinActionButton(): JButton {
|
||||||
|
val btn = JButton(Icons.pin)
|
||||||
|
btn.isSelected = pinAction.isSelected
|
||||||
|
|
||||||
|
val actionListener = ActionListener { btn.isSelected = pinAction.isSelected }
|
||||||
|
pinAction.addActionListener(actionListener)
|
||||||
|
btn.addActionListener(pinAction)
|
||||||
|
|
||||||
|
Disposer.register(this, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
btn.removeActionListener(pinAction)
|
||||||
|
pinAction.removeActionListener(actionListener)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return btn
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initCloseActionButton(): JButton {
|
||||||
|
val btn = JButton(Icons.closeSmall)
|
||||||
|
btn.pressedIcon = Icons.closeSmallHovered
|
||||||
|
btn.rolloverIcon = Icons.closeSmallHovered
|
||||||
|
btn.addActionListener {
|
||||||
|
closed = true
|
||||||
|
triggerHide()
|
||||||
|
}
|
||||||
|
return btn
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initReconnectActionButton(): JButton {
|
||||||
|
val btn = JButton(Icons.refresh)
|
||||||
|
btn.toolTipText = I18n.getString("termora.tabbed.contextmenu.reconnect")
|
||||||
|
|
||||||
|
btn.addActionListener(object : AnAction() {
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
||||||
|
if (tab.canReconnect()) {
|
||||||
|
tab.reconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return btn
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package app.termora.terminal.panel
|
package app.termora.terminal.panel
|
||||||
|
|
||||||
|
import app.termora.Database
|
||||||
import app.termora.DynamicColor
|
import app.termora.DynamicColor
|
||||||
import app.termora.assertEventDispatchThread
|
import app.termora.assertEventDispatchThread
|
||||||
import app.termora.Database
|
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
@@ -49,6 +49,8 @@ class TerminalDisplay(
|
|||||||
init {
|
init {
|
||||||
terminalPanel.addTerminalPaintListener(toaster)
|
terminalPanel.addTerminalPaintListener(toaster)
|
||||||
putClientProperty(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)
|
putClientProperty(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)
|
||||||
|
|
||||||
|
cursor = Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun paint(g: Graphics) {
|
override fun paint(g: Graphics) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.termora.terminal.panel
|
package app.termora.terminal.panel
|
||||||
|
|
||||||
|
import app.termora.Disposable
|
||||||
|
import app.termora.Disposer
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.actions.DataProviderSupport
|
import app.termora.actions.DataProviderSupport
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
@@ -30,7 +32,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||||||
|
|
||||||
|
|
||||||
class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnector) :
|
class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnector) :
|
||||||
JPanel(BorderLayout()), DataProvider {
|
JPanel(BorderLayout()), DataProvider, Disposable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val Debug = DataKey(Boolean::class)
|
val Debug = DataKey(Boolean::class)
|
||||||
@@ -39,10 +41,12 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val terminalFindPanel = TerminalFindPanel(this, terminal)
|
private val terminalFindPanel = TerminalFindPanel(this, terminal)
|
||||||
|
private val floatingToolbar = FloatingToolbarPanel()
|
||||||
private val terminalDisplay = TerminalDisplay(this, terminal)
|
private val terminalDisplay = TerminalDisplay(this, terminal)
|
||||||
val scrollBar = TerminalScrollBar(this@TerminalPanel, terminalFindPanel, terminal)
|
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
|
|
||||||
|
val scrollBar = TerminalScrollBar(this@TerminalPanel, terminalFindPanel, terminal)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 键盘事件
|
* 键盘事件
|
||||||
@@ -116,6 +120,7 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
|||||||
val layeredPane = TerminalLayeredPane()
|
val layeredPane = TerminalLayeredPane()
|
||||||
layeredPane.add(terminalDisplay, JLayeredPane.DEFAULT_LAYER as Any)
|
layeredPane.add(terminalDisplay, JLayeredPane.DEFAULT_LAYER as Any)
|
||||||
layeredPane.add(terminalFindPanel, JLayeredPane.POPUP_LAYER as Any)
|
layeredPane.add(terminalFindPanel, JLayeredPane.POPUP_LAYER as Any)
|
||||||
|
layeredPane.add(floatingToolbar, JLayeredPane.POPUP_LAYER as Any)
|
||||||
add(layeredPane, BorderLayout.CENTER)
|
add(layeredPane, BorderLayout.CENTER)
|
||||||
add(scrollBar, BorderLayout.EAST)
|
add(scrollBar, BorderLayout.EAST)
|
||||||
|
|
||||||
@@ -126,6 +131,7 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
|||||||
dataProviderSupport.addData(DataProviders.TerminalPanel, this)
|
dataProviderSupport.addData(DataProviders.TerminalPanel, this)
|
||||||
dataProviderSupport.addData(DataProviders.Terminal, terminal)
|
dataProviderSupport.addData(DataProviders.Terminal, terminal)
|
||||||
dataProviderSupport.addData(DataProviders.PtyConnector, ptyConnector)
|
dataProviderSupport.addData(DataProviders.PtyConnector, ptyConnector)
|
||||||
|
dataProviderSupport.addData(FloatingToolbarPanel.FloatingToolbar, floatingToolbar)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
@@ -157,6 +163,11 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
|||||||
this.addMouseListener(trackingAdapter)
|
this.addMouseListener(trackingAdapter)
|
||||||
this.addMouseWheelListener(trackingAdapter)
|
this.addMouseWheelListener(trackingAdapter)
|
||||||
|
|
||||||
|
// 悬浮工具栏
|
||||||
|
val floatingToolBarAdapter = TerminalPanelMouseFloatingToolBarAdapter(this, terminalDisplay)
|
||||||
|
this.addMouseMotionListener(floatingToolBarAdapter)
|
||||||
|
this.addMouseListener(floatingToolBarAdapter)
|
||||||
|
|
||||||
// 滚动相关
|
// 滚动相关
|
||||||
this.addMouseWheelListener(object : MouseWheelListener {
|
this.addMouseWheelListener(object : MouseWheelListener {
|
||||||
override fun mouseWheelMoved(e: MouseWheelEvent) {
|
override fun mouseWheelMoved(e: MouseWheelEvent) {
|
||||||
@@ -196,6 +207,8 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
|||||||
// 开启拖拽
|
// 开启拖拽
|
||||||
enableDropTarget()
|
enableDropTarget()
|
||||||
|
|
||||||
|
// 监听悬浮工具栏变化,然后重新渲染
|
||||||
|
floatingToolbar.addPropertyChangeListener { repaintImmediate() }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +311,7 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
|||||||
|
|
||||||
// 输入法提交
|
// 输入法提交
|
||||||
if (committedCharacterCount > 0) {
|
if (committedCharacterCount > 0) {
|
||||||
ptyConnector.write(sb.toString())
|
ptyConnector.write(sb.toString().toByteArray(ptyConnector.getCharset()))
|
||||||
} else {
|
} else {
|
||||||
val breakIterator = BreakIterator.getCharacterInstance()
|
val breakIterator = BreakIterator.getCharacterInstance()
|
||||||
val chars = mutableListOf<Char>()
|
val chars = mutableListOf<Char>()
|
||||||
@@ -372,6 +385,9 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
Disposer.dispose(floatingToolbar)
|
||||||
|
}
|
||||||
|
|
||||||
fun getAverageCharWidth(): Int {
|
fun getAverageCharWidth(): Int {
|
||||||
return terminalDisplay.getAverageCharWidth()
|
return terminalDisplay.getAverageCharWidth()
|
||||||
@@ -397,16 +413,20 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
|||||||
* 执行粘贴操作
|
* 执行粘贴操作
|
||||||
*/
|
*/
|
||||||
fun paste(text: String) {
|
fun paste(text: String) {
|
||||||
val content = if (SystemInfo.isWindows) {
|
var content = text
|
||||||
text.replace("${ControlCharacters.CR}${ControlCharacters.LF}", "${ControlCharacters.LF}")
|
if (!SystemInfo.isWindows) {
|
||||||
} else {
|
content = content.replace("\r\n", "\n")
|
||||||
text.replace(ControlCharacters.LF, ControlCharacters.CR)
|
|
||||||
}
|
}
|
||||||
|
content = content.replace('\n', '\r')
|
||||||
|
|
||||||
if (terminal.getTerminalModel().getData(DataKey.BracketedPasteMode, false)) {
|
if (terminal.getTerminalModel().getData(DataKey.BracketedPasteMode, false)) {
|
||||||
ptyConnector.write("${ControlCharacters.ESC}[200~${content}${ControlCharacters.ESC}[201~")
|
ptyConnector.write(
|
||||||
|
"${ControlCharacters.ESC}[200~${content}${ControlCharacters.ESC}[201~".toByteArray(
|
||||||
|
ptyConnector.getCharset()
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
ptyConnector.write(content)
|
ptyConnector.write(content.toByteArray(ptyConnector.getCharset()))
|
||||||
}
|
}
|
||||||
|
|
||||||
terminal.getScrollingModel().scrollToRow(
|
terminal.getScrollingModel().scrollToRow(
|
||||||
@@ -445,6 +465,7 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
|||||||
synchronized(treeLock) {
|
synchronized(treeLock) {
|
||||||
val w = width
|
val w = width
|
||||||
val h = height
|
val h = height
|
||||||
|
val findPanelHeight = max(terminalFindPanel.preferredSize.height, terminalFindPanel.height)
|
||||||
for (c in components) {
|
for (c in components) {
|
||||||
when (c) {
|
when (c) {
|
||||||
terminalDisplay -> {
|
terminalDisplay -> {
|
||||||
@@ -462,7 +483,19 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
|||||||
w - width,
|
w - width,
|
||||||
0,
|
0,
|
||||||
width,
|
width,
|
||||||
max(terminalFindPanel.preferredSize.height, terminalFindPanel.height)
|
findPanelHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
floatingToolbar -> {
|
||||||
|
val width = floatingToolbar.preferredSize.width
|
||||||
|
val height = floatingToolbar.preferredSize.height
|
||||||
|
val y = 4
|
||||||
|
c.setBounds(
|
||||||
|
w - width,
|
||||||
|
if (terminalFindPanel.isVisible) findPanelHeight + y else y,
|
||||||
|
width,
|
||||||
|
height
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.termora.terminal.panel
|
package app.termora.terminal.panel
|
||||||
|
|
||||||
|
import app.termora.keymap.KeyShortcut
|
||||||
|
import app.termora.keymap.KeymapManager
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
import app.termora.terminal.Terminal
|
import app.termora.terminal.Terminal
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
@@ -12,8 +14,9 @@ class TerminalPanelKeyAdapter(
|
|||||||
private val terminalPanel: TerminalPanel,
|
private val terminalPanel: TerminalPanel,
|
||||||
private val terminal: Terminal,
|
private val terminal: Terminal,
|
||||||
private val ptyConnector: PtyConnector
|
private val ptyConnector: PtyConnector
|
||||||
) :
|
) : KeyAdapter() {
|
||||||
KeyAdapter() {
|
|
||||||
|
private val activeKeymap get() = KeymapManager.getInstance().getActiveKeymap()
|
||||||
|
|
||||||
override fun keyTyped(e: KeyEvent) {
|
override fun keyTyped(e: KeyEvent) {
|
||||||
if (Character.isISOControl(e.keyChar)) {
|
if (Character.isISOControl(e.keyChar)) {
|
||||||
@@ -21,7 +24,7 @@ class TerminalPanelKeyAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
terminal.getSelectionModel().clearSelection()
|
terminal.getSelectionModel().clearSelection()
|
||||||
ptyConnector.write("${e.keyChar}")
|
ptyConnector.write("${e.keyChar}".toByteArray(ptyConnector.getCharset()))
|
||||||
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -44,7 +47,7 @@ class TerminalPanelKeyAdapter(
|
|||||||
|
|
||||||
val encode = terminal.getKeyEncoder().encode(AWTTerminalKeyEvent(e))
|
val encode = terminal.getKeyEncoder().encode(AWTTerminalKeyEvent(e))
|
||||||
if (encode.isNotEmpty()) {
|
if (encode.isNotEmpty()) {
|
||||||
ptyConnector.write(encode)
|
ptyConnector.write(encode.toByteArray(ptyConnector.getCharset()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/TermoraDev/termora/issues/52
|
// https://github.com/TermoraDev/termora/issues/52
|
||||||
@@ -52,11 +55,16 @@ class TerminalPanelKeyAdapter(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果命中了全局快捷键,那么不处理
|
||||||
|
if (keyStroke.modifiers != 0 && activeKeymap.getActionIds(KeyShortcut(keyStroke)).isNotEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (Character.isISOControl(e.keyChar)) {
|
if (Character.isISOControl(e.keyChar)) {
|
||||||
terminal.getSelectionModel().clearSelection()
|
terminal.getSelectionModel().clearSelection()
|
||||||
// 如果不为空表示已经发送过了,所以这里为空的时候再发送
|
// 如果不为空表示已经发送过了,所以这里为空的时候再发送
|
||||||
if (encode.isEmpty()) {
|
if (encode.isEmpty()) {
|
||||||
ptyConnector.write("${e.keyChar}")
|
ptyConnector.write("${e.keyChar}".toByteArray(ptyConnector.getCharset()))
|
||||||
}
|
}
|
||||||
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package app.termora.terminal.panel
|
||||||
|
|
||||||
|
import app.termora.Database
|
||||||
|
import java.awt.Rectangle
|
||||||
|
import java.awt.event.MouseAdapter
|
||||||
|
import java.awt.event.MouseEvent
|
||||||
|
|
||||||
|
class TerminalPanelMouseFloatingToolBarAdapter(
|
||||||
|
private val terminalPanel: TerminalPanel,
|
||||||
|
private val terminalDisplay: TerminalDisplay
|
||||||
|
) : MouseAdapter() {
|
||||||
|
|
||||||
|
private val floatingToolbarEnable get() = Database.getDatabase().terminal.floatingToolbar
|
||||||
|
|
||||||
|
override fun mouseMoved(e: MouseEvent) {
|
||||||
|
if (!floatingToolbarEnable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val floatingToolbar = terminalPanel.getData(FloatingToolbarPanel.FloatingToolbar) ?: return
|
||||||
|
val width = terminalPanel.width
|
||||||
|
val height = terminalPanel.height
|
||||||
|
val widthDiff = (width * 0.25).toInt()
|
||||||
|
val heightDiff = (height * 0.25).toInt()
|
||||||
|
|
||||||
|
if (e.x in width - widthDiff..width && e.y in 0..heightDiff) {
|
||||||
|
floatingToolbar.triggerShow()
|
||||||
|
} else {
|
||||||
|
floatingToolbar.triggerHide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseExited(e: MouseEvent) {
|
||||||
|
val floatingToolbar = terminalPanel.getData(FloatingToolbarPanel.FloatingToolbar) ?: return
|
||||||
|
|
||||||
|
if (terminalDisplay.isShowing) {
|
||||||
|
val rectangle = Rectangle(terminalDisplay.locationOnScreen, terminalDisplay.size)
|
||||||
|
// 如果鼠标指针还在 terminalDisplay 中,那么就不需要隐藏
|
||||||
|
if (rectangle.contains(e.locationOnScreen)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
floatingToolbar.triggerHide()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -70,9 +70,9 @@ class TerminalPanelMouseTrackingAdapter(
|
|||||||
val encode = terminal.getKeyEncoder()
|
val encode = terminal.getKeyEncoder()
|
||||||
.encode(TerminalKeyEvent(if (e.wheelRotation < 0) KeyEvent.VK_UP else KeyEvent.VK_DOWN))
|
.encode(TerminalKeyEvent(if (e.wheelRotation < 0) KeyEvent.VK_UP else KeyEvent.VK_DOWN))
|
||||||
if (encode.isBlank()) return
|
if (encode.isBlank()) return
|
||||||
|
val bytes = encode.toByteArray(ptyConnector.getCharset())
|
||||||
for (i in 0 until abs(unitsToScroll)) {
|
for (i in 0 until abs(unitsToScroll)) {
|
||||||
ptyConnector.write(encode)
|
ptyConnector.write(bytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import java.io.FileNotFoundException
|
|||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
|
|
||||||
class LogViewerTerminalTab(windowScope: WindowScope, private val file: File) : PtyHostTerminalTab(
|
class LogViewerTerminalTab(
|
||||||
|
windowScope: WindowScope,
|
||||||
|
private val file: File,
|
||||||
|
) : PtyHostTerminalTab(
|
||||||
windowScope,
|
windowScope,
|
||||||
Host(
|
Host(
|
||||||
name = file.name,
|
name = file.name,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app.termora.transport
|
|||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.SettingsAction
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
@@ -12,6 +13,7 @@ import com.formdev.flatlaf.util.SystemInfo
|
|||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.io.file.PathUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
@@ -19,25 +21,30 @@ import org.apache.sshd.sftp.client.SftpClient
|
|||||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
||||||
import org.apache.sshd.sftp.client.fs.SftpPath
|
import org.apache.sshd.sftp.client.fs.SftpPath
|
||||||
import org.jdesktop.swingx.JXBusyLabel
|
import org.jdesktop.swingx.JXBusyLabel
|
||||||
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
import java.awt.datatransfer.DataFlavor
|
import java.awt.datatransfer.DataFlavor
|
||||||
import java.awt.datatransfer.StringSelection
|
import java.awt.datatransfer.StringSelection
|
||||||
import java.awt.dnd.DnDConstants
|
import java.awt.datatransfer.Transferable
|
||||||
import java.awt.dnd.DropTarget
|
import java.awt.datatransfer.UnsupportedFlavorException
|
||||||
import java.awt.dnd.DropTargetDropEvent
|
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.file.*
|
import java.nio.file.*
|
||||||
|
import java.text.MessageFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.regex.Pattern
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.table.DefaultTableCellRenderer
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
import kotlin.io.path.exists
|
import kotlin.io.path.exists
|
||||||
|
import kotlin.io.path.getLastModifiedTime
|
||||||
import kotlin.io.path.isDirectory
|
import kotlin.io.path.isDirectory
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,9 +52,8 @@ import kotlin.io.path.isDirectory
|
|||||||
*/
|
*/
|
||||||
class FileSystemPanel(
|
class FileSystemPanel(
|
||||||
private val fileSystem: FileSystem,
|
private val fileSystem: FileSystem,
|
||||||
private val transportManager: TransportManager,
|
|
||||||
private val host: Host
|
private val host: Host
|
||||||
) : JPanel(BorderLayout()), Disposable, FileSystemTransportListener.Provider {
|
) : JPanel(BorderLayout()), Disposable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(FileSystemPanel::class.java)
|
private val log = LoggerFactory.getLogger(FileSystemPanel::class.java)
|
||||||
@@ -65,6 +71,14 @@ class FileSystemPanel(
|
|||||||
private val showHiddenFilesBtn = JButton(Icons.eyeClose)
|
private val showHiddenFilesBtn = JButton(Icons.eyeClose)
|
||||||
private val properties get() = Database.getDatabase().properties
|
private val properties get() = Database.getDatabase().properties
|
||||||
private val showHiddenFilesKey by lazy { "termora.transport.host.${host.id}.show-hidden-files" }
|
private val showHiddenFilesKey by lazy { "termora.transport.host.${host.id}.show-hidden-files" }
|
||||||
|
private val evt by lazy { AnActionEvent(this, StringUtils.EMPTY, EventObject(this)) }
|
||||||
|
private val sftp get() = Database.getDatabase().sftp
|
||||||
|
private val actionManager get() = ActionManager.getInstance()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit
|
||||||
|
*/
|
||||||
|
private val coroutineScope by lazy { CoroutineScope(Dispatchers.IO + SupervisorJob()) }
|
||||||
|
|
||||||
val workdir get() = tableModel.workdir
|
val workdir get() = tableModel.workdir
|
||||||
|
|
||||||
@@ -80,6 +94,8 @@ class FileSystemPanel(
|
|||||||
bookmarkBtn.isBookmark = bookmarkBtn.getBookmarks().contains(workdir.toString())
|
bookmarkBtn.isBookmark = bookmarkBtn.getBookmarks().contains(workdir.toString())
|
||||||
|
|
||||||
table.setUI(FlatTableUI())
|
table.setUI(FlatTableUI())
|
||||||
|
table.dragEnabled = true
|
||||||
|
table.dropMode = DropMode.INSERT_ROWS
|
||||||
table.rowHeight = UIManager.getInt("Table.rowHeight")
|
table.rowHeight = UIManager.getInt("Table.rowHeight")
|
||||||
table.autoResizeMode = JTable.AUTO_RESIZE_OFF
|
table.autoResizeMode = JTable.AUTO_RESIZE_OFF
|
||||||
table.fillsViewportHeight = true
|
table.fillsViewportHeight = true
|
||||||
@@ -231,17 +247,45 @@ class FileSystemPanel(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 本地文件系统不支持本地拖拽进去
|
|
||||||
if (!tableModel.isLocalFileSystem) {
|
table.transferHandler = object : TransferHandler() {
|
||||||
table.dropTarget = object : DropTarget() {
|
override fun canImport(support: TransferSupport): Boolean {
|
||||||
override fun drop(dtde: DropTargetDropEvent) {
|
if (support.isDataFlavorSupported(FileSystemTableRowTransferable.dataFlavor)) {
|
||||||
dtde.acceptDrop(DnDConstants.ACTION_COPY)
|
val data = support.transferable.getTransferData(FileSystemTableRowTransferable.dataFlavor)
|
||||||
val files = dtde.transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*>
|
return data is FileSystemTableRowTransferable && data.fileSystemPanel != this@FileSystemPanel
|
||||||
if (files.isEmpty()) return
|
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||||
copyLocalFileToFileSystem(files.filterIsInstance<File>())
|
return !tableModel.isLocalFileSystem
|
||||||
}
|
}
|
||||||
}.apply {
|
return false
|
||||||
this.defaultActions = DnDConstants.ACTION_COPY
|
}
|
||||||
|
|
||||||
|
override fun importData(comp: JComponent?, t: Transferable): Boolean {
|
||||||
|
if (t.isDataFlavorSupported(FileSystemTableRowTransferable.dataFlavor)) {
|
||||||
|
val data = t.getTransferData(FileSystemTableRowTransferable.dataFlavor)
|
||||||
|
if (data !is FileSystemTableRowTransferable) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
data.fileSystemPanel.transport(data.paths)
|
||||||
|
return true
|
||||||
|
} else if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||||
|
val files = t.getTransferData(DataFlavor.javaFileListFlavor) as List<*>
|
||||||
|
if (files.isEmpty()) return false
|
||||||
|
copyLocalFileToFileSystem(files.filterIsInstance<File>())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSourceActions(c: JComponent?): Int {
|
||||||
|
return COPY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createTransferable(c: JComponent?): Transferable? {
|
||||||
|
val paths = table.selectedRows.filter { it != 0 }.map { tableModel.getCacheablePath(it) }
|
||||||
|
if (paths.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return FileSystemTableRowTransferable(this@FileSystemPanel, paths)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +357,9 @@ class FileSystemPanel(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
coroutineScope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
private fun copyLocalFileToFileSystem(files: List<File>) {
|
private fun copyLocalFileToFileSystem(files: List<File>) {
|
||||||
val event = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
val event = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||||
@@ -396,14 +443,6 @@ class FileSystemPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun addFileSystemTransportListener(listener: FileSystemTransportListener) {
|
|
||||||
listenerList.add(FileSystemTransportListener::class.java, listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removeFileSystemTransportListener(listener: FileSystemTransportListener) {
|
|
||||||
listenerList.remove(FileSystemTransportListener::class.java, listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openFolder() {
|
private fun openFolder() {
|
||||||
val row = table.selectedRow
|
val row = table.selectedRow
|
||||||
if (row < 0) return
|
if (row < 0) return
|
||||||
@@ -431,6 +470,7 @@ class FileSystemPanel(
|
|||||||
|
|
||||||
|
|
||||||
private fun showContextMenu(rows: IntArray, event: MouseEvent) {
|
private fun showContextMenu(rows: IntArray, event: MouseEvent) {
|
||||||
|
val paths = rows.filter { it != 0 }.map { tableModel.getCacheablePath(it) }
|
||||||
val popupMenu = FlatPopupMenu()
|
val popupMenu = FlatPopupMenu()
|
||||||
val newMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.new"))
|
val newMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.new"))
|
||||||
|
|
||||||
@@ -448,11 +488,22 @@ class FileSystemPanel(
|
|||||||
// 传输
|
// 传输
|
||||||
val transfer = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.transfer"))
|
val transfer = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.transfer"))
|
||||||
transfer.addActionListener {
|
transfer.addActionListener {
|
||||||
val paths = rows.filter { it != 0 }.map { tableModel.getCacheablePath(it) }
|
|
||||||
if (paths.isNotEmpty()) {
|
if (paths.isNotEmpty()) {
|
||||||
transport(paths)
|
transport(paths)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
val edit = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.edit"))
|
||||||
|
// 不是本地文件系统 & 包含文件
|
||||||
|
edit.isEnabled = !tableModel.isLocalFileSystem && paths.any { !it.isDirectory }
|
||||||
|
edit.addActionListener {
|
||||||
|
val files = paths.filter { !it.isDirectory }
|
||||||
|
if (files.isNotEmpty()) {
|
||||||
|
editFiles(files)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
|
|
||||||
// 复制路径
|
// 复制路径
|
||||||
@@ -545,6 +596,127 @@ class FileSystemPanel(
|
|||||||
popupMenu.show(table, event.x, event.y)
|
popupMenu.show(table, event.x, event.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun editFiles(files: List<FileSystemTableModel.CacheablePath>) {
|
||||||
|
if (files.isEmpty()) return
|
||||||
|
val transportManager = evt.getData(TransportDataProviders.TransportManager) ?: return
|
||||||
|
|
||||||
|
if (SystemInfo.isLinux) {
|
||||||
|
if (sftp.editCommand.isBlank()) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
I18n.getString("termora.transport.table.contextmenu.edit-command"),
|
||||||
|
messageType = JOptionPane.INFORMATION_MESSAGE
|
||||||
|
)
|
||||||
|
actionManager.getAction(SettingsAction.SETTING)
|
||||||
|
?.actionPerformed(AnActionEvent(this, StringUtils.EMPTY, EventObject(this)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val temporary = Application.getTemporaryDir().toPath()
|
||||||
|
|
||||||
|
for (file in files) {
|
||||||
|
val dir = Files.createTempDirectory(temporary, "termora-")
|
||||||
|
val path = Paths.get(dir.absolutePathString(), file.fileName)
|
||||||
|
transportManager.addTransport(
|
||||||
|
transport = FileTransport(
|
||||||
|
name = file.fileName,
|
||||||
|
source = file.path,
|
||||||
|
target = path,
|
||||||
|
sourceHolder = this,
|
||||||
|
targetHolder = this,
|
||||||
|
listener = editFileTransportListener(file.path, path)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun editFileTransportListener(source: Path, localPath: Path): TransportListener {
|
||||||
|
return object : TransportListener {
|
||||||
|
override fun onTransportChanged(transport: Transport) {
|
||||||
|
// 传输成功
|
||||||
|
if (transport.state == TransportState.Done) {
|
||||||
|
val transportManager = evt.getData(TransportDataProviders.TransportManager) ?: return
|
||||||
|
var lastModifiedTime = localPath.getLastModifiedTime().toMillis()
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (sftp.editCommand.isNotBlank()) {
|
||||||
|
ProcessBuilder(
|
||||||
|
parseCommand(
|
||||||
|
MessageFormat.format(
|
||||||
|
sftp.editCommand,
|
||||||
|
localPath.absolutePathString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).start()
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
ProcessBuilder("open", "-a", "TextEdit", localPath.absolutePathString()).start()
|
||||||
|
} else if (SystemInfo.isWindows) {
|
||||||
|
ProcessBuilder("notepad", localPath.absolutePathString()).start()
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
while (coroutineScope.isActive) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (!Files.exists(localPath)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
val nowModifiedTime = localPath.getLastModifiedTime().toMillis()
|
||||||
|
if (nowModifiedTime != lastModifiedTime) {
|
||||||
|
lastModifiedTime = nowModifiedTime
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
// upload
|
||||||
|
transportManager.addTransport(
|
||||||
|
transport = FileTransport(
|
||||||
|
name = PathUtils.getFileNameString(localPath.fileName),
|
||||||
|
source = localPath,
|
||||||
|
target = source,
|
||||||
|
sourceHolder = this@FileSystemPanel,
|
||||||
|
targetHolder = this@FileSystemPanel,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(250.milliseconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseCommand(command: String): List<String> {
|
||||||
|
val result = mutableListOf<String>()
|
||||||
|
val matcher = Pattern.compile("\"([^\"]*)\"|(\\S+)").matcher(command)
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
if (matcher.group(1) != null) {
|
||||||
|
result.add(matcher.group(1)) // 处理双引号部分
|
||||||
|
} else {
|
||||||
|
result.add(matcher.group(2).replace("\\\\ ", " "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
private fun renamePath(path: Path) {
|
private fun renamePath(path: Path) {
|
||||||
@@ -760,17 +932,31 @@ class FileSystemPanel(
|
|||||||
|
|
||||||
private suspend fun doTransport(paths: List<FileSystemTableModel.CacheablePath>) {
|
private suspend fun doTransport(paths: List<FileSystemTableModel.CacheablePath>) {
|
||||||
if (paths.isEmpty()) return
|
if (paths.isEmpty()) return
|
||||||
|
val transportPanel = evt.getData(TransportDataProviders.TransportPanel) ?: return
|
||||||
val listeners = listenerList.getListeners(FileSystemTransportListener::class.java)
|
val leftFileSystemPanel = evt.getData(TransportDataProviders.LeftFileSystemPanel) ?: return
|
||||||
if (listeners.isEmpty()) return
|
val rightFileSystemPanel = evt.getData(TransportDataProviders.RightFileSystemPanel) ?: return
|
||||||
|
val sourceFileSystemPanel = this
|
||||||
|
val targetFileSystemPanel = if (this == leftFileSystemPanel) rightFileSystemPanel else leftFileSystemPanel
|
||||||
|
|
||||||
// 收集数据
|
// 收集数据
|
||||||
for (e in paths) {
|
for (e in paths) {
|
||||||
|
|
||||||
if (!e.isDirectory) {
|
if (!e.isDirectory) {
|
||||||
|
val job = TransportJob(
|
||||||
|
fileSystemPanel = this,
|
||||||
|
workdir = workdir,
|
||||||
|
isDirectory = false,
|
||||||
|
path = e.path,
|
||||||
|
)
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
listeners.forEach { it.transport(this@FileSystemPanel, workdir, false, e.path) }
|
transportPanel.transport(
|
||||||
|
sourceWorkdir = workdir,
|
||||||
|
targetWorkdir = targetFileSystemPanel.workdir,
|
||||||
|
isSourceDirectory = false,
|
||||||
|
sourcePath = e.path,
|
||||||
|
sourceHolder = sourceFileSystemPanel,
|
||||||
|
targetHolder = targetFileSystemPanel
|
||||||
|
)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -782,12 +968,26 @@ class FileSystemPanel(
|
|||||||
val isDirectory = if (path.attributes != null)
|
val isDirectory = if (path.attributes != null)
|
||||||
path.attributes.isDirectory else path.isDirectory()
|
path.attributes.isDirectory else path.isDirectory()
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
listeners.forEach { it.transport(this@FileSystemPanel, workdir, isDirectory, path) }
|
transportPanel.transport(
|
||||||
|
sourceWorkdir = workdir,
|
||||||
|
targetWorkdir = targetFileSystemPanel.workdir,
|
||||||
|
isSourceDirectory = isDirectory,
|
||||||
|
sourcePath = path,
|
||||||
|
sourceHolder = sourceFileSystemPanel,
|
||||||
|
targetHolder = targetFileSystemPanel
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val isDirectory = path.isDirectory()
|
val isDirectory = path.isDirectory()
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
listeners.forEach { it.transport(this@FileSystemPanel, workdir, isDirectory, path) }
|
transportPanel.transport(
|
||||||
|
sourceWorkdir = workdir,
|
||||||
|
targetWorkdir = targetFileSystemPanel.workdir,
|
||||||
|
isSourceDirectory = isDirectory,
|
||||||
|
sourcePath = path,
|
||||||
|
sourceHolder = sourceFileSystemPanel,
|
||||||
|
targetHolder = targetFileSystemPanel
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -838,4 +1038,28 @@ class FileSystemPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class FileSystemTableRowTransferable(
|
||||||
|
val fileSystemPanel: FileSystemPanel,
|
||||||
|
val paths: List<FileSystemTableModel.CacheablePath>
|
||||||
|
) : Transferable {
|
||||||
|
companion object {
|
||||||
|
val dataFlavor = DataFlavor(FileSystemTableRowTransferable::class.java, "TableRowTransferable")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTransferDataFlavors(): Array<DataFlavor> {
|
||||||
|
return arrayOf(dataFlavor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isDataFlavorSupported(flavor: DataFlavor?): Boolean {
|
||||||
|
return flavor == dataFlavor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTransferData(flavor: DataFlavor?): Any {
|
||||||
|
if (flavor != dataFlavor) {
|
||||||
|
throw UnsupportedFlavorException(flavor)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,9 @@ package app.termora.transport
|
|||||||
import app.termora.*
|
import app.termora.*
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.Component
|
||||||
import java.awt.Point
|
import java.awt.Point
|
||||||
import java.nio.file.FileSystems
|
import java.nio.file.FileSystems
|
||||||
import java.nio.file.Path
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
@@ -13,9 +13,8 @@ import kotlin.math.max
|
|||||||
class FileSystemTabbed(
|
class FileSystemTabbed(
|
||||||
private val transportManager: TransportManager,
|
private val transportManager: TransportManager,
|
||||||
private val isLeft: Boolean = false
|
private val isLeft: Boolean = false
|
||||||
) : FlatTabbedPane(), FileSystemTransportListener.Provider, Disposable {
|
) : FlatTabbedPane(), Disposable {
|
||||||
private val addBtn = JButton(Icons.add)
|
private val addBtn = JButton(Icons.add)
|
||||||
private val listeners = mutableListOf<FileSystemTransportListener>()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -36,23 +35,20 @@ class FileSystemTabbed(
|
|||||||
trailingComponent = toolbar
|
trailingComponent = toolbar
|
||||||
|
|
||||||
if (isLeft) {
|
if (isLeft) {
|
||||||
addFileSystemTransportProvider(
|
addTab(
|
||||||
I18n.getString("termora.transport.local"),
|
I18n.getString("termora.transport.local"), FileSystemPanel(
|
||||||
FileSystemPanel(
|
|
||||||
FileSystems.getDefault(),
|
FileSystems.getDefault(),
|
||||||
transportManager,
|
|
||||||
host = Host(
|
host = Host(
|
||||||
id = "local",
|
id = "local",
|
||||||
name = I18n.getString("termora.transport.local"),
|
name = I18n.getString("termora.transport.local"),
|
||||||
protocol = Protocol.Local,
|
protocol = Protocol.Local,
|
||||||
)
|
)
|
||||||
).apply { reload() }
|
).apply { reload() })
|
||||||
)
|
|
||||||
setTabClosable(0, false)
|
setTabClosable(0, false)
|
||||||
} else {
|
} else {
|
||||||
addFileSystemTransportProvider(
|
addTab(
|
||||||
I18n.getString("termora.transport.sftp.select-host"),
|
I18n.getString("termora.transport.sftp.select-host"),
|
||||||
SftpFileSystemPanel(transportManager)
|
SftpFileSystemPanel()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,16 +58,15 @@ class FileSystemTabbed(
|
|||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
addBtn.addActionListener {
|
addBtn.addActionListener {
|
||||||
val dialog = HostTreeDialog(SwingUtilities.getWindowAncestor(this))
|
val dialog = HostTreeDialog(SwingUtilities.getWindowAncestor(this))
|
||||||
|
|
||||||
dialog.location = Point(
|
dialog.location = Point(
|
||||||
addBtn.locationOnScreen.x - dialog.width / 2 + addBtn.width / 2,
|
max(0, addBtn.locationOnScreen.x - dialog.width / 2 + addBtn.width / 2),
|
||||||
addBtn.locationOnScreen.y + max(tabHeight, addBtn.height)
|
addBtn.locationOnScreen.y + max(tabHeight, addBtn.height)
|
||||||
)
|
)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
|
|
||||||
for (host in dialog.hosts) {
|
for (host in dialog.hosts) {
|
||||||
val panel = SftpFileSystemPanel(transportManager, host)
|
val panel = SftpFileSystemPanel(host)
|
||||||
addFileSystemTransportProvider(host.name, panel)
|
addTab(host.name, panel)
|
||||||
panel.connect()
|
panel.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,9 +115,9 @@ class FileSystemTabbed(
|
|||||||
|
|
||||||
if (tabCount == 0) {
|
if (tabCount == 0) {
|
||||||
if (!isLeft) {
|
if (!isLeft) {
|
||||||
addFileSystemTransportProvider(
|
addTab(
|
||||||
I18n.getString("termora.transport.sftp.select-host"),
|
I18n.getString("termora.transport.sftp.select-host"),
|
||||||
SftpFileSystemPanel(transportManager)
|
SftpFileSystemPanel()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,39 +125,31 @@ class FileSystemTabbed(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addFileSystemTransportProvider(title: String, provider: FileSystemTransportListener.Provider) {
|
override fun addTab(title: String, component: Component) {
|
||||||
if (provider !is JComponent) {
|
super.addTab(title, component)
|
||||||
throw IllegalArgumentException("Provider is not an JComponent")
|
|
||||||
}
|
|
||||||
|
|
||||||
provider.addFileSystemTransportListener(object : FileSystemTransportListener {
|
selectedIndex = tabCount - 1
|
||||||
override fun transport(fileSystemPanel: FileSystemPanel, workdir: Path, isDirectory: Boolean, path: Path) {
|
|
||||||
listeners.forEach { it.transport(fileSystemPanel, workdir, isDirectory, path) }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 修改 Tab名称
|
if (component is SftpFileSystemPanel) {
|
||||||
provider.addPropertyChangeListener("TabName") { e ->
|
component.addPropertyChangeListener("TabName") { e ->
|
||||||
SwingUtilities.invokeLater {
|
SwingUtilities.invokeLater {
|
||||||
val name = StringUtils.defaultIfEmpty(
|
val name = StringUtils.defaultIfEmpty(
|
||||||
e.newValue.toString(),
|
e.newValue.toString(),
|
||||||
I18n.getString("termora.transport.sftp.select-host")
|
I18n.getString("termora.transport.sftp.select-host")
|
||||||
)
|
)
|
||||||
for (i in 0 until tabCount) {
|
for (i in 0 until tabCount) {
|
||||||
if (getComponentAt(i) == provider) {
|
if (getComponentAt(i) == component) {
|
||||||
setTitleAt(i, name)
|
setTitleAt(i, name)
|
||||||
break
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addTab(title, provider)
|
|
||||||
|
|
||||||
if (tabCount > 0)
|
|
||||||
selectedIndex = tabCount - 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getSelectedFileSystemPanel(): FileSystemPanel? {
|
fun getSelectedFileSystemPanel(): FileSystemPanel? {
|
||||||
return getFileSystemPanel(selectedIndex)
|
return getFileSystemPanel(selectedIndex)
|
||||||
}
|
}
|
||||||
@@ -184,14 +171,6 @@ class FileSystemTabbed(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addFileSystemTransportListener(listener: FileSystemTransportListener) {
|
|
||||||
listeners.add(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removeFileSystemTransportListener(listener: FileSystemTransportListener) {
|
|
||||||
listeners.remove(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
while (tabCount > 0) {
|
while (tabCount > 0) {
|
||||||
val c = getComponentAt(0)
|
val c = getComponentAt(0)
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ class FileSystemTableModel(private val fileSystem: FileSystem) : DefaultTableMod
|
|||||||
when (column) {
|
when (column) {
|
||||||
COLUMN_NAME -> path
|
COLUMN_NAME -> path
|
||||||
COLUMN_FILE_SIZE -> if (path.isDirectory) StringUtils.EMPTY else formatBytes(path.fileSize)
|
COLUMN_FILE_SIZE -> if (path.isDirectory) StringUtils.EMPTY else formatBytes(path.fileSize)
|
||||||
COLUMN_TYPE -> if (path.isDirectory) I18n.getString("termora.transport.table.type.folder") else path.extension
|
COLUMN_TYPE -> if (path.isDirectory) I18n.getString("termora.transport.table.type.folder")
|
||||||
|
else if (path.isSymbolicLink) I18n.getString("termora.transport.table.type.symbolic-link")
|
||||||
|
else path.extension
|
||||||
|
|
||||||
COLUMN_LAST_MODIFIED_TIME -> DateFormatUtils.format(Date(path.lastModifiedTime), "yyyy/MM/dd HH:mm")
|
COLUMN_LAST_MODIFIED_TIME -> DateFormatUtils.format(Date(path.lastModifiedTime), "yyyy/MM/dd HH:mm")
|
||||||
|
|
||||||
// 如果是本地的并且还是Windows系统
|
// 如果是本地的并且还是Windows系统
|
||||||
@@ -173,6 +176,7 @@ class FileSystemTableModel(private val fileSystem: FileSystem) : DefaultTableMod
|
|||||||
val extension by lazy { path.extension }
|
val extension by lazy { path.extension }
|
||||||
|
|
||||||
open val isDirectory by lazy { path.isDirectory() }
|
open val isDirectory by lazy { path.isDirectory() }
|
||||||
|
open val isSymbolicLink by lazy { path.isSymbolicLink() }
|
||||||
open val isHidden by lazy { fileName != ".." && path.isHidden() }
|
open val isHidden by lazy { fileName != ".." && path.isHidden() }
|
||||||
open val fileSize by lazy { path.fileSize() }
|
open val fileSize by lazy { path.fileSize() }
|
||||||
open val lastModifiedTime by lazy { Files.getLastModifiedTime(path).toMillis() }
|
open val lastModifiedTime by lazy { Files.getLastModifiedTime(path).toMillis() }
|
||||||
@@ -227,8 +231,10 @@ class FileSystemTableModel(private val fileSystem: FileSystem) : DefaultTableMod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val isDirectory: Boolean
|
override val isDirectory by lazy { attributes.isDirectory || (isSymbolicLink && Files.isDirectory(path)) }
|
||||||
get() = attributes.isDirectory
|
|
||||||
|
override val isSymbolicLink: Boolean
|
||||||
|
get() = attributes.isSymbolicLink
|
||||||
|
|
||||||
override val isHidden: Boolean
|
override val isHidden: Boolean
|
||||||
get() = fileName != ".." && fileName.startsWith(".")
|
get() = fileName != ".." && fileName.startsWith(".")
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package app.termora.transport
|
|
||||||
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
interface FileSystemTransportListener : EventListener {
|
|
||||||
/**
|
|
||||||
* @param workdir 当前工作目录
|
|
||||||
* @param isDirectory 要传输的是否是文件夹
|
|
||||||
* @param path 要传输的文件/文件夹
|
|
||||||
*/
|
|
||||||
fun transport(fileSystemPanel: FileSystemPanel, workdir: Path, isDirectory: Boolean, path: Path)
|
|
||||||
|
|
||||||
|
|
||||||
interface Provider {
|
|
||||||
fun addFileSystemTransportListener(listener: FileSystemTransportListener)
|
|
||||||
fun removeFileSystemTransportListener(listener: FileSystemTransportListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.termora.transport
|
package app.termora.transport
|
||||||
|
|
||||||
import app.termora.Icons
|
import app.termora.*
|
||||||
import app.termora.SFTPTerminalTab
|
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
@@ -9,15 +8,71 @@ import app.termora.actions.DataProviders
|
|||||||
class SFTPAction : AnAction("SFTP", Icons.folder) {
|
class SFTPAction : AnAction("SFTP", Icons.folder) {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||||
|
val selectedTerminalTab = terminalTabbedManager.getSelectedTerminalTab()
|
||||||
|
val host = if (selectedTerminalTab is SSHTerminalTab || selectedTerminalTab is SFTPPtyTerminalTab)
|
||||||
|
selectedTerminalTab.host else null
|
||||||
|
val tab = openOrCreateSFTPTerminalTab(evt) ?: return
|
||||||
|
|
||||||
|
if (host != null) {
|
||||||
|
connectHost(host.copy(protocol = Protocol.SSH), tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开一个已经存在或者创建一个 SFTP Tab
|
||||||
|
*
|
||||||
|
* @return null 表示当前条件下无法创建
|
||||||
|
*/
|
||||||
|
fun openOrCreateSFTPTerminalTab(evt: AnActionEvent): SFTPTerminalTab? {
|
||||||
|
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return null
|
||||||
|
|
||||||
val tabs = terminalTabbedManager.getTerminalTabs()
|
val tabs = terminalTabbedManager.getTerminalTabs()
|
||||||
for (tab in tabs) {
|
for (tab in tabs) {
|
||||||
if (tab is SFTPTerminalTab) {
|
if (tab is SFTPTerminalTab) {
|
||||||
terminalTabbedManager.setSelectedTerminalTab(tab)
|
terminalTabbedManager.setSelectedTerminalTab(tab)
|
||||||
return
|
return tab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建一个新的
|
// 创建一个新的
|
||||||
terminalTabbedManager.addTerminalTab(SFTPTerminalTab())
|
val tab = SFTPTerminalTab()
|
||||||
|
terminalTabbedManager.addTerminalTab(tab)
|
||||||
|
|
||||||
|
return tab
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果当前选中的是 SSH 服务器 Tab,那么直接打开 SFTP 通道
|
||||||
|
*/
|
||||||
|
fun connectHost(host: Host, tab: SFTPTerminalTab) {
|
||||||
|
val tabbed = tab.getData(TransportDataProviders.TransportPanel)
|
||||||
|
?.getData(TransportDataProviders.RightFileSystemTabbed) ?: return
|
||||||
|
|
||||||
|
// 如果已经有对应的连接
|
||||||
|
for (i in 0 until tabbed.tabCount) {
|
||||||
|
val c = tabbed.getComponentAt(i)
|
||||||
|
if (c is SftpFileSystemPanel) {
|
||||||
|
if (c.host == host) {
|
||||||
|
tabbed.selectedIndex = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 寻找空的 Tab,如果有则占用
|
||||||
|
for (i in 0 until tabbed.tabCount) {
|
||||||
|
val c = tabbed.getComponentAt(i)
|
||||||
|
if (c is SftpFileSystemPanel) {
|
||||||
|
if (c.host == null) {
|
||||||
|
c.host = host
|
||||||
|
c.connect()
|
||||||
|
tabbed.selectedIndex = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开启一个新的
|
||||||
|
tabbed.addTab(host.name, SftpFileSystemPanel(host).apply { connect() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,15 +21,12 @@ import org.slf4j.LoggerFactory
|
|||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.CardLayout
|
import java.awt.CardLayout
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
import java.nio.file.Path
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
class SftpFileSystemPanel(
|
class SftpFileSystemPanel(
|
||||||
private val transportManager: TransportManager,
|
var host: Host? = null
|
||||||
private var host: Host? = null
|
) : JPanel(BorderLayout()), Disposable {
|
||||||
) : JPanel(BorderLayout()), Disposable,
|
|
||||||
FileSystemTransportListener.Provider {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(SftpFileSystemPanel::class.java)
|
private val log = LoggerFactory.getLogger(SftpFileSystemPanel::class.java)
|
||||||
@@ -50,7 +47,6 @@ class SftpFileSystemPanel(
|
|||||||
private val connectingPanel = ConnectingPanel()
|
private val connectingPanel = ConnectingPanel()
|
||||||
private val selectHostPanel = SelectHostPanel()
|
private val selectHostPanel = SelectHostPanel()
|
||||||
private val connectFailedPanel = ConnectFailedPanel()
|
private val connectFailedPanel = ConnectFailedPanel()
|
||||||
private val listeners = mutableListOf<FileSystemTransportListener>()
|
|
||||||
private val isDisposed = AtomicBoolean(false)
|
private val isDisposed = AtomicBoolean(false)
|
||||||
|
|
||||||
private var client: SshClient? = null
|
private var client: SshClient? = null
|
||||||
@@ -108,15 +104,28 @@ class SftpFileSystemPanel(
|
|||||||
|
|
||||||
private suspend fun doConnect() {
|
private suspend fun doConnect() {
|
||||||
|
|
||||||
val host = this.host ?: return
|
val thisHost = this.host ?: return
|
||||||
|
var host = thisHost.copy(authentication = thisHost.authentication.copy())
|
||||||
|
|
||||||
closeIO()
|
closeIO()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val client = SshClients.openClient(host).apply { client = this }
|
val client = SshClients.openClient(host).apply { client = this }
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
client.userInteraction =
|
val owner = SwingUtilities.getWindowAncestor(this@SftpFileSystemPanel)
|
||||||
TerminalUserInteraction(SwingUtilities.getWindowAncestor(this@SftpFileSystemPanel))
|
client.userInteraction = TerminalUserInteraction(owner)
|
||||||
|
client.serverKeyVerifier = DialogServerKeyVerifier(owner)
|
||||||
|
// 弹出授权框
|
||||||
|
if (host.authentication.type == AuthenticationType.No) {
|
||||||
|
val dialog = RequestAuthenticationDialog(owner)
|
||||||
|
val authentication = dialog.getAuthentication()
|
||||||
|
host = host.copy(authentication = authentication)
|
||||||
|
// save
|
||||||
|
if (dialog.isRemembered()) {
|
||||||
|
HostManager.getInstance()
|
||||||
|
.addHost(host.copy(authentication = authentication))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val session = SshClients.openSession(host, client).apply { session = this }
|
val session = SshClients.openSession(host, client).apply { session = this }
|
||||||
fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
|
fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
|
||||||
@@ -135,17 +144,7 @@ class SftpFileSystemPanel(
|
|||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
state = State.Connected
|
state = State.Connected
|
||||||
|
|
||||||
val fileSystemPanel = FileSystemPanel(fileSystem, transportManager, host)
|
val fileSystemPanel = FileSystemPanel(fileSystem, host)
|
||||||
fileSystemPanel.addFileSystemTransportListener(object : FileSystemTransportListener {
|
|
||||||
override fun transport(
|
|
||||||
fileSystemPanel: FileSystemPanel,
|
|
||||||
workdir: Path,
|
|
||||||
isDirectory: Boolean,
|
|
||||||
path: Path
|
|
||||||
) {
|
|
||||||
listeners.forEach { it.transport(fileSystemPanel, workdir, isDirectory, path) }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
cardPanel.add(fileSystemPanel, State.Connected.name)
|
cardPanel.add(fileSystemPanel, State.Connected.name)
|
||||||
cardLayout.show(cardPanel, State.Connected.name)
|
cardLayout.show(cardPanel, State.Connected.name)
|
||||||
@@ -311,11 +310,4 @@ class SftpFileSystemPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun addFileSystemTransportListener(listener: FileSystemTransportListener) {
|
|
||||||
listeners.add(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removeFileSystemTransportListener(listener: FileSystemTransportListener) {
|
|
||||||
listeners.remove(listener)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package app.termora.transport
|
|||||||
|
|
||||||
import app.termora.Disposable
|
import app.termora.Disposable
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.ObjectUtils
|
||||||
import org.apache.commons.net.io.CopyStreamEvent
|
import org.apache.commons.net.io.CopyStreamEvent
|
||||||
import org.apache.commons.net.io.CopyStreamListener
|
import org.apache.commons.net.io.CopyStreamListener
|
||||||
import org.apache.commons.net.io.Util
|
import org.apache.commons.net.io.Util
|
||||||
@@ -31,10 +32,15 @@ abstract class Transport(
|
|||||||
val target: Path,
|
val target: Path,
|
||||||
val sourceHolder: Disposable,
|
val sourceHolder: Disposable,
|
||||||
val targetHolder: Disposable,
|
val targetHolder: Disposable,
|
||||||
|
val listener: TransportListener = TransportListener.EMPTY
|
||||||
) : Disposable, Runnable {
|
) : Disposable, Runnable {
|
||||||
|
|
||||||
private val listeners = ArrayList<TransportListener>()
|
private val listeners = ArrayList<TransportListener>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
listeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
var state = TransportState.Waiting
|
var state = TransportState.Waiting
|
||||||
protected set(value) {
|
protected set(value) {
|
||||||
@@ -100,7 +106,10 @@ abstract class Transport(
|
|||||||
if (fileSystem is SftpFileSystem) {
|
if (fileSystem is SftpFileSystem) {
|
||||||
val clientSession = fileSystem.session
|
val clientSession = fileSystem.session
|
||||||
if (clientSession is JGitClientSession) {
|
if (clientSession is JGitClientSession) {
|
||||||
return clientSession.hostConfigEntry.host
|
return ObjectUtils.defaultIfNull(
|
||||||
|
clientSession.hostConfigEntry.host,
|
||||||
|
clientSession.hostConfigEntry.hostName
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "file"
|
return "file"
|
||||||
@@ -142,9 +151,9 @@ private class SlidingWindowByteCounter {
|
|||||||
*/
|
*/
|
||||||
class FileTransport(
|
class FileTransport(
|
||||||
name: String, source: Path, target: Path,
|
name: String, source: Path, target: Path,
|
||||||
sourceHolder: Disposable, targetHolder: Disposable,
|
sourceHolder: Disposable, targetHolder: Disposable, listener: TransportListener = TransportListener.EMPTY
|
||||||
) : Transport(
|
) : Transport(
|
||||||
name, source, target, sourceHolder, targetHolder,
|
name, source, target, sourceHolder, targetHolder, listener
|
||||||
), CopyStreamListener {
|
), CopyStreamListener {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
27
src/main/kotlin/app/termora/transport/TransportJob.kt
Normal file
27
src/main/kotlin/app/termora/transport/TransportJob.kt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package app.termora.transport
|
||||||
|
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
data class TransportJob(
|
||||||
|
/**
|
||||||
|
* 发起方
|
||||||
|
*/
|
||||||
|
val fileSystemPanel: FileSystemPanel,
|
||||||
|
/**
|
||||||
|
* 发起方工作目录
|
||||||
|
*/
|
||||||
|
val workdir: Path,
|
||||||
|
/**
|
||||||
|
* 要传输的文件是否是文件夹
|
||||||
|
*/
|
||||||
|
val isDirectory: Boolean,
|
||||||
|
/**
|
||||||
|
* 要传输的文件/文件夹
|
||||||
|
*/
|
||||||
|
val path: Path,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听
|
||||||
|
*/
|
||||||
|
val listener: TransportListener? = null
|
||||||
|
)
|
||||||
@@ -3,18 +3,33 @@ package app.termora.transport
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
interface TransportListener : EventListener {
|
interface TransportListener : EventListener {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val EMPTY = object : TransportListener {
|
||||||
|
override fun onTransportAdded(transport: Transport) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTransportRemoved(transport: Transport) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTransportChanged(transport: Transport) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Added
|
* Added
|
||||||
*/
|
*/
|
||||||
fun onTransportAdded(transport: Transport)
|
fun onTransportAdded(transport: Transport){}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removed
|
* Removed
|
||||||
*/
|
*/
|
||||||
fun onTransportRemoved(transport: Transport)
|
fun onTransportRemoved(transport: Transport){}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态变化
|
* 状态变化
|
||||||
*/
|
*/
|
||||||
fun onTransportChanged(transport: Transport)
|
fun onTransportChanged(transport: Transport){}
|
||||||
}
|
}
|
||||||
@@ -107,32 +107,6 @@ class TransportPanel : JPanel(BorderLayout()), Disposable, DataProvider {
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
leftFileSystemTabbed.addFileSystemTransportListener(object : FileSystemTransportListener {
|
|
||||||
override fun transport(fileSystemPanel: FileSystemPanel, workdir: Path, isDirectory: Boolean, path: Path) {
|
|
||||||
val target = rightFileSystemTabbed.getSelectedFileSystemPanel() ?: return
|
|
||||||
transport(
|
|
||||||
fileSystemPanel.workdir, target.workdir,
|
|
||||||
isSourceDirectory = isDirectory,
|
|
||||||
sourcePath = path,
|
|
||||||
sourceHolder = fileSystemPanel,
|
|
||||||
targetHolder = target,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
rightFileSystemTabbed.addFileSystemTransportListener(object : FileSystemTransportListener {
|
|
||||||
override fun transport(fileSystemPanel: FileSystemPanel, workdir: Path, isDirectory: Boolean, path: Path) {
|
|
||||||
val target = leftFileSystemTabbed.getSelectedFileSystemPanel() ?: return
|
|
||||||
transport(
|
|
||||||
fileSystemPanel.workdir, target.workdir,
|
|
||||||
isSourceDirectory = isDirectory,
|
|
||||||
sourcePath = path,
|
|
||||||
sourceHolder = fileSystemPanel,
|
|
||||||
targetHolder = target,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun transport(
|
fun transport(
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ termora.date-format=MM/dd/yyyy hh:mm:ss a
|
|||||||
termora.finder=Finder
|
termora.finder=Finder
|
||||||
termora.folder=Folder
|
termora.folder=Folder
|
||||||
termora.explorer=Explorer
|
termora.explorer=Explorer
|
||||||
|
termora.quit-confirm=Quit {0}?
|
||||||
|
|
||||||
|
|
||||||
# update
|
# update
|
||||||
termora.update.title=New version
|
termora.update.title=New version
|
||||||
@@ -36,6 +38,9 @@ termora.doorman.mnemonic.title=Enter 12 mnemonic words
|
|||||||
termora.doorman.mnemonic.incorrect=Incorrect mnemonic
|
termora.doorman.mnemonic.incorrect=Incorrect mnemonic
|
||||||
|
|
||||||
|
|
||||||
|
# Hosts
|
||||||
|
termora.host.verify-server-key=Host [{0}] key has been changed<br/><br/>{1} key fingerprint is {2}<br/><br/>Are you sure you want to continue connecting?
|
||||||
|
termora.host.modified-server-key=HOST [{0}] IDENTIFICATION HAS CHANGED<br/><br/>Expected: {1} key fingerprint is {2}<br/><br/>Actual: {3} key fingerprint is {4}<br/><br/>Are you sure you want to continue connecting?
|
||||||
|
|
||||||
|
|
||||||
# Settings
|
# Settings
|
||||||
@@ -61,17 +66,25 @@ termora.settings.terminal.font=Font
|
|||||||
termora.settings.terminal.size=Size
|
termora.settings.terminal.size=Size
|
||||||
termora.settings.terminal.max-rows=Max rows
|
termora.settings.terminal.max-rows=Max rows
|
||||||
termora.settings.terminal.debug=Debug mode
|
termora.settings.terminal.debug=Debug mode
|
||||||
|
termora.settings.terminal.beep=Beep
|
||||||
termora.settings.terminal.select-copy=Select copy
|
termora.settings.terminal.select-copy=Select copy
|
||||||
termora.settings.terminal.cursor-style=Cursor type
|
termora.settings.terminal.cursor-style=Cursor type
|
||||||
termora.settings.terminal.local-shell=Local shell
|
termora.settings.terminal.local-shell=Local shell
|
||||||
|
termora.settings.terminal.floating-toolbar=Floating Toolbar
|
||||||
|
termora.settings.terminal.auto-close-tab=Auto Close Tab
|
||||||
|
termora.settings.terminal.auto-close-tab-description=Automatically close the tab when the terminal is disconnected normally
|
||||||
|
|
||||||
termora.settings.sync=Sync
|
termora.settings.sync=Sync
|
||||||
termora.settings.sync.push=Push
|
termora.settings.sync.push=Push
|
||||||
termora.settings.sync.push-warning=Pushing will overwrite the configuration. It is recommended to pull before pushing
|
termora.settings.sync.push-warning=Pushing will overwrite the configuration. It is recommended to pull before pushing
|
||||||
termora.settings.sync.pull=Pull
|
termora.settings.sync.pull=Pull
|
||||||
termora.settings.sync.done=Synchronized data successfully
|
termora.settings.sync.done=Synchronized data successfully
|
||||||
termora.settings.sync.export=Export
|
termora.settings.sync.export=${termora.keymgr.export}
|
||||||
|
termora.settings.sync.import=${termora.keymgr.import}
|
||||||
|
termora.settings.sync.import.file-too-large=The file is too large
|
||||||
|
termora.settings.sync.import.successful=Import data successfully
|
||||||
termora.settings.sync.export-done=The export was successful
|
termora.settings.sync.export-done=The export was successful
|
||||||
|
termora.settings.sync.export-encrypt=Enter password to encrypt file (optional)
|
||||||
termora.settings.sync.export-done-open-folder=The export was successful. Do you want to open the folder?
|
termora.settings.sync.export-done-open-folder=The export was successful. Do you want to open the folder?
|
||||||
termora.settings.sync.range=Range
|
termora.settings.sync.range=Range
|
||||||
termora.settings.sync.range.keys=My keys
|
termora.settings.sync.range.keys=My keys
|
||||||
@@ -80,6 +93,7 @@ termora.settings.sync.last-sync-time=Last sync time
|
|||||||
termora.settings.sync.gist=Gist
|
termora.settings.sync.gist=Gist
|
||||||
termora.settings.sync.token=Token
|
termora.settings.sync.token=Token
|
||||||
termora.settings.sync.type=Type
|
termora.settings.sync.type=Type
|
||||||
|
termora.settings.sync.webdav.help=WebDAV storage address, e.g. https://yourhost/webdav/termora.json
|
||||||
|
|
||||||
termora.settings.about=About
|
termora.settings.about=About
|
||||||
termora.settings.about.author=Author
|
termora.settings.about.author=Author
|
||||||
@@ -94,6 +108,9 @@ termora.settings.keymap.action=Action
|
|||||||
termora.settings.keymap.already-exists=The shortcut [{0}] is already in use by [{1}]
|
termora.settings.keymap.already-exists=The shortcut [{0}] is already in use by [{1}]
|
||||||
|
|
||||||
|
|
||||||
|
termora.settings.sftp.edit-command=Edit Command
|
||||||
|
|
||||||
|
|
||||||
termora.settings.restart.title=Restart
|
termora.settings.restart.title=Restart
|
||||||
termora.settings.restart.message=Changes will take effect after restarting the application
|
termora.settings.restart.message=Changes will take effect after restarting the application
|
||||||
|
|
||||||
@@ -106,10 +123,14 @@ termora.find-everywhere.groups.opened-hosts=Opened hosts
|
|||||||
termora.find-everywhere.groups.tools=Tools
|
termora.find-everywhere.groups.tools=Tools
|
||||||
termora.find-everywhere.groups.settings=${termora.setting}
|
termora.find-everywhere.groups.settings=${termora.setting}
|
||||||
termora.find-everywhere.quick-command.local-terminal=Local Terminal
|
termora.find-everywhere.quick-command.local-terminal=Local Terminal
|
||||||
|
termora.find-everywhere.double-shift-deprecated=The double-click Shift shortcut will be removed in a future version
|
||||||
|
termora.find-everywhere.double-shift-deprecated-instead=${termora.find-everywhere.double-shift-deprecated}, use {0} instead
|
||||||
|
|
||||||
# Welcome
|
# Welcome
|
||||||
termora.welcome.my-hosts=My hosts
|
termora.welcome.my-hosts=My hosts
|
||||||
termora.welcome.contextmenu.open=Open
|
termora.welcome.contextmenu.connect=Connect
|
||||||
|
termora.welcome.contextmenu.connect-with=Connect with
|
||||||
|
termora.welcome.contextmenu.open-in-new-window=${termora.tabbed.contextmenu.open-in-new-window}
|
||||||
termora.welcome.contextmenu.copy=${termora.copy}
|
termora.welcome.contextmenu.copy=${termora.copy}
|
||||||
termora.welcome.contextmenu.remove=${termora.remove}
|
termora.welcome.contextmenu.remove=${termora.remove}
|
||||||
termora.welcome.contextmenu.rename=Rename
|
termora.welcome.contextmenu.rename=Rename
|
||||||
@@ -120,6 +141,7 @@ termora.welcome.contextmenu.new.folder=${termora.folder}
|
|||||||
termora.welcome.contextmenu.new.host=Host
|
termora.welcome.contextmenu.new.host=Host
|
||||||
termora.welcome.contextmenu.new.folder.name=New Folder
|
termora.welcome.contextmenu.new.folder.name=New Folder
|
||||||
termora.welcome.contextmenu.property=Properties
|
termora.welcome.contextmenu.property=Properties
|
||||||
|
termora.welcome.contextmenu.show-more-info=Show more info
|
||||||
|
|
||||||
# New Host
|
# New Host
|
||||||
termora.new-host.title=Create a new host
|
termora.new-host.title=Create a new host
|
||||||
@@ -141,6 +163,14 @@ termora.new-host.terminal.heartbeat-interval=Heartbeat Interval
|
|||||||
termora.new-host.terminal.startup-commands=Startup Command
|
termora.new-host.terminal.startup-commands=Startup Command
|
||||||
termora.new-host.terminal.env=Environment
|
termora.new-host.terminal.env=Environment
|
||||||
|
|
||||||
|
termora.new-host.serial=Serial
|
||||||
|
termora.new-host.serial.port=Port
|
||||||
|
termora.new-host.serial.baud-rate=Baud rate
|
||||||
|
termora.new-host.serial.data-bits=Data bits
|
||||||
|
termora.new-host.serial.parity=Parity
|
||||||
|
termora.new-host.serial.stop-bits=Stop bits
|
||||||
|
termora.new-host.serial.flow-control=Flow control
|
||||||
|
|
||||||
termora.new-host.tunneling=Tunneling
|
termora.new-host.tunneling=Tunneling
|
||||||
termora.new-host.tunneling.table.name=Name
|
termora.new-host.tunneling.table.name=Name
|
||||||
termora.new-host.tunneling.table.type=Type
|
termora.new-host.tunneling.table.type=Type
|
||||||
@@ -168,8 +198,16 @@ termora.keymgr.table.type=Type
|
|||||||
termora.keymgr.table.length=Length
|
termora.keymgr.table.length=Length
|
||||||
termora.keymgr.table.remark=Description
|
termora.keymgr.table.remark=Description
|
||||||
|
|
||||||
|
termora.keymgr.ssh-copy-id.number=Number of hosts [{0}] Number of public keys [{1}]
|
||||||
|
termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied}
|
||||||
|
termora.keymgr.ssh-copy-id.failed=Copy Failure
|
||||||
|
termora.keymgr.ssh-copy-id.end=End of public key copying
|
||||||
|
|
||||||
|
|
||||||
# Tabbed
|
# Tabbed
|
||||||
termora.tabbed.contextmenu.rename=Rename
|
termora.tabbed.contextmenu.rename=Rename
|
||||||
|
termora.tabbed.contextmenu.sftp-command=SFTP Command
|
||||||
|
termora.tabbed.contextmenu.sftp-not-install=SFTP programme not found, please install and try again
|
||||||
termora.tabbed.contextmenu.clone=Clone
|
termora.tabbed.contextmenu.clone=Clone
|
||||||
termora.tabbed.contextmenu.open-in-new-window=Open in New Window
|
termora.tabbed.contextmenu.open-in-new-window=Open in New Window
|
||||||
termora.tabbed.contextmenu.close=Close
|
termora.tabbed.contextmenu.close=Close
|
||||||
@@ -221,6 +259,7 @@ termora.transport.bookmarks.down=Down
|
|||||||
termora.transport.table.filename=Filename
|
termora.transport.table.filename=Filename
|
||||||
termora.transport.table.type=Type
|
termora.transport.table.type=Type
|
||||||
termora.transport.table.type.folder=${termora.welcome.contextmenu.new.folder}
|
termora.transport.table.type.folder=${termora.welcome.contextmenu.new.folder}
|
||||||
|
termora.transport.table.type.symbolic-link=Symbolic Link
|
||||||
termora.transport.table.size=Size
|
termora.transport.table.size=Size
|
||||||
termora.transport.table.modified-time=Modified
|
termora.transport.table.modified-time=Modified
|
||||||
termora.transport.table.permissions=Permissions
|
termora.transport.table.permissions=Permissions
|
||||||
@@ -228,6 +267,8 @@ termora.transport.table.owner=Owner
|
|||||||
|
|
||||||
# contextmenu
|
# contextmenu
|
||||||
termora.transport.table.contextmenu.transfer=Transfer
|
termora.transport.table.contextmenu.transfer=Transfer
|
||||||
|
termora.transport.table.contextmenu.edit=${termora.keymgr.edit}
|
||||||
|
termora.transport.table.contextmenu.edit-command=You must configure the "Edit Command" in "Settings - SFTP" before you can edit the file
|
||||||
termora.transport.table.contextmenu.copy-path=Copy Path
|
termora.transport.table.contextmenu.copy-path=Copy Path
|
||||||
termora.transport.table.contextmenu.open-in-folder=Open in {0}
|
termora.transport.table.contextmenu.open-in-folder=Open in {0}
|
||||||
termora.transport.table.contextmenu.rename=${termora.welcome.contextmenu.rename}
|
termora.transport.table.contextmenu.rename=${termora.welcome.contextmenu.rename}
|
||||||
@@ -293,11 +334,14 @@ termora.actions.zoom-reset-terminal=Reset Terminal Zoom
|
|||||||
termora.actions.open-local-terminal=Open Local Terminal
|
termora.actions.open-local-terminal=Open Local Terminal
|
||||||
termora.actions.open-find-everywhere=Open FindEverywhere
|
termora.actions.open-find-everywhere=Open FindEverywhere
|
||||||
termora.actions.open-new-window=Open new Window
|
termora.actions.open-new-window=Open new Window
|
||||||
|
termora.actions.clear-screen=Clear Terminal Screen
|
||||||
termora.actions.switch-tab=Switch to specific Tab [1..9]
|
termora.actions.switch-tab=Switch to specific Tab [1..9]
|
||||||
|
|
||||||
# Terminal
|
# Terminal
|
||||||
termora.terminal.size=Size: {0} x {1}
|
termora.terminal.size=Size: {0} x {1}
|
||||||
termora.terminal.copied=Copied
|
termora.terminal.copied=Copied
|
||||||
|
termora.terminal.channel-disconnected=Channel has been disconnected.\u0020
|
||||||
|
termora.terminal.channel-reconnect=Type {0} to reconnect.
|
||||||
|
|
||||||
|
|
||||||
# zmodem
|
# zmodem
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ termora.date-format=yyyy-MM-dd HH:mm:ss
|
|||||||
termora.finder=访达
|
termora.finder=访达
|
||||||
termora.folder=文件夹
|
termora.folder=文件夹
|
||||||
termora.explorer=文件管理器
|
termora.explorer=文件管理器
|
||||||
|
termora.quit-confirm=你要退出 {0} 吗?
|
||||||
|
|
||||||
# update
|
# update
|
||||||
termora.update.title=新版本
|
termora.update.title=新版本
|
||||||
@@ -35,6 +36,11 @@ termora.doorman.mnemonic.title=输入 12 个助记词
|
|||||||
termora.doorman.mnemonic.incorrect=助记词错误
|
termora.doorman.mnemonic.incorrect=助记词错误
|
||||||
|
|
||||||
|
|
||||||
|
# Hosts
|
||||||
|
termora.host.verify-server-key=主机 [{0}] 密钥已经改变<br/><br/>{1} 的指纹 {2}<br/><br/>你确定要继续连接吗?
|
||||||
|
termora.host.modified-server-key=主机 [{0}] 身份已发生变化<br/><br/>期待: {1} 的指纹 {2}<br/><br/>实际: {3} 的指纹 {4}<br/><br/>你确定要继续连接吗?
|
||||||
|
|
||||||
|
|
||||||
termora.setting=设置
|
termora.setting=设置
|
||||||
termora.settings.restart.title=重启
|
termora.settings.restart.title=重启
|
||||||
termora.settings.restart.message=设置修改将在重启后生效
|
termora.settings.restart.message=设置修改将在重启后生效
|
||||||
@@ -59,31 +65,40 @@ termora.find-everywhere.groups.opened-hosts=已打开的主机
|
|||||||
termora.find-everywhere.groups.tools=工具
|
termora.find-everywhere.groups.tools=工具
|
||||||
termora.find-everywhere.groups.settings=${termora.setting}
|
termora.find-everywhere.groups.settings=${termora.setting}
|
||||||
termora.find-everywhere.quick-command.local-terminal=本地终端
|
termora.find-everywhere.quick-command.local-terminal=本地终端
|
||||||
|
termora.find-everywhere.double-shift-deprecated=双击 Shift 快捷键将会在未来的版本中移除
|
||||||
|
termora.find-everywhere.double-shift-deprecated-instead=${termora.find-everywhere.double-shift-deprecated},请使用 {0} 代替
|
||||||
|
|
||||||
termora.settings.terminal=终端
|
termora.settings.terminal=终端
|
||||||
termora.settings.terminal.font=字体
|
termora.settings.terminal.font=字体
|
||||||
termora.settings.terminal.size=大小
|
termora.settings.terminal.size=大小
|
||||||
termora.settings.terminal.max-rows=最大行数
|
termora.settings.terminal.max-rows=最大行数
|
||||||
termora.settings.terminal.debug=调试模式
|
termora.settings.terminal.debug=调试模式
|
||||||
|
termora.settings.terminal.beep=蜂鸣声
|
||||||
termora.settings.terminal.select-copy=选中复制
|
termora.settings.terminal.select-copy=选中复制
|
||||||
termora.settings.terminal.cursor-style=光标样式
|
termora.settings.terminal.cursor-style=光标样式
|
||||||
termora.settings.terminal.local-shell=本地终端
|
termora.settings.terminal.local-shell=本地终端
|
||||||
|
termora.settings.terminal.floating-toolbar=悬浮工具栏
|
||||||
|
termora.settings.terminal.auto-close-tab=自动关闭标签
|
||||||
|
termora.settings.terminal.auto-close-tab-description=当终端正常断开连接时自动关闭标签页
|
||||||
|
|
||||||
|
|
||||||
termora.settings.sync=同步
|
termora.settings.sync=同步
|
||||||
termora.settings.sync.push=推送
|
termora.settings.sync.push=推送
|
||||||
termora.settings.sync.push-warning=推送将覆盖已有配置,建议先拉取再推送
|
termora.settings.sync.push-warning=推送将覆盖已有配置,建议先拉取再推送
|
||||||
termora.settings.sync.pull=拉取
|
termora.settings.sync.pull=拉取
|
||||||
termora.settings.sync.export=导出
|
|
||||||
termora.settings.sync.export-done=导出成功
|
termora.settings.sync.export-done=导出成功
|
||||||
|
termora.settings.sync.export-encrypt=输入密码加密文件 (可选)
|
||||||
termora.settings.sync.export-done-open-folder=导出成功,是否需要打开所在文件夹?
|
termora.settings.sync.export-done-open-folder=导出成功,是否需要打开所在文件夹?
|
||||||
termora.settings.sync.range=范围
|
termora.settings.sync.range=范围
|
||||||
termora.settings.sync.range.keys=我的密钥
|
termora.settings.sync.range.keys=我的密钥
|
||||||
termora.settings.sync.last-sync-time=最后同步时间
|
termora.settings.sync.last-sync-time=最后同步时间
|
||||||
termora.settings.sync.done=同步数据成功
|
termora.settings.sync.done=同步数据成功
|
||||||
|
termora.settings.sync.import.file-too-large=文件太大
|
||||||
|
termora.settings.sync.import.successful=导入数据成功
|
||||||
termora.settings.sync.gist=片段
|
termora.settings.sync.gist=片段
|
||||||
termora.settings.sync.token=令牌
|
termora.settings.sync.token=令牌
|
||||||
termora.settings.sync.type=类型
|
termora.settings.sync.type=类型
|
||||||
|
termora.settings.sync.webdav.help=WebDAV 的存储地址,例如:https://yourhost/webdav/termora.json
|
||||||
|
|
||||||
termora.settings.about=关于
|
termora.settings.about=关于
|
||||||
termora.settings.about.author=作者
|
termora.settings.about.author=作者
|
||||||
@@ -97,9 +112,14 @@ termora.settings.keymap.shortcut=快捷键
|
|||||||
termora.settings.keymap.action=操作
|
termora.settings.keymap.action=操作
|
||||||
termora.settings.keymap.already-exists=快捷键 [{0}] 已经被 [{1}] 占用
|
termora.settings.keymap.already-exists=快捷键 [{0}] 已经被 [{1}] 占用
|
||||||
|
|
||||||
|
|
||||||
|
termora.settings.sftp.edit-command=编辑命令
|
||||||
|
|
||||||
|
|
||||||
# Welcome
|
# Welcome
|
||||||
termora.welcome.my-hosts=我的主机
|
termora.welcome.my-hosts=我的主机
|
||||||
termora.welcome.contextmenu.open=打开
|
termora.welcome.contextmenu.connect=连接
|
||||||
|
termora.welcome.contextmenu.connect-with=连接到
|
||||||
termora.welcome.contextmenu.copy=${termora.copy}
|
termora.welcome.contextmenu.copy=${termora.copy}
|
||||||
termora.welcome.contextmenu.remove=${termora.remove}
|
termora.welcome.contextmenu.remove=${termora.remove}
|
||||||
termora.welcome.contextmenu.rename=重命名
|
termora.welcome.contextmenu.rename=重命名
|
||||||
@@ -110,6 +130,7 @@ termora.welcome.contextmenu.new.folder=文件夹
|
|||||||
termora.welcome.contextmenu.new.host=主机
|
termora.welcome.contextmenu.new.host=主机
|
||||||
termora.welcome.contextmenu.new.folder.name=新建文件夹
|
termora.welcome.contextmenu.new.folder.name=新建文件夹
|
||||||
termora.welcome.contextmenu.property=属性
|
termora.welcome.contextmenu.property=属性
|
||||||
|
termora.welcome.contextmenu.show-more-info=显示更多信息
|
||||||
|
|
||||||
# New Host
|
# New Host
|
||||||
termora.new-host.title=新建主机
|
termora.new-host.title=新建主机
|
||||||
@@ -131,6 +152,14 @@ termora.new-host.terminal.startup-commands=启动命令
|
|||||||
termora.new-host.terminal.env=环境
|
termora.new-host.terminal.env=环境
|
||||||
|
|
||||||
|
|
||||||
|
termora.new-host.serial=串口
|
||||||
|
termora.new-host.serial.port=端口
|
||||||
|
termora.new-host.serial.baud-rate=波特率
|
||||||
|
termora.new-host.serial.data-bits=数据位
|
||||||
|
termora.new-host.serial.parity=校验位
|
||||||
|
termora.new-host.serial.stop-bits=停止位
|
||||||
|
termora.new-host.serial.flow-control=流控
|
||||||
|
|
||||||
|
|
||||||
termora.new-host.test-connection=测试连接
|
termora.new-host.test-connection=测试连接
|
||||||
termora.new-host.test-connection-successful=连接成功
|
termora.new-host.test-connection-successful=连接成功
|
||||||
@@ -158,6 +187,10 @@ termora.keymgr.table.type=类型
|
|||||||
termora.keymgr.table.length=长度
|
termora.keymgr.table.length=长度
|
||||||
termora.keymgr.table.remark=备注
|
termora.keymgr.table.remark=备注
|
||||||
|
|
||||||
|
termora.keymgr.ssh-copy-id.number=主机数量 [{0}] 公钥数量 [{1}]
|
||||||
|
termora.keymgr.ssh-copy-id.failed=复制失败
|
||||||
|
termora.keymgr.ssh-copy-id.end=复制公钥结束
|
||||||
|
|
||||||
# Tools
|
# Tools
|
||||||
termora.tools.multiple=将命令发送到所有会话
|
termora.tools.multiple=将命令发送到所有会话
|
||||||
|
|
||||||
@@ -165,6 +198,8 @@ termora.tools.multiple=将命令发送到所有会话
|
|||||||
|
|
||||||
# Tabbed
|
# Tabbed
|
||||||
termora.tabbed.contextmenu.rename=重命名
|
termora.tabbed.contextmenu.rename=重命名
|
||||||
|
termora.tabbed.contextmenu.sftp-command=SFTP 终端
|
||||||
|
termora.tabbed.contextmenu.sftp-not-install=没有找到 SFTP 程序,请安装后重试
|
||||||
termora.tabbed.contextmenu.clone=克隆
|
termora.tabbed.contextmenu.clone=克隆
|
||||||
termora.tabbed.contextmenu.open-in-new-window=在新窗口打开
|
termora.tabbed.contextmenu.open-in-new-window=在新窗口打开
|
||||||
termora.tabbed.contextmenu.close=关闭
|
termora.tabbed.contextmenu.close=关闭
|
||||||
@@ -217,6 +252,7 @@ termora.transport.bookmarks.down=下移
|
|||||||
termora.transport.table.filename=文件名
|
termora.transport.table.filename=文件名
|
||||||
termora.transport.table.type=类型
|
termora.transport.table.type=类型
|
||||||
termora.transport.table.size=大小
|
termora.transport.table.size=大小
|
||||||
|
termora.transport.table.type.symbolic-link=软链接
|
||||||
termora.transport.table.modified-time=修改时间
|
termora.transport.table.modified-time=修改时间
|
||||||
termora.transport.table.permissions=权限
|
termora.transport.table.permissions=权限
|
||||||
termora.transport.table.owner=所有者
|
termora.transport.table.owner=所有者
|
||||||
@@ -224,6 +260,7 @@ termora.transport.table.owner=所有者
|
|||||||
# contextmenu
|
# contextmenu
|
||||||
termora.transport.table.contextmenu.transfer=传输
|
termora.transport.table.contextmenu.transfer=传输
|
||||||
termora.transport.table.contextmenu.copy-path=复制路径
|
termora.transport.table.contextmenu.copy-path=复制路径
|
||||||
|
termora.transport.table.contextmenu.edit-command=你必须在 “设置 - SFTP” 中配置 “编辑命令” 后才能编辑文件
|
||||||
termora.transport.table.contextmenu.open-in-folder=在{0}中打开
|
termora.transport.table.contextmenu.open-in-folder=在{0}中打开
|
||||||
termora.transport.table.contextmenu.change-permissions=更改权限...
|
termora.transport.table.contextmenu.change-permissions=更改权限...
|
||||||
termora.transport.table.contextmenu.refresh=刷新
|
termora.transport.table.contextmenu.refresh=刷新
|
||||||
@@ -273,6 +310,8 @@ termora.toolbar.customize-toolbar=自定义工具栏...
|
|||||||
|
|
||||||
termora.terminal.size=大小: {0} x {1}
|
termora.terminal.size=大小: {0} x {1}
|
||||||
termora.terminal.copied=已复制
|
termora.terminal.copied=已复制
|
||||||
|
termora.terminal.channel-disconnected=终端断开连接,
|
||||||
|
termora.terminal.channel-reconnect=按 {0} 进行重连。
|
||||||
|
|
||||||
|
|
||||||
# Actions
|
# Actions
|
||||||
@@ -287,6 +326,7 @@ termora.actions.zoom-reset-terminal=重置终端缩放
|
|||||||
termora.actions.open-local-terminal=打开本地终端
|
termora.actions.open-local-terminal=打开本地终端
|
||||||
termora.actions.open-find-everywhere=打开全局查找
|
termora.actions.open-find-everywhere=打开全局查找
|
||||||
termora.actions.open-new-window=打开新窗口
|
termora.actions.open-new-window=打开新窗口
|
||||||
|
termora.actions.clear-screen=清除终端屏幕
|
||||||
termora.actions.switch-tab=切换到特定标签页 [1..9]
|
termora.actions.switch-tab=切换到特定标签页 [1..9]
|
||||||
|
|
||||||
# zmodem
|
# zmodem
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ termora.date-format=yyyy/MM/dd HH:mm:ss
|
|||||||
termora.finder=訪達
|
termora.finder=訪達
|
||||||
termora.folder=資料夾
|
termora.folder=資料夾
|
||||||
termora.explorer=檔案管理器
|
termora.explorer=檔案管理器
|
||||||
|
termora.quit-confirm=你要退出 {0} 嗎?
|
||||||
|
|
||||||
# update
|
# update
|
||||||
termora.update.title=新版本
|
termora.update.title=新版本
|
||||||
@@ -34,6 +35,13 @@ termora.doorman.mnemonic-data-corrupted=無法從助記詞解密數據,資料
|
|||||||
termora.doorman.mnemonic.title=輸入 12 個助記詞
|
termora.doorman.mnemonic.title=輸入 12 個助記詞
|
||||||
termora.doorman.mnemonic.incorrect=助記詞錯誤
|
termora.doorman.mnemonic.incorrect=助記詞錯誤
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Hosts
|
||||||
|
termora.host.verify-server-key=主機 [{0}] 金鑰已經改變<br/><br/>{1} 的指紋 {2}<br/><br/>你確定要繼續連線嗎?
|
||||||
|
termora.host.modified-server-key=主機 [{0}] 身分已變更<br/><br/>期待: {1} 的指紋 {2}<br/><br/>實際: {3} 的指紋 {4}<br/><br/>你確定要繼續連線嗎?
|
||||||
|
|
||||||
|
|
||||||
termora.setting=設定
|
termora.setting=設定
|
||||||
termora.settings.restart.title=重啟
|
termora.settings.restart.title=重啟
|
||||||
termora.settings.restart.message=設定修改將在重新啟動後生效
|
termora.settings.restart.message=設定修改將在重新啟動後生效
|
||||||
@@ -54,6 +62,8 @@ termora.settings.keymap.shortcut=快捷鍵
|
|||||||
termora.settings.keymap.action=操作
|
termora.settings.keymap.action=操作
|
||||||
termora.settings.keymap.already-exists=快捷鍵 [{0}] 已經被 [{1}] 占用
|
termora.settings.keymap.already-exists=快捷鍵 [{0}] 已經被 [{1}] 占用
|
||||||
|
|
||||||
|
termora.settings.sftp.edit-command=編輯命令
|
||||||
|
|
||||||
|
|
||||||
# Find everywhere
|
# Find everywhere
|
||||||
termora.find-everywhere=尋找
|
termora.find-everywhere=尋找
|
||||||
@@ -64,30 +74,39 @@ termora.find-everywhere.groups.opened-hosts=已開啟的主機
|
|||||||
termora.find-everywhere.groups.tools=工具
|
termora.find-everywhere.groups.tools=工具
|
||||||
termora.find-everywhere.groups.settings=${termora.setting}
|
termora.find-everywhere.groups.settings=${termora.setting}
|
||||||
termora.find-everywhere.quick-command.local-terminal=本地端
|
termora.find-everywhere.quick-command.local-terminal=本地端
|
||||||
|
termora.find-everywhere.double-shift-deprecated=雙擊 Shift 快捷鍵將會在未來的版本中移除
|
||||||
|
termora.find-everywhere.double-shift-deprecated-instead=${termora.find-everywhere.double-shift-deprecated},請使用 {0} 代替
|
||||||
|
|
||||||
termora.settings.terminal=終端
|
termora.settings.terminal=終端
|
||||||
termora.settings.terminal.font=字體
|
termora.settings.terminal.font=字體
|
||||||
termora.settings.terminal.size=大小
|
termora.settings.terminal.size=大小
|
||||||
termora.settings.terminal.max-rows=最大行數
|
termora.settings.terminal.max-rows=最大行數
|
||||||
termora.settings.terminal.debug=偵錯模式
|
termora.settings.terminal.debug=偵錯模式
|
||||||
|
termora.settings.terminal.beep=蜂鳴聲
|
||||||
termora.settings.terminal.select-copy=選取複製
|
termora.settings.terminal.select-copy=選取複製
|
||||||
termora.settings.terminal.cursor-style=遊標風格
|
termora.settings.terminal.cursor-style=遊標風格
|
||||||
termora.settings.terminal.local-shell=本地端
|
termora.settings.terminal.local-shell=本地端
|
||||||
|
termora.settings.terminal.floating-toolbar=懸浮工具列
|
||||||
|
termora.settings.terminal.auto-close-tab=自動關閉標籤
|
||||||
|
termora.settings.terminal.auto-close-tab-description=當終端正常斷開連線時自動關閉標籤頁
|
||||||
|
|
||||||
termora.settings.sync=同步
|
termora.settings.sync=同步
|
||||||
termora.settings.sync.push=推送
|
termora.settings.sync.push=推送
|
||||||
termora.settings.sync.push-warning=推送將覆蓋先前的配置,建議先拉取再推送
|
termora.settings.sync.push-warning=推送將覆蓋先前的配置,建議先拉取再推送
|
||||||
termora.settings.sync.pull=拉取
|
termora.settings.sync.pull=拉取
|
||||||
termora.settings.sync.export=匯出
|
|
||||||
termora.settings.sync.export-done=匯出成功
|
termora.settings.sync.export-done=匯出成功
|
||||||
|
termora.settings.sync.export-encrypt=輸入密碼加密檔案 (可選)
|
||||||
termora.settings.sync.export-done-open-folder=匯出成功,是否需要打開所在資料夾?
|
termora.settings.sync.export-done-open-folder=匯出成功,是否需要打開所在資料夾?
|
||||||
termora.settings.sync.range=範圍
|
termora.settings.sync.range=範圍
|
||||||
termora.settings.sync.range.keys=我的密鑰
|
termora.settings.sync.range.keys=我的密鑰
|
||||||
termora.settings.sync.last-sync-time=最後同步時間
|
termora.settings.sync.last-sync-time=最後同步時間
|
||||||
termora.settings.sync.done=同步資料成功
|
termora.settings.sync.done=同步資料成功
|
||||||
|
termora.settings.sync.import.file-too-large=檔案太大
|
||||||
|
termora.settings.sync.import.successful=導入資料成功
|
||||||
termora.settings.sync.gist=片段
|
termora.settings.sync.gist=片段
|
||||||
termora.settings.sync.token=令牌
|
termora.settings.sync.token=令牌
|
||||||
termora.settings.sync.type=類型
|
termora.settings.sync.type=類型
|
||||||
|
termora.settings.sync.webdav.help=WebDAV 的儲存位址,例如:https://yourhost/webdav/termora.json
|
||||||
|
|
||||||
termora.settings.about=關於
|
termora.settings.about=關於
|
||||||
termora.settings.about.author=作者
|
termora.settings.about.author=作者
|
||||||
@@ -98,7 +117,8 @@ termora.settings.about.termora=<html><b>${termora.title}</b> ({0}) 是一個跨
|
|||||||
|
|
||||||
# Welcome
|
# Welcome
|
||||||
termora.welcome.my-hosts=我的主機
|
termora.welcome.my-hosts=我的主機
|
||||||
termora.welcome.contextmenu.open=打開
|
termora.welcome.contextmenu.connect=連接
|
||||||
|
termora.welcome.contextmenu.connect-with=連接到
|
||||||
termora.welcome.contextmenu.copy=複製
|
termora.welcome.contextmenu.copy=複製
|
||||||
termora.welcome.contextmenu.remove=${termora.remove}
|
termora.welcome.contextmenu.remove=${termora.remove}
|
||||||
termora.welcome.contextmenu.rename=重新命名
|
termora.welcome.contextmenu.rename=重新命名
|
||||||
@@ -109,6 +129,7 @@ termora.welcome.contextmenu.new.folder=${termora.folder}
|
|||||||
termora.welcome.contextmenu.new.host=主機
|
termora.welcome.contextmenu.new.host=主機
|
||||||
termora.welcome.contextmenu.new.folder.name=新建資料夾
|
termora.welcome.contextmenu.new.folder.name=新建資料夾
|
||||||
termora.welcome.contextmenu.property=屬性
|
termora.welcome.contextmenu.property=屬性
|
||||||
|
termora.welcome.contextmenu.show-more-info=顯示更多信息
|
||||||
|
|
||||||
# New Host
|
# New Host
|
||||||
termora.new-host.title=新主機
|
termora.new-host.title=新主機
|
||||||
@@ -129,6 +150,14 @@ termora.new-host.terminal.startup-commands=啟動命令
|
|||||||
termora.new-host.terminal.heartbeat-interval=心跳間隔
|
termora.new-host.terminal.heartbeat-interval=心跳間隔
|
||||||
termora.new-host.terminal.env=環境
|
termora.new-host.terminal.env=環境
|
||||||
|
|
||||||
|
termora.new-host.serial=串口
|
||||||
|
termora.new-host.serial.port=端口
|
||||||
|
termora.new-host.serial.baud-rate=波特率
|
||||||
|
termora.new-host.serial.data-bits=資料位
|
||||||
|
termora.new-host.serial.parity=校驗位
|
||||||
|
termora.new-host.serial.stop-bits=停止位
|
||||||
|
termora.new-host.serial.flow-control=流控
|
||||||
|
|
||||||
termora.new-host.test-connection=測試連接
|
termora.new-host.test-connection=測試連接
|
||||||
termora.new-host.test-connection-successful=連線成功
|
termora.new-host.test-connection-successful=連線成功
|
||||||
|
|
||||||
@@ -155,12 +184,18 @@ termora.keymgr.table.type=型別
|
|||||||
termora.keymgr.table.length=長度
|
termora.keymgr.table.length=長度
|
||||||
termora.keymgr.table.remark=備註
|
termora.keymgr.table.remark=備註
|
||||||
|
|
||||||
|
termora.keymgr.ssh-copy-id.number=主機數量 [{0}] 公鑰數量 [{1}]
|
||||||
|
termora.keymgr.ssh-copy-id.failed=複製失敗
|
||||||
|
termora.keymgr.ssh-copy-id.end=複製公鑰結束
|
||||||
|
|
||||||
# Tools
|
# Tools
|
||||||
termora.tools.multiple=將指令傳送到所有會話
|
termora.tools.multiple=將指令傳送到所有會話
|
||||||
|
|
||||||
|
|
||||||
# Tabbed
|
# Tabbed
|
||||||
termora.tabbed.contextmenu.rename=重新命名
|
termora.tabbed.contextmenu.rename=重新命名
|
||||||
|
termora.tabbed.contextmenu.sftp-command=SFTP 終端
|
||||||
|
termora.tabbed.contextmenu.sftp-not-install=沒有找到 SFTP 程序,請安裝後重試
|
||||||
termora.tabbed.contextmenu.clone=克隆
|
termora.tabbed.contextmenu.clone=克隆
|
||||||
termora.tabbed.contextmenu.open-in-new-window=在新視窗打開
|
termora.tabbed.contextmenu.open-in-new-window=在新視窗打開
|
||||||
termora.tabbed.contextmenu.close=關閉
|
termora.tabbed.contextmenu.close=關閉
|
||||||
@@ -211,6 +246,7 @@ termora.transport.bookmarks.down=下移
|
|||||||
termora.transport.table.filename=檔名
|
termora.transport.table.filename=檔名
|
||||||
termora.transport.table.type=類型
|
termora.transport.table.type=類型
|
||||||
termora.transport.table.size=大小
|
termora.transport.table.size=大小
|
||||||
|
termora.transport.table.type.symbolic-link=軟連結
|
||||||
termora.transport.table.modified-time=修改時間
|
termora.transport.table.modified-time=修改時間
|
||||||
termora.transport.table.permissions=權限
|
termora.transport.table.permissions=權限
|
||||||
termora.transport.table.owner=所有者
|
termora.transport.table.owner=所有者
|
||||||
@@ -218,6 +254,7 @@ termora.transport.table.owner=所有者
|
|||||||
# contextmenu
|
# contextmenu
|
||||||
termora.transport.table.contextmenu.transfer=傳輸
|
termora.transport.table.contextmenu.transfer=傳輸
|
||||||
termora.transport.table.contextmenu.copy-path=複製路徑
|
termora.transport.table.contextmenu.copy-path=複製路徑
|
||||||
|
termora.transport.table.contextmenu.edit-command=你必須在 “設定 - SFTP” 中設定 “編輯指令” 後才能編輯文件
|
||||||
termora.transport.table.contextmenu.open-in-folder=在{0}中打開
|
termora.transport.table.contextmenu.open-in-folder=在{0}中打開
|
||||||
termora.transport.table.contextmenu.change-permissions=更改權限...
|
termora.transport.table.contextmenu.change-permissions=更改權限...
|
||||||
termora.transport.table.contextmenu.refresh=刷新
|
termora.transport.table.contextmenu.refresh=刷新
|
||||||
@@ -255,6 +292,8 @@ termora.toolbar.customize-toolbar=自訂工具列...
|
|||||||
|
|
||||||
termora.terminal.size=大小: {0} x {1}
|
termora.terminal.size=大小: {0} x {1}
|
||||||
termora.terminal.copied=已複製
|
termora.terminal.copied=已複製
|
||||||
|
termora.terminal.channel-disconnected=終端機連線中斷,
|
||||||
|
termora.terminal.channel-reconnect=按 {0} 進行重新連線。
|
||||||
|
|
||||||
# Actions
|
# Actions
|
||||||
termora.actions.copy-from-terminal=從終端複製
|
termora.actions.copy-from-terminal=從終端複製
|
||||||
@@ -268,6 +307,7 @@ termora.actions.zoom-reset-terminal=重置終端縮放
|
|||||||
termora.actions.open-local-terminal=開啟本地終端
|
termora.actions.open-local-terminal=開啟本地終端
|
||||||
termora.actions.open-find-everywhere=開啟全域搜尋
|
termora.actions.open-find-everywhere=開啟全域搜尋
|
||||||
termora.actions.open-new-window=開啟新視窗
|
termora.actions.open-new-window=開啟新視窗
|
||||||
|
termora.actions.clear-screen=清除終端機螢幕
|
||||||
termora.actions.switch-tab=切換到特定分頁 [1..9]
|
termora.actions.switch-tab=切換到特定分頁 [1..9]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
6
src/main/resources/icons/closeSmall.svg
Normal file
6
src/main/resources/icons/closeSmall.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M11.4939 4.48784C11.3002 4.28007 10.9724 4.27548 10.7729 4.47775L8.00074 7.28849L5.22871 4.47788C5.02922 4.27561 4.70143 4.2802 4.50768 4.48797C4.32506 4.68382 4.32933 4.98882 4.51736 5.17947L7.29908 7.99991L4.51756 10.8201C4.32953 11.0108 4.32526 11.3158 4.50788 11.5116C4.70163 11.7194 5.02942 11.724 5.22892 11.5217L8.00074 8.71133L10.7727 11.5219C10.9722 11.7241 11.3 11.7196 11.4937 11.5118C11.6764 11.3159 11.6721 11.0109 11.484 10.8203L8.7024 7.99991L11.4843 5.17934C11.6723 4.98869 11.6766 4.68368 11.4939 4.48784Z"
|
||||||
|
fill="#A8ADBD"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 844 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user