mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
134 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc4333da21 | ||
|
|
184f6d46dc | ||
|
|
68788905fe | ||
|
|
fc46216a3f | ||
|
|
563143645e | ||
|
|
891ccb901b | ||
|
|
928a866fe7 | ||
|
|
ea25b5b46f | ||
|
|
1de10e6129 | ||
|
|
aaf9c2e8d2 | ||
|
|
b8196b5730 | ||
|
|
0a83e8beb4 | ||
|
|
bdf29b27e7 | ||
|
|
96da7eac41 | ||
|
|
71c0751692 | ||
|
|
442f334af2 | ||
|
|
48302a519f | ||
|
|
c00f759f15 | ||
|
|
1736dd909e | ||
|
|
1f01e368dd | ||
|
|
bfba958b7e | ||
|
|
758121b523 | ||
|
|
06e9a89e82 | ||
|
|
0ba6ac3305 | ||
|
|
993f220b8b | ||
|
|
8755c4ad23 | ||
|
|
77cb102dd6 | ||
|
|
89cfb0b451 | ||
|
|
6bdd83f208 | ||
|
|
8f86057dcc | ||
|
|
a7d7ffa2cc | ||
|
|
d51cbeee13 | ||
|
|
deb2a0151e | ||
|
|
e1c4e9312d | ||
|
|
c7233357bd | ||
|
|
eff8d565d0 | ||
|
|
932db49868 | ||
|
|
4d71c6cd05 | ||
|
|
96133e5abf | ||
|
|
f06e5d7dc1 | ||
|
|
d4b96edccf | ||
|
|
e9876d5b91 | ||
|
|
8b9a78a7bd | ||
|
|
6b48f577e9 | ||
|
|
da9b6c21d6 | ||
|
|
f1f889df14 | ||
|
|
ed65853ebe | ||
|
|
5ffdd219d9 | ||
|
|
4f84d6741c | ||
|
|
2568e7fcc8 | ||
|
|
dddbb49084 | ||
|
|
95846ab135 | ||
|
|
b5207e56c1 | ||
|
|
160771e912 | ||
|
|
0fbe180f3f | ||
|
|
41a0409e9e | ||
|
|
79e59143fb | ||
|
|
54e0f621ce | ||
|
|
4c8944d248 | ||
|
|
64bd95d8a8 | ||
|
|
1d88942e8e | ||
|
|
129e1b149a | ||
|
|
01aac98437 | ||
|
|
f9aaf7143f | ||
|
|
28174483f4 | ||
|
|
46412054c4 | ||
|
|
1ab0d26bab | ||
|
|
d90fb9aa35 | ||
|
|
744e64b359 | ||
|
|
2c5442f1f3 | ||
|
|
054c4701d2 | ||
|
|
54044625ea | ||
|
|
ca82704738 | ||
|
|
e98ec3fa8e | ||
|
|
6a4abf7e50 | ||
|
|
e2a6cceafd | ||
|
|
283404b6b9 | ||
|
|
c714f33a44 | ||
|
|
30fe047e5c | ||
|
|
827d814c7b | ||
|
|
ccb2c6daa0 | ||
|
|
1516d6d81e | ||
|
|
09b3655c4e | ||
|
|
614514c87e | ||
|
|
30cba6720d | ||
|
|
dce6551de2 | ||
|
|
95943cdeec | ||
|
|
18a26ee6bf | ||
|
|
f23aae371a | ||
|
|
757bc1c001 | ||
|
|
a19222dc60 | ||
|
|
24677ca4a6 | ||
|
|
0c5b6f8112 | ||
|
|
7c26e3d08a | ||
|
|
9b84fb4ec8 | ||
|
|
d8ec7b6d4a | ||
|
|
769c0d990b | ||
|
|
3f1ae38b61 | ||
|
|
e10fce21a2 | ||
|
|
a00557bb9d | ||
|
|
e478535ae5 | ||
|
|
7756758738 | ||
|
|
e0ea42faee | ||
|
|
e72c6b77b5 | ||
|
|
bcd3aacd6f | ||
|
|
570b0e08ad | ||
|
|
d703850e87 | ||
|
|
4bb1a411e8 | ||
|
|
9884ed19fa | ||
|
|
1ffaed3f36 | ||
|
|
4cb42953ad | ||
|
|
0248992dc3 | ||
|
|
9bab9db875 | ||
|
|
b283a3ea38 | ||
|
|
98ac2928b4 | ||
|
|
a0a6f43c10 | ||
|
|
0c158acbe0 | ||
|
|
9a97b3a304 | ||
|
|
aef44bd0da | ||
|
|
75c65d9ba8 | ||
|
|
93755db77f | ||
|
|
79d0a9a348 | ||
|
|
422e9aac84 | ||
|
|
9915c373b7 | ||
|
|
eba85e6348 | ||
|
|
483a7772f4 | ||
|
|
dcc96358f6 | ||
|
|
b5c30d505b | ||
|
|
1f3ef5f3f0 | ||
|
|
d388bcfc92 | ||
|
|
562c1f98fe | ||
|
|
f3c5009a45 | ||
|
|
09a1d9f51e | ||
|
|
84b48278ad |
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gradle"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 25
|
||||
2
.github/workflows/linux-aarch64.yml
vendored
2
.github/workflows/linux-aarch64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
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
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-aarch64-b895.91.tar.gz
|
||||
|
||||
# appimagetool
|
||||
- run: sudo apt install libfuse2
|
||||
|
||||
2
.github/workflows/linux-x86-64.yml
vendored
2
.github/workflows/linux-x86-64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
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-x64-b825.69.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-b895.91.tar.gz
|
||||
|
||||
# appimagetool
|
||||
- run: sudo apt install libfuse2
|
||||
|
||||
8
.github/workflows/osx-aarch64.yml
vendored
8
.github/workflows/osx-aarch64.yml
vendored
@@ -33,8 +33,8 @@ jobs:
|
||||
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
- name: Setup the Notary Information
|
||||
if: github.ref_type == 'tag' && github.repository == 'TermoraDev/termora'
|
||||
- name: Setup the Notary information
|
||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||
|
||||
# download jdk
|
||||
- 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
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b895.91.tar.gz
|
||||
|
||||
# install jdk
|
||||
- name: Installing Java
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
|
||||
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||
# 只有发布版本时才需要公证
|
||||
TERMORA_MAC_NOTARY: ${{ github.ref_type == 'tag' && github.repository == 'TermoraDev/termora' }}
|
||||
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
||||
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||
run: |
|
||||
./gradlew dist --no-daemon
|
||||
|
||||
8
.github/workflows/osx-x86-64.yml
vendored
8
.github/workflows/osx-x86-64.yml
vendored
@@ -33,8 +33,8 @@ jobs:
|
||||
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
- name: Setup the Notary Information
|
||||
if: github.ref_type == 'tag' && github.repository == 'TermoraDev/termora'
|
||||
- name: Setup the Notary information
|
||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||
|
||||
# download jdk
|
||||
- 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
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b895.91.tar.gz
|
||||
|
||||
# install jdk
|
||||
- name: Installing Java
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
|
||||
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||
# 只有发布版本时才需要公证
|
||||
TERMORA_MAC_NOTARY: ${{ github.ref_type == 'tag' && github.repository == 'TermoraDev/termora' }}
|
||||
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
||||
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||
run: |
|
||||
./gradlew dist --no-daemon
|
||||
|
||||
1
.github/workflows/windows-x86-64.yml
vendored
1
.github/workflows/windows-x86-64.yml
vendored
@@ -45,5 +45,4 @@ jobs:
|
||||
name: termora-windows-x86-64
|
||||
path: |
|
||||
build/distributions/*.zip
|
||||
build/distributions/*.msi
|
||||
build/distributions/*.exe
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ certs/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
.vs
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
- Compatible with Windows, macOS, and Linux
|
||||
- Zmodem protocol support
|
||||
- SSH port forwarding & Jump hosts
|
||||
- Support for X11 and SSH-Agent
|
||||
- Terminal log
|
||||
- Configuration synchronization via [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
||||
- Macro support (record and replay scripts)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
- 支持 Windows、macOS、Linux 平台
|
||||
- 支持 Zmodem 协议
|
||||
- 支持 SSH 端口转发和跳板机
|
||||
- 支持 X11 和 SSH-Agent
|
||||
- 终端日志记录
|
||||
- 支持配置同步到 [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
||||
- 支持宏(录制脚本并回放)
|
||||
|
||||
136
THIRDPARTY
136
THIRDPARTY
@@ -1,244 +1,248 @@
|
||||
annotations 24.0.1
|
||||
annotations
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/java-annotations/blob/master/LICENSE.txt
|
||||
|
||||
bip39-lib-jvm 1.0.8
|
||||
kotlin-bip39
|
||||
MIT License
|
||||
https://github.com/Electric-Coin-Company/kotlin-bip39/blob/main/LICENSE
|
||||
|
||||
colorpicker 2.0.1
|
||||
colorpicker
|
||||
BSD 3-Clause "New" or "Revised" License
|
||||
https://github.com/dheid/colorpicker/blob/main/LICENSE
|
||||
|
||||
commonmark 0.24.0
|
||||
commonmark
|
||||
BSD 2-Clause "Simplified" License
|
||||
https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt
|
||||
|
||||
commons-codec 1.18.0
|
||||
commons-codec
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-codec/blob/master/LICENSE.txt
|
||||
|
||||
commons-compress 1.27.1
|
||||
commons-compress
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-compress/blob/master/LICENSE.txt
|
||||
|
||||
commons-io 2.18.0
|
||||
commons-vfs2
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-vfs/blob/master/LICENSE.txt
|
||||
|
||||
commons-io
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-io/blob/master/LICENSE.txt
|
||||
|
||||
commons-lang3 3.17.0
|
||||
commons-lang3
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-lang/blob/master/LICENSE.txt
|
||||
|
||||
commons-net 3.11.1
|
||||
commons-net
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-net/blob/master/LICENSE.txt
|
||||
|
||||
commons-text 1.13.0
|
||||
commons-text
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-text/blob/master/LICENSE.txt
|
||||
|
||||
commons-csv 1.13.0
|
||||
commons-csv
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-csv/blob/master/LICENSE.txt
|
||||
|
||||
ini4j 0.5.5-2
|
||||
ini4j
|
||||
Apache License 2.0
|
||||
http://www.apache.org/licenses/LICENSE-2.0.txt
|
||||
|
||||
eddsa 0.3.0
|
||||
eddsa
|
||||
Creative Commons Zero v1.0 Universal
|
||||
https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt
|
||||
|
||||
flatlaf 3.5.4
|
||||
flatlaf
|
||||
Apache License 2.0
|
||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||
|
||||
flatlaf 3.5.4-no-natives
|
||||
flatlaf-no-natives
|
||||
Apache License 2.0
|
||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||
|
||||
flatlaf-extras 3.5.4
|
||||
flatlaf-extras
|
||||
Apache License 2.0
|
||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||
|
||||
flatlaf-swingx 3.5.4
|
||||
flatlaf-swingx
|
||||
Apache License 2.0
|
||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||
|
||||
JavaEWAH 1.2.3
|
||||
JavaEWAH
|
||||
Apache License 2.0
|
||||
https://github.com/lemire/javaewah/blob/master/LICENSE
|
||||
|
||||
jbr-api 17.1.10.1
|
||||
jbr-api
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/JetBrainsRuntimeApi/blob/main/LICENSE
|
||||
|
||||
jcl-over-slf4j 1.7.36
|
||||
jcl-over-slf4j
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0.txt
|
||||
|
||||
jfa 1.2.0
|
||||
jfa
|
||||
Apache License 2.0
|
||||
https://github.com/0x4a616e/jfa/blob/main/LICENSE
|
||||
|
||||
jgoodies-common 1.8.1
|
||||
jgoodies-common
|
||||
BSD-2-Clause License
|
||||
http://www.opensource.org/licenses/bsd-license.html
|
||||
|
||||
jgoodies-forms 1.9.0
|
||||
jgoodies-forms
|
||||
BSD-2-Clause License
|
||||
http://www.opensource.org/licenses/bsd-license.html
|
||||
|
||||
jna 5.16.0
|
||||
jna
|
||||
Apache License 2.0
|
||||
https://github.com/java-native-access/jna/blob/master/AL2.0
|
||||
|
||||
jna-platform 5.16.0
|
||||
jna-platform
|
||||
Apache License 2.0
|
||||
https://github.com/java-native-access/jna/blob/master/AL2.0
|
||||
|
||||
jnafilechooser-api 1.1.2
|
||||
jnafilechooser-api
|
||||
BSD 3-Clause "New" or "Revised" License
|
||||
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
||||
|
||||
jnafilechooser-win32 1.1.2
|
||||
jnafilechooser-win32
|
||||
BSD 3-Clause "New" or "Revised" License
|
||||
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
||||
|
||||
jsvg 1.4.0
|
||||
jsvg
|
||||
MIT License
|
||||
https://github.com/weisJ/jsvg/blob/master/LICENSE
|
||||
|
||||
jSystemThemeDetector 3.9.1
|
||||
jSystemThemeDetector
|
||||
Apache License 2.0
|
||||
https://github.com/Dansoftowner/jSystemThemeDetector/blob/master/LICENSE
|
||||
|
||||
kotlin-logging 1.7.9
|
||||
kotlin-logging
|
||||
Apache License 2.0
|
||||
https://github.com/oshai/kotlin-logging/blob/master/LICENSE
|
||||
|
||||
kotlin-stdlib 2.1.10
|
||||
kotlin-stdlib
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||
|
||||
kotlin-stdlib-jdk7 1.9.10
|
||||
kotlin-stdlib-jdk7
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||
|
||||
kotlin-stdlib-jdk8 1.9.10
|
||||
kotlin-stdlib-jdk8
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||
|
||||
kotlin-stdlib-jdk8 1.9.10
|
||||
kotlin-stdlib-jdk8
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||
|
||||
restart4j 0.0.1
|
||||
restart4j
|
||||
Apache License 2.0
|
||||
https://github.com/hstyi/restart4j/blob/main/LICENSE
|
||||
|
||||
kotlinx-coroutines-core-jvm 1.10.1
|
||||
kotlinx-coroutines-core
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
kotlinx-coroutines-swing 1.10.1
|
||||
kotlinx-coroutines-swing
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
kotlinx-serialization-core-jvm 1.8.0
|
||||
kotlinx-serialization-json
|
||||
Apache License 2.0
|
||||
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
||||
|
||||
kotlinx-serialization-json-jvm 1.8.0
|
||||
Apache License 2.0
|
||||
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
||||
|
||||
logging-interceptor 4.12.0
|
||||
logging-interceptor
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
okhttp 4.12.0
|
||||
okhttp
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
okio-jvm 3.6.0
|
||||
okio-jvm
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
org.eclipse.jgit.ssh.apache 7.1.0.202411261347-r
|
||||
org.eclipse.jgit.ssh.apache
|
||||
Eclipse Distribution License
|
||||
https://www.eclipse.org/org/documents/edl-v10.php
|
||||
|
||||
org.eclipse.jgit 7.1.0.202411261347-r
|
||||
org.eclipse.jgit.ssh.apache.agent
|
||||
Eclipse Distribution License
|
||||
https://www.eclipse.org/org/documents/edl-v10.php
|
||||
|
||||
oshi-core 6.6.5
|
||||
org.eclipse.jgit
|
||||
Eclipse Distribution License
|
||||
https://www.eclipse.org/org/documents/edl-v10.php
|
||||
|
||||
oshi-core
|
||||
MIT License
|
||||
https://github.com/oshi/oshi/blob/master/LICENSE
|
||||
|
||||
pty4j 0.13.2
|
||||
pty4j
|
||||
Eclipse Public License 1.0
|
||||
https://github.com/JetBrains/pty4j/blob/master/LICENSE
|
||||
|
||||
slf4j-api 2.0.16
|
||||
slf4j-api
|
||||
MIT License
|
||||
https://github.com/qos-ch/slf4j/blob/master/LICENSE.txt
|
||||
|
||||
slf4j-tinylog 2.7.0
|
||||
slf4j-tinylog
|
||||
Apache License 2.0
|
||||
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||
|
||||
sshd-common 2.14.0
|
||||
sshd-common
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
sshd-core 2.14.0
|
||||
sshd-core
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
sshd-osgi 2.14.0
|
||||
sshd-osgi
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
sshd-sftp 2.14.0
|
||||
sshd-sftp
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
swingx-all 1.6.5-1
|
||||
swingx-all
|
||||
GNU LESSER GENERAL PUBLIC LICENSE v3
|
||||
https://www.gnu.org/licenses/lgpl-3.0
|
||||
|
||||
tinylog-api 2.7.0
|
||||
tinylog-api
|
||||
Apache License 2.0
|
||||
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||
|
||||
tinylog-impl 2.7.0
|
||||
tinylog-impl
|
||||
Apache License 2.0
|
||||
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||
|
||||
versioncompare 1.4.1
|
||||
versioncompare
|
||||
Apache License 2.0
|
||||
https://github.com/G00fY2/version-compare/blob/main/LICENSE
|
||||
|
||||
xodus-compress 2.0.1
|
||||
xodus-compress
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||
|
||||
xodus-environment 2.0.1
|
||||
xodus-environment
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||
|
||||
xodus-openAPI 2.0.1
|
||||
xodus-openAPI
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||
|
||||
xodus-utils 2.0.1
|
||||
xodus-utils
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||
|
||||
xodus-vfs 2.0.1
|
||||
xodus-vfs
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||
|
||||
@@ -246,7 +250,7 @@ jediterm
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
|
||||
|
||||
mixpanel-java 1.5.3
|
||||
mixpanel-java
|
||||
Apache License 2.0
|
||||
https://github.com/mixpanel/mixpanel-java/blob/master/LICENSE
|
||||
|
||||
@@ -254,6 +258,6 @@ json-20231013
|
||||
Public Domain.
|
||||
https://github.com/stleary/JSON-java/blob/master/LICENSE
|
||||
|
||||
jSerialComm 2.11.0
|
||||
jSerialComm
|
||||
Apache License 2.0
|
||||
https://github.com/Fazecast/jSerialComm/blob/master/LICENSE-APACHE-2.0
|
||||
162
build.gradle.kts
162
build.gradle.kts
@@ -14,13 +14,14 @@ plugins {
|
||||
java
|
||||
idea
|
||||
application
|
||||
`maven-publish`
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlinx.serialization)
|
||||
}
|
||||
|
||||
|
||||
group = "app.termora"
|
||||
version = "1.0.9"
|
||||
version = "1.0.15"
|
||||
|
||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||
@@ -56,69 +57,71 @@ dependencies {
|
||||
|
||||
// implementation(platform(libs.koin.bom))
|
||||
// implementation(libs.koin.core)
|
||||
implementation(libs.slf4j.api)
|
||||
implementation(libs.pty4j)
|
||||
implementation(libs.slf4j.tinylog)
|
||||
implementation(libs.tinylog.impl)
|
||||
implementation(libs.commons.codec)
|
||||
implementation(libs.commons.io)
|
||||
implementation(libs.commons.lang3)
|
||||
implementation(libs.commons.csv)
|
||||
implementation(libs.commons.net)
|
||||
implementation(libs.commons.text)
|
||||
implementation(libs.commons.compress)
|
||||
implementation(libs.kotlinx.coroutines.swing)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
api(libs.slf4j.api)
|
||||
api(libs.pty4j)
|
||||
api(libs.slf4j.tinylog)
|
||||
api(libs.tinylog.impl)
|
||||
api(libs.commons.codec)
|
||||
api(libs.commons.io)
|
||||
api(libs.commons.lang3)
|
||||
api(libs.commons.csv)
|
||||
api(libs.commons.net)
|
||||
api(libs.commons.text)
|
||||
api(libs.commons.compress)
|
||||
api(libs.commons.vfs2) { exclude(group = "*", module = "*") }
|
||||
api(libs.kotlinx.coroutines.swing)
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
|
||||
implementation(libs.flatlaf) {
|
||||
api(libs.flatlaf) {
|
||||
artifact {
|
||||
if (useNoNativesFlatLaf) {
|
||||
classifier = "no-natives"
|
||||
}
|
||||
}
|
||||
}
|
||||
implementation(libs.flatlaf.extras) {
|
||||
api(libs.flatlaf.extras) {
|
||||
if (useNoNativesFlatLaf) {
|
||||
exclude(group = "com.formdev", module = "flatlaf")
|
||||
}
|
||||
}
|
||||
implementation(libs.flatlaf.swingx) {
|
||||
api(libs.flatlaf.swingx) {
|
||||
if (useNoNativesFlatLaf) {
|
||||
exclude(group = "com.formdev", module = "flatlaf")
|
||||
}
|
||||
}
|
||||
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.swingx)
|
||||
implementation(libs.jgoodies.forms)
|
||||
implementation(libs.jna)
|
||||
implementation(libs.jna.platform)
|
||||
implementation(libs.versioncompare)
|
||||
implementation(libs.oshi.core)
|
||||
implementation(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
||||
implementation(libs.jfa) { exclude(group = "*", module = "*") }
|
||||
implementation(libs.jbr.api)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.okhttp.logging)
|
||||
implementation(libs.sshd.core)
|
||||
implementation(libs.commonmark)
|
||||
implementation(libs.jgit)
|
||||
implementation(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
||||
implementation(libs.jnafilechooser)
|
||||
implementation(libs.xodus.vfs)
|
||||
implementation(libs.xodus.openAPI)
|
||||
implementation(libs.xodus.environment)
|
||||
implementation(libs.bip39)
|
||||
implementation(libs.colorpicker)
|
||||
implementation(libs.mixpanel)
|
||||
implementation(libs.jSerialComm)
|
||||
implementation(libs.ini4j)
|
||||
implementation(libs.restart4j)
|
||||
api(libs.kotlinx.serialization.json)
|
||||
api(libs.swingx)
|
||||
api(libs.jgoodies.forms)
|
||||
api(libs.jna)
|
||||
api(libs.jna.platform)
|
||||
api(libs.versioncompare)
|
||||
api(libs.oshi.core)
|
||||
api(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
||||
api(libs.jfa) { exclude(group = "*", module = "*") }
|
||||
api(libs.jbr.api)
|
||||
api(libs.okhttp)
|
||||
api(libs.okhttp.logging)
|
||||
api(libs.sshd.core)
|
||||
api(libs.commonmark)
|
||||
api(libs.jgit)
|
||||
api(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
||||
api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
||||
api(libs.eddsa)
|
||||
api(libs.jnafilechooser)
|
||||
api(libs.xodus.vfs)
|
||||
api(libs.xodus.openAPI)
|
||||
api(libs.xodus.environment)
|
||||
api(libs.bip39)
|
||||
api(libs.colorpicker)
|
||||
api(libs.mixpanel)
|
||||
api(libs.jSerialComm)
|
||||
api(libs.ini4j)
|
||||
api(libs.restart4j)
|
||||
}
|
||||
|
||||
application {
|
||||
val args = mutableListOf(
|
||||
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
||||
"-Xmx2g",
|
||||
"-XX:+UseZGC",
|
||||
"-XX:+ZUncommit",
|
||||
@@ -127,7 +130,10 @@ application {
|
||||
)
|
||||
|
||||
if (os.isMacOsX) {
|
||||
args.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||
// macOS NSWindow
|
||||
args.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||
args.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||
args.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||
args.add("-Dsun.java2d.metal=true")
|
||||
args.add("-Dapple.awt.application.appearance=system")
|
||||
}
|
||||
@@ -142,6 +148,37 @@ application {
|
||||
mainClass = "app.termora.MainKt"
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("mavenJava") {
|
||||
from(components["java"])
|
||||
pom {
|
||||
name = project.name
|
||||
description = "Termora is a terminal emulator and SSH client for Windows, macOS and Linux"
|
||||
url = "https://github.com/TermoraDev/termora"
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name = "AGPL-3.0"
|
||||
url = "https://opensource.org/license/agpl-v3"
|
||||
}
|
||||
}
|
||||
|
||||
developers {
|
||||
developer {
|
||||
name = "hstyi"
|
||||
url = "https://github.com/hstyi"
|
||||
}
|
||||
}
|
||||
|
||||
scm {
|
||||
url = "https://github.com/TermoraDev/termora"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -345,6 +382,10 @@ tasks.register<Exec>("jpackage") {
|
||||
options.add("-Dsun.java2d.metal=true")
|
||||
|
||||
if (os.isMacOsX) {
|
||||
// NSWindow
|
||||
options.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||
options.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||
options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||
options.add("-Dapple.awt.application.appearance=system")
|
||||
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||
}
|
||||
@@ -421,15 +462,9 @@ tasks.register<Exec>("jpackage") {
|
||||
|
||||
tasks.register("dist") {
|
||||
doLast {
|
||||
val vendor = Jvm.current().vendor ?: StringUtils.EMPTY
|
||||
@Suppress("UnstableApiUsage")
|
||||
if (!JvmVendorSpec.JETBRAINS.matches(vendor)) {
|
||||
throw GradleException("JVM: $vendor is not supported")
|
||||
}
|
||||
|
||||
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
|
||||
|
||||
|
||||
// 清空目录
|
||||
exec { commandLine(gradlew, "clean") }
|
||||
|
||||
@@ -455,33 +490,26 @@ tasks.register("dist") {
|
||||
|
||||
tasks.register("check-license") {
|
||||
doLast {
|
||||
val thirdParty = mutableMapOf<String, String>()
|
||||
val iterator = File(projectDir, "THIRDPARTY").readLines().iterator()
|
||||
val thirdPartyNames = mutableSetOf<String>()
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
val nameWithVersion = iterator.next()
|
||||
if (nameWithVersion.isBlank()) {
|
||||
val name = iterator.next()
|
||||
if (name.isBlank()) {
|
||||
continue
|
||||
}
|
||||
|
||||
// ignore license name
|
||||
iterator.next()
|
||||
// ignore license url
|
||||
iterator.next()
|
||||
|
||||
val license = iterator.next()
|
||||
thirdParty[nameWithVersion.replace(StringUtils.SPACE, "-")] = license
|
||||
thirdPartyNames.add(nameWithVersion.split(StringUtils.SPACE).first())
|
||||
thirdPartyNames.add(name)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
for (dependency in configurations.runtimeClasspath.get().allDependencies) {
|
||||
if (!thirdPartyNames.contains(dependency.name)) {
|
||||
throw GradleException("${dependency.name} No license found")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -736,8 +764,6 @@ fun stapleMacOSLocalFile(file: File) {
|
||||
kotlin {
|
||||
jvmToolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
@Suppress("UnstableApiUsage")
|
||||
vendor = JvmVendorSpec.JETBRAINS
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +1,46 @@
|
||||
[versions]
|
||||
kotlin = "2.1.10"
|
||||
slf4j = "2.0.16"
|
||||
pty4j = "0.13.2"
|
||||
kotlin = "2.1.21"
|
||||
slf4j = "2.0.17"
|
||||
pty4j = "0.13.4"
|
||||
tinylog = "2.7.0"
|
||||
kotlinx-coroutines = "1.10.1"
|
||||
flatlaf = "3.5.4"
|
||||
trove4j = "1.0.20200330"
|
||||
kotlinx-serialization-json = "1.8.0"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
flatlaf = "3.6"
|
||||
kotlinx-serialization-json = "1.8.1"
|
||||
commons-codec = "1.18.0"
|
||||
commons-lang3 = "3.17.0"
|
||||
commons-csv = "1.13.0"
|
||||
commons-csv = "1.14.0"
|
||||
commons-net = "3.11.1"
|
||||
commons-text = "1.13.0"
|
||||
commons-text = "1.13.1"
|
||||
commons-compress = "1.27.1"
|
||||
koin-bom = "4.0.0"
|
||||
commons-vfs2="2.10.0"
|
||||
swingx = "1.6.5-1"
|
||||
jgoodies-forms = "1.9.0"
|
||||
jfa = "1.2.0"
|
||||
oshi = "6.6.5"
|
||||
oshi = "6.8.1"
|
||||
versioncompare = "1.4.1"
|
||||
jna = "5.16.0"
|
||||
jna = "5.17.0"
|
||||
jSystemThemeDetector = "3.9.1"
|
||||
commons-io = "2.18.0"
|
||||
commons-io = "2.19.0"
|
||||
jbr-api = "17.1.10.1"
|
||||
leveldb = "0.12"
|
||||
guava = "33.3.1-jre"
|
||||
credential-secure-storage = "1.0.3"
|
||||
hutool = "5.8.34"
|
||||
jsch = "0.2.21"
|
||||
hutool = "5.8.37"
|
||||
jsch = "0.2.26"
|
||||
okhttp = "4.12.0"
|
||||
bcprov = "1.79"
|
||||
sshj = "0.39.0"
|
||||
sshd-core = "2.14.0"
|
||||
jgit = "7.1.0.202411261347-r"
|
||||
sshd-core = "2.15.0"
|
||||
jgit = "7.2.0.202503040940-r"
|
||||
commonmark = "0.24.0"
|
||||
jnafilechooser = "1.1.2"
|
||||
xodus = "2.0.1"
|
||||
bip39 = "1.0.8"
|
||||
bip39 = "1.0.9"
|
||||
colorpicker = "2.0.1"
|
||||
rhino = "1.7.15"
|
||||
rhino = "1.8.0"
|
||||
delight-rhino-sandbox = "0.0.17"
|
||||
testcontainers = "1.20.4"
|
||||
testcontainers = "1.21.0"
|
||||
mixpanel = "1.5.3"
|
||||
jSerialComm = "2.11.0"
|
||||
ini4j = "0.5.5-2"
|
||||
restart4j = "0.0.1"
|
||||
eddsa = "0.3.0"
|
||||
|
||||
[libraries]
|
||||
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||
@@ -59,15 +55,13 @@ commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.
|
||||
commons-csv = { group = "org.apache.commons", name = "commons-csv", version.ref = "commons-csv" }
|
||||
commons-text = { group = "org.apache.commons", name = "commons-text", version.ref = "commons-text" }
|
||||
commons-compress = { group = "org.apache.commons", name = "commons-compress", version.ref = "commons-compress" }
|
||||
commons-vfs2 = { group = "org.apache.commons", name = "commons-vfs2", version.ref = "commons-vfs2" }
|
||||
pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" }
|
||||
ini4j = { module = "org.jetbrains.intellij.deps:ini4j", version.ref = "ini4j" }
|
||||
flatlaf = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
|
||||
flatlaf-extras = { group = "com.formdev", name = "flatlaf-extras", version.ref = "flatlaf" }
|
||||
trove4j = { group = "org.jetbrains.intellij.deps", name = "trove4j", version.ref = "trove4j" }
|
||||
koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
|
||||
testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "testcontainers" }
|
||||
testcontainers = { module = "org.testcontainers:testcontainers" }
|
||||
koin-core = { module = "io.insert-koin:koin-core" }
|
||||
swingx = { module = "org.swinglabs.swingx:swingx-all", version.ref = "swingx" }
|
||||
jgoodies-forms = { module = "com.jgoodies:jgoodies-forms", version.ref = "jgoodies-forms" }
|
||||
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
||||
@@ -80,31 +74,27 @@ commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
|
||||
restart4j = { module = "com.github.hstyi:restart4j", version.ref = "restart4j" }
|
||||
jbr-api = { module = "com.jetbrains:jbr-api", version.ref = "jbr-api" }
|
||||
flatlaf-swingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
|
||||
leveldb = { module = "org.iq80.leveldb:leveldb", version.ref = "leveldb" }
|
||||
guava = { module = "com.google.guava:guava", version.ref = "guava" }
|
||||
hutool = { module = "cn.hutool:hutool-all", version.ref = "hutool" }
|
||||
credential-secure-storage = { module = "com.microsoft:credential-secure-storage", version.ref = "credential-secure-storage" }
|
||||
jsch = { module = "com.github.mwiede:jsch", version.ref = "jsch" }
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
||||
bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bcprov" }
|
||||
sshj = { module = "com.hierynomus:sshj", version.ref = "sshj" }
|
||||
sshd-core = { module = "org.apache.sshd:sshd-core", version.ref = "sshd-core" }
|
||||
jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" }
|
||||
commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
|
||||
jgit-sshd = { module = "org.eclipse.jgit:org.eclipse.jgit.ssh.apache", version.ref = "jgit" }
|
||||
jgit-agent = { module = "org.eclipse.jgit:org.eclipse.jgit.ssh.apache.agent", version.ref = "jgit" }
|
||||
xodus-openAPI = { module = "org.jetbrains.xodus:xodus-openAPI", version.ref = "xodus" }
|
||||
xodus-entity-store = { module = "org.jetbrains.xodus:xodus-entity-store", version.ref = "xodus" }
|
||||
xodus-environment = { module = "org.jetbrains.xodus:xodus-environment", version.ref = "xodus" }
|
||||
xodus-crypto = { module = "org.jetbrains.xodus:xodus-crypto", version.ref = "xodus" }
|
||||
xodus-vfs = { module = "org.jetbrains.xodus:xodus-vfs", version.ref = "xodus" }
|
||||
jnafilechooser = { module = "com.github.steos.jnafilechooser:jnafilechooser-api", version.ref = "jnafilechooser" }
|
||||
bip39 = { module = "cash.z.ecc.android:kotlin-bip39-jvm", version.ref = "bip39" }
|
||||
bip39 = { module = "cash.z.ecc.android:kotlin-bip39", version.ref = "bip39" }
|
||||
rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
||||
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
|
||||
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
|
||||
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
|
||||
jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "jSerialComm" }
|
||||
eddsa = { module = "net.i2p.crypto:eddsa", version.ref = "eddsa" }
|
||||
|
||||
[plugins]
|
||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0"
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0"
|
||||
}
|
||||
rootProject.name = "termora"
|
||||
|
||||
|
||||
70
src/main/java/app/termora/CombinedKeyIdentityProvider.java
Normal file
70
src/main/java/app/termora/CombinedKeyIdentityProvider.java
Normal file
@@ -0,0 +1,70 @@
|
||||
package app.termora;
|
||||
|
||||
import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
|
||||
import org.apache.sshd.common.session.SessionContext;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.util.*;
|
||||
|
||||
@Deprecated
|
||||
public class CombinedKeyIdentityProvider implements KeyIdentityProvider {
|
||||
|
||||
private final List<KeyIdentityProvider> providers = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public Iterable<KeyPair> loadKeys(SessionContext context) {
|
||||
return () -> new Iterator<>() {
|
||||
|
||||
private final Iterator<KeyIdentityProvider> factories = providers
|
||||
.iterator();
|
||||
private Iterator<KeyPair> current;
|
||||
|
||||
private Boolean hasElement;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
if (hasElement != null) {
|
||||
return hasElement;
|
||||
}
|
||||
while (current == null || !current.hasNext()) {
|
||||
if (factories.hasNext()) {
|
||||
try {
|
||||
current = factories.next().loadKeys(context)
|
||||
.iterator();
|
||||
} catch (IOException | GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else {
|
||||
current = null;
|
||||
hasElement = Boolean.FALSE;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
hasElement = Boolean.TRUE;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyPair next() {
|
||||
if ((hasElement == null && !hasNext()) || !hasElement) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
hasElement = null;
|
||||
KeyPair result;
|
||||
try {
|
||||
result = current.next();
|
||||
} catch (NoSuchElementException e) {
|
||||
result = null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
public void addKeyKeyIdentityProvider(KeyIdentityProvider provider) {
|
||||
providers.add(Objects.requireNonNull(provider));
|
||||
}
|
||||
}
|
||||
140
src/main/java/app/termora/MyFlatTabbedPaneUI.java
Normal file
140
src/main/java/app/termora/MyFlatTabbedPaneUI.java
Normal file
@@ -0,0 +1,140 @@
|
||||
package app.termora;
|
||||
|
||||
import com.formdev.flatlaf.ui.FlatTabbedPaneUI;
|
||||
import com.formdev.flatlaf.ui.FlatUIUtils;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
|
||||
import static com.formdev.flatlaf.FlatClientProperties.*;
|
||||
import static com.formdev.flatlaf.util.UIScale.scale;
|
||||
|
||||
/**
|
||||
* 如果要升级 FlatLaf 需要检查是否兼容
|
||||
*/
|
||||
@Deprecated
|
||||
public class MyFlatTabbedPaneUI extends FlatTabbedPaneUI {
|
||||
@Override
|
||||
protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {
|
||||
if (tabPane.getTabCount() <= 0 ||
|
||||
contentSeparatorHeight == 0 ||
|
||||
!clientPropertyBoolean(tabPane, TABBED_PANE_SHOW_CONTENT_SEPARATOR, showContentSeparator))
|
||||
return;
|
||||
|
||||
Insets insets = tabPane.getInsets();
|
||||
Insets tabAreaInsets = getTabAreaInsets(tabPlacement);
|
||||
|
||||
int x = insets.left;
|
||||
int y = insets.top;
|
||||
int w = tabPane.getWidth() - insets.right - insets.left;
|
||||
int h = tabPane.getHeight() - insets.top - insets.bottom;
|
||||
|
||||
// remove tabs from bounds
|
||||
switch (tabPlacement) {
|
||||
case BOTTOM:
|
||||
h -= calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
|
||||
h += tabAreaInsets.top;
|
||||
break;
|
||||
|
||||
case LEFT:
|
||||
x += calculateTabAreaWidth(tabPlacement, runCount, maxTabWidth);
|
||||
x -= tabAreaInsets.right;
|
||||
w -= (x - insets.left);
|
||||
break;
|
||||
|
||||
case RIGHT:
|
||||
w -= calculateTabAreaWidth(tabPlacement, runCount, maxTabWidth);
|
||||
w += tabAreaInsets.left;
|
||||
break;
|
||||
|
||||
case TOP:
|
||||
default:
|
||||
y += calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
|
||||
y -= tabAreaInsets.bottom;
|
||||
h -= (y - insets.top);
|
||||
break;
|
||||
}
|
||||
|
||||
// compute insets for separator or full border
|
||||
boolean hasFullBorder = clientPropertyBoolean(tabPane, TABBED_PANE_HAS_FULL_BORDER, this.hasFullBorder);
|
||||
int sh = scale(contentSeparatorHeight * 100); // multiply by 100 because rotateInsets() does not use floats
|
||||
Insets ci = new Insets(0, 0, 0, 0);
|
||||
rotateInsets(hasFullBorder ? new Insets(sh, sh, sh, sh) : new Insets(sh, 0, 0, 0), ci, tabPlacement);
|
||||
|
||||
// create path for content separator or full border
|
||||
Path2D path = new Path2D.Float(Path2D.WIND_EVEN_ODD);
|
||||
path.append(new Rectangle2D.Float(x, y, w, h), false);
|
||||
path.append(new Rectangle2D.Float(x + (ci.left / 100f), y + (ci.top / 100f),
|
||||
w - (ci.left / 100f) - (ci.right / 100f), h - (ci.top / 100f) - (ci.bottom / 100f)), false);
|
||||
|
||||
// add gap for selected tab to path
|
||||
if (getTabType() == TAB_TYPE_CARD && selectedIndex >= 0) {
|
||||
float csh = scale((float) contentSeparatorHeight);
|
||||
|
||||
Rectangle tabRect = getTabBounds(tabPane, selectedIndex);
|
||||
boolean componentHasFullBorder = false;
|
||||
if (tabPane.getComponentAt(selectedIndex) instanceof JComponent c) {
|
||||
componentHasFullBorder = c.getClientProperty(TABBED_PANE_HAS_FULL_BORDER) == Boolean.TRUE;
|
||||
}
|
||||
Rectangle2D.Float innerTabRect = new Rectangle2D.Float(tabRect.x + csh, tabRect.y + csh,
|
||||
componentHasFullBorder ? 0 : tabRect.width - (csh * 2), tabRect.height - (csh * 2));
|
||||
|
||||
// Ensure that the separator outside the tabViewport is present (doesn't get cutoff by the active tab)
|
||||
// If left unsolved the active tab is "visible" in the separator (the gap) even when outside the viewport
|
||||
if (tabViewport != null)
|
||||
Rectangle2D.intersect(tabViewport.getBounds(), innerTabRect, innerTabRect);
|
||||
|
||||
Rectangle2D.Float gap = null;
|
||||
if (isHorizontalTabPlacement(tabPlacement)) {
|
||||
if (innerTabRect.width > 0) {
|
||||
float y2 = (tabPlacement == TOP) ? y : y + h - csh;
|
||||
gap = new Rectangle2D.Float(innerTabRect.x, y2, innerTabRect.width, csh);
|
||||
}
|
||||
} else {
|
||||
if (innerTabRect.height > 0) {
|
||||
float x2 = (tabPlacement == LEFT) ? x : x + w - csh;
|
||||
gap = new Rectangle2D.Float(x2, innerTabRect.y, csh, innerTabRect.height);
|
||||
}
|
||||
}
|
||||
|
||||
if (gap != null) {
|
||||
path.append(gap, false);
|
||||
|
||||
// fill gap in case that the tab is colored (e.g. focused or hover)
|
||||
Color background = getTabBackground(tabPlacement, selectedIndex, true);
|
||||
g.setColor(FlatUIUtils.deriveColor(background, tabPane.getBackground()));
|
||||
((Graphics2D) g).fill(gap);
|
||||
}
|
||||
}
|
||||
|
||||
// paint content separator or full border
|
||||
g.setColor(contentAreaColor);
|
||||
((Graphics2D) g).fill(path);
|
||||
|
||||
// repaint selection in scroll-tab-layout because it may be painted before
|
||||
// the content border was painted (from BasicTabbedPaneUI$ScrollableTabPanel)
|
||||
if (isScrollTabLayout() && selectedIndex >= 0 && tabViewport != null) {
|
||||
Rectangle tabRect = getTabBounds(tabPane, selectedIndex);
|
||||
|
||||
// clip to "scrolling sides" of viewport
|
||||
// (left and right if horizontal, top and bottom if vertical)
|
||||
Shape oldClip = g.getClip();
|
||||
Rectangle vr = tabViewport.getBounds();
|
||||
if (isHorizontalTabPlacement(tabPlacement))
|
||||
g.clipRect(vr.x, 0, vr.width, tabPane.getHeight());
|
||||
else
|
||||
g.clipRect(0, vr.y, tabPane.getWidth(), vr.height);
|
||||
|
||||
paintTabSelection(g, tabPlacement, selectedIndex, tabRect.x, tabRect.y, tabRect.width, tabRect.height);
|
||||
g.setClip(oldClip);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean isScrollTabLayout() {
|
||||
return tabPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT;
|
||||
}
|
||||
|
||||
}
|
||||
38
src/main/java/app/termora/MyKernel32.java
Normal file
38
src/main/java/app/termora/MyKernel32.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package app.termora;
|
||||
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.WString;
|
||||
import com.sun.jna.win32.StdCallLibrary;
|
||||
|
||||
interface MyKernel32 extends StdCallLibrary {
|
||||
|
||||
MyKernel32 INSTANCE = Native.load("Kernel32", MyKernel32.class);
|
||||
WString INVARIANT_LOCALE = new WString("");
|
||||
|
||||
int CompareStringEx(WString lpLocaleName,
|
||||
int dwCmpFlags,
|
||||
WString lpString1,
|
||||
int cchCount1,
|
||||
WString lpString2,
|
||||
int cchCount2,
|
||||
Pointer lpVersionInformation,
|
||||
Pointer lpReserved,
|
||||
int lParam);
|
||||
|
||||
default int CompareStringEx(int dwCmpFlags,
|
||||
String str1,
|
||||
String str2) {
|
||||
return MyKernel32.INSTANCE
|
||||
.CompareStringEx(
|
||||
INVARIANT_LOCALE,
|
||||
dwCmpFlags,
|
||||
new WString(str1),
|
||||
str1.length(),
|
||||
new WString(str2),
|
||||
str2.length(),
|
||||
Pointer.NULL,
|
||||
Pointer.NULL,
|
||||
0);
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,6 @@ package app.termora
|
||||
|
||||
object Actions {
|
||||
|
||||
|
||||
/**
|
||||
* 将命令发送到多个会话
|
||||
*/
|
||||
const val MULTIPLE = "MultipleAction"
|
||||
|
||||
/**
|
||||
* 关键词高亮
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,6 @@ package app.termora
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jthemedetecor.util.OsInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -123,19 +122,18 @@ object Application {
|
||||
return "Termora"
|
||||
}
|
||||
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
fun browse(uri: URI, async: Boolean = true) {
|
||||
// https://github.com/TermoraDev/termora/issues/178
|
||||
if (SystemInfo.isWindows && uri.scheme == "file") {
|
||||
if (async) {
|
||||
GlobalScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
||||
swingCoroutineScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
||||
} else {
|
||||
tryBrowse(uri)
|
||||
}
|
||||
} else if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||
Desktop.getDesktop().browse(uri)
|
||||
} else if (async) {
|
||||
GlobalScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
||||
swingCoroutineScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
||||
} else {
|
||||
tryBrowse(uri)
|
||||
}
|
||||
@@ -150,6 +148,16 @@ object Application {
|
||||
ProcessBuilder("xdg-open", uri.toString()).start()
|
||||
}
|
||||
}
|
||||
|
||||
fun browseInFolder(file: File) {
|
||||
if (SystemInfo.isWindows) {
|
||||
ProcessBuilder("explorer", "/select," + file.absolutePath).start()
|
||||
} else if (SystemInfo.isMacOS) {
|
||||
ProcessBuilder("open", "-R", file.absolutePath).start()
|
||||
} else if (Desktop.getDesktop().isSupported(Desktop.Action.BROWSE_FILE_DIR)) {
|
||||
Desktop.getDesktop().browseFileDirectory(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun formatBytes(bytes: Long): String {
|
||||
@@ -168,11 +176,33 @@ fun formatSeconds(seconds: Long): String {
|
||||
val minutes = (seconds % 3600) / 60
|
||||
val remainingSeconds = seconds % 60
|
||||
|
||||
|
||||
return when {
|
||||
days > 0 -> "${days}天${hours}小时${minutes}分${remainingSeconds}秒"
|
||||
hours > 0 -> "${hours}小时${minutes}分${remainingSeconds}秒"
|
||||
minutes > 0 -> "${minutes}分${remainingSeconds}秒"
|
||||
else -> "${remainingSeconds}秒"
|
||||
days > 0 -> I18n.getString(
|
||||
"termora.transport.jobs.table.estimated-time-days-format",
|
||||
days,
|
||||
hours,
|
||||
minutes,
|
||||
remainingSeconds
|
||||
)
|
||||
|
||||
hours > 0 -> I18n.getString(
|
||||
"termora.transport.jobs.table.estimated-time-hours-format",
|
||||
hours,
|
||||
minutes,
|
||||
remainingSeconds
|
||||
)
|
||||
|
||||
minutes > 0 -> I18n.getString(
|
||||
"termora.transport.jobs.table.estimated-time-minutes-format",
|
||||
minutes,
|
||||
remainingSeconds
|
||||
)
|
||||
|
||||
else -> I18n.getString(
|
||||
"termora.transport.jobs.table.estimated-time-seconds-format",
|
||||
remainingSeconds
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
91
src/main/kotlin/app/termora/ApplicationInitializr.kt
Normal file
91
src/main/kotlin/app/termora/ApplicationInitializr.kt
Normal file
@@ -0,0 +1,91 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.pty4j.util.PtyUtil
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
import org.tinylog.configuration.Configuration
|
||||
import java.io.File
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class ApplicationInitializr {
|
||||
|
||||
fun run() {
|
||||
|
||||
// 由于 macOS 签名和公证问题,依赖二进制依赖会单独在一个文件夹
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
setupNativeLibraries()
|
||||
}
|
||||
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
System.setProperty("apple.awt.application.name", Application.getName())
|
||||
}
|
||||
|
||||
// 设置 tinylog
|
||||
setupTinylog()
|
||||
|
||||
// 检查是否单例
|
||||
checkSingleton()
|
||||
|
||||
// 启动
|
||||
ApplicationRunner().run()
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun setupNativeLibraries() {
|
||||
if (!SystemUtils.IS_OS_MAC_OSX) {
|
||||
return
|
||||
}
|
||||
|
||||
val appPath = Application.getAppPath()
|
||||
if (StringUtils.isBlank(appPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
val contents = File(appPath).parentFile?.parentFile ?: return
|
||||
val dylib = FileUtils.getFile(contents, "app", "dylib")
|
||||
if (!dylib.exists()) {
|
||||
return
|
||||
}
|
||||
|
||||
val jna = FileUtils.getFile(dylib, "jna")
|
||||
if (jna.exists()) {
|
||||
System.setProperty("jna.boot.library.path", jna.absolutePath)
|
||||
}
|
||||
|
||||
val pty4j = FileUtils.getFile(dylib, "pty4j")
|
||||
if (pty4j.exists()) {
|
||||
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)
|
||||
}
|
||||
|
||||
val restart4j = FileUtils.getFile(dylib, "restart4j", "restarter")
|
||||
if (restart4j.exists()) {
|
||||
System.setProperty("restarter.path", restart4j.absolutePath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows 情况覆盖
|
||||
*/
|
||||
private fun setupTinylog() {
|
||||
if (SystemInfo.isWindows) {
|
||||
val dir = File(Application.getBaseDataDir(), "logs")
|
||||
FileUtils.forceMkdir(dir)
|
||||
Configuration.set("writer_file.latest", "${dir.absolutePath}/${Application.getName().lowercase()}.log")
|
||||
Configuration.set("writer_file.file", "${dir.absolutePath}/{date:yyyy}-{date:MM}-{date:dd}.log")
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkSingleton() {
|
||||
if (ApplicationSingleton.getInstance().isSingleton()) return
|
||||
System.err.println("Program is already running")
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
@@ -2,55 +2,49 @@ package app.termora
|
||||
|
||||
import app.termora.actions.ActionManager
|
||||
import app.termora.keymap.KeymapManager
|
||||
import app.termora.vfs2.sftp.MySftpFileProvider
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.FlatSystemProperties
|
||||
import com.formdev.flatlaf.extras.FlatDesktop
|
||||
import com.formdev.flatlaf.extras.FlatDesktop.QuitResponse
|
||||
import com.formdev.flatlaf.extras.FlatInspector
|
||||
import com.formdev.flatlaf.ui.FlatTableCellBorder
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jthemedetecor.OsThemeDetector
|
||||
import com.mixpanel.mixpanelapi.ClientDelivery
|
||||
import com.mixpanel.mixpanelapi.MessageBuilder
|
||||
import com.mixpanel.mixpanelapi.MixpanelAPI
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.lang3.LocaleUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
import org.apache.commons.vfs2.VFS
|
||||
import org.apache.commons.vfs2.cache.WeakRefFilesCache
|
||||
import org.apache.commons.vfs2.impl.DefaultFileSystemManager
|
||||
import org.apache.commons.vfs2.provider.local.DefaultLocalFileProvider
|
||||
import org.json.JSONObject
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.tinylog.configuration.Configuration
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.io.File
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.channels.FileLock
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.awt.MenuItem
|
||||
import java.awt.PopupMenu
|
||||
import java.awt.SystemTray
|
||||
import java.awt.TrayIcon
|
||||
import java.awt.desktop.AppReopenedEvent
|
||||
import java.awt.desktop.AppReopenedListener
|
||||
import java.awt.desktop.SystemEventListener
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.WindowEvent
|
||||
import java.util.*
|
||||
import java.util.function.Consumer
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.*
|
||||
import kotlin.system.exitProcess
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
class ApplicationRunner {
|
||||
private lateinit var singletonChannel: FileChannel
|
||||
private lateinit var singletonLock: FileLock
|
||||
private val log by lazy {
|
||||
if (!::singletonLock.isInitialized) {
|
||||
throw UnsupportedOperationException("Singleton lock is not initialized")
|
||||
}
|
||||
LoggerFactory.getLogger(ApplicationRunner::class.java)
|
||||
}
|
||||
private val log by lazy { LoggerFactory.getLogger(ApplicationRunner::class.java) }
|
||||
|
||||
fun run() {
|
||||
measureTimeMillis {
|
||||
// 覆盖 tinylog 配置
|
||||
val setupTinylog = measureTimeMillis { setupTinylog() }
|
||||
|
||||
// 是否单例
|
||||
val checkSingleton = measureTimeMillis { checkSingleton() }
|
||||
|
||||
// 打印系统信息
|
||||
val printSystemInfo = measureTimeMillis { printSystemInfo() }
|
||||
@@ -64,11 +58,20 @@ class ApplicationRunner {
|
||||
// 统计
|
||||
val enableAnalytics = measureTimeMillis { enableAnalytics() }
|
||||
|
||||
// init ActionManager、KeymapManager
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
// init ActionManager、KeymapManager、VFS
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
ActionManager.getInstance()
|
||||
KeymapManager.getInstance()
|
||||
|
||||
val fileSystemManager = DefaultFileSystemManager()
|
||||
fileSystemManager.addProvider("sftp", MySftpFileProvider())
|
||||
fileSystemManager.addProvider("file", DefaultLocalFileProvider())
|
||||
fileSystemManager.filesCache = WeakRefFilesCache()
|
||||
fileSystemManager.init()
|
||||
VFS.setManager(fileSystemManager)
|
||||
|
||||
// async init
|
||||
BackgroundManager.getInstance().getBackgroundImage()
|
||||
}
|
||||
|
||||
// 设置 LAF
|
||||
@@ -84,8 +87,6 @@ class ApplicationRunner {
|
||||
val startMainFrame = measureTimeMillis { startMainFrame() }
|
||||
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("setupTinylog: {}ms", setupTinylog)
|
||||
log.debug("checkSingleton: {}ms", checkSingleton)
|
||||
log.debug("printSystemInfo: {}ms", printSystemInfo)
|
||||
log.debug("openDatabase: {}ms", openDatabase)
|
||||
log.debug("loadSettings: {}ms", loadSettings)
|
||||
@@ -101,9 +102,8 @@ class ApplicationRunner {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
private fun clearTemporary() {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
// 启动时清除
|
||||
FileUtils.cleanDirectory(Application.getTemporaryDir())
|
||||
}
|
||||
@@ -120,36 +120,69 @@ class ApplicationRunner {
|
||||
|
||||
private fun startMainFrame() {
|
||||
|
||||
|
||||
TermoraFrameManager.getInstance().createWindow().isVisible = true
|
||||
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
if (SystemInfo.isMacOS) {
|
||||
SwingUtilities.invokeLater {
|
||||
FlatDesktop.setQuitHandler(object : Consumer<QuitResponse> {
|
||||
override fun accept(response: QuitResponse) {
|
||||
quitHandler(response)
|
||||
|
||||
try {
|
||||
// 设置 Dock
|
||||
setupMacOSDock()
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
// Command + Q
|
||||
FlatDesktop.setQuitHandler { quitHandler() }
|
||||
}
|
||||
} else if (SystemInfo.isWindows) {
|
||||
// 设置托盘
|
||||
SwingUtilities.invokeLater { setupSystemTray() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSystemTray() {
|
||||
if (!SystemInfo.isWindows || !SystemTray.isSupported()) return
|
||||
|
||||
val tray = SystemTray.getSystemTray()
|
||||
val image = ImageIO.read(TermoraFrame::class.java.getResourceAsStream("/icons/termora_16x16.png"))
|
||||
val trayIcon = TrayIcon(image)
|
||||
val popupMenu = PopupMenu()
|
||||
trayIcon.popupMenu = popupMenu
|
||||
trayIcon.toolTip = Application.getName()
|
||||
|
||||
// PopupMenu 不支持中文
|
||||
val exitMenu = MenuItem("Exit")
|
||||
exitMenu.addActionListener { SwingUtilities.invokeLater { quitHandler() } }
|
||||
popupMenu.add(exitMenu)
|
||||
|
||||
// double click
|
||||
trayIcon.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
TermoraFrameManager.getInstance().tick()
|
||||
}
|
||||
})
|
||||
|
||||
tray.add(trayIcon)
|
||||
|
||||
Disposer.register(ApplicationScope.forApplicationScope(), object : Disposable {
|
||||
override fun dispose() {
|
||||
tray.remove(trayIcon)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun quitHandler(response: QuitResponse) {
|
||||
val keyboardFocusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
|
||||
if (OptionPane.showConfirmDialog(
|
||||
keyboardFocusManager.focusedWindow,
|
||||
I18n.getString("termora.quit-confirm", Application.getName()),
|
||||
optionType = JOptionPane.YES_NO_OPTION,
|
||||
) != JOptionPane.YES_OPTION
|
||||
) {
|
||||
response.cancelQuit()
|
||||
return
|
||||
}
|
||||
|
||||
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
||||
frame.dispose()
|
||||
|
||||
private fun quitHandler() {
|
||||
val windows = TermoraFrameManager.getInstance().getWindows()
|
||||
|
||||
for (frame in windows) {
|
||||
frame.dispatchEvent(WindowEvent(frame, WindowEvent.WINDOW_CLOSED))
|
||||
}
|
||||
|
||||
Disposer.dispose(TermoraFrameManager.getInstance())
|
||||
}
|
||||
|
||||
private fun loadSettings() {
|
||||
@@ -164,7 +197,7 @@ class ApplicationRunner {
|
||||
|
||||
private fun setupLaf() {
|
||||
|
||||
System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux}")
|
||||
System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux || SystemInfo.isWindows}")
|
||||
System.setProperty(FlatSystemProperties.USE_ROUNDED_POPUP_BORDER, "false")
|
||||
|
||||
if (SystemInfo.isLinux) {
|
||||
@@ -186,6 +219,7 @@ class ApplicationRunner {
|
||||
|
||||
themeManager.change(theme, true)
|
||||
|
||||
|
||||
if (Application.isUnknownVersion())
|
||||
FlatInspector.install("ctrl shift alt X")
|
||||
|
||||
@@ -218,9 +252,8 @@ class ApplicationRunner {
|
||||
}
|
||||
|
||||
UIManager.put("Table.rowHeight", 24)
|
||||
UIManager.put("Table.cellNoFocusBorder", BorderFactory.createEmptyBorder())
|
||||
UIManager.put("Table.focusCellHighlightBorder", BorderFactory.createEmptyBorder())
|
||||
UIManager.put("Table.focusSelectedCellHighlightBorder", BorderFactory.createEmptyBorder())
|
||||
UIManager.put("Table.focusCellHighlightBorder", FlatTableCellBorder.Default())
|
||||
UIManager.put("Table.focusSelectedCellHighlightBorder", FlatTableCellBorder.Default())
|
||||
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
||||
|
||||
UIManager.put("Tree.rowHeight", 24)
|
||||
@@ -231,7 +264,35 @@ class ApplicationRunner {
|
||||
|
||||
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
||||
|
||||
}
|
||||
|
||||
private fun setupMacOSDock() {
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
val cls = Class.forName("com.apple.eawt.Application")
|
||||
val app = cls.getMethod("getApplication").invoke(null)
|
||||
val addAppEventListener = cls.getMethod("addAppEventListener", SystemEventListener::class.java)
|
||||
|
||||
addAppEventListener.invoke(app, object : AppReopenedListener {
|
||||
override fun appReopened(e: AppReopenedEvent) {
|
||||
val manager = TermoraFrameManager.getInstance()
|
||||
if (manager.getWindows().isEmpty()) {
|
||||
manager.createWindow().isVisible = true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 当应用程序销毁时,驻守线程也可以退出了
|
||||
Disposer.register(ApplicationScope.forApplicationScope(), object : Disposable {
|
||||
override fun dispose() {
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
})
|
||||
|
||||
// 驻守线程,不然当所有窗口都关闭时,程序会自动退出
|
||||
// wait application exit
|
||||
Thread.ofPlatform().daemon(false)
|
||||
.priority(Thread.MIN_PRIORITY)
|
||||
.start { countDownLatch.await() }
|
||||
}
|
||||
|
||||
private fun printSystemInfo() {
|
||||
@@ -254,36 +315,6 @@ class ApplicationRunner {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Windows 情况覆盖
|
||||
*/
|
||||
private fun setupTinylog() {
|
||||
if (SystemInfo.isWindows) {
|
||||
val dir = File(Application.getBaseDataDir(), "logs")
|
||||
FileUtils.forceMkdir(dir)
|
||||
Configuration.set("writer_file.latest", "${dir.absolutePath}/${Application.getName().lowercase()}.log")
|
||||
Configuration.set("writer_file.file", "${dir.absolutePath}/{date:yyyy}-{date:MM}-{date:dd}.log")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun checkSingleton() {
|
||||
singletonChannel = FileChannel.open(
|
||||
Paths.get(Application.getBaseDataDir().absolutePath, "lock"),
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.WRITE,
|
||||
)
|
||||
|
||||
val lock = singletonChannel.tryLock()
|
||||
if (lock == null) {
|
||||
System.err.println("Program is already running")
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
singletonLock = lock
|
||||
}
|
||||
|
||||
|
||||
private fun openDatabase() {
|
||||
try {
|
||||
Database.getDatabase()
|
||||
@@ -302,13 +333,12 @@ class ApplicationRunner {
|
||||
/**
|
||||
* 统计 https://mixpanel.com
|
||||
*/
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun enableAnalytics() {
|
||||
if (Application.isUnknownVersion()) {
|
||||
return
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val properties = JSONObject()
|
||||
properties.put("os", SystemUtils.OS_NAME)
|
||||
|
||||
201
src/main/kotlin/app/termora/ApplicationSingleton.kt
Normal file
201
src/main/kotlin/app/termora/ApplicationSingleton.kt
Normal file
@@ -0,0 +1,201 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.sun.jna.platform.win32.Kernel32
|
||||
import com.sun.jna.platform.win32.User32
|
||||
import com.sun.jna.platform.win32.WinDef.*
|
||||
import com.sun.jna.platform.win32.WinError
|
||||
import com.sun.jna.platform.win32.WinUser.*
|
||||
import com.sun.jna.platform.win32.Wtsapi32
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.channels.FileLock
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class ApplicationSingleton private constructor() : Disposable {
|
||||
|
||||
@Volatile
|
||||
private var isSingleton = null as Boolean?
|
||||
|
||||
|
||||
companion object {
|
||||
fun getInstance(): ApplicationSingleton {
|
||||
return ApplicationScope.forApplicationScope()
|
||||
.getOrCreate(ApplicationSingleton::class) { ApplicationSingleton() }
|
||||
}
|
||||
}
|
||||
|
||||
fun isSingleton(): Boolean {
|
||||
var singleton = this.isSingleton
|
||||
if (singleton != null) return singleton
|
||||
|
||||
try {
|
||||
synchronized(this) {
|
||||
singleton = this.isSingleton
|
||||
if (singleton != null) return singleton as Boolean
|
||||
|
||||
if (SystemInfo.isWindows) {
|
||||
val handle = Kernel32.INSTANCE.CreateMutex(null, false, Application.getName())
|
||||
singleton = handle != null && Kernel32.INSTANCE.GetLastError() != WinError.ERROR_ALREADY_EXISTS
|
||||
if (singleton == true) {
|
||||
// 启动监听器,方便激活窗口
|
||||
Thread.ofVirtual().start(Win32HelperWindow.getInstance())
|
||||
} else {
|
||||
// 尝试激活窗口
|
||||
Win32HelperWindow.tick()
|
||||
}
|
||||
} else {
|
||||
singleton = FileLocker.getInstance().tryLock()
|
||||
}
|
||||
|
||||
this.isSingleton = singleton == true
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace(System.err)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
return this.isSingleton == true
|
||||
|
||||
}
|
||||
|
||||
private class FileLocker private constructor() {
|
||||
companion object {
|
||||
fun getInstance(): FileLocker {
|
||||
return ApplicationScope.forApplicationScope()
|
||||
.getOrCreate(FileLocker::class) { FileLocker() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private lateinit var singletonChannel: FileChannel
|
||||
private lateinit var singletonLock: FileLock
|
||||
|
||||
|
||||
fun tryLock(): Boolean {
|
||||
singletonChannel = FileChannel.open(
|
||||
Paths.get(Application.getBaseDataDir().absolutePath, "lock"),
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.WRITE,
|
||||
)
|
||||
|
||||
val lock = singletonChannel.tryLock() ?: return false
|
||||
|
||||
this.singletonLock = lock
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class Win32HelperWindow private constructor() : Runnable {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(Win32HelperWindow::class.java)
|
||||
private val WindowClass = "${Application.getName()}HelperWindowClass"
|
||||
private val WindowName =
|
||||
"${Application.getName()} hidden helper window, used only to catch the windows events"
|
||||
private const val TICK: Int = WM_USER + 1
|
||||
|
||||
fun getInstance(): Win32HelperWindow {
|
||||
return ApplicationScope.forApplicationScope()
|
||||
.getOrCreate(Win32HelperWindow::class) { Win32HelperWindow() }
|
||||
}
|
||||
|
||||
|
||||
fun tick() {
|
||||
val hWnd = User32.INSTANCE.FindWindow(WindowClass, WindowName) ?: return
|
||||
User32.INSTANCE.SendMessage(hWnd, TICK, WPARAM(), LPARAM())
|
||||
}
|
||||
}
|
||||
|
||||
private val isRunning = AtomicBoolean(false)
|
||||
|
||||
override fun run() {
|
||||
if (SystemInfo.isWindows) {
|
||||
if (isRunning.compareAndSet(false, true)) {
|
||||
Win32Window()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class Win32Window : WindowProc {
|
||||
/**
|
||||
* Instantiates a new win32 window test.
|
||||
*/
|
||||
init {
|
||||
// define new window class
|
||||
val hInst = Kernel32.INSTANCE.GetModuleHandle(null)
|
||||
|
||||
val wClass = WNDCLASSEX()
|
||||
wClass.hInstance = hInst
|
||||
wClass.lpfnWndProc = this
|
||||
wClass.lpszClassName = WindowClass
|
||||
|
||||
// register window class
|
||||
User32.INSTANCE.RegisterClassEx(wClass)
|
||||
|
||||
// create new window
|
||||
val hWnd = User32.INSTANCE.CreateWindowEx(
|
||||
User32.WS_EX_TOPMOST,
|
||||
WindowClass,
|
||||
WindowName,
|
||||
0, 0, 0, 0, 0,
|
||||
null, // WM_DEVICECHANGE contradicts parent=WinUser.HWND_MESSAGE
|
||||
null, hInst, null
|
||||
)
|
||||
|
||||
|
||||
val msg = MSG()
|
||||
while (User32.INSTANCE.GetMessage(msg, hWnd, 0, 0) > 0) {
|
||||
User32.INSTANCE.TranslateMessage(msg)
|
||||
User32.INSTANCE.DispatchMessage(msg)
|
||||
}
|
||||
|
||||
Wtsapi32.INSTANCE.WTSUnRegisterSessionNotification(hWnd)
|
||||
User32.INSTANCE.UnregisterClass(WindowClass, hInst)
|
||||
User32.INSTANCE.DestroyWindow(hWnd)
|
||||
|
||||
}
|
||||
|
||||
override fun callback(hwnd: HWND, uMsg: Int, wParam: WPARAM, lParam: LPARAM): LRESULT {
|
||||
when (uMsg) {
|
||||
WM_CREATE -> {
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("win32 helper window created")
|
||||
}
|
||||
return LRESULT()
|
||||
}
|
||||
|
||||
TICK -> {
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("win32 helper window tick")
|
||||
}
|
||||
onTick()
|
||||
return LRESULT()
|
||||
}
|
||||
|
||||
WM_DESTROY -> {
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("win32 helper window destroyed")
|
||||
}
|
||||
User32.INSTANCE.PostQuitMessage(0)
|
||||
return LRESULT()
|
||||
}
|
||||
|
||||
else -> return User32.INSTANCE.DefWindowProc(hwnd, uMsg, wParam, lParam)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTick() {
|
||||
TermoraFrameManager.getInstance().tick()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/main/kotlin/app/termora/BackgroundManager.kt
Normal file
88
src/main/kotlin/app/termora/BackgroundManager.kt
Normal file
@@ -0,0 +1,88 @@
|
||||
package app.termora
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.File
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
class BackgroundManager private constructor() {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(BackgroundManager::class.java)
|
||||
fun getInstance(): BackgroundManager {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(BackgroundManager::class) { BackgroundManager() }
|
||||
}
|
||||
}
|
||||
|
||||
private val appearance get() = Database.getDatabase().appearance
|
||||
private var bufferedImage: BufferedImage? = null
|
||||
private var imageFilepath = StringUtils.EMPTY
|
||||
|
||||
fun setBackgroundImage(file: File) {
|
||||
synchronized(this) {
|
||||
try {
|
||||
bufferedImage = file.inputStream().use { ImageIO.read(it) }
|
||||
imageFilepath = file.absolutePath
|
||||
appearance.backgroundImage = file.absolutePath
|
||||
|
||||
SwingUtilities.invokeLater {
|
||||
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
||||
SwingUtilities.updateComponentTreeUI(window)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getBackgroundImage(): BufferedImage? {
|
||||
val bg = doGetBackgroundImage()
|
||||
if (bg == null) {
|
||||
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||
return null
|
||||
} else {
|
||||
JPopupMenu.setDefaultLightWeightPopupEnabled(true)
|
||||
}
|
||||
} else {
|
||||
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||
JPopupMenu.setDefaultLightWeightPopupEnabled(false)
|
||||
}
|
||||
}
|
||||
return bg
|
||||
}
|
||||
|
||||
private fun doGetBackgroundImage(): BufferedImage? {
|
||||
synchronized(this) {
|
||||
if (bufferedImage == null || imageFilepath.isEmpty()) {
|
||||
if (appearance.backgroundImage.isBlank()) {
|
||||
return null
|
||||
}
|
||||
val file = File(appearance.backgroundImage)
|
||||
if (file.exists()) {
|
||||
setBackgroundImage(file)
|
||||
}
|
||||
}
|
||||
|
||||
return bufferedImage
|
||||
}
|
||||
}
|
||||
|
||||
fun clearBackgroundImage() {
|
||||
synchronized(this) {
|
||||
bufferedImage = null
|
||||
imageFilepath = StringUtils.EMPTY
|
||||
appearance.backgroundImage = StringUtils.EMPTY
|
||||
SwingUtilities.invokeLater {
|
||||
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
||||
SwingUtilities.updateComponentTreeUI(window)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,14 @@ import app.termora.highlight.KeywordHighlight
|
||||
import app.termora.keymap.Keymap
|
||||
import app.termora.keymgr.OhKeyPair
|
||||
import app.termora.macro.Macro
|
||||
import app.termora.snippet.Snippet
|
||||
import app.termora.sync.SyncManager
|
||||
import app.termora.sync.SyncType
|
||||
import app.termora.terminal.CursorStyle
|
||||
import jetbrains.exodus.bindings.StringBinding
|
||||
import jetbrains.exodus.env.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -26,9 +26,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
companion object {
|
||||
private const val KEYMAP_STORE = "Keymap"
|
||||
private const val HOST_STORE = "Host"
|
||||
private const val SNIPPET_STORE = "Snippet"
|
||||
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
|
||||
private const val MACRO_STORE = "Macro"
|
||||
private const val KEY_PAIR_STORE = "KeyPair"
|
||||
private const val DELETED_DATA_STORE = "DeletedData"
|
||||
private val log = LoggerFactory.getLogger(Database::class.java)
|
||||
|
||||
|
||||
@@ -105,17 +107,6 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllHost() {
|
||||
env.executeInTransaction { tx ->
|
||||
val store = env.openStore(HOST_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
||||
store.openCursor(tx).use {
|
||||
while (it.next) {
|
||||
it.deleteCurrent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllKeyPair() {
|
||||
env.executeInTransaction { tx ->
|
||||
val store = env.openStore(KEY_PAIR_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
||||
@@ -156,11 +147,67 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
env.executeInTransaction {
|
||||
delete(it, HOST_STORE, id)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Removed Host: $id")
|
||||
log.debug("Removed host: $id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addDeletedData(deletedData: DeletedData) {
|
||||
val text = ohMyJson.encodeToString(deletedData)
|
||||
env.executeInTransaction {
|
||||
put(it, DELETED_DATA_STORE, deletedData.id, text)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Added DeletedData: ${deletedData.id} , $text")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getDeletedData(): Collection<DeletedData> {
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<DeletedData?>(tx, DELETED_DATA_STORE) { _, value ->
|
||||
try {
|
||||
ohMyJson.decodeFromString(value)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}.values.filterNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
fun addSnippet(snippet: Snippet) {
|
||||
var text = ohMyJson.encodeToString(snippet)
|
||||
if (doorman.isWorking()) {
|
||||
text = doorman.encrypt(text)
|
||||
}
|
||||
env.executeInTransaction {
|
||||
put(it, SNIPPET_STORE, snippet.id, text)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Added Snippet: ${snippet.id} , ${snippet.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeSnippet(id: String) {
|
||||
env.executeInTransaction {
|
||||
delete(it, SNIPPET_STORE, id)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Removed snippet: $id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getSnippets(): Collection<Snippet> {
|
||||
val isWorking = doorman.isWorking()
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<Snippet>(tx, SNIPPET_STORE) { _, value ->
|
||||
if (isWorking)
|
||||
ohMyJson.decodeFromString(doorman.decrypt(value))
|
||||
else
|
||||
ohMyJson.decodeFromString(value)
|
||||
}.values
|
||||
}
|
||||
}
|
||||
|
||||
fun getKeywordHighlights(): Collection<KeywordHighlight> {
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<KeywordHighlight>(tx, KEYWORD_HIGHLIGHT_STORE) { _, value ->
|
||||
@@ -242,6 +289,18 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
val k = StringBinding.stringToEntry(key)
|
||||
val v = StringBinding.stringToEntry(value)
|
||||
store.put(tx, k, v)
|
||||
|
||||
// 数据变动时触发一次同步
|
||||
if (name == HOST_STORE ||
|
||||
name == KEYMAP_STORE ||
|
||||
name == SNIPPET_STORE ||
|
||||
name == KEYWORD_HIGHLIGHT_STORE ||
|
||||
name == MACRO_STORE ||
|
||||
name == KEY_PAIR_STORE ||
|
||||
name == DELETED_DATA_STORE
|
||||
) {
|
||||
SyncManager.getInstance().triggerOnChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private fun delete(tx: Transaction, name: String, key: String) {
|
||||
@@ -301,8 +360,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
private val properties = Collections.synchronizedMap(mutableMapOf<String, String>())
|
||||
|
||||
init {
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
GlobalScope.launch(Dispatchers.IO) { properties.putAll(getProperties()) }
|
||||
swingCoroutineScope.launch(Dispatchers.IO) { properties.putAll(getProperties()) }
|
||||
}
|
||||
|
||||
protected open fun getString(key: String): String? {
|
||||
@@ -374,6 +432,13 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
protected inner class DoublePropertyDelegate(defaultValue: Double) :
|
||||
PropertyDelegate<Double>(defaultValue) {
|
||||
override fun convertValue(value: String): Double {
|
||||
return value.toDoubleOrNull() ?: initializer.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected inner class LongPropertyDelegate(defaultValue: Long) :
|
||||
PropertyDelegate<Long>(defaultValue) {
|
||||
@@ -458,6 +523,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
*/
|
||||
var beep by BooleanPropertyDelegate(true)
|
||||
|
||||
/**
|
||||
* 超链接
|
||||
*/
|
||||
var hyperlink by BooleanPropertyDelegate(true)
|
||||
|
||||
/**
|
||||
* 光标闪烁
|
||||
*/
|
||||
@@ -573,6 +643,16 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
var darkTheme by StringPropertyDelegate("Dark")
|
||||
var lightTheme by StringPropertyDelegate("Light")
|
||||
|
||||
/**
|
||||
* 允许后台运行,也就是托盘
|
||||
*/
|
||||
var backgroundRunning by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 背景图片的地址
|
||||
*/
|
||||
var backgroundImage by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
|
||||
/**
|
||||
* 语言
|
||||
*/
|
||||
@@ -580,6 +660,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
I18n.containsLanguage(Locale.getDefault()) ?: Locale.US.toString()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 透明度
|
||||
*/
|
||||
var opacity by DoublePropertyDelegate(1.0)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -599,12 +684,22 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
*/
|
||||
var sftpCommand by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
|
||||
/**
|
||||
* defaultDirectory
|
||||
*/
|
||||
var defaultDirectory by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
|
||||
|
||||
/**
|
||||
* 是否固定在标签栏
|
||||
*/
|
||||
var pinTab by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 是否保留原始文件时间
|
||||
*/
|
||||
var preserveModificationTime by BooleanPropertyDelegate(false)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -621,6 +716,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
*/
|
||||
var rangeHosts by BooleanPropertyDelegate(true)
|
||||
var rangeKeyPairs by BooleanPropertyDelegate(true)
|
||||
var rangeSnippets by BooleanPropertyDelegate(true)
|
||||
var rangeKeywordHighlights by BooleanPropertyDelegate(true)
|
||||
var rangeMacros by BooleanPropertyDelegate(true)
|
||||
var rangeKeymap by BooleanPropertyDelegate(true)
|
||||
@@ -644,6 +740,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
* 最后同步时间
|
||||
*/
|
||||
var lastSyncTime by LongPropertyDelegate(0L)
|
||||
|
||||
/**
|
||||
* 同步策略,为空就是默认手动
|
||||
*/
|
||||
var policy by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
|
||||
52
src/main/kotlin/app/termora/DeleteDataManager.kt
Normal file
52
src/main/kotlin/app/termora/DeleteDataManager.kt
Normal file
@@ -0,0 +1,52 @@
|
||||
package app.termora
|
||||
|
||||
/**
|
||||
* 仅标记
|
||||
*/
|
||||
class DeleteDataManager private constructor() {
|
||||
companion object {
|
||||
fun getInstance(): DeleteDataManager {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(DeleteDataManager::class) { DeleteDataManager() }
|
||||
}
|
||||
}
|
||||
|
||||
private val data = mutableMapOf<String, DeletedData>()
|
||||
private val database get() = Database.getDatabase()
|
||||
|
||||
fun removeHost(id: String, deleteDate: Long = System.currentTimeMillis()) {
|
||||
addDeletedData(DeletedData(id, "Host", deleteDate))
|
||||
}
|
||||
|
||||
fun removeKeymap(id: String, deleteDate: Long = System.currentTimeMillis()) {
|
||||
addDeletedData(DeletedData(id, "Keymap", deleteDate))
|
||||
}
|
||||
|
||||
fun removeKeyPair(id: String, deleteDate: Long = System.currentTimeMillis()) {
|
||||
addDeletedData(DeletedData(id, "KeyPair", deleteDate))
|
||||
}
|
||||
|
||||
fun removeKeywordHighlight(id: String, deleteDate: Long = System.currentTimeMillis()) {
|
||||
addDeletedData(DeletedData(id, "KeywordHighlight", deleteDate))
|
||||
}
|
||||
|
||||
fun removeMacro(id: String, deleteDate: Long = System.currentTimeMillis()) {
|
||||
addDeletedData(DeletedData(id, "Macro", deleteDate))
|
||||
}
|
||||
|
||||
fun removeSnippet(id: String, deleteDate: Long = System.currentTimeMillis()) {
|
||||
addDeletedData(DeletedData(id, "Snippet", deleteDate))
|
||||
}
|
||||
|
||||
private fun addDeletedData(deletedData: DeletedData) {
|
||||
if (data.containsKey(deletedData.id)) return
|
||||
data[deletedData.id] = deletedData
|
||||
database.addDeletedData(deletedData)
|
||||
}
|
||||
|
||||
fun getDeletedData(): List<DeletedData> {
|
||||
if (data.isEmpty()) {
|
||||
data.putAll(database.getDeletedData().associateBy { it.id })
|
||||
}
|
||||
return data.values.sortedBy { it.deleteDate }
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ package app.termora
|
||||
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.native.osx.NativeMacLibrary
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jetbrains.JBR
|
||||
import java.awt.*
|
||||
@@ -12,29 +12,60 @@ import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import javax.swing.*
|
||||
|
||||
|
||||
abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
||||
private val rootPanel = JPanel(BorderLayout())
|
||||
private val titleLabel = JLabel()
|
||||
private val titleBar by lazy { LogicCustomTitleBar.createCustomTitleBar(this) }
|
||||
val disposable = Disposer.newDisposable()
|
||||
private val customTitleBar = if (SystemInfo.isMacOS && JBR.isWindowDecorationsSupported())
|
||||
JBR.getWindowDecorations().createCustomTitleBar() else null
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_ACTION = "DEFAULT_ACTION"
|
||||
private const val PROCESS_GLOBAL_KEYMAP = "PROCESS_GLOBAL_KEYMAP"
|
||||
}
|
||||
|
||||
|
||||
protected var controlsVisible = true
|
||||
set(value) {
|
||||
field = value
|
||||
titleBar.putProperty("controls.visible", value)
|
||||
if (SystemInfo.isMacOS) {
|
||||
if (customTitleBar != null) {
|
||||
customTitleBar.putProperty("controls.visible", value)
|
||||
} else {
|
||||
NativeMacLibrary.setControlsVisible(this, value)
|
||||
}
|
||||
} else {
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICONIFFY, value)
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_MAXIMIZE, value)
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_CLOSE, value)
|
||||
}
|
||||
}
|
||||
|
||||
protected var titleBarHeight = UIManager.getInt("TabbedPane.tabHeight").toFloat()
|
||||
protected var fullWindowContent = false
|
||||
set(value) {
|
||||
titleBar.height = value
|
||||
field = value
|
||||
rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, value)
|
||||
}
|
||||
|
||||
protected var titleVisible = true
|
||||
set(value) {
|
||||
field = value
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, value)
|
||||
}
|
||||
|
||||
protected var titleIconVisible = false
|
||||
set(value) {
|
||||
field = value
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICON, value)
|
||||
}
|
||||
|
||||
|
||||
protected var titleBarHeight = UIManager.getInt("TabbedPane.tabHeight")
|
||||
set(value) {
|
||||
field = value
|
||||
if (SystemInfo.isMacOS) {
|
||||
customTitleBar?.height = height.toFloat()
|
||||
} else {
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, value)
|
||||
}
|
||||
}
|
||||
|
||||
protected var lostFocusDispose = false
|
||||
@@ -51,24 +82,42 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
||||
super.rootPane.putClientProperty(PROCESS_GLOBAL_KEYMAP, value)
|
||||
}
|
||||
|
||||
init {
|
||||
super.setDefaultCloseOperation(DISPOSE_ON_CLOSE)
|
||||
|
||||
// 使用 FlatLaf 的 TitlePane
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
rootPane.windowDecorationStyle = JRootPane.PLAIN_DIALOG
|
||||
}
|
||||
}
|
||||
|
||||
protected fun init() {
|
||||
|
||||
|
||||
defaultCloseOperation = DISPOSE_ON_CLOSE
|
||||
|
||||
initTitleBar()
|
||||
initEvents()
|
||||
|
||||
if (JBR.isWindowDecorationsSupported()) {
|
||||
if (rootPane.getClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE) != false) {
|
||||
val rootPanel = JPanel(BorderLayout())
|
||||
rootPanel.add(createCenterPanel(), BorderLayout.CENTER)
|
||||
|
||||
if (SystemInfo.isMacOS) {
|
||||
rootPane.putClientProperty("apple.awt.windowTitleVisible", false)
|
||||
rootPane.putClientProperty("apple.awt.fullWindowContent", true)
|
||||
rootPane.putClientProperty("apple.awt.transparentTitleBar", true)
|
||||
rootPane.putClientProperty(
|
||||
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING,
|
||||
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING_MEDIUM
|
||||
)
|
||||
|
||||
val titlePanel = createTitlePanel()
|
||||
if (titlePanel != null) {
|
||||
rootPanel.add(titlePanel, BorderLayout.NORTH)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rootPanel.add(createCenterPanel(), BorderLayout.CENTER)
|
||||
val customTitleBar = this.customTitleBar
|
||||
if (customTitleBar != null) {
|
||||
customTitleBar.putProperty("controls.visible", controlsVisible)
|
||||
customTitleBar.height = titleBarHeight.toFloat()
|
||||
JBR.getWindowDecorations().setCustomTitleBar(this, customTitleBar)
|
||||
}
|
||||
}
|
||||
|
||||
val southPanel = createSouthPanel()
|
||||
if (southPanel != null) {
|
||||
@@ -122,7 +171,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
||||
|
||||
val panel = JPanel(BorderLayout())
|
||||
panel.add(titleLabel, BorderLayout.CENTER)
|
||||
panel.preferredSize = Dimension(-1, titleBar.height.toInt())
|
||||
panel.preferredSize = Dimension(-1, titleBarHeight)
|
||||
|
||||
|
||||
return panel
|
||||
@@ -173,7 +222,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
||||
return
|
||||
}
|
||||
|
||||
doCancelAction()
|
||||
SwingUtilities.invokeLater { doCancelAction() }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -191,30 +240,20 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
||||
}
|
||||
})
|
||||
|
||||
if (SystemInfo.isWindows) {
|
||||
addWindowListener(object : WindowAdapter(), ThemeChangeListener {
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
ThemeManager.getInstance().removeThemeChangeListener(this)
|
||||
}
|
||||
|
||||
override fun windowOpened(e: WindowEvent) {
|
||||
onChanged()
|
||||
ThemeManager.getInstance().addThemeChangeListener(this)
|
||||
override fun addNotify() {
|
||||
super.addNotify()
|
||||
|
||||
// 显示后触发一次重绘制
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
this.controlsVisible = controlsVisible
|
||||
this.titleBarHeight = titleBarHeight
|
||||
this.titleIconVisible = titleIconVisible
|
||||
this.titleVisible = titleVisible
|
||||
this.fullWindowContent = fullWindowContent
|
||||
}
|
||||
|
||||
override fun onChanged() {
|
||||
titleBar.putProperty("controls.dark", FlatLaf.isLafDark())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun initTitleBar() {
|
||||
titleBar.height = titleBarHeight
|
||||
titleBar.putProperty("controls.visible", controlsVisible)
|
||||
if (JBR.isWindowDecorationsSupported()) {
|
||||
JBR.getWindowDecorations().setCustomTitleBar(this, titleBar)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun doOKAction() {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package app.termora
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
||||
init {
|
||||
generalOption.portTextField.value = host.port
|
||||
@@ -13,6 +16,8 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
||||
generalOption.passwordTextField.text = host.authentication.password
|
||||
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||
generalOption.publicKeyComboBox.selectedItem = host.authentication.password
|
||||
} else if (host.authentication.type == AuthenticationType.SSHAgent) {
|
||||
generalOption.sshAgentComboBox.selectedItem = host.authentication.password
|
||||
}
|
||||
|
||||
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||
@@ -28,6 +33,8 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
||||
terminalOption.heartbeatIntervalTextField.value = host.options.heartbeatInterval
|
||||
|
||||
tunnelingOption.tunnelings.addAll(host.tunnelings)
|
||||
tunnelingOption.x11ForwardingCheckBox.isSelected = host.options.enableX11Forwarding
|
||||
tunnelingOption.x11ServerTextField.text = StringUtils.defaultIfBlank(host.options.x11Forwarding, "localhost:0")
|
||||
|
||||
if (host.options.jumpHosts.isNotEmpty()) {
|
||||
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
||||
@@ -47,6 +54,8 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
||||
serialCommOption.parityComboBox.selectedItem = serialComm.parity
|
||||
serialCommOption.stopBitsComboBox.selectedItem = serialComm.stopBits
|
||||
serialCommOption.flowControlComboBox.selectedItem = serialComm.flowControl
|
||||
|
||||
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
||||
}
|
||||
|
||||
override fun getHost(): Host {
|
||||
|
||||
@@ -41,7 +41,7 @@ class FilterableHostTreeModel(
|
||||
continue
|
||||
}
|
||||
|
||||
if (c.host.protocol != Protocol.Folder) {
|
||||
if (c.data.protocol != Protocol.Folder) {
|
||||
if (filters.isNotEmpty() && filters.none { it.apply(c) }) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ enum class Protocol {
|
||||
SSH,
|
||||
Local,
|
||||
Serial,
|
||||
RDP,
|
||||
|
||||
/**
|
||||
* 交互式的 SFTP,此协议只在系统内部交互不应该暴露给用户也不应该持久化
|
||||
@@ -38,6 +39,7 @@ enum class AuthenticationType {
|
||||
No,
|
||||
Password,
|
||||
PublicKey,
|
||||
SSHAgent,
|
||||
KeyboardInteractive,
|
||||
}
|
||||
|
||||
@@ -132,6 +134,21 @@ data class Options(
|
||||
* 串口配置
|
||||
*/
|
||||
val serialComm: SerialComm = SerialComm(),
|
||||
|
||||
/**
|
||||
* SFTP 默认目录
|
||||
*/
|
||||
val sftpDefaultDirectory: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* X11 Forwarding
|
||||
*/
|
||||
val enableX11Forwarding: Boolean = false,
|
||||
|
||||
/**
|
||||
* X11 Server,Format: host.port. default: localhost:0
|
||||
*/
|
||||
val x11Forwarding: String = StringUtils.EMPTY,
|
||||
) {
|
||||
companion object {
|
||||
val Default = Options()
|
||||
@@ -209,6 +226,27 @@ data class EncryptedHost(
|
||||
var updateDate: Long = 0L,
|
||||
)
|
||||
|
||||
/**
|
||||
* 被删除的数据
|
||||
*/
|
||||
@Serializable
|
||||
data class DeletedData(
|
||||
/**
|
||||
* 被删除的 ID
|
||||
*/
|
||||
val id: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 数据类型:Host、Keymap、KeyPair、KeywordHighlight、Macro、Snippet
|
||||
*/
|
||||
val type: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 被删除的时间
|
||||
*/
|
||||
val deleteDate: Long,
|
||||
)
|
||||
|
||||
|
||||
@Serializable
|
||||
data class Host(
|
||||
|
||||
@@ -2,15 +2,17 @@ package app.termora
|
||||
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import org.apache.sshd.client.SshClient
|
||||
import org.apache.sshd.client.session.ClientSession
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.util.*
|
||||
import javax.swing.*
|
||||
|
||||
class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
||||
@@ -52,9 +54,9 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
||||
putValue(NAME, "${I18n.getString("termora.new-host.test-connection")}...")
|
||||
isEnabled = false
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
testConnection(pane.getHost())
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
// 因为测试连接的时候从数据库读取会导致失效,所以这里生成随机ID
|
||||
testConnection(pane.getHost().copy(id = UUID.randomUUID().toSimpleString()))
|
||||
withContext(Dispatchers.Swing) {
|
||||
putValue(NAME, I18n.getString("termora.new-host.test-connection"))
|
||||
isEnabled = true
|
||||
@@ -103,8 +105,7 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
||||
var client: SshClient? = null
|
||||
var session: ClientSession? = null
|
||||
try {
|
||||
client = SshClients.openClient(host)
|
||||
client.userInteraction = TerminalUserInteraction(owner)
|
||||
client = SshClients.openClient(host, this)
|
||||
session = SshClients.openSession(host, client)
|
||||
} finally {
|
||||
session?.close()
|
||||
|
||||
@@ -16,14 +16,20 @@ class HostManager private constructor() {
|
||||
*/
|
||||
fun addHost(host: Host) {
|
||||
assertEventDispatchThread()
|
||||
database.addHost(host)
|
||||
if (host.deleted) {
|
||||
hosts.entries.removeIf { it.value.id == host.id || it.value.parentId == host.id }
|
||||
removeHost(host.id)
|
||||
} else {
|
||||
database.addHost(host)
|
||||
hosts[host.id] = host
|
||||
}
|
||||
}
|
||||
|
||||
fun removeHost(id: String) {
|
||||
hosts.entries.removeIf { it.value.id == id || it.value.parentId == id }
|
||||
database.removeHost(id)
|
||||
DeleteDataManager.getInstance().removeHost(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 第一次调用从数据库中获取,后续从缓存中获取
|
||||
*/
|
||||
|
||||
@@ -6,14 +6,17 @@ import com.fazecast.jSerialComm.SerialPort
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
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.eclipse.jgit.internal.transport.sshd.agent.connector.PageantConnector
|
||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocketConnector
|
||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.WinPipeConnector
|
||||
import java.awt.*
|
||||
import java.awt.event.*
|
||||
import java.nio.charset.Charset
|
||||
@@ -21,7 +24,7 @@ import javax.swing.*
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
import javax.swing.table.DefaultTableModel
|
||||
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
open class HostOptionsPane : OptionsPane() {
|
||||
protected val tunnelingOption = TunnelingOption()
|
||||
protected val generalOption = GeneralOption()
|
||||
@@ -29,6 +32,7 @@ open class HostOptionsPane : OptionsPane() {
|
||||
protected val terminalOption = TerminalOption()
|
||||
protected val jumpHostsOption = JumpHostsOption()
|
||||
protected val serialCommOption = SerialCommOption()
|
||||
protected val sftpOption = SFTPOption()
|
||||
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||
|
||||
init {
|
||||
@@ -38,6 +42,7 @@ open class HostOptionsPane : OptionsPane() {
|
||||
addOption(jumpHostsOption)
|
||||
addOption(terminalOption)
|
||||
addOption(serialCommOption)
|
||||
addOption(sftpOption)
|
||||
|
||||
setContentBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
||||
}
|
||||
@@ -50,18 +55,23 @@ open class HostOptionsPane : OptionsPane() {
|
||||
val port = (generalOption.portTextField.value ?: 22) as Int
|
||||
var authentication = Authentication.No
|
||||
var proxy = Proxy.No
|
||||
val authenticationType = generalOption.authenticationTypeComboBox.selectedItem as AuthenticationType
|
||||
|
||||
|
||||
if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.Password) {
|
||||
if (authenticationType == AuthenticationType.Password) {
|
||||
authentication = authentication.copy(
|
||||
type = AuthenticationType.Password,
|
||||
type = authenticationType,
|
||||
password = String(generalOption.passwordTextField.password)
|
||||
)
|
||||
} else if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
|
||||
} else if (authenticationType == AuthenticationType.PublicKey) {
|
||||
authentication = authentication.copy(
|
||||
type = AuthenticationType.PublicKey,
|
||||
type = authenticationType,
|
||||
password = generalOption.publicKeyComboBox.selectedItem?.toString() ?: StringUtils.EMPTY
|
||||
)
|
||||
} else if (authenticationType == AuthenticationType.SSHAgent) {
|
||||
authentication = authentication.copy(
|
||||
type = authenticationType,
|
||||
password = generalOption.sshAgentComboBox.selectedItem?.toString() ?: StringUtils.EMPTY
|
||||
)
|
||||
}
|
||||
|
||||
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||
@@ -91,7 +101,10 @@ open class HostOptionsPane : OptionsPane() {
|
||||
startupCommand = terminalOption.startupCommandTextField.text,
|
||||
heartbeatInterval = (terminalOption.heartbeatIntervalTextField.value ?: 30) as Int,
|
||||
jumpHosts = jumpHostsOption.jumpHosts.map { it.id },
|
||||
serialComm = serialComm
|
||||
serialComm = serialComm,
|
||||
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||
enableX11Forwarding = tunnelingOption.x11ForwardingCheckBox.isSelected,
|
||||
x11Forwarding = tunnelingOption.x11ServerTextField.text,
|
||||
)
|
||||
|
||||
return Host(
|
||||
@@ -157,6 +170,17 @@ open class HostOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
// tunnel
|
||||
if (tunnelingOption.x11ForwardingCheckBox.isSelected) {
|
||||
if (validateField(tunnelingOption.x11ServerTextField)) {
|
||||
return false
|
||||
}
|
||||
val segments = tunnelingOption.x11ServerTextField.text.split(":")
|
||||
if (segments.size != 2 || segments[1].toIntOrNull() == null) {
|
||||
setOutlineError(tunnelingOption.x11ServerTextField)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -166,14 +190,18 @@ open class HostOptionsPane : OptionsPane() {
|
||||
*/
|
||||
private fun validateField(textField: JTextField): Boolean {
|
||||
if (textField.isEnabled && textField.text.isBlank()) {
|
||||
selectOptionJComponent(textField)
|
||||
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||
textField.requestFocusInWindow()
|
||||
setOutlineError(textField)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun setOutlineError(textField: JTextField) {
|
||||
selectOptionJComponent(textField)
|
||||
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||
textField.requestFocusInWindow()
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 true 表示有错误
|
||||
*/
|
||||
@@ -197,6 +225,7 @@ open class HostOptionsPane : OptionsPane() {
|
||||
private val passwordPanel = JPanel(BorderLayout())
|
||||
private val chooseKeyBtn = JButton(Icons.greyKey)
|
||||
val passwordTextField = OutlinePasswordField(255)
|
||||
val sshAgentComboBox = OutlineComboBox<String>()
|
||||
val publicKeyComboBox = OutlineComboBox<String>()
|
||||
val remarkTextArea = FixedLengthTextArea(512)
|
||||
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||
@@ -212,6 +241,10 @@ open class HostOptionsPane : OptionsPane() {
|
||||
publicKeyComboBox.isEditable = false
|
||||
chooseKeyBtn.isFocusable = false
|
||||
|
||||
// 只有 Windows 允许修改
|
||||
sshAgentComboBox.isEditable = SystemInfo.isWindows
|
||||
sshAgentComboBox.isEnabled = SystemInfo.isWindows
|
||||
|
||||
protocolTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
@@ -287,10 +320,22 @@ open class HostOptionsPane : OptionsPane() {
|
||||
protocolTypeComboBox.addItem(Protocol.SSH)
|
||||
protocolTypeComboBox.addItem(Protocol.Local)
|
||||
protocolTypeComboBox.addItem(Protocol.Serial)
|
||||
protocolTypeComboBox.addItem(Protocol.RDP)
|
||||
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.PublicKey)
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.SSHAgent)
|
||||
|
||||
if (SystemInfo.isWindows) {
|
||||
// 不要修改 addItem 的顺序,因为第一个是默认的
|
||||
sshAgentComboBox.addItem(PageantConnector.DESCRIPTOR.identityAgent)
|
||||
sshAgentComboBox.addItem(WinPipeConnector.DESCRIPTOR.identityAgent)
|
||||
sshAgentComboBox.placeholderText = PageantConnector.DESCRIPTOR.identityAgent
|
||||
} else {
|
||||
sshAgentComboBox.addItem(UnixDomainSocketConnector.DESCRIPTOR.identityAgent)
|
||||
sshAgentComboBox.placeholderText = UnixDomainSocketConnector.DESCRIPTOR.identityAgent
|
||||
}
|
||||
|
||||
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
|
||||
|
||||
@@ -454,6 +499,8 @@ open class HostOptionsPane : OptionsPane() {
|
||||
.add(chooseKeyBtn).xy(3, 1)
|
||||
.build(), BorderLayout.CENTER
|
||||
)
|
||||
} else if (authenticationTypeComboBox.selectedItem == AuthenticationType.SSHAgent) {
|
||||
passwordPanel.add(sshAgentComboBox, BorderLayout.CENTER)
|
||||
} else {
|
||||
passwordPanel.add(passwordTextField, BorderLayout.CENTER)
|
||||
}
|
||||
@@ -669,8 +716,58 @@ open class HostOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
protected inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||
val defaultDirectoryField = OutlineTextField(255)
|
||||
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
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, $formMargin",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
|
||||
.build()
|
||||
|
||||
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
protected inner class TunnelingOption : JPanel(BorderLayout()), Option {
|
||||
val tunnelings = mutableListOf<Tunneling>()
|
||||
val x11ForwardingCheckBox = JCheckBox("X DISPLAY:")
|
||||
val x11ServerTextField = OutlineTextField(255)
|
||||
|
||||
private val model = object : DefaultTableModel() {
|
||||
override fun getRowCount(): Int {
|
||||
@@ -745,13 +842,36 @@ open class HostOptionsPane : OptionsPane() {
|
||||
box.add(Box.createHorizontalStrut(4))
|
||||
box.add(deleteBtn)
|
||||
|
||||
add(JLabel("TCP/IP Forwarding:"), BorderLayout.NORTH)
|
||||
add(scrollPane, BorderLayout.CENTER)
|
||||
add(box, BorderLayout.SOUTH)
|
||||
x11ForwardingCheckBox.isFocusable = false
|
||||
|
||||
if (x11ServerTextField.text.isBlank()) {
|
||||
x11ServerTextField.text = "localhost:0"
|
||||
}
|
||||
|
||||
val x11Forwarding = Box.createHorizontalBox()
|
||||
x11Forwarding.border = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createTitledBorder("X11 Forwarding"),
|
||||
BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
)
|
||||
x11Forwarding.add(x11ForwardingCheckBox)
|
||||
x11Forwarding.add(x11ServerTextField)
|
||||
|
||||
x11ServerTextField.isEnabled = x11ForwardingCheckBox.isSelected
|
||||
|
||||
val panel = JPanel(BorderLayout())
|
||||
panel.add(JLabel("TCP/IP Forwarding:"), BorderLayout.NORTH)
|
||||
panel.add(scrollPane, BorderLayout.CENTER)
|
||||
panel.add(box, BorderLayout.SOUTH)
|
||||
panel.border = BorderFactory.createEmptyBorder(0, 0, 8, 0)
|
||||
|
||||
add(panel, BorderLayout.CENTER)
|
||||
add(x11Forwarding, BorderLayout.SOUTH)
|
||||
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
x11ForwardingCheckBox.addChangeListener { x11ServerTextField.isEnabled = x11ForwardingCheckBox.isSelected }
|
||||
|
||||
addBtn.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent?) {
|
||||
val dialog = PortForwardingDialog(SwingUtilities.getWindowAncestor(this@HostOptionsPane))
|
||||
@@ -1003,8 +1123,7 @@ open class HostOptionsPane : OptionsPane() {
|
||||
addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentShown(e: ComponentEvent) {
|
||||
removeComponentListener(this)
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
for (commPort in SerialPort.getCommPorts()) {
|
||||
withContext(Dispatchers.Swing) {
|
||||
serialPortComboBox.addItem(commPort.systemPortName)
|
||||
|
||||
@@ -5,6 +5,7 @@ import app.termora.actions.DataProviders
|
||||
import app.termora.terminal.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import java.beans.PropertyChangeEvent
|
||||
@@ -13,13 +14,13 @@ import javax.swing.Icon
|
||||
abstract class HostTerminalTab(
|
||||
val windowScope: WindowScope,
|
||||
val host: Host,
|
||||
protected val terminal: Terminal = TerminalFactory.getInstance(windowScope).createTerminal()
|
||||
protected val terminal: Terminal = TerminalFactory.getInstance().createTerminal()
|
||||
) : PropertyTerminalTab(), DataProvider {
|
||||
companion object {
|
||||
val Host = DataKey(app.termora.Host::class)
|
||||
}
|
||||
|
||||
protected val coroutineScope by lazy { CoroutineScope(Dispatchers.Swing) }
|
||||
protected val coroutineScope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.Swing) }
|
||||
protected val terminalModel get() = terminal.getTerminalModel()
|
||||
protected var unread = false
|
||||
set(value) {
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
package app.termora
|
||||
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
||||
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
||||
import javax.swing.Icon
|
||||
import javax.swing.tree.TreeNode
|
||||
|
||||
class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
class HostTreeNode(host: Host) : SimpleTreeNode<Host>(host) {
|
||||
companion object {
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
}
|
||||
|
||||
var host: Host
|
||||
get() = data
|
||||
set(value) = setUserObject(value)
|
||||
|
||||
override val isFolder: Boolean
|
||||
get() = data.protocol == Protocol.Folder
|
||||
|
||||
override val id: String
|
||||
get() = data.id
|
||||
|
||||
/**
|
||||
* 如果要重新赋值,记得修改 [Host.updateDate] 否则下次取出时可能时缓存的
|
||||
*/
|
||||
var host: Host
|
||||
override var data: Host
|
||||
get() {
|
||||
val cacheHost = hostManager.getHost((userObject as Host).id)
|
||||
val myHost = userObject as Host
|
||||
@@ -22,22 +34,24 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
}
|
||||
set(value) = setUserObject(value)
|
||||
|
||||
val folderCount
|
||||
get() = children().toList().count { if (it is HostTreeNode) it.host.protocol == Protocol.Folder else false }
|
||||
override val folderCount
|
||||
get() = children().toList().count { if (it is HostTreeNode) it.data.protocol == Protocol.Folder else false }
|
||||
|
||||
override fun getParent(): HostTreeNode? {
|
||||
return super.getParent() as HostTreeNode?
|
||||
}
|
||||
|
||||
fun getAllChildren(): List<HostTreeNode> {
|
||||
val children = mutableListOf<HostTreeNode>()
|
||||
for (child in children()) {
|
||||
if (child is HostTreeNode) {
|
||||
children.add(child)
|
||||
children.addAll(child.getAllChildren())
|
||||
override fun getAllChildren(): List<HostTreeNode> {
|
||||
return super.getAllChildren().filterIsInstance<HostTreeNode>()
|
||||
}
|
||||
|
||||
override fun getIcon(selected: Boolean, expanded: Boolean, hasFocus: Boolean): Icon {
|
||||
return when (host.protocol) {
|
||||
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||
Protocol.Serial -> if (selected && hasFocus) Icons.plugin.dark else Icons.plugin
|
||||
Protocol.RDP -> if (selected && hasFocus) Icons.microsoftWindows.dark else Icons.microsoftWindows
|
||||
else -> if (selected && hasFocus) Icons.terminal.dark else Icons.terminal
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
fun childrenNode(): List<HostTreeNode> {
|
||||
@@ -57,7 +71,7 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
|
||||
private fun deepClone(newNode: HostTreeNode, oldNode: HostTreeNode, scopes: Set<Protocol> = emptySet()) {
|
||||
for (child in oldNode.childrenNode()) {
|
||||
if (scopes.isNotEmpty() && !scopes.contains(child.host.protocol)) continue
|
||||
if (scopes.isNotEmpty() && !scopes.contains(child.data.protocol)) continue
|
||||
val newChildNode = child.clone() as HostTreeNode
|
||||
deepClone(newChildNode, child, scopes)
|
||||
newNode.add(newChildNode)
|
||||
@@ -65,7 +79,7 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
}
|
||||
|
||||
override fun clone(): Any {
|
||||
val newNode = HostTreeNode(host)
|
||||
val newNode = HostTreeNode(data)
|
||||
newNode.children = null
|
||||
newNode.parent = null
|
||||
return newNode
|
||||
@@ -74,7 +88,7 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
override fun isNodeChild(aNode: TreeNode?): Boolean {
|
||||
if (aNode is HostTreeNode) {
|
||||
for (node in childrenNode()) {
|
||||
if (node.host == aNode.host) {
|
||||
if (node.data == aNode.data) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -88,10 +102,10 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
|
||||
other as HostTreeNode
|
||||
|
||||
return host == other.host
|
||||
return data == other.data
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return host.hashCode()
|
||||
return data.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,11 @@ object Icons {
|
||||
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 close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") }
|
||||
val settingSync by lazy { DynamicIcon("icons/settingSync.svg", "icons/settingSync_dark.svg") }
|
||||
val openInNewWindow by lazy { DynamicIcon("icons/openInNewWindow.svg", "icons/openInNewWindow_dark.svg") }
|
||||
val openInToolWindow by lazy { DynamicIcon("icons/openInToolWindow.svg", "icons/openInToolWindow_dark.svg") }
|
||||
val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
|
||||
val inspectionsEye by lazy { DynamicIcon("icons/inspectionsEye.svg", "icons/inspectionsEye_dark.svg") }
|
||||
val eye by lazy { DynamicIcon("icons/eye.svg", "icons/eye_dark.svg") }
|
||||
val eyeClose by lazy { DynamicIcon("icons/eyeClose.svg", "icons/eyeClose_dark.svg") }
|
||||
val matchCase by lazy { DynamicIcon("icons/matchCase.svg", "icons/matchCase_dark.svg") }
|
||||
@@ -32,6 +34,7 @@ object Icons {
|
||||
val percentage by lazy { DynamicIcon("icons/percentage.svg", "icons/percentage_dark.svg") }
|
||||
val text by lazy { DynamicIcon("icons/text.svg", "icons/text_dark.svg") }
|
||||
val errorIntroduction by lazy { DynamicIcon("icons/errorIntroduction.svg", "icons/errorIntroduction_dark.svg") }
|
||||
val warningIntroduction by lazy { DynamicIcon("icons/warningIntroduction.svg", "icons/warningIntroduction_dark.svg") }
|
||||
val networkPolicy by lazy { DynamicIcon("icons/networkPolicy.svg", "icons/networkPolicy_dark.svg") }
|
||||
val clusterRole by lazy { DynamicIcon("icons/clusterRole.svg", "icons/clusterRole_dark.svg") }
|
||||
val daemonSets by lazy { DynamicIcon("icons/daemonSets.svg", "icons/daemonSets_dark.svg") }
|
||||
@@ -50,6 +53,7 @@ object Icons {
|
||||
val moreVertical by lazy { DynamicIcon("icons/moreVertical.svg", "icons/moreVertical_dark.svg") }
|
||||
val colors by lazy { DynamicIcon("icons/colors.svg", "icons/colors_dark.svg") }
|
||||
val chevronDown by lazy { DynamicIcon("icons/chevronDownLarge.svg", "icons/chevronDownLarge_dark.svg") }
|
||||
val chevronRight by lazy { DynamicIcon("icons/chevronRight.svg", "icons/chevronRight_dark.svg") }
|
||||
val homeFolder by lazy { DynamicIcon("icons/homeFolder.svg", "icons/homeFolder_dark.svg") }
|
||||
val openNewTab by lazy { DynamicIcon("icons/openNewTab.svg", "icons/openNewTab_dark.svg") }
|
||||
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") }
|
||||
@@ -60,6 +64,7 @@ object Icons {
|
||||
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 microsoft by lazy { DynamicIcon("icons/microsoft.svg", "icons/microsoft_dark.svg") }
|
||||
val microsoftWindows by lazy { DynamicIcon("icons/microsoftWindows.svg", "icons/microsoftWindows_dark.svg") }
|
||||
val tencent by lazy { DynamicIcon("icons/tencent.svg") }
|
||||
val google by lazy { DynamicIcon("icons/google-small.svg") }
|
||||
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }
|
||||
@@ -90,10 +95,14 @@ object Icons {
|
||||
val sortedSet by lazy { DynamicIcon("icons/sortedSet.svg", "icons/sortedSet_dark.svg") }
|
||||
val colorPicker by lazy { DynamicIcon("icons/colorPicker.svg", "icons/colorPicker_dark.svg") }
|
||||
val folder by lazy { DynamicIcon("icons/folder.svg", "icons/folder_dark.svg") }
|
||||
val file by lazy { DynamicIcon("icons/file.svg", "icons/file_dark.svg") }
|
||||
val listFiles by lazy { DynamicIcon("icons/listFiles.svg", "icons/listFiles_dark.svg") }
|
||||
val left by lazy { DynamicIcon("icons/left.svg", "icons/left_dark.svg") }
|
||||
val right by lazy { DynamicIcon("icons/right.svg", "icons/right_dark.svg") }
|
||||
val dotListFiles by lazy { DynamicIcon("icons/dotListFiles.svg", "icons/dotListFiles_dark.svg") }
|
||||
val anyType by lazy { DynamicIcon("icons/anyType.svg", "icons/anyType_dark.svg") }
|
||||
val toolWindowJsonPath by lazy { DynamicIcon("icons/toolWindowJsonPath.svg", "icons/toolWindowJsonPath_dark.svg") }
|
||||
val codeSpan by lazy { DynamicIcon("icons/codeSpan.svg", "icons/codeSpan_dark.svg") }
|
||||
val fileTransfer by lazy { DynamicIcon("icons/fileTransfer.svg", "icons/fileTransfer_dark.svg") }
|
||||
val help by lazy { DynamicIcon("icons/help.svg", "icons/help_dark.svg") }
|
||||
val applyNotConflictsLeft by lazy {
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.extras.components.FlatTextField
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.Window
|
||||
import java.awt.event.KeyAdapter
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.UIManager
|
||||
|
||||
class InputDialog(
|
||||
owner: Window,
|
||||
title: String,
|
||||
text: String = StringUtils.EMPTY,
|
||||
placeholderText: String = StringUtils.EMPTY
|
||||
) : DialogWrapper(owner) {
|
||||
private val textField = FlatTextField()
|
||||
private var text: String? = null
|
||||
|
||||
init {
|
||||
setSize(340, 60)
|
||||
setLocationRelativeTo(owner)
|
||||
|
||||
super.setTitle(title)
|
||||
|
||||
isResizable = false
|
||||
isModal = true
|
||||
controlsVisible = false
|
||||
titleBarHeight = UIManager.getInt("TabbedPane.tabHeight") * 0.8f
|
||||
|
||||
|
||||
textField.placeholderText = placeholderText
|
||||
textField.text = text
|
||||
textField.addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
if (e.keyCode == KeyEvent.VK_ENTER) {
|
||||
if (textField.text.isBlank()) {
|
||||
return
|
||||
}
|
||||
doOKAction()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
textField.background = UIManager.getColor("window")
|
||||
textField.border = BorderFactory.createEmptyBorder(0, 13, 0, 13)
|
||||
|
||||
return textField
|
||||
}
|
||||
|
||||
fun getText(): String? {
|
||||
isVisible = true
|
||||
return text
|
||||
}
|
||||
|
||||
override fun doCancelAction() {
|
||||
text = null
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
text = textField.text
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
override fun createSouthPanel(): JComponent? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,25 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.terminal.PtyConnector
|
||||
import app.termora.terminal.PtyConnectorDelegate
|
||||
import app.termora.terminal.PtyProcessConnector
|
||||
import org.apache.commons.io.Charsets
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.charset.StandardCharsets
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
PtyHostTerminalTab(windowScope, host) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(LocalTerminalTab::class.java)
|
||||
}
|
||||
|
||||
override suspend fun openPtyConnector(): PtyConnector {
|
||||
val winSize = terminalPanel.winSize()
|
||||
val ptyConnector = PtyConnectorFactory.getInstance(windowScope).createPtyConnector(
|
||||
val ptyConnector = PtyConnectorFactory.getInstance().createPtyConnector(
|
||||
winSize.rows, winSize.cols,
|
||||
host.options.envs(),
|
||||
Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8),
|
||||
@@ -18,4 +28,42 @@ class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
return ptyConnector
|
||||
}
|
||||
|
||||
|
||||
override fun willBeClose(): Boolean {
|
||||
val ptyProcessConnector = getPtyProcessConnector() ?: return true
|
||||
val process = ptyProcessConnector.process
|
||||
var consoleProcessCount = 0
|
||||
|
||||
try {
|
||||
val processHandle = ProcessHandle.of(process.pid()).getOrNull()
|
||||
if (processHandle != null) {
|
||||
consoleProcessCount = processHandle.children().count().toInt()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
// 没有正在运行的进程
|
||||
if (consoleProcessCount < 1) return true
|
||||
|
||||
val owner = SwingUtilities.getWindowAncestor(terminalPanel) ?: return true
|
||||
return OptionPane.showConfirmDialog(
|
||||
owner,
|
||||
I18n.getString("termora.tabbed.local-tab.close-prompt"),
|
||||
messageType = JOptionPane.INFORMATION_MESSAGE,
|
||||
optionType = JOptionPane.OK_CANCEL_OPTION
|
||||
) == JOptionPane.OK_OPTION
|
||||
}
|
||||
|
||||
|
||||
private fun getPtyProcessConnector(): PtyProcessConnector? {
|
||||
var p = getPtyConnector() as PtyConnector?
|
||||
while (p != null) {
|
||||
if (p is PtyProcessConnector) return p
|
||||
if (p is PtyConnectorDelegate) p = p.ptyConnector
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.jetbrains.JBR
|
||||
import com.jetbrains.WindowDecorations.CustomTitleBar
|
||||
import java.awt.Rectangle
|
||||
import java.awt.Window
|
||||
import javax.swing.RootPaneContainer
|
||||
|
||||
class LogicCustomTitleBar(private val titleBar: CustomTitleBar) : CustomTitleBar {
|
||||
companion object {
|
||||
fun createCustomTitleBar(rootPaneContainer: RootPaneContainer): CustomTitleBar {
|
||||
if (!JBR.isWindowDecorationsSupported()) {
|
||||
return LogicCustomTitleBar(object : CustomTitleBar {
|
||||
override fun getHeight(): Float {
|
||||
val bounds = rootPaneContainer.rootPane
|
||||
.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
||||
if (bounds is Rectangle) {
|
||||
return bounds.height.toFloat()
|
||||
}
|
||||
return 0f
|
||||
}
|
||||
|
||||
override fun setHeight(height: Float) {
|
||||
rootPaneContainer.rootPane.putClientProperty(
|
||||
FlatClientProperties.TITLE_BAR_HEIGHT,
|
||||
height.toInt()
|
||||
)
|
||||
}
|
||||
|
||||
override fun getProperties(): MutableMap<String, Any> {
|
||||
return mutableMapOf()
|
||||
}
|
||||
|
||||
override fun putProperties(m: MutableMap<String, *>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun putProperty(key: String?, value: Any?) {
|
||||
if (key == "controls.visible" && value is Boolean) {
|
||||
rootPaneContainer.rootPane.putClientProperty(
|
||||
FlatClientProperties.TITLE_BAR_SHOW_CLOSE,
|
||||
value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLeftInset(): Float {
|
||||
return 0f
|
||||
}
|
||||
|
||||
override fun getRightInset(): Float {
|
||||
val bounds = rootPaneContainer.rootPane
|
||||
.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
||||
if (bounds is Rectangle) {
|
||||
return bounds.width.toFloat()
|
||||
}
|
||||
return 0f
|
||||
}
|
||||
|
||||
override fun forceHitTest(client: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
override fun getContainingWindow(): Window {
|
||||
return rootPaneContainer as Window
|
||||
}
|
||||
})
|
||||
}
|
||||
return JBR.getWindowDecorations().createCustomTitleBar()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getHeight(): Float {
|
||||
return titleBar.height
|
||||
}
|
||||
|
||||
override fun setHeight(height: Float) {
|
||||
titleBar.height = height
|
||||
}
|
||||
|
||||
override fun getProperties(): MutableMap<String, Any> {
|
||||
return titleBar.properties
|
||||
}
|
||||
|
||||
override fun putProperties(m: MutableMap<String, *>?) {
|
||||
titleBar.putProperties(m)
|
||||
}
|
||||
|
||||
override fun putProperty(key: String?, value: Any?) {
|
||||
titleBar.putProperty(key, value)
|
||||
}
|
||||
|
||||
override fun getLeftInset(): Float {
|
||||
return titleBar.leftInset
|
||||
}
|
||||
|
||||
override fun getRightInset(): Float {
|
||||
return titleBar.rightInset
|
||||
}
|
||||
|
||||
override fun forceHitTest(client: Boolean) {
|
||||
titleBar.forceHitTest(client)
|
||||
}
|
||||
|
||||
override fun getContainingWindow(): Window {
|
||||
return titleBar.containingWindow
|
||||
}
|
||||
}
|
||||
@@ -1,58 +1,6 @@
|
||||
package app.termora
|
||||
|
||||
import com.pty4j.util.PtyUtil
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
import java.io.File
|
||||
|
||||
fun main() {
|
||||
// 由于 macOS 签名和公证问题,依赖二进制依赖会单独在一个文件夹
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
setupNativeLibraries()
|
||||
ApplicationInitializr().run()
|
||||
}
|
||||
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
System.setProperty("apple.awt.application.name", Application.getName())
|
||||
}
|
||||
|
||||
ApplicationRunner().run()
|
||||
}
|
||||
|
||||
|
||||
private fun setupNativeLibraries() {
|
||||
if (!SystemUtils.IS_OS_MAC_OSX) {
|
||||
return
|
||||
}
|
||||
|
||||
val appPath = Application.getAppPath()
|
||||
if (StringUtils.isBlank(appPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
val contents = File(appPath).parentFile?.parentFile ?: return
|
||||
val dylib = FileUtils.getFile(contents, "app", "dylib")
|
||||
if (!dylib.exists()) {
|
||||
return
|
||||
}
|
||||
|
||||
val jna = FileUtils.getFile(dylib, "jna")
|
||||
if (jna.exists()) {
|
||||
System.setProperty("jna.boot.library.path", jna.absolutePath)
|
||||
}
|
||||
|
||||
val pty4j = FileUtils.getFile(dylib, "pty4j")
|
||||
if (pty4j.exists()) {
|
||||
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)
|
||||
}
|
||||
|
||||
val restart4j = FileUtils.getFile(dylib, "restart4j", "restarter")
|
||||
if (restart4j.exists()) {
|
||||
System.setProperty("restarter.path", restart4j.absolutePath)
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package app.termora
|
||||
|
||||
|
||||
import app.termora.terminal.PtyConnector
|
||||
import app.termora.terminal.PtyConnectorDelegate
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
|
||||
/**
|
||||
* 当开启转发时,会获取到所有的 [PtyConnector] 然后跳过中间层,直接找到最近的一个 [MultiplePtyConnector],如果找不到那就以最后一个匹配不到的为准 [getMultiplePtyConnector]。
|
||||
*/
|
||||
class MultiplePtyConnector(
|
||||
private val myConnector: PtyConnector
|
||||
) : PtyConnectorDelegate(myConnector) {
|
||||
|
||||
private val isMultiple get() = ActionManager.getInstance().isSelected(Actions.MULTIPLE)
|
||||
private val ptyConnectors
|
||||
get() = ApplicationScope.forApplicationScope()
|
||||
.windowScopes().map { PtyConnectorFactory.getInstance(it).getPtyConnectors() }
|
||||
.flatten()
|
||||
|
||||
override fun write(buffer: ByteArray, offset: Int, len: Int) {
|
||||
if (isMultiple) {
|
||||
for (connector in ptyConnectors) {
|
||||
getMultiplePtyConnector(connector).write(buffer, offset, len)
|
||||
}
|
||||
} else {
|
||||
myConnector.write(buffer, offset, len)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun getMultiplePtyConnector(connector: PtyConnector): PtyConnector {
|
||||
if (connector is MultiplePtyConnector) {
|
||||
val c = connector.myConnector
|
||||
if (c is MultiplePtyConnector) {
|
||||
return getMultiplePtyConnector(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
if (connector is PtyConnectorDelegate) {
|
||||
val c = connector.ptyConnector
|
||||
if (c != null) {
|
||||
return getMultiplePtyConnector(c)
|
||||
}
|
||||
}
|
||||
|
||||
return connector
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package app.termora
|
||||
|
||||
|
||||
import app.termora.actions.ActionManager
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.actions.MultipleAction
|
||||
import app.termora.terminal.Terminal
|
||||
import app.termora.terminal.TerminalColor
|
||||
import app.termora.terminal.TextStyle
|
||||
@@ -9,8 +11,10 @@ import app.termora.terminal.panel.FloatingToolbarPanel
|
||||
import app.termora.terminal.panel.TerminalDisplay
|
||||
import app.termora.terminal.panel.TerminalPaintListener
|
||||
import app.termora.terminal.panel.TerminalPanel
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.Color
|
||||
import java.awt.Graphics
|
||||
import java.util.*
|
||||
|
||||
class MultipleTerminalListener : TerminalPaintListener {
|
||||
override fun after(
|
||||
@@ -21,9 +25,9 @@ class MultipleTerminalListener : TerminalPaintListener {
|
||||
terminalDisplay: TerminalDisplay,
|
||||
terminal: Terminal
|
||||
) {
|
||||
if (!ActionManager.getInstance().isSelected(Actions.MULTIPLE)) {
|
||||
return
|
||||
}
|
||||
val windowScope = AnActionEvent(terminalPanel, StringUtils.EMPTY, EventObject(terminalPanel))
|
||||
.getData(DataProviders.WindowScope) ?: return
|
||||
if (!MultipleAction.getInstance(windowScope).isSelected) return
|
||||
|
||||
val oldFont = g.font
|
||||
val colorPalette = terminal.getTerminalModel().getColorPalette()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviders
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
@@ -23,6 +22,7 @@ class MyTabbedPane : FlatTabbedPane() {
|
||||
.getData(DataProviders.TermoraFrame) as TermoraFrame
|
||||
|
||||
init {
|
||||
isFocusable = false
|
||||
initEvents()
|
||||
}
|
||||
|
||||
@@ -229,11 +229,8 @@ class MyTabbedPane : FlatTabbedPane() {
|
||||
|
||||
private fun dragToAnotherWindow(oldFrame: TermoraFrame, frame: TermoraFrame) {
|
||||
val tab = this.terminalTab ?: return
|
||||
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
||||
val tabbedManager = frame.getData(DataProviders.TerminalTabbed) ?: return
|
||||
val tabbedPane = frame.getData(DataProviders.TabbedPane) ?: return
|
||||
val windowScope = frame.getData(DataProviders.WindowScope) ?: return
|
||||
val oldWindowScope = oldFrame.getData(DataProviders.WindowScope) ?: return
|
||||
val location = Point(MouseInfo.getPointerInfo().location)
|
||||
SwingUtilities.convertPointFromScreen(location, tabbedPane)
|
||||
val index = tabbedPane.indexAtLocation(location.x, location.y)
|
||||
@@ -245,11 +242,6 @@ class MyTabbedPane : FlatTabbedPane() {
|
||||
index
|
||||
)
|
||||
|
||||
TerminalPanelFactory.getInstance(oldWindowScope).removeTerminalPanel(terminalPanel)
|
||||
TerminalPanelFactory.getInstance(windowScope).addTerminalPanel(terminalPanel)
|
||||
|
||||
|
||||
|
||||
if (frame.hasFocus()) {
|
||||
return
|
||||
}
|
||||
|
||||
41
src/main/kotlin/app/termora/NativeStringComparator.kt
Normal file
41
src/main/kotlin/app/termora/NativeStringComparator.kt
Normal file
@@ -0,0 +1,41 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import de.jangassen.jfa.foundation.Foundation
|
||||
import de.jangassen.jfa.foundation.Foundation.NSAutoreleasePool
|
||||
import java.text.Collator
|
||||
import java.util.*
|
||||
|
||||
class NativeStringComparator private constructor() : Comparator<String> {
|
||||
private val collator by lazy { Collator.getInstance(Locale.getDefault()).apply { strength = Collator.PRIMARY } }
|
||||
|
||||
companion object {
|
||||
fun getInstance(): NativeStringComparator {
|
||||
return ApplicationScope.forApplicationScope()
|
||||
.getOrCreate(NativeStringComparator::class) { NativeStringComparator() }
|
||||
}
|
||||
|
||||
private const val SORT_DIGITSASNUMBERS: Int = 0x00000008
|
||||
|
||||
}
|
||||
|
||||
override fun compare(o1: String, o2: String): Int {
|
||||
if (SystemInfo.isWindows) {
|
||||
// CompareStringEx returns 1, 2, 3 respectively instead of -1, 0, 1
|
||||
return MyKernel32.INSTANCE.CompareStringEx(SORT_DIGITSASNUMBERS, o1, o2) - 2
|
||||
} else if (SystemInfo.isMacOS) {
|
||||
val pool = NSAutoreleasePool()
|
||||
try {
|
||||
val a = Foundation.nsString(o1)
|
||||
val b = Foundation.nsString(o2)
|
||||
return Foundation.invoke(a, "localizedStandardCompare:", b).toInt()
|
||||
} finally {
|
||||
pool.drain()
|
||||
}
|
||||
}
|
||||
|
||||
return collator.compare(o1, o2)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.OpenHostAction
|
||||
import app.termora.transport.SFTPAction
|
||||
import app.termora.sftp.SFTPActionEvent
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
||||
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.*
|
||||
import org.apache.commons.csv.CSVFormat
|
||||
@@ -17,29 +14,20 @@ import org.apache.commons.io.FilenameUtils
|
||||
import org.apache.commons.io.filefilter.FileFilterUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import org.apache.sshd.client.config.hosts.HostConfigEntry
|
||||
import org.ini4j.Ini
|
||||
import org.ini4j.Reg
|
||||
import org.jdesktop.swingx.JXTree
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.NodeList
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.Transferable
|
||||
import java.awt.datatransfer.UnsupportedFlavorException
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.ActionListener
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.*
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import java.util.function.Function
|
||||
import javax.swing.*
|
||||
import javax.swing.event.CellEditorListener
|
||||
import javax.swing.event.ChangeEvent
|
||||
import javax.swing.event.PopupMenuEvent
|
||||
import javax.swing.event.PopupMenuListener
|
||||
import javax.swing.filechooser.FileNameExtensionFilter
|
||||
@@ -48,26 +36,25 @@ import javax.swing.tree.TreeSelectionModel
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.xpath.XPathConstants
|
||||
import javax.xml.xpath.XPathFactory
|
||||
import kotlin.math.min
|
||||
|
||||
class NewHostTree : JXTree() {
|
||||
@Suppress("CascadeIf")
|
||||
class NewHostTree : SimpleTree() {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(NewHostTree::class.java)
|
||||
private val CSV_HEADERS = arrayOf("Folders", "Label", "Hostname", "Port", "Username", "Protocol")
|
||||
}
|
||||
|
||||
private val tree = this
|
||||
private val editor = OutlineTextField(64)
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||
private val openHostAction get() = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
|
||||
private val sftpAction get() = ActionManager.getInstance().getAction(app.termora.Actions.SFTP)
|
||||
private var isShowMoreInfo
|
||||
get() = properties.getString("HostTree.showMoreInfo", "false").toBoolean()
|
||||
set(value) = properties.putString("HostTree.showMoreInfo", value.toString())
|
||||
|
||||
private val model = NewHostTreeModel()
|
||||
private var isPopupMenu = false
|
||||
override val model = NewHostTreeModel()
|
||||
|
||||
/**
|
||||
* 是否允许显示右键菜单
|
||||
@@ -92,7 +79,6 @@ class NewHostTree : JXTree() {
|
||||
isRootVisible = true
|
||||
dropMode = DropMode.ON_OR_INSERT
|
||||
selectionModel.selectionMode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION
|
||||
editor.preferredSize = Dimension(220, 0)
|
||||
|
||||
// renderer
|
||||
setCellRenderer(object : DefaultXTreeCellRenderer() {
|
||||
@@ -112,7 +98,7 @@ class NewHostTree : JXTree() {
|
||||
// 是否显示更多信息
|
||||
if (isShowMoreInfo) {
|
||||
val color = if (sel) {
|
||||
if (tree.hasFocus()) {
|
||||
if (tree.hasFocus() || isPopupMenu) {
|
||||
UIManager.getColor("textHighlightText")
|
||||
} else {
|
||||
this.foreground
|
||||
@@ -125,236 +111,72 @@ class NewHostTree : JXTree() {
|
||||
"""<font color=rgb(${color.red},${color.green},${color.blue})>${it}</font>"""
|
||||
}
|
||||
|
||||
if (host.protocol == Protocol.SSH) {
|
||||
text =
|
||||
"<html>${host.name} ${fontTag.apply("${host.username}@${host.host}")}</html>"
|
||||
// @formatter:off
|
||||
if (host.protocol == Protocol.SSH || host.protocol == Protocol.RDP) {
|
||||
text = "<html>${host.name} ${fontTag.apply("${host.username}@${host.host}")}</html>"
|
||||
} else if (host.protocol == Protocol.Serial) {
|
||||
text =
|
||||
"<html>${host.name} ${fontTag.apply(host.options.serialComm.port)}</html>"
|
||||
text = "<html>${host.name} ${fontTag.apply(host.options.serialComm.port)}</html>"
|
||||
} else if (host.protocol == Protocol.Folder) {
|
||||
text = "<html>${host.name}${fontTag.apply(" (${node.childCount})")}</html>"
|
||||
text = "<html>${host.name}${fontTag.apply(" (${node.getAllChildren().size})")}</html>"
|
||||
}
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
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 && tree.hasFocus()) Icons.plugin.dark else Icons.plugin
|
||||
else -> if (sel && tree.hasFocus()) Icons.terminal.dark else Icons.terminal
|
||||
}
|
||||
icon = node.getIcon(sel, expanded, tree.hasFocus() || isPopupMenu)
|
||||
return c
|
||||
}
|
||||
})
|
||||
|
||||
// rename
|
||||
setCellEditor(object : DefaultCellEditor(editor) {
|
||||
override fun isCellEditable(e: EventObject?): Boolean {
|
||||
if (e is MouseEvent) {
|
||||
return false
|
||||
}
|
||||
return super.isCellEditable(e)
|
||||
}
|
||||
|
||||
override fun getCellEditorValue(): Any {
|
||||
val node = lastSelectedPathComponent as HostTreeNode
|
||||
return node.host
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
// 右键选中
|
||||
// double click
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (!SwingUtilities.isRightMouseButton(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
requestFocusInWindow()
|
||||
|
||||
val selectionRows = selectionModel.selectionRows
|
||||
|
||||
val selRow = getClosestRowForLocation(e.x, e.y)
|
||||
if (selRow < 0) {
|
||||
selectionModel.clearSelection()
|
||||
return
|
||||
} else if (selectionRows != null && selectionRows.contains(selRow)) {
|
||||
return
|
||||
}
|
||||
|
||||
selectionPath = getPathForLocation(e.x, e.y)
|
||||
|
||||
setSelectionRow(selRow)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// contextmenu
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (!(SwingUtilities.isRightMouseButton(e))) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Objects.isNull(lastSelectedPathComponent)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (contextmenu) {
|
||||
SwingUtilities.invokeLater { showContextmenu(e) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||
val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return
|
||||
if (lastNode.host.protocol != Protocol.Folder) {
|
||||
val path = tree.getClosestPathForLocation(e.x, e.y) ?: return
|
||||
val bounds = tree.getRowBounds(tree.getRowForPath(path)) ?: return
|
||||
if ((e.y >= bounds.y && e.y < (bounds.y + bounds.height)).not()) return
|
||||
openHostAction?.actionPerformed(OpenHostActionEvent(e.source, lastNode.host, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// rename
|
||||
getCellEditor().addCellEditorListener(object : CellEditorListener {
|
||||
override fun editingStopped(e: ChangeEvent) {
|
||||
val lastHost = lastSelectedPathComponent
|
||||
if (lastHost !is HostTreeNode || editor.text.isBlank() || editor.text == lastHost.host.name) {
|
||||
return
|
||||
addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
if (e.keyCode == KeyEvent.VK_ENTER && doubleClickConnection) {
|
||||
val nodes = getSelectionSimpleTreeNodes()
|
||||
if (nodes.size == 1 && nodes.first().host.protocol == Protocol.Folder) {
|
||||
val path = TreePath(model.getPathToRoot(nodes.first()))
|
||||
if (isExpanded(path)) {
|
||||
collapsePath(path)
|
||||
} else {
|
||||
expandPath(path)
|
||||
}
|
||||
} else {
|
||||
for (node in getSelectionSimpleTreeNodes(true)) {
|
||||
openHostAction?.actionPerformed(OpenHostActionEvent(e.source, node.host, e))
|
||||
}
|
||||
}
|
||||
lastHost.host = lastHost.host.copy(name = editor.text, updateDate = System.currentTimeMillis())
|
||||
hostManager.addHost(lastHost.host)
|
||||
}
|
||||
|
||||
override fun editingCanceled(e: ChangeEvent) {
|
||||
}
|
||||
})
|
||||
|
||||
// drag
|
||||
transferHandler = object : TransferHandler() {
|
||||
|
||||
override fun createTransferable(c: JComponent): Transferable? {
|
||||
val nodes = getSelectionHostTreeNodes().toMutableList()
|
||||
if (nodes.isEmpty()) return null
|
||||
if (nodes.contains(model.root)) return null
|
||||
|
||||
val iterator = nodes.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val node = iterator.next()
|
||||
val parents = model.getPathToRoot(node).filter { it != node }
|
||||
if (parents.any { nodes.contains(it) }) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
|
||||
return MoveHostTransferable(nodes)
|
||||
}
|
||||
|
||||
override fun getSourceActions(c: JComponent?): Int {
|
||||
return MOVE
|
||||
}
|
||||
|
||||
override fun canImport(support: TransferSupport): Boolean {
|
||||
if (support.component != tree) return false
|
||||
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
|
||||
val node = dropLocation.path.lastPathComponent as? HostTreeNode ?: return false
|
||||
if (!support.isDataFlavorSupported(MoveHostTransferable.dataFlavor)) return false
|
||||
val nodes = (support.transferable.getTransferData(MoveHostTransferable.dataFlavor) as? List<*>)
|
||||
?.filterIsInstance<HostTreeNode>() ?: return false
|
||||
if (nodes.isEmpty()) return false
|
||||
if (node.host.protocol != Protocol.Folder) return false
|
||||
|
||||
for (e in nodes) {
|
||||
// 禁止拖拽到自己的子下面
|
||||
if (dropLocation.path.equals(TreePath(e.path)) || TreePath(e.path).isDescendant(dropLocation.path)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 文件夹只能拖拽到文件夹的下面
|
||||
if (e.host.protocol == Protocol.Folder) {
|
||||
if (dropLocation.childIndex > node.folderCount) {
|
||||
return false
|
||||
}
|
||||
} else if (dropLocation.childIndex != -1) {
|
||||
// 非文件夹也不能拖拽到文件夹的上面
|
||||
if (dropLocation.childIndex < node.folderCount) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val p = e.parent ?: continue
|
||||
// 如果是同级目录排序,那么判断是不是自己的上下,如果是的话也禁止
|
||||
if (p == node && dropLocation.childIndex != -1) {
|
||||
val idx = p.getIndex(e)
|
||||
if (dropLocation.childIndex in idx..idx + 1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
support.setShowDropLocation(true)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun importData(support: TransferSupport): Boolean {
|
||||
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
|
||||
val node = dropLocation.path.lastPathComponent as? HostTreeNode ?: return false
|
||||
val nodes = (support.transferable.getTransferData(MoveHostTransferable.dataFlavor) as? List<*>)
|
||||
?.filterIsInstance<HostTreeNode>() ?: return false
|
||||
|
||||
// 展开的 host id
|
||||
val expanded = mutableSetOf(node.host.id)
|
||||
for (e in nodes) {
|
||||
e.getAllChildren().filter { isExpanded(TreePath(model.getPathToRoot(it))) }
|
||||
.map { it.host.id }.forEach { expanded.add(it) }
|
||||
}
|
||||
|
||||
// 转移
|
||||
for (e in nodes) {
|
||||
model.removeNodeFromParent(e)
|
||||
e.host = e.host.copy(parentId = node.host.id, updateDate = System.currentTimeMillis())
|
||||
hostManager.addHost(e.host)
|
||||
|
||||
if (dropLocation.childIndex == -1) {
|
||||
if (e.host.protocol == Protocol.Folder) {
|
||||
model.insertNodeInto(e, node, node.folderCount)
|
||||
} else {
|
||||
model.insertNodeInto(e, node, node.childCount)
|
||||
}
|
||||
} else {
|
||||
if (e.host.protocol == Protocol.Folder) {
|
||||
model.insertNodeInto(e, node, min(node.folderCount, dropLocation.childIndex))
|
||||
} else {
|
||||
model.insertNodeInto(e, node, min(node.childCount, dropLocation.childIndex))
|
||||
}
|
||||
}
|
||||
|
||||
selectionPath = TreePath(model.getPathToRoot(e))
|
||||
}
|
||||
|
||||
// 先展开最顶级的
|
||||
expandPath(TreePath(model.getPathToRoot(node)))
|
||||
|
||||
for (child in node.getAllChildren()) {
|
||||
if (expanded.contains(child.host.id)) {
|
||||
expandPath(TreePath(model.getPathToRoot(child)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun showContextmenu(event: MouseEvent) {
|
||||
override fun showContextmenu(evt: MouseEvent) {
|
||||
if (!contextmenu) return
|
||||
val lastNode = lastSelectedPathComponent
|
||||
if (lastNode !is HostTreeNode) return
|
||||
|
||||
val nodes = getSelectionSimpleTreeNodes()
|
||||
val fullNodes = getSelectionSimpleTreeNodes(true)
|
||||
val lastNodeParent = lastNode.parent ?: model.root
|
||||
val lastHost = lastNode.host
|
||||
|
||||
@@ -370,8 +192,10 @@ class NewHostTree : JXTree() {
|
||||
val finalShellMenu = importMenu.add("FinalShell")
|
||||
val windTermMenu = importMenu.add("WindTerm")
|
||||
val secureCRTMenu = importMenu.add("SecureCRT")
|
||||
val sshMenu = importMenu.add(".ssh/config")
|
||||
val mobaXtermMenu = importMenu.add("MobaXterm")
|
||||
|
||||
|
||||
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")
|
||||
@@ -403,6 +227,7 @@ class NewHostTree : JXTree() {
|
||||
secureCRTMenu.addActionListener { importHosts(lastNode, ImportType.SecureCRT) }
|
||||
electermMenu.addActionListener { importHosts(lastNode, ImportType.electerm) }
|
||||
mobaXtermMenu.addActionListener { importHosts(lastNode, ImportType.MobaXterm) }
|
||||
sshMenu.addActionListener { importHosts(lastNode, ImportType.SSH) }
|
||||
finalShellMenu.addActionListener { importHosts(lastNode, ImportType.FinalShell) }
|
||||
csvMenu.addActionListener { importHosts(lastNode, ImportType.CSV) }
|
||||
windTermMenu.addActionListener { importHosts(lastNode, ImportType.WindTerm) }
|
||||
@@ -426,7 +251,6 @@ class NewHostTree : JXTree() {
|
||||
}
|
||||
remove.addActionListener(object : ActionListener {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val nodes = getSelectionHostTreeNodes()
|
||||
if (nodes.isEmpty()) return
|
||||
if (OptionPane.showConfirmDialog(
|
||||
SwingUtilities.getWindowAncestor(tree),
|
||||
@@ -453,7 +277,7 @@ class NewHostTree : JXTree() {
|
||||
}
|
||||
})
|
||||
copy.addActionListener {
|
||||
for (c in getSelectionHostTreeNodes()) {
|
||||
for (c in nodes) {
|
||||
val p = c.parent ?: continue
|
||||
val newNode = copyNode(c, p.host.id)
|
||||
model.insertNodeInto(newNode, p, lastNodeParent.getIndex(c) + 1)
|
||||
@@ -462,12 +286,12 @@ class NewHostTree : JXTree() {
|
||||
}
|
||||
rename.addActionListener { startEditingAtPath(TreePath(model.getPathToRoot(lastNode))) }
|
||||
expandAll.addActionListener {
|
||||
for (node in getSelectionHostTreeNodes(true)) {
|
||||
for (node in fullNodes) {
|
||||
expandPath(TreePath(model.getPathToRoot(node)))
|
||||
}
|
||||
}
|
||||
colspanAll.addActionListener {
|
||||
for (node in getSelectionHostTreeNodes(true).reversed()) {
|
||||
for (node in fullNodes.reversed()) {
|
||||
collapsePath(TreePath(model.getPathToRoot(node)))
|
||||
}
|
||||
}
|
||||
@@ -495,29 +319,10 @@ class NewHostTree : JXTree() {
|
||||
model.nodeStructureChanged(lastNode)
|
||||
}
|
||||
})
|
||||
refresh.addActionListener {
|
||||
val expanded = mutableSetOf(lastNode.host.id)
|
||||
for (e in lastNode.getAllChildren()) {
|
||||
if (e.host.protocol == Protocol.Folder && isExpanded(TreePath(model.getPathToRoot(e)))) {
|
||||
expanded.add(e.host.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新
|
||||
model.reload(lastNode)
|
||||
|
||||
// 先展开最顶级的
|
||||
expandPath(TreePath(model.getPathToRoot(lastNode)))
|
||||
|
||||
for (child in lastNode.getAllChildren()) {
|
||||
if (expanded.contains(child.host.id)) {
|
||||
expandPath(TreePath(model.getPathToRoot(child)))
|
||||
}
|
||||
}
|
||||
}
|
||||
refresh.addActionListener { refreshNode(lastNode) }
|
||||
|
||||
newMenu.isEnabled = lastHost.protocol == Protocol.Folder
|
||||
remove.isEnabled = getSelectionHostTreeNodes().none { it == model.root }
|
||||
remove.isEnabled = getSelectionSimpleTreeNodes().none { it == model.root }
|
||||
copy.isEnabled = remove.isEnabled
|
||||
rename.isEnabled = remove.isEnabled
|
||||
property.isEnabled = lastHost.protocol != Protocol.Folder
|
||||
@@ -525,28 +330,42 @@ class NewHostTree : JXTree() {
|
||||
importMenu.isEnabled = lastHost.protocol == Protocol.Folder
|
||||
|
||||
// 如果选中了 SSH 服务器,那么才启用
|
||||
openWithSFTP.isEnabled = getSelectionHostTreeNodes(true).map { it.host }.any { it.protocol == Protocol.SSH }
|
||||
openWithSFTP.isEnabled = fullNodes.map { it.host }.any { it.protocol == Protocol.SSH }
|
||||
openWithSFTPCommand.isEnabled = openWithSFTP.isEnabled
|
||||
openWith.isEnabled = openWith.menuComponents.any { it is JMenuItem && it.isEnabled }
|
||||
|
||||
popupMenu.addPopupMenuListener(object : PopupMenuListener {
|
||||
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
||||
tree.grabFocus()
|
||||
isPopupMenu = true
|
||||
}
|
||||
|
||||
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {
|
||||
tree.requestFocusInWindow()
|
||||
isPopupMenu = false
|
||||
}
|
||||
|
||||
override fun popupMenuCanceled(e: PopupMenuEvent) {
|
||||
override fun popupMenuCanceled(e: PopupMenuEvent?) {
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
popupMenu.show(this, event.x, event.y)
|
||||
popupMenu.show(this, evt.x, evt.y)
|
||||
}
|
||||
|
||||
override fun onRenamed(node: SimpleTreeNode<*>, text: String) {
|
||||
val lastNode = node as? HostTreeNode ?: return
|
||||
lastNode.host = lastNode.host.copy(name = text, updateDate = System.currentTimeMillis())
|
||||
model.nodeStructureChanged(lastNode)
|
||||
hostManager.addHost(lastNode.host)
|
||||
}
|
||||
|
||||
override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>) {
|
||||
val nNode = node as? HostTreeNode ?: return
|
||||
val nParent = parent as? HostTreeNode ?: return
|
||||
nNode.data = nNode.data.copy(parentId = nParent.id, updateDate = System.currentTimeMillis())
|
||||
hostManager.addHost(nNode.host)
|
||||
}
|
||||
|
||||
|
||||
private fun copyNode(
|
||||
node: HostTreeNode,
|
||||
parentId: String,
|
||||
@@ -583,30 +402,14 @@ class NewHostTree : JXTree() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 包含孙子
|
||||
*/
|
||||
fun getSelectionHostTreeNodes(include: Boolean = false): List<HostTreeNode> {
|
||||
val paths = selectionPaths ?: return emptyList()
|
||||
if (paths.isEmpty()) return emptyList()
|
||||
val nodes = mutableListOf<HostTreeNode>()
|
||||
val parents = paths.mapNotNull { it.lastPathComponent }
|
||||
.filterIsInstance<HostTreeNode>().toMutableList()
|
||||
|
||||
if (include) {
|
||||
while (parents.isNotEmpty()) {
|
||||
val node = parents.removeFirst()
|
||||
nodes.add(node)
|
||||
parents.addAll(node.children().toList().filterIsInstance<HostTreeNode>())
|
||||
}
|
||||
override fun getSelectionSimpleTreeNodes(include: Boolean): List<HostTreeNode> {
|
||||
return super.getSelectionSimpleTreeNodes(include).filterIsInstance<HostTreeNode>()
|
||||
}
|
||||
|
||||
return if (include) nodes else parents
|
||||
}
|
||||
|
||||
private fun openHosts(evt: EventObject, openInNewWindow: Boolean) {
|
||||
assertEventDispatchThread()
|
||||
val nodes = getSelectionHostTreeNodes(true).map { it.host }.filter { it.protocol != Protocol.Folder }
|
||||
val nodes = getSelectionSimpleTreeNodes(true).map { it.host }.filter { it.protocol != Protocol.Folder }
|
||||
if (nodes.isEmpty()) return
|
||||
val source = if (openInNewWindow)
|
||||
TermoraFrameManager.getInstance().createWindow().apply { isVisible = true }
|
||||
@@ -615,18 +418,16 @@ class NewHostTree : JXTree() {
|
||||
}
|
||||
|
||||
private fun openWithSFTP(evt: EventObject) {
|
||||
val nodes = getSelectionHostTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
|
||||
val nodes = getSelectionSimpleTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
|
||||
if (nodes.isEmpty()) return
|
||||
|
||||
val sftpAction = ActionManager.getInstance().getAction(app.termora.Actions.SFTP) as SFTPAction? ?: return
|
||||
val tab = sftpAction.openOrCreateSFTPTerminalTab(AnActionEvent(this, StringUtils.EMPTY, evt)) ?: return
|
||||
for (node in nodes) {
|
||||
sftpAction.connectHost(node, tab)
|
||||
sftpAction.actionPerformed(SFTPActionEvent(this, node.id, evt))
|
||||
}
|
||||
}
|
||||
|
||||
private fun openWithSFTPCommand(evt: EventObject) {
|
||||
val nodes = getSelectionHostTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
|
||||
val nodes = getSelectionSimpleTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
|
||||
if (nodes.isEmpty()) return
|
||||
for (host in nodes) {
|
||||
openHostAction.actionPerformed(OpenHostActionEvent(this, host.copy(protocol = Protocol.SFTPPty), evt))
|
||||
@@ -652,6 +453,7 @@ class NewHostTree : JXTree() {
|
||||
|
||||
when (type) {
|
||||
ImportType.WindTerm -> chooser.fileFilter = FileNameExtensionFilter("WindTerm (*.sessions)", "sessions")
|
||||
ImportType.SSH -> chooser.fileFilter = FileNameExtensionFilter("SSH (config)", "config")
|
||||
ImportType.CSV -> chooser.fileFilter = FileNameExtensionFilter("CSV (*.csv)", "csv")
|
||||
ImportType.SecureCRT -> chooser.fileFilter = FileNameExtensionFilter("SecureCRT (*.xml)", "xml")
|
||||
ImportType.electerm -> chooser.fileFilter = FileNameExtensionFilter("electerm (*.json)", "json")
|
||||
@@ -717,19 +519,23 @@ class NewHostTree : JXTree() {
|
||||
}
|
||||
|
||||
// 选择文件
|
||||
if (type != ImportType.SSH) {
|
||||
val code = chooser.showOpenDialog(owner)
|
||||
|
||||
if (code != JFileChooser.APPROVE_OPTION) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val file = chooser.selectedFile
|
||||
if (file != null && file.parentFile != null) {
|
||||
properties.putString(
|
||||
"NewHostTree.ImportHosts.defaultDir",
|
||||
(if (FileUtils.isDirectory(file)) file else file.parentFile).absolutePath
|
||||
)
|
||||
}
|
||||
|
||||
val nodes = when (type) {
|
||||
ImportType.SSH -> parseFromSSH(folder)
|
||||
ImportType.WindTerm -> parseFromWindTerm(folder, file)
|
||||
ImportType.SecureCRT -> parseFromSecureCRT(folder, file)
|
||||
ImportType.MobaXterm -> parseFromMobaXterm(folder, file)
|
||||
@@ -761,6 +567,9 @@ class NewHostTree : JXTree() {
|
||||
|
||||
// 重新加载
|
||||
model.reload(folder)
|
||||
|
||||
// expand root
|
||||
expandPath(TreePath(model.getPathToRoot(folder)))
|
||||
}
|
||||
|
||||
private fun parseFromWindTerm(folder: HostTreeNode, file: File): List<HostTreeNode> {
|
||||
@@ -786,6 +595,26 @@ class NewHostTree : JXTree() {
|
||||
return parseFromCSV(folder, StringReader(sw.toString()))
|
||||
}
|
||||
|
||||
private fun parseFromSSH(folder: HostTreeNode): List<HostTreeNode> {
|
||||
val entries = HostConfigEntry.readHostConfigEntries(HostConfigEntry.getDefaultHostConfigFile())
|
||||
|
||||
val sw = StringWriter()
|
||||
CSVPrinter(sw, CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).get()).use { printer ->
|
||||
for (entry in entries) {
|
||||
printer.printRecord(
|
||||
StringUtils.EMPTY,
|
||||
StringUtils.defaultString(entry.host),
|
||||
StringUtils.defaultString(entry.hostName),
|
||||
if (entry.port == 0) 22 else entry.port,
|
||||
StringUtils.defaultString(entry.username),
|
||||
"SSH"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return parseFromCSV(folder, StringReader(sw.toString()))
|
||||
}
|
||||
|
||||
private fun parseFromSecureCRT(folder: HostTreeNode, file: File): List<HostTreeNode> {
|
||||
val xPath = XPathFactory.newInstance().newXPath()
|
||||
val db = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||
@@ -1085,32 +914,10 @@ class NewHostTree : JXTree() {
|
||||
PuTTY,
|
||||
SecureCRT,
|
||||
MobaXterm,
|
||||
SSH,
|
||||
FinalShell,
|
||||
electerm,
|
||||
}
|
||||
|
||||
private class MoveHostTransferable(val nodes: List<HostTreeNode>) : Transferable {
|
||||
companion object {
|
||||
val dataFlavor =
|
||||
DataFlavor("${DataFlavor.javaJVMLocalObjectMimeType};class=${MoveHostTransferable::class.java.name}")
|
||||
}
|
||||
|
||||
|
||||
override fun getTransferDataFlavors(): Array<DataFlavor> {
|
||||
return arrayOf(dataFlavor)
|
||||
}
|
||||
|
||||
override fun isDataFlavorSupported(flavor: DataFlavor?): Boolean {
|
||||
return dataFlavor == flavor
|
||||
}
|
||||
|
||||
override fun getTransferData(flavor: DataFlavor?): Any {
|
||||
if (flavor == dataFlavor) {
|
||||
return nodes
|
||||
}
|
||||
throw UnsupportedFlavorException(flavor)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,11 +3,10 @@ package app.termora
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.function.Function
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JScrollPane
|
||||
import javax.swing.UIManager
|
||||
import javax.swing.*
|
||||
|
||||
class NewHostTreeDialog(
|
||||
owner: Window,
|
||||
@@ -19,7 +18,7 @@ class NewHostTreeDialog(
|
||||
private val tree = NewHostTree()
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 250, UIManager.getInt("Dialog.height") - 150)
|
||||
isModal = true
|
||||
isResizable = false
|
||||
controlsVisible = false
|
||||
@@ -29,6 +28,15 @@ class NewHostTreeDialog(
|
||||
tree.doubleClickConnection = false
|
||||
tree.dragEnabled = false
|
||||
|
||||
tree.addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||
val node = tree.getLastSelectedPathNode() ?: return
|
||||
if (node.isFolder) return
|
||||
doOKAction()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
init()
|
||||
@@ -60,7 +68,7 @@ class NewHostTreeDialog(
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
hosts = tree.getSelectionHostTreeNodes(true)
|
||||
hosts = tree.getSelectionSimpleTreeNodes(true)
|
||||
.filter { filter.apply(it) }
|
||||
.map { it.host }
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package app.termora
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import javax.swing.tree.DefaultTreeModel
|
||||
import javax.swing.tree.MutableTreeNode
|
||||
import javax.swing.tree.TreeNode
|
||||
|
||||
|
||||
class NewHostTreeModel : DefaultTreeModel(
|
||||
class NewHostTreeModel : SimpleTreeModel<Host>(
|
||||
HostTreeNode(
|
||||
Host(
|
||||
id = "0",
|
||||
|
||||
7
src/main/kotlin/app/termora/NotifyListener.kt
Normal file
7
src/main/kotlin/app/termora/NotifyListener.kt
Normal file
@@ -0,0 +1,7 @@
|
||||
package app.termora
|
||||
|
||||
import java.util.*
|
||||
|
||||
interface NotifyListener : EventListener {
|
||||
fun addNotify()
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.native.osx.NativeMacLibrary
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatTextPane
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jetbrains.JBR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.Desktop
|
||||
@@ -18,6 +22,8 @@ import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
object OptionPane {
|
||||
private val coroutineScope = swingCoroutineScope
|
||||
|
||||
fun showConfirmDialog(
|
||||
parentComponent: Component?,
|
||||
message: Any,
|
||||
@@ -27,6 +33,7 @@ object OptionPane {
|
||||
icon: Icon? = null,
|
||||
options: Array<Any>? = null,
|
||||
initialValue: Any? = null,
|
||||
customizeDialog: (JDialog) -> Unit = {},
|
||||
): Int {
|
||||
|
||||
val panel = if (message is JComponent) {
|
||||
@@ -45,6 +52,9 @@ object OptionPane {
|
||||
override fun selectInitialValue() {
|
||||
super.selectInitialValue()
|
||||
if (message is JComponent) {
|
||||
if (message.getClientProperty("SKIP_requestFocusInWindow") == true) {
|
||||
return
|
||||
}
|
||||
message.requestFocusInWindow()
|
||||
}
|
||||
}
|
||||
@@ -56,6 +66,7 @@ object OptionPane {
|
||||
}
|
||||
})
|
||||
dialog.setLocationRelativeTo(parentComponent)
|
||||
customizeDialog.invoke(dialog)
|
||||
dialog.isVisible = true
|
||||
dialog.dispose()
|
||||
val selectedValue = pane.value
|
||||
@@ -97,9 +108,8 @@ object OptionPane {
|
||||
val dialog = initDialog(pane.createDialog(parentComponent, title))
|
||||
if (duration.inWholeMilliseconds > 0) {
|
||||
dialog.addWindowListener(object : WindowAdapter() {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun windowOpened(e: WindowEvent) {
|
||||
GlobalScope.launch(Dispatchers.Swing) {
|
||||
coroutineScope.launch(Dispatchers.Swing) {
|
||||
delay(duration.inWholeMilliseconds)
|
||||
if (dialog.isVisible) {
|
||||
dialog.isVisible = false
|
||||
@@ -113,6 +123,36 @@ object OptionPane {
|
||||
dialog.dispose()
|
||||
}
|
||||
|
||||
fun showInputDialog(
|
||||
parentComponent: Component?,
|
||||
title: String = UIManager.getString("OptionPane.messageDialogTitle"),
|
||||
value: String = StringUtils.EMPTY,
|
||||
placeholder: String = StringUtils.EMPTY,
|
||||
): String? {
|
||||
val pane = JOptionPane(StringUtils.EMPTY, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION)
|
||||
val dialog = initDialog(pane.createDialog(parentComponent, title))
|
||||
pane.wantsInput = true
|
||||
pane.initialSelectionValue = value
|
||||
|
||||
val textField = SwingUtils.getDescendantsOfType(JTextField::class.java, pane, true).firstOrNull()
|
||||
if (textField?.name == "OptionPane.textField") {
|
||||
textField.border = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor),
|
||||
BorderFactory.createEmptyBorder(0, 0, 2, 0)
|
||||
)
|
||||
textField.background = UIManager.getColor("window")
|
||||
textField.putClientProperty(FlatClientProperties.PLACEHOLDER_TEXT, placeholder)
|
||||
}
|
||||
|
||||
dialog.isVisible = true
|
||||
dialog.dispose()
|
||||
|
||||
val inputValue = pane.inputValue
|
||||
if (inputValue == JOptionPane.UNINITIALIZED_VALUE) return null
|
||||
|
||||
return inputValue as? String
|
||||
}
|
||||
|
||||
fun openFileInFolder(
|
||||
parentComponent: Component,
|
||||
file: File,
|
||||
@@ -140,14 +180,31 @@ object OptionPane {
|
||||
}
|
||||
|
||||
private fun initDialog(dialog: JDialog): JDialog {
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
dialog.rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_CLOSE, false)
|
||||
dialog.rootPane.putClientProperty(
|
||||
FlatClientProperties.TITLE_BAR_HEIGHT,
|
||||
UIManager.getInt("TabbedPane.tabHeight")
|
||||
)
|
||||
} else if (SystemInfo.isMacOS) {
|
||||
dialog.rootPane.putClientProperty("apple.awt.windowTitleVisible", false)
|
||||
dialog.rootPane.putClientProperty("apple.awt.fullWindowContent", true)
|
||||
dialog.rootPane.putClientProperty("apple.awt.transparentTitleBar", true)
|
||||
dialog.rootPane.putClientProperty(
|
||||
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING,
|
||||
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING_MEDIUM
|
||||
)
|
||||
|
||||
|
||||
val height = UIManager.getInt("TabbedPane.tabHeight") - 10
|
||||
if (JBR.isWindowDecorationsSupported()) {
|
||||
|
||||
val windowDecorations = JBR.getWindowDecorations()
|
||||
val titleBar = windowDecorations.createCustomTitleBar()
|
||||
titleBar.putProperty("controls.visible", false)
|
||||
titleBar.height = UIManager.getInt("TabbedPane.tabHeight") - if (SystemInfo.isMacOS) 10f else 6f
|
||||
windowDecorations.setCustomTitleBar(dialog, titleBar)
|
||||
val customTitleBar = JBR.getWindowDecorations().createCustomTitleBar()
|
||||
customTitleBar.putProperty("controls.visible", false)
|
||||
customTitleBar.height = height.toFloat()
|
||||
JBR.getWindowDecorations().setCustomTitleBar(dialog, customTitleBar)
|
||||
} else {
|
||||
NativeMacLibrary.setControlsVisible(dialog, false)
|
||||
}
|
||||
|
||||
val label = JLabel(dialog.title)
|
||||
label.putClientProperty(FlatClientProperties.STYLE, "font: bold")
|
||||
@@ -155,11 +212,9 @@ object OptionPane {
|
||||
box.add(Box.createHorizontalGlue())
|
||||
box.add(label)
|
||||
box.add(Box.createHorizontalGlue())
|
||||
box.preferredSize = Dimension(-1, titleBar.height.toInt())
|
||||
|
||||
box.preferredSize = Dimension(-1, height)
|
||||
dialog.contentPane.add(box, BorderLayout.NORTH)
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,9 @@ class PtyConnectorFactory : Disposable {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(PtyConnectorFactory::class.java)
|
||||
fun getInstance(scope: Scope): PtyConnectorFactory {
|
||||
return scope.getOrCreate(PtyConnectorFactory::class) { PtyConnectorFactory() }
|
||||
fun getInstance(): PtyConnectorFactory {
|
||||
return ApplicationScope.forApplicationScope()
|
||||
.getOrCreate(PtyConnectorFactory::class) { PtyConnectorFactory() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,14 +34,21 @@ class PtyConnectorFactory : Disposable {
|
||||
if (SystemUtils.IS_OS_UNIX) {
|
||||
commands.add("-l")
|
||||
}
|
||||
return createPtyConnector(commands.toTypedArray(), rows, cols, env, charset)
|
||||
return createPtyConnector(
|
||||
commands = commands.toTypedArray(),
|
||||
rows = rows,
|
||||
cols = cols,
|
||||
env = env,
|
||||
charset = charset
|
||||
)
|
||||
}
|
||||
|
||||
fun createPtyConnector(
|
||||
commands: Array<String>,
|
||||
rows: Int = 24, cols: Int = 80,
|
||||
env: Map<String, String> = emptyMap(),
|
||||
charset: Charset = StandardCharsets.UTF_8
|
||||
directory: String = SystemUtils.USER_HOME,
|
||||
charset: Charset = StandardCharsets.UTF_8,
|
||||
): PtyConnector {
|
||||
val envs = mutableMapOf<String, String>()
|
||||
envs.putAll(System.getenv())
|
||||
@@ -67,7 +75,7 @@ class PtyConnectorFactory : Disposable {
|
||||
.setInitialRows(rows)
|
||||
.setInitialColumns(cols)
|
||||
.setConsole(false)
|
||||
.setDirectory(SystemUtils.USER_HOME)
|
||||
.setDirectory(StringUtils.defaultIfBlank(directory, SystemUtils.USER_HOME))
|
||||
.setCygwin(false)
|
||||
.setUseWinConPty(SystemUtils.IS_OS_WINDOWS)
|
||||
.setRedirectErrorStream(false)
|
||||
@@ -79,20 +87,14 @@ class PtyConnectorFactory : Disposable {
|
||||
}
|
||||
|
||||
fun decorate(ptyConnector: PtyConnector): PtyConnector {
|
||||
// 集成转发,如果PtyConnector支持转发那么应该在当前注释行前面代理
|
||||
val multiplePtyConnector = MultiplePtyConnector(ptyConnector)
|
||||
// 宏应该在转发前面执行,不然会导致重复录制
|
||||
val macroPtyConnector = MacroPtyConnector(multiplePtyConnector)
|
||||
// 宏
|
||||
val macroPtyConnector = MacroPtyConnector(ptyConnector)
|
||||
// 集成自动删除
|
||||
val autoRemovePtyConnector = AutoRemovePtyConnector(macroPtyConnector)
|
||||
ptyConnectors.add(autoRemovePtyConnector)
|
||||
return autoRemovePtyConnector
|
||||
}
|
||||
|
||||
fun getPtyConnectors(): List<PtyConnector> {
|
||||
return ptyConnectors
|
||||
}
|
||||
|
||||
private inner class AutoRemovePtyConnector(connector: PtyConnector) : PtyConnectorDelegate(connector) {
|
||||
override fun close() {
|
||||
ptyConnectors.remove(this)
|
||||
|
||||
@@ -13,7 +13,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
abstract class PtyHostTerminalTab(
|
||||
windowScope: WindowScope,
|
||||
host: Host,
|
||||
terminal: Terminal = TerminalFactory.getInstance(windowScope).createTerminal()
|
||||
terminal: Terminal = TerminalFactory.getInstance().createTerminal()
|
||||
) : HostTerminalTab(windowScope, host, terminal) {
|
||||
|
||||
companion object {
|
||||
@@ -23,15 +23,8 @@ abstract class PtyHostTerminalTab(
|
||||
|
||||
private var readerJob: Job? = null
|
||||
private val ptyConnectorDelegate = PtyConnectorDelegate()
|
||||
|
||||
private val terminalPanelFactory = TerminalPanelFactory.getInstance(windowScope)
|
||||
protected val terminalPanel = terminalPanelFactory.createTerminalPanel(terminal, ptyConnectorDelegate)
|
||||
.apply { Disposer.register(this@PtyHostTerminalTab, this) }
|
||||
protected val ptyConnectorFactory get() = PtyConnectorFactory.getInstance(windowScope)
|
||||
|
||||
init {
|
||||
terminal.getTerminalModel().setData(DataKey.PtyConnector, ptyConnectorDelegate)
|
||||
}
|
||||
protected val terminalPanel = TerminalPanelFactory.getInstance().createTerminalPanel(terminal, ptyConnectorDelegate)
|
||||
protected val ptyConnectorFactory get() = PtyConnectorFactory.getInstance()
|
||||
|
||||
override fun start() {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
@@ -122,10 +115,9 @@ abstract class PtyHostTerminalTab(
|
||||
|
||||
override fun dispose() {
|
||||
stop()
|
||||
terminalPanel
|
||||
Disposer.dispose(terminalPanel)
|
||||
super.dispose()
|
||||
|
||||
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("Host: {} disposed", host.name)
|
||||
}
|
||||
@@ -141,6 +133,8 @@ abstract class PtyHostTerminalTab(
|
||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||
if (dataKey == DataProviders.TerminalPanel) {
|
||||
return terminalPanel as T?
|
||||
} else if (dataKey == DataProviders.TerminalWriter) {
|
||||
return terminalPanel.getData(DataKey.TerminalWriter) as T?
|
||||
}
|
||||
return super.getData(dataKey)
|
||||
}
|
||||
|
||||
16
src/main/kotlin/app/termora/RememberFocusTerminalTab.kt
Normal file
16
src/main/kotlin/app/termora/RememberFocusTerminalTab.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package app.termora
|
||||
|
||||
import java.awt.Component
|
||||
import java.awt.KeyboardFocusManager
|
||||
|
||||
abstract class RememberFocusTerminalTab : TerminalTab {
|
||||
private var lastFocusedComponent: Component? = null
|
||||
|
||||
override fun onLostFocus() {
|
||||
lastFocusedComponent = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||
}
|
||||
|
||||
override fun onGrabFocus() {
|
||||
lastFocusedComponent?.requestFocusInWindow()
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import kotlin.math.max
|
||||
class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(owner) {
|
||||
|
||||
private val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||
private val rememberCheckBox = JCheckBox("Remember")
|
||||
private val rememberCheckBox = JCheckBox(I18n.getString("termora.new-host.general.remember"))
|
||||
private val passwordPanel = JPanel(BorderLayout())
|
||||
private val passwordPasswordField = OutlinePasswordField()
|
||||
private val usernameTextField = OutlineTextField()
|
||||
@@ -34,8 +34,8 @@ class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(own
|
||||
pack()
|
||||
|
||||
size = Dimension(max(380, size.width), size.height)
|
||||
|
||||
setLocationRelativeTo(null)
|
||||
preferredSize = size
|
||||
minimumSize = size
|
||||
|
||||
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
@@ -65,6 +65,10 @@ class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(own
|
||||
}
|
||||
}
|
||||
|
||||
if (host.authentication.type != AuthenticationType.No) {
|
||||
authenticationTypeComboBox.selectedItem = host.authentication.type
|
||||
}
|
||||
|
||||
usernameTextField.text = host.username
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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.commons.lang3.SystemUtils
|
||||
import org.apache.sshd.client.ClientBuilder
|
||||
import org.apache.sshd.client.SshClient
|
||||
import org.apache.sshd.client.session.ClientSession
|
||||
@@ -28,6 +29,12 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
||||
private var sshSession: ClientSession? = null
|
||||
private var lastPasswordReporterDataListener: PasswordReporterDataListener? = null
|
||||
private val sftpCommand get() = Database.getDatabase().sftp.sftpCommand
|
||||
private val defaultDirectory get() = Database.getDatabase().sftp.defaultDirectory
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(terminalPanel)
|
||||
|
||||
init {
|
||||
terminalPanel.dropFiles = true
|
||||
}
|
||||
|
||||
companion object {
|
||||
val canSupports by lazy {
|
||||
@@ -61,7 +68,7 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
||||
)
|
||||
)
|
||||
|
||||
val sshClient = SshClients.openClient(host).apply { sshClient = this }
|
||||
val sshClient = SshClients.openClient(host, owner).apply { sshClient = this }
|
||||
val sshSession = SshClients.openSession(host, sshClient).apply { sshSession = this }
|
||||
|
||||
// 打开通道
|
||||
@@ -115,16 +122,21 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
||||
if (envs.containsKey("CurrentDir")) {
|
||||
val currentDir = envs.getValue("CurrentDir")
|
||||
commands.add("${host.username}@${host.host}:${currentDir}")
|
||||
} else if (host.options.sftpDefaultDirectory.isNotBlank()) {
|
||||
commands.add("${host.username}@${host.host}:${host.options.sftpDefaultDirectory.trim()}")
|
||||
} else {
|
||||
commands.add("${host.username}@${host.host}")
|
||||
}
|
||||
|
||||
val directory = FileUtils.getFile(StringUtils.defaultIfBlank(defaultDirectory, SystemUtils.USER_HOME))
|
||||
|
||||
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),
|
||||
commands = commands.toTypedArray(),
|
||||
rows = winSize.rows, cols = winSize.cols,
|
||||
env = host.options.envs(),
|
||||
charset = Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8),
|
||||
directory = if (directory.exists()) directory.absolutePath else SystemUtils.USER_HOME
|
||||
)
|
||||
|
||||
return ptyConnector
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.transport.TransportDataProviders
|
||||
import app.termora.transport.TransportPanel
|
||||
import java.beans.PropertyChangeListener
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
class SFTPTerminalTab : Disposable, TerminalTab, DataProvider {
|
||||
|
||||
private val sftp get() = Database.getDatabase().sftp
|
||||
private val transportPanel = TransportPanel()
|
||||
|
||||
init {
|
||||
Disposer.register(this, transportPanel)
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return "SFTP"
|
||||
}
|
||||
|
||||
override fun getIcon(): Icon {
|
||||
return Icons.folder
|
||||
}
|
||||
|
||||
override fun addPropertyChangeListener(listener: PropertyChangeListener) {
|
||||
|
||||
}
|
||||
|
||||
override fun removePropertyChangeListener(listener: PropertyChangeListener) {
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return transportPanel
|
||||
}
|
||||
|
||||
override fun canClone(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun canClose(): Boolean {
|
||||
assertEventDispatchThread()
|
||||
|
||||
if (sftp.pinTab) {
|
||||
return false
|
||||
}
|
||||
|
||||
val transportManager = transportPanel.getData(TransportDataProviders.TransportManager) ?: return true
|
||||
if (transportManager.getTransports().isEmpty()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return OptionPane.showConfirmDialog(
|
||||
SwingUtilities.getWindowAncestor(getJComponent()),
|
||||
I18n.getString("termora.transport.sftp.close-tab"),
|
||||
messageType = JOptionPane.QUESTION_MESSAGE,
|
||||
optionType = JOptionPane.OK_CANCEL_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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.actions.TabReconnectAction
|
||||
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||
import app.termora.keymap.KeyShortcut
|
||||
import app.termora.keymap.KeymapManager
|
||||
import app.termora.terminal.ControlCharacters
|
||||
@@ -53,6 +52,7 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
|
||||
init {
|
||||
terminalPanel.dropFiles = false
|
||||
terminalPanel.dataProviderSupport.addData(DataProviders.TerminalTab, this)
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
@@ -89,35 +89,8 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
terminal.write("SSH client is opening...\r\n")
|
||||
}
|
||||
|
||||
var host =
|
||||
this.host.copy(authentication = this.host.authentication.copy(), updateDate = System.currentTimeMillis())
|
||||
val owner = SwingUtilities.getWindowAncestor(terminalPanel)
|
||||
val client = SshClients.openClient(host).also { sshClient = it }
|
||||
client.serverKeyVerifier = DialogServerKeyVerifier(owner)
|
||||
// keyboard interactive
|
||||
client.userInteraction = TerminalUserInteraction(owner)
|
||||
|
||||
if (host.authentication.type == AuthenticationType.No) {
|
||||
withContext(Dispatchers.Swing) {
|
||||
val dialog = RequestAuthenticationDialog(owner, host)
|
||||
val authentication = dialog.getAuthentication()
|
||||
host = host.copy(
|
||||
authentication = authentication,
|
||||
username = dialog.getUsername(),
|
||||
updateDate = System.currentTimeMillis(),
|
||||
)
|
||||
// save
|
||||
if (dialog.isRemembered()) {
|
||||
HostManager.getInstance().addHost(
|
||||
tab.host.copy(
|
||||
authentication = authentication,
|
||||
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val client = SshClients.openClient(host, owner).also { sshClient = it }
|
||||
val sessionListener = MySessionListener()
|
||||
val channelListener = MyChannelListener()
|
||||
|
||||
@@ -250,6 +223,11 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
}
|
||||
}
|
||||
|
||||
override fun willBeClose(): Boolean {
|
||||
// 保存窗口状态
|
||||
terminalPanel.storeVisualWindows(host.id)
|
||||
return super.willBeClose()
|
||||
}
|
||||
|
||||
private inner class MySessionListener : SessionListener, Disposable {
|
||||
override fun sessionEvent(session: Session, event: Event) {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package app.termora
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Component
|
||||
import java.awt.Window
|
||||
@@ -8,6 +13,8 @@ import javax.swing.JPopupMenu
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
val swingCoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Swing)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open class Scope(
|
||||
private val beans: MutableMap<KClass<*>, Any> = ConcurrentHashMap(),
|
||||
@@ -144,6 +151,7 @@ class ApplicationScope private constructor() : Scope() {
|
||||
}
|
||||
|
||||
fun windowScopes(): List<WindowScope> {
|
||||
if (scopes.isEmpty()) return emptyList()
|
||||
return scopes.values.toList()
|
||||
}
|
||||
|
||||
@@ -151,6 +159,7 @@ class ApplicationScope private constructor() : Scope() {
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("ApplicationScope disposed")
|
||||
}
|
||||
swingCoroutineScope.cancel()
|
||||
super.dispose()
|
||||
}
|
||||
|
||||
|
||||
@@ -15,15 +15,14 @@ import app.termora.keymgr.OhKeyPair
|
||||
import app.termora.macro.Macro
|
||||
import app.termora.macro.MacroManager
|
||||
import app.termora.native.FileChooser
|
||||
import app.termora.sync.SyncConfig
|
||||
import app.termora.sync.SyncRange
|
||||
import app.termora.sync.SyncType
|
||||
import app.termora.sync.SyncerProvider
|
||||
import app.termora.sftp.SFTPTab
|
||||
import app.termora.snippet.Snippet
|
||||
import app.termora.snippet.SnippetManager
|
||||
import app.termora.sync.*
|
||||
import app.termora.terminal.CursorStyle
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.panel.FloatingToolbarPanel
|
||||
import app.termora.terminal.panel.TerminalPanel
|
||||
import app.termora.transport.SFTPAction
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||
@@ -34,49 +33,55 @@ import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import com.jthemedetecor.OsThemeDetector
|
||||
import com.sun.jna.LastErrorException
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.*
|
||||
import org.apache.commons.codec.binary.Base64
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import org.apache.commons.lang3.time.DateFormatUtils
|
||||
import org.jdesktop.swingx.JXEditorPane
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.Toolkit
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.ItemEvent
|
||||
import java.awt.event.ItemListener
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.*
|
||||
import java.util.function.Consumer
|
||||
import javax.swing.*
|
||||
import javax.swing.JSpinner.NumberEditor
|
||||
import javax.swing.event.DocumentEvent
|
||||
import javax.swing.event.PopupMenuEvent
|
||||
import javax.swing.event.PopupMenuListener
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
class SettingsOptionsPane : OptionsPane() {
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane)
|
||||
private val database get() = Database.getDatabase()
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
private val snippetManager get() = SnippetManager.getInstance()
|
||||
private val keymapManager get() = KeymapManager.getInstance()
|
||||
private val macroManager get() = MacroManager.getInstance()
|
||||
private val actionManager get() = ActionManager.getInstance()
|
||||
private val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
||||
private val keyManager get() = KeyManager.getInstance()
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java)
|
||||
private val localShells by lazy { loadShells() }
|
||||
var pulled = false
|
||||
|
||||
private fun loadShells(): List<String> {
|
||||
val shells = mutableListOf<String>()
|
||||
@@ -126,9 +131,15 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
val themeManager = ThemeManager.getInstance()
|
||||
val themeComboBox = FlatComboBox<String>()
|
||||
val languageComboBox = FlatComboBox<String>()
|
||||
val backgroundComBoBox = YesOrNoComboBox()
|
||||
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
|
||||
val preferredThemeBtn = JButton(Icons.settings)
|
||||
val opacitySpinner = NumberSpinner(100, 0, 100)
|
||||
val backgroundImageTextField = OutlineTextField()
|
||||
|
||||
private val appearance get() = database.appearance
|
||||
private val backgroundButton = JButton(Icons.folder)
|
||||
private val backgroundClearButton = FlatButton()
|
||||
|
||||
init {
|
||||
initView()
|
||||
@@ -137,8 +148,38 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
private fun initView() {
|
||||
|
||||
backgroundComBoBox.isEnabled = SystemInfo.isWindows || SystemInfo.isMacOS
|
||||
backgroundImageTextField.isEditable = false
|
||||
backgroundImageTextField.trailingComponent = backgroundButton
|
||||
backgroundImageTextField.text = FilenameUtils.getName(appearance.backgroundImage)
|
||||
backgroundImageTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||
override fun changedUpdate(e: DocumentEvent) {
|
||||
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
|
||||
}
|
||||
})
|
||||
|
||||
backgroundClearButton.isFocusable = false
|
||||
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
|
||||
backgroundClearButton.icon = Icons.delete
|
||||
backgroundClearButton.buttonType = FlatButton.ButtonType.toolBarButton
|
||||
|
||||
|
||||
opacitySpinner.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows
|
||||
opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) {
|
||||
override fun getNextValue(): Any {
|
||||
return super.getNextValue() ?: maximum
|
||||
}
|
||||
|
||||
override fun getPreviousValue(): Any {
|
||||
return super.getPreviousValue() ?: minimum
|
||||
}
|
||||
}
|
||||
opacitySpinner.editor = NumberEditor(opacitySpinner, "#.##")
|
||||
opacitySpinner.model.stepSize = 0.05
|
||||
|
||||
followSystemCheckBox.isSelected = appearance.followSystem
|
||||
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
|
||||
backgroundComBoBox.selectedItem = appearance.backgroundRunning
|
||||
|
||||
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
||||
themeManager.themes.keys.forEach { themeComboBox.addItem(it) }
|
||||
@@ -175,6 +216,20 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
opacitySpinner.addChangeListener {
|
||||
val opacity = opacitySpinner.value
|
||||
if (opacity is Double) {
|
||||
TermoraFrameManager.getInstance().setOpacity(opacity)
|
||||
appearance.opacity = opacity
|
||||
}
|
||||
}
|
||||
|
||||
backgroundComBoBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
appearance.backgroundRunning = backgroundComBoBox.selectedItem as Boolean
|
||||
}
|
||||
}
|
||||
|
||||
followSystemCheckBox.addActionListener {
|
||||
appearance.followSystem = followSystemCheckBox.isSelected
|
||||
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
||||
@@ -204,6 +259,46 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
|
||||
preferredThemeBtn.addActionListener { showPreferredThemeContextmenu() }
|
||||
|
||||
backgroundButton.addActionListener {
|
||||
val chooser = FileChooser()
|
||||
chooser.osxAllowedFileTypes = listOf("png", "jpg", "jpeg")
|
||||
chooser.allowsMultiSelection = false
|
||||
chooser.win32Filters.add(Pair("Image files", listOf("png", "jpg", "jpeg")))
|
||||
chooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
||||
chooser.showOpenDialog(owner).thenAccept {
|
||||
if (it.isNotEmpty()) {
|
||||
onSelectedBackgroundImage(it.first())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
backgroundClearButton.addActionListener {
|
||||
BackgroundManager.getInstance().clearBackgroundImage()
|
||||
backgroundImageTextField.text = StringUtils.EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSelectedBackgroundImage(file: File) {
|
||||
try {
|
||||
val destFile = FileUtils.getFile(Application.getBaseDataDir(), "background", file.name)
|
||||
FileUtils.forceMkdirParent(destFile)
|
||||
FileUtils.deleteQuietly(destFile)
|
||||
FileUtils.copyFile(file, destFile, StandardCopyOption.REPLACE_EXISTING)
|
||||
backgroundImageTextField.text = destFile.name
|
||||
BackgroundManager.getInstance().setBackgroundImage(destFile)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
SwingUtilities.invokeLater {
|
||||
OptionPane.showMessageDialog(
|
||||
owner,
|
||||
ExceptionUtils.getRootCauseMessage(e),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
@@ -273,7 +368,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private fun getFormPanel(): JPanel {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow, $formMargin, default, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin"
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
val box = FlatToolBar()
|
||||
box.add(followSystemCheckBox)
|
||||
@@ -282,7 +377,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
return FormBuilder.create().layout(layout)
|
||||
val builder = FormBuilder.create().layout(layout)
|
||||
.add("${I18n.getString("termora.settings.appearance.theme")}:").xy(1, rows)
|
||||
.add(themeComboBox).xy(3, rows)
|
||||
.add(box).xy(5, rows).apply { rows += step }
|
||||
@@ -293,7 +388,22 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
Application.browse(URI.create("https://github.com/TermoraDev/termora/tree/main/src/main/resources/i18n"))
|
||||
}
|
||||
})).xy(5, rows).apply { rows += step }
|
||||
.build()
|
||||
|
||||
|
||||
val bgClearBox = Box.createHorizontalBox()
|
||||
bgClearBox.add(backgroundClearButton)
|
||||
builder.add("${I18n.getString("termora.settings.appearance.background-image")}:").xy(1, rows)
|
||||
.add(backgroundImageTextField).xy(3, rows)
|
||||
.add(bgClearBox).xy(5, rows)
|
||||
.apply { rows += step }
|
||||
|
||||
builder.add("${I18n.getString("termora.settings.appearance.opacity")}:").xy(1, rows)
|
||||
.add(opacitySpinner).xy(3, rows).apply { rows += step }
|
||||
|
||||
builder.add("${I18n.getString("termora.settings.appearance.background-running")}:").xy(1, rows)
|
||||
.add(backgroundComBoBox).xy(3, rows)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -312,6 +422,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private val selectCopyComboBox = YesOrNoComboBox()
|
||||
private val autoCloseTabComboBox = YesOrNoComboBox()
|
||||
private val floatingToolbarComboBox = YesOrNoComboBox()
|
||||
private val hyperlinkComboBox = YesOrNoComboBox()
|
||||
|
||||
init {
|
||||
initView()
|
||||
@@ -337,7 +448,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
floatingToolbarComboBox.addItemListener { e ->
|
||||
if (e.stateChange == ItemEvent.SELECTED) {
|
||||
terminalSetting.floatingToolbar = floatingToolbarComboBox.selectedItem as Boolean
|
||||
TerminalPanelFactory.getAllTerminalPanel().forEach { tp ->
|
||||
TerminalPanelFactory.getInstance().getTerminalPanels().forEach { tp ->
|
||||
if (terminalSetting.floatingToolbar && FloatingToolbarPanel.isPined) {
|
||||
tp.getData(FloatingToolbarPanel.FloatingToolbar)?.triggerShow()
|
||||
} else {
|
||||
@@ -366,7 +477,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
val style = cursorStyleComboBox.selectedItem as CursorStyle
|
||||
terminalSetting.cursor = style
|
||||
TerminalFactory.getInstance(ApplicationScope.forWindowScope(owner)).getTerminals().forEach { e ->
|
||||
TerminalFactory.getInstance().getTerminals().forEach { e ->
|
||||
e.getTerminalModel().setData(DataKey.CursorStyle, style)
|
||||
}
|
||||
}
|
||||
@@ -376,7 +487,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
debugComboBox.addItemListener { e ->
|
||||
if (e.stateChange == ItemEvent.SELECTED) {
|
||||
terminalSetting.debug = debugComboBox.selectedItem as Boolean
|
||||
TerminalFactory.getInstance(ApplicationScope.forWindowScope(owner)).getTerminals().forEach {
|
||||
TerminalFactory.getInstance().getTerminals().forEach {
|
||||
it.getTerminalModel().setData(TerminalPanel.Debug, terminalSetting.debug)
|
||||
}
|
||||
}
|
||||
@@ -389,6 +500,13 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
hyperlinkComboBox.addItemListener { e ->
|
||||
if (e.stateChange == ItemEvent.SELECTED) {
|
||||
terminalSetting.hyperlink = hyperlinkComboBox.selectedItem as Boolean
|
||||
TerminalPanelFactory.getInstance().repaintAll()
|
||||
}
|
||||
}
|
||||
|
||||
cursorBlinkComboBox.addItemListener { e ->
|
||||
if (e.stateChange == ItemEvent.SELECTED) {
|
||||
terminalSetting.cursorBlink = cursorBlinkComboBox.selectedItem as Boolean
|
||||
@@ -405,11 +523,9 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
|
||||
private fun fireFontChanged() {
|
||||
ApplicationScope.windowScopes().forEach {
|
||||
TerminalPanelFactory.getInstance(it)
|
||||
TerminalPanelFactory.getInstance()
|
||||
.fireResize()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
|
||||
@@ -469,7 +585,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
shellComboBox.selectedItem = terminalSetting.localShell
|
||||
|
||||
val fonts = linkedSetOf<String>("JetBrains Mono", "Source Code Pro", "Monospaced")
|
||||
val fonts = linkedSetOf("JetBrains Mono", "Source Code Pro", "Monospaced")
|
||||
FontUtils.getAllFonts().forEach {
|
||||
if (!fonts.contains(it.family)) {
|
||||
fonts.addLast(it.family)
|
||||
@@ -483,6 +599,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
fontComboBox.selectedItem = terminalSetting.font
|
||||
debugComboBox.selectedItem = terminalSetting.debug
|
||||
beepComboBox.selectedItem = terminalSetting.beep
|
||||
hyperlinkComboBox.selectedItem = terminalSetting.hyperlink
|
||||
cursorBlinkComboBox.selectedItem = terminalSetting.cursorBlink
|
||||
cursorStyleComboBox.selectedItem = terminalSetting.cursor
|
||||
selectCopyComboBox.selectedItem = terminalSetting.selectCopy
|
||||
@@ -505,7 +622,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, 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, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
val beepBtn = JButton(Icons.run)
|
||||
@@ -528,6 +645,8 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
.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.hyperlink")}:").xy(1, rows)
|
||||
.add(hyperlinkComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.select-copy")}:").xy(1, rows)
|
||||
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
|
||||
@@ -552,15 +671,16 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
val typeComboBox = FlatComboBox<SyncType>()
|
||||
val tokenTextField = OutlinePasswordField(255)
|
||||
val gistTextField = OutlineTextField(255)
|
||||
val policyComboBox = JComboBox<SyncPolicy>()
|
||||
val domainTextField = OutlineTextField(255)
|
||||
val uploadConfigButton = JButton(I18n.getString("termora.settings.sync.push"), Icons.upload)
|
||||
val syncConfigButton = JButton(I18n.getString("termora.settings.sync"), Icons.settingSync)
|
||||
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 lastSyncTimeLabel = JLabel()
|
||||
val sync get() = database.sync
|
||||
val hostsCheckBox = JCheckBox(I18n.getString("termora.welcome.my-hosts"))
|
||||
val keysCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keys"))
|
||||
val snippetsCheckBox = JCheckBox(I18n.getString("termora.snippet.title"))
|
||||
val keywordHighlightsCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keyword-highlights"))
|
||||
val macrosCheckBox = JCheckBox(I18n.getString("termora.macro"))
|
||||
val keymapCheckBox = JCheckBox(I18n.getString("termora.settings.keymap"))
|
||||
@@ -573,19 +693,23 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun initEvents() {
|
||||
downloadConfigButton.addActionListener {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
pushOrPull(false)
|
||||
syncConfigButton.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
if (typeComboBox.selectedItem == SyncType.WebDAV) {
|
||||
if (tokenTextField.password.isEmpty()) {
|
||||
tokenTextField.outline = FlatClientProperties.OUTLINE_ERROR
|
||||
tokenTextField.requestFocusInWindow()
|
||||
return
|
||||
} else if (gistTextField.text.isEmpty()) {
|
||||
gistTextField.outline = FlatClientProperties.OUTLINE_ERROR
|
||||
gistTextField.requestFocusInWindow()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
uploadConfigButton.addActionListener {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
pushOrPull(true)
|
||||
}
|
||||
swingCoroutineScope.launch(Dispatchers.IO) { sync() }
|
||||
}
|
||||
})
|
||||
|
||||
typeComboBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
@@ -604,6 +728,12 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
policyComboBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
sync.policy = (policyComboBox.selectedItem as SyncPolicy).name
|
||||
}
|
||||
}
|
||||
|
||||
tokenTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||
override fun changedUpdate(e: DocumentEvent) {
|
||||
sync.token = String(tokenTextField.password)
|
||||
@@ -624,6 +754,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
visitGistBtn.addActionListener {
|
||||
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
||||
if (domainTextField.text.isNotBlank()) {
|
||||
@@ -665,20 +796,52 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
keysCheckBox.addActionListener { refreshButtons() }
|
||||
hostsCheckBox.addActionListener { refreshButtons() }
|
||||
snippetsCheckBox.addActionListener { refreshButtons() }
|
||||
keywordHighlightsCheckBox.addActionListener { refreshButtons() }
|
||||
|
||||
}
|
||||
|
||||
private suspend fun sync() {
|
||||
|
||||
// 如果 gist 为空说明要创建一个 gist
|
||||
if (gistTextField.text.isBlank()) {
|
||||
if (!pushOrPull(true)) return
|
||||
} else {
|
||||
if (!pushOrPull(false)) return
|
||||
if (!pushOrPull(true)) return
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
if (hostsCheckBox.isSelected) {
|
||||
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
||||
visit(window.rootPane) {
|
||||
if (it is NewHostTree) it.refreshNode()
|
||||
}
|
||||
}
|
||||
}
|
||||
OptionPane.showMessageDialog(owner, message = I18n.getString("termora.settings.sync.done"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun visit(c: JComponent, consumer: Consumer<JComponent>) {
|
||||
for (e in c.components) {
|
||||
if (e is JComponent) {
|
||||
consumer.accept(e)
|
||||
visit(e, consumer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshButtons() {
|
||||
sync.rangeKeyPairs = keysCheckBox.isSelected
|
||||
sync.rangeHosts = hostsCheckBox.isSelected
|
||||
sync.rangeSnippets = snippetsCheckBox.isSelected
|
||||
sync.rangeKeywordHighlights = keywordHighlightsCheckBox.isSelected
|
||||
|
||||
downloadConfigButton.isEnabled = keysCheckBox.isSelected || hostsCheckBox.isSelected
|
||||
syncConfigButton.isEnabled = keysCheckBox.isSelected || hostsCheckBox.isSelected
|
||||
|| keywordHighlightsCheckBox.isSelected
|
||||
uploadConfigButton.isEnabled = downloadConfigButton.isEnabled
|
||||
exportConfigButton.isEnabled = downloadConfigButton.isEnabled
|
||||
importConfigButton.isEnabled = downloadConfigButton.isEnabled
|
||||
exportConfigButton.isEnabled = syncConfigButton.isEnabled
|
||||
importConfigButton.isEnabled = syncConfigButton.isEnabled
|
||||
}
|
||||
|
||||
private fun export() {
|
||||
@@ -848,6 +1011,17 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
if (ranges.contains(SyncRange.Snippets)) {
|
||||
val snippets = json["snippets"]
|
||||
if (snippets is JsonArray) {
|
||||
ohMyJson.runCatching { decodeFromJsonElement<List<Snippet>>(snippets.jsonArray) }.onSuccess {
|
||||
for (snippet in it) {
|
||||
snippetManager.addSnippet(snippet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ranges.contains(SyncRange.KeyPairs)) {
|
||||
val keyPairs = json["keyPairs"]
|
||||
if (keyPairs is JsonArray) {
|
||||
@@ -909,6 +1083,9 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
if (syncConfig.ranges.contains(SyncRange.Hosts)) {
|
||||
put("hosts", ohMyJson.encodeToJsonElement(hostManager.hosts()))
|
||||
}
|
||||
if (syncConfig.ranges.contains(SyncRange.Snippets)) {
|
||||
put("snippets", ohMyJson.encodeToJsonElement(snippetManager.snippets()))
|
||||
}
|
||||
if (syncConfig.ranges.contains(SyncRange.KeyPairs)) {
|
||||
put("keyPairs", ohMyJson.encodeToJsonElement(keyManager.getOhKeyPairs()))
|
||||
}
|
||||
@@ -978,6 +1155,9 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
if (keymapCheckBox.isSelected) {
|
||||
range.add(SyncRange.Keymap)
|
||||
}
|
||||
if (snippetsCheckBox.isSelected) {
|
||||
range.add(SyncRange.Snippets)
|
||||
}
|
||||
return SyncConfig(
|
||||
type = typeComboBox.selectedItem as SyncType,
|
||||
token = String(tokenTextField.password),
|
||||
@@ -987,8 +1167,11 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true 同步成功
|
||||
*/
|
||||
@Suppress("DuplicatedCode")
|
||||
private suspend fun pushOrPull(push: Boolean) {
|
||||
private suspend fun pushOrPull(push: Boolean): Boolean {
|
||||
|
||||
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
||||
if (domainTextField.text.isBlank()) {
|
||||
@@ -996,7 +1179,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
domainTextField.outline = "error"
|
||||
domainTextField.requestFocusInWindow()
|
||||
}
|
||||
return
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1005,7 +1188,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
tokenTextField.outline = "error"
|
||||
tokenTextField.requestFocusInWindow()
|
||||
}
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
if (gistTextField.text.isBlank() && !push) {
|
||||
@@ -1013,39 +1196,13 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
gistTextField.outline = "error"
|
||||
gistTextField.requestFocusInWindow()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 没有拉取过 && 是推送 && gistId 不为空
|
||||
if (!pulled && push && gistTextField.text.isNotBlank()) {
|
||||
val code = withContext(Dispatchers.Swing) {
|
||||
// 提示第一次推送
|
||||
OptionPane.showConfirmDialog(
|
||||
owner,
|
||||
I18n.getString("termora.settings.sync.push-warning"),
|
||||
messageType = JOptionPane.WARNING_MESSAGE,
|
||||
optionType = JOptionPane.YES_NO_CANCEL_OPTION,
|
||||
options = arrayOf(
|
||||
uploadConfigButton.text,
|
||||
downloadConfigButton.text,
|
||||
I18n.getString("termora.cancel")
|
||||
),
|
||||
initialValue = I18n.getString("termora.cancel")
|
||||
)
|
||||
}
|
||||
when (code) {
|
||||
-1, JOptionPane.CANCEL_OPTION -> return
|
||||
JOptionPane.NO_OPTION -> pushOrPull(false) // pull
|
||||
JOptionPane.YES_OPTION -> pulled = true // force push
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
exportConfigButton.isEnabled = false
|
||||
importConfigButton.isEnabled = false
|
||||
downloadConfigButton.isEnabled = false
|
||||
uploadConfigButton.isEnabled = false
|
||||
syncConfigButton.isEnabled = false
|
||||
typeComboBox.isEnabled = false
|
||||
gistTextField.isEnabled = false
|
||||
tokenTextField.isEnabled = false
|
||||
@@ -1054,20 +1211,16 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
keymapCheckBox.isEnabled = false
|
||||
keywordHighlightsCheckBox.isEnabled = false
|
||||
hostsCheckBox.isEnabled = false
|
||||
snippetsCheckBox.isEnabled = false
|
||||
domainTextField.isEnabled = false
|
||||
|
||||
if (push) {
|
||||
uploadConfigButton.text = "${I18n.getString("termora.settings.sync.push")}..."
|
||||
} else {
|
||||
downloadConfigButton.text = "${I18n.getString("termora.settings.sync.pull")}..."
|
||||
}
|
||||
syncConfigButton.text = "${I18n.getString("termora.settings.sync")}..."
|
||||
}
|
||||
|
||||
val syncConfig = getSyncConfig()
|
||||
|
||||
// sync
|
||||
val syncResult = kotlin.runCatching {
|
||||
val syncer = SyncerProvider.getInstance().getSyncer(syncConfig.type)
|
||||
val syncer = SyncManager.getInstance()
|
||||
if (push) {
|
||||
syncer.push(syncConfig)
|
||||
} else {
|
||||
@@ -1077,12 +1230,12 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
// 恢复状态
|
||||
withContext(Dispatchers.Swing) {
|
||||
downloadConfigButton.isEnabled = true
|
||||
syncConfigButton.isEnabled = true
|
||||
exportConfigButton.isEnabled = true
|
||||
importConfigButton.isEnabled = true
|
||||
uploadConfigButton.isEnabled = true
|
||||
keysCheckBox.isEnabled = true
|
||||
hostsCheckBox.isEnabled = true
|
||||
snippetsCheckBox.isEnabled = true
|
||||
typeComboBox.isEnabled = true
|
||||
macrosCheckBox.isEnabled = true
|
||||
keymapCheckBox.isEnabled = true
|
||||
@@ -1090,11 +1243,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
tokenTextField.isEnabled = true
|
||||
domainTextField.isEnabled = true
|
||||
keywordHighlightsCheckBox.isEnabled = true
|
||||
if (push) {
|
||||
uploadConfigButton.text = I18n.getString("termora.settings.sync.push")
|
||||
} else {
|
||||
downloadConfigButton.text = I18n.getString("termora.settings.sync.pull")
|
||||
}
|
||||
syncConfigButton.text = I18n.getString("termora.settings.sync")
|
||||
}
|
||||
|
||||
// 如果失败,提示错误
|
||||
@@ -1114,10 +1263,8 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
withContext(Dispatchers.Swing) {
|
||||
OptionPane.showMessageDialog(owner, message, messageType = JOptionPane.ERROR_MESSAGE)
|
||||
}
|
||||
} else {
|
||||
// pulled
|
||||
if (!pulled) pulled = !push
|
||||
|
||||
} else {
|
||||
withContext(Dispatchers.Swing) {
|
||||
val now = System.currentTimeMillis()
|
||||
sync.lastSyncTime = now
|
||||
@@ -1126,14 +1273,10 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
if (push && gistTextField.text.isBlank()) {
|
||||
gistTextField.text = syncResult.map { it.config }.getOrDefault(syncConfig).gistId
|
||||
}
|
||||
OptionPane.showMessageDialog(
|
||||
owner,
|
||||
message = I18n.getString("termora.settings.sync.done"),
|
||||
duration = 1500.milliseconds,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return syncResult.isSuccess
|
||||
|
||||
}
|
||||
|
||||
@@ -1143,18 +1286,29 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
typeComboBox.addItem(SyncType.Gitee)
|
||||
typeComboBox.addItem(SyncType.WebDAV)
|
||||
|
||||
policyComboBox.addItem(SyncPolicy.Manual)
|
||||
policyComboBox.addItem(SyncPolicy.OnChange)
|
||||
|
||||
hostsCheckBox.isFocusable = false
|
||||
snippetsCheckBox.isFocusable = false
|
||||
keysCheckBox.isFocusable = false
|
||||
keywordHighlightsCheckBox.isFocusable = false
|
||||
macrosCheckBox.isFocusable = false
|
||||
keymapCheckBox.isFocusable = false
|
||||
|
||||
hostsCheckBox.isSelected = sync.rangeHosts
|
||||
snippetsCheckBox.isSelected = sync.rangeSnippets
|
||||
keysCheckBox.isSelected = sync.rangeKeyPairs
|
||||
keywordHighlightsCheckBox.isSelected = sync.rangeKeywordHighlights
|
||||
macrosCheckBox.isSelected = sync.rangeMacros
|
||||
keymapCheckBox.isSelected = sync.rangeKeymap
|
||||
|
||||
if (sync.policy == SyncPolicy.Manual.name) {
|
||||
policyComboBox.selectedItem = SyncPolicy.Manual
|
||||
} else if (sync.policy == SyncPolicy.OnChange.name) {
|
||||
policyComboBox.selectedItem = SyncPolicy.OnChange
|
||||
}
|
||||
|
||||
typeComboBox.selectedItem = sync.type
|
||||
gistTextField.text = sync.gist
|
||||
tokenTextField.text = sync.token
|
||||
@@ -1202,6 +1356,23 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
policyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = value?.toString() ?: StringUtils.EMPTY
|
||||
if (value == SyncPolicy.Manual) {
|
||||
text = I18n.getString("termora.settings.sync.policy.manual")
|
||||
} else if (value == SyncPolicy.OnChange) {
|
||||
text = I18n.getString("termora.settings.sync.policy.on-change")
|
||||
}
|
||||
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
||||
}
|
||||
}
|
||||
|
||||
val lastSyncTime = sync.lastSyncTime
|
||||
lastSyncTimeLabel.text = "${I18n.getString("termora.settings.sync.last-sync-time")}: ${
|
||||
@@ -1212,6 +1383,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
refreshButtons()
|
||||
|
||||
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
@@ -1229,14 +1401,14 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow, 30dlu",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
val rangeBox = FormBuilder.create()
|
||||
.layout(
|
||||
FormLayout(
|
||||
"left:pref, $formMargin, left:pref, $formMargin, left:pref",
|
||||
"pref, $formMargin, pref"
|
||||
"pref, 2dlu, pref"
|
||||
)
|
||||
)
|
||||
.add(hostsCheckBox).xy(1, 1)
|
||||
@@ -1244,6 +1416,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
.add(keywordHighlightsCheckBox).xy(5, 1)
|
||||
.add(macrosCheckBox).xy(1, 3)
|
||||
.add(keymapCheckBox).xy(3, 3)
|
||||
.add(snippetsCheckBox).xy(5, 3)
|
||||
.build()
|
||||
|
||||
var rows = 1
|
||||
@@ -1278,20 +1451,26 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
gistTextField.trailingComponent = visitGistBtn
|
||||
}
|
||||
|
||||
val syncPolicyBox = Box.createHorizontalBox()
|
||||
syncPolicyBox.add(policyComboBox)
|
||||
syncPolicyBox.add(Box.createHorizontalGlue())
|
||||
syncPolicyBox.add(Box.createHorizontalGlue())
|
||||
|
||||
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.policy")}:").xy(1, rows)
|
||||
.add(syncPolicyBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.sync.range")}:").xy(1, rows)
|
||||
.add(rangeBox).xy(3, rows).apply { rows += step }
|
||||
// Sync buttons
|
||||
.add(
|
||||
FormBuilder.create()
|
||||
.layout(FormLayout("pref, 2dlu, pref, 2dlu, pref, 2dlu, pref", "pref"))
|
||||
.add(uploadConfigButton).xy(1, 1)
|
||||
.add(downloadConfigButton).xy(3, 1)
|
||||
.add(exportConfigButton).xy(5, 1)
|
||||
.add(importConfigButton).xy(7, 1)
|
||||
.layout(FormLayout("pref, 2dlu, pref, 2dlu, pref", "pref"))
|
||||
.add(syncConfigButton).xy(1, 1)
|
||||
.add(exportConfigButton).xy(3, 1)
|
||||
.add(importConfigButton).xy(5, 1)
|
||||
.build()
|
||||
).xy(3, rows, "center, fill").apply { rows += step }
|
||||
.add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step }
|
||||
@@ -1306,9 +1485,11 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
private val editCommandField = OutlineTextField(255)
|
||||
private val sftpCommandField = OutlineTextField(255)
|
||||
private val defaultDirectoryField = OutlineTextField(255)
|
||||
private val browseDirectoryBtn = JButton(Icons.folder)
|
||||
private val pinTabComboBox = YesOrNoComboBox()
|
||||
private val preserveModificationTimeComboBox = YesOrNoComboBox()
|
||||
private val sftp get() = database.sftp
|
||||
private val sftpAction get() = actionManager.getAction(Actions.SFTP) as SFTPAction
|
||||
|
||||
init {
|
||||
initView()
|
||||
@@ -1330,27 +1511,55 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
})
|
||||
|
||||
pinTabComboBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
defaultDirectoryField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||
override fun changedUpdate(e: DocumentEvent) {
|
||||
sftp.defaultDirectory = defaultDirectoryField.text
|
||||
}
|
||||
})
|
||||
|
||||
pinTabComboBox.addItemListener(object : ItemListener {
|
||||
override fun itemStateChanged(e: ItemEvent) {
|
||||
if (e.stateChange != ItemEvent.SELECTED) return
|
||||
sftp.pinTab = pinTabComboBox.selectedItem as Boolean
|
||||
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
||||
val evt = AnActionEvent(window, StringUtils.EMPTY, EventObject(window))
|
||||
if (pinTabComboBox.selectedItem == true) {
|
||||
sftpAction.openOrCreateSFTPTerminalTab(evt)
|
||||
}
|
||||
val tabbed = evt.getData(DataProviders.TabbedPane) ?: continue
|
||||
val manager = evt.getData(DataProviders.TerminalTabbedManager) ?: continue
|
||||
for ((index, tab) in manager.getTerminalTabs().withIndex()) {
|
||||
if (tab is SFTPTerminalTab) {
|
||||
tabbed.setTabClosable(index, pinTabComboBox.selectedItem != true)
|
||||
break
|
||||
|
||||
if (sftp.pinTab) {
|
||||
if (manager.getTerminalTabs().none { it is SFTPTab }) {
|
||||
manager.addTerminalTab(1, SFTPTab(), false)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新状态
|
||||
manager.refreshTerminalTabs()
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
preserveModificationTimeComboBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
sftp.preserveModificationTime = preserveModificationTimeComboBox.selectedItem as Boolean
|
||||
}
|
||||
}
|
||||
|
||||
browseDirectoryBtn.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val chooser = FileChooser()
|
||||
chooser.allowsMultiSelection = false
|
||||
chooser.defaultDirectory = StringUtils.defaultIfBlank(
|
||||
defaultDirectoryField.text,
|
||||
SystemUtils.USER_HOME
|
||||
)
|
||||
chooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||
chooser.showOpenDialog(owner).thenAccept { files ->
|
||||
if (files.isNotEmpty()) defaultDirectoryField.text = files.first().absolutePath
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private fun initView() {
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
@@ -1365,9 +1574,14 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
sftpCommandField.placeholderText = "sftp"
|
||||
}
|
||||
|
||||
defaultDirectoryField.placeholderText = SystemUtils.USER_HOME
|
||||
defaultDirectoryField.trailingComponent = browseDirectoryBtn
|
||||
|
||||
defaultDirectoryField.text = sftp.defaultDirectory
|
||||
editCommandField.text = sftp.editCommand
|
||||
sftpCommandField.text = sftp.sftpCommand
|
||||
pinTabComboBox.selectedItem = sftp.pinTab
|
||||
preserveModificationTimeComboBox.selectedItem = sftp.preserveModificationTime
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
@@ -1388,13 +1602,23 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
val box = Box.createHorizontalBox()
|
||||
box.add(JLabel("${I18n.getString("termora.settings.sftp.preserve-time")}:"))
|
||||
box.add(Box.createHorizontalStrut(8))
|
||||
box.add(preserveModificationTimeComboBox)
|
||||
|
||||
var rows = 1
|
||||
val builder = FormBuilder.create().layout(layout).debug(false)
|
||||
builder.add("${I18n.getString("termora.settings.sftp.fixed-tab")}:").xy(1, 1)
|
||||
builder.add(pinTabComboBox).xy(3, 1)
|
||||
builder.add("${I18n.getString("termora.settings.sftp.edit-command")}:").xy(1, 3)
|
||||
builder.add(editCommandField).xy(3, 3)
|
||||
builder.add("${I18n.getString("termora.tabbed.contextmenu.sftp-command")}:").xy(1, 5)
|
||||
builder.add(sftpCommandField).xy(3, 5)
|
||||
builder.add("${I18n.getString("termora.settings.sftp.fixed-tab")}:").xy(1, rows)
|
||||
builder.add(pinTabComboBox).xy(3, rows).apply { rows += 2 }
|
||||
builder.add("${I18n.getString("termora.settings.sftp.edit-command")}:").xy(1, rows)
|
||||
builder.add(editCommandField).xy(3, rows).apply { rows += 2 }
|
||||
builder.add("${I18n.getString("termora.tabbed.contextmenu.sftp-command")}:").xy(1, rows)
|
||||
builder.add(sftpCommandField).xy(3, rows).apply { rows += 2 }
|
||||
builder.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||
builder.add(defaultDirectoryField).xy(3, rows).apply { rows += 2 }
|
||||
builder.add(box).xyw(1, rows, 3).apply { rows += 2 }
|
||||
|
||||
|
||||
return builder.build()
|
||||
|
||||
@@ -1612,13 +1836,15 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
val hosts = hostManager.hosts()
|
||||
val keyPairs = keyManager.getOhKeyPairs()
|
||||
val snippets = snippetManager.snippets()
|
||||
|
||||
// 获取到安全的属性,如果设置密码那表示之前并未加密
|
||||
// 这里取出来之后重新存储加密
|
||||
val properties = database.getSafetyProperties().map { Pair(it, it.getProperties()) }
|
||||
|
||||
val key = doorman.work(passwordTextField.password)
|
||||
|
||||
hosts.forEach { hostManager.addHost(it) }
|
||||
snippets.forEach { snippetManager.addSnippet(it) }
|
||||
keyPairs.forEach { keyManager.addOhKeyPair(it) }
|
||||
for (e in properties) {
|
||||
for ((k, v) in e.second) {
|
||||
|
||||
344
src/main/kotlin/app/termora/SimpleTree.kt
Normal file
344
src/main/kotlin/app/termora/SimpleTree.kt
Normal file
@@ -0,0 +1,344 @@
|
||||
package app.termora
|
||||
|
||||
import org.jdesktop.swingx.JXTree
|
||||
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.Transferable
|
||||
import java.awt.datatransfer.UnsupportedFlavorException
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.*
|
||||
import javax.swing.*
|
||||
import javax.swing.event.CellEditorListener
|
||||
import javax.swing.event.ChangeEvent
|
||||
import javax.swing.tree.TreePath
|
||||
import kotlin.math.min
|
||||
|
||||
open class SimpleTree : JXTree() {
|
||||
|
||||
protected open val model get() = super.getModel() as SimpleTreeModel<*>
|
||||
private val editor = OutlineTextField(64)
|
||||
protected val tree get() = this
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
|
||||
private fun initViews() {
|
||||
|
||||
|
||||
// renderer
|
||||
setCellRenderer(object : DefaultXTreeCellRenderer() {
|
||||
override fun getTreeCellRendererComponent(
|
||||
tree: JTree,
|
||||
value: Any,
|
||||
sel: Boolean,
|
||||
expanded: Boolean,
|
||||
leaf: Boolean,
|
||||
row: Int,
|
||||
hasFocus: Boolean
|
||||
): Component {
|
||||
val node = value as SimpleTreeNode<*>
|
||||
val c = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus)
|
||||
icon = node.getIcon(sel, expanded, tree.hasFocus())
|
||||
return c
|
||||
}
|
||||
})
|
||||
|
||||
// rename
|
||||
setCellEditor(object : DefaultCellEditor(editor) {
|
||||
override fun isCellEditable(e: EventObject?): Boolean {
|
||||
if (e is MouseEvent || !tree.isCellEditable(e)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return super.isCellEditable(e).apply {
|
||||
if (this) {
|
||||
editor.preferredSize = Dimension(min(220, width - 64), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCellEditorValue(): Any? {
|
||||
return getLastSelectedPathNode()?.data
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
// 右键选中
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (!SwingUtilities.isRightMouseButton(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
requestFocusInWindow()
|
||||
|
||||
val selectionRows = selectionModel.selectionRows
|
||||
|
||||
val selRow = getClosestRowForLocation(e.x, e.y)
|
||||
if (selRow < 0) {
|
||||
selectionModel.clearSelection()
|
||||
return
|
||||
} else if (selectionRows != null && selectionRows.contains(selRow)) {
|
||||
return
|
||||
}
|
||||
|
||||
selectionPath = getPathForLocation(e.x, e.y)
|
||||
|
||||
setSelectionRow(selRow)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// contextmenu
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (!(SwingUtilities.isRightMouseButton(e))) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Objects.isNull(lastSelectedPathComponent)) {
|
||||
return
|
||||
}
|
||||
|
||||
showContextmenu(e)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// rename
|
||||
getCellEditor().addCellEditorListener(object : CellEditorListener {
|
||||
override fun editingStopped(e: ChangeEvent) {
|
||||
val node = getLastSelectedPathNode() ?: return
|
||||
if (editor.text.isBlank() || editor.text == node.toString()) {
|
||||
return
|
||||
}
|
||||
onRenamed(node, editor.text)
|
||||
}
|
||||
|
||||
override fun editingCanceled(e: ChangeEvent) {
|
||||
}
|
||||
})
|
||||
|
||||
// drag
|
||||
transferHandler = object : TransferHandler() {
|
||||
|
||||
override fun createTransferable(c: JComponent): Transferable? {
|
||||
val nodes = getSelectionSimpleTreeNodes().toMutableList()
|
||||
if (nodes.isEmpty()) return null
|
||||
if (nodes.contains(model.root)) return null
|
||||
|
||||
val iterator = nodes.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val node = iterator.next()
|
||||
val parents = model.getPathToRoot(node).filter { it != node }
|
||||
if (parents.any { nodes.contains(it) }) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
|
||||
return MoveNodeTransferable(nodes)
|
||||
}
|
||||
|
||||
override fun getSourceActions(c: JComponent?): Int {
|
||||
return MOVE
|
||||
}
|
||||
|
||||
override fun canImport(support: TransferSupport): Boolean {
|
||||
if (support.component != tree) return false
|
||||
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
|
||||
val path = dropLocation.path ?: return false
|
||||
val node = path.lastPathComponent as? SimpleTreeNode<*> ?: return false
|
||||
if (!support.isDataFlavorSupported(MoveNodeTransferable.dataFlavor)) return false
|
||||
val nodes = (support.transferable.getTransferData(MoveNodeTransferable.dataFlavor) as? List<*>)
|
||||
?.filterIsInstance<SimpleTreeNode<*>>() ?: return false
|
||||
if (nodes.isEmpty()) return false
|
||||
if (!node.isFolder) return false
|
||||
|
||||
for (e in nodes) {
|
||||
// 禁止拖拽到自己的子下面
|
||||
if (path.equals(TreePath(e.path)) || TreePath(e.path).isDescendant(path)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 文件夹只能拖拽到文件夹的下面
|
||||
if (e.isFolder) {
|
||||
if (dropLocation.childIndex > node.folderCount) {
|
||||
return false
|
||||
}
|
||||
} else if (dropLocation.childIndex != -1) {
|
||||
// 非文件夹也不能拖拽到文件夹的上面
|
||||
if (dropLocation.childIndex < node.folderCount) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val p = e.parent ?: continue
|
||||
// 如果是同级目录排序,那么判断是不是自己的上下,如果是的话也禁止
|
||||
if (p == node && dropLocation.childIndex != -1) {
|
||||
val idx = p.getIndex(e)
|
||||
if (dropLocation.childIndex in idx..idx + 1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
support.setShowDropLocation(true)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun importData(support: TransferSupport): Boolean {
|
||||
if (!support.isDrop) return false
|
||||
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
|
||||
val node = dropLocation.path.lastPathComponent as? SimpleTreeNode<*> ?: return false
|
||||
val nodes = (support.transferable.getTransferData(MoveNodeTransferable.dataFlavor) as? List<*>)
|
||||
?.filterIsInstance<SimpleTreeNode<*>>() ?: return false
|
||||
|
||||
// 展开的 node
|
||||
val expanded = mutableSetOf(node.id)
|
||||
for (e in nodes) {
|
||||
e.getAllChildren().filter { isExpanded(TreePath(model.getPathToRoot(it))) }
|
||||
.map { it }.forEach { expanded.add(it.id) }
|
||||
}
|
||||
|
||||
// 转移
|
||||
for (e in nodes) {
|
||||
model.removeNodeFromParent(e)
|
||||
rebase(e, node)
|
||||
|
||||
if (dropLocation.childIndex == -1) {
|
||||
if (e.isFolder) {
|
||||
model.insertNodeInto(e, node, node.folderCount)
|
||||
} else {
|
||||
model.insertNodeInto(e, node, node.childCount)
|
||||
}
|
||||
} else {
|
||||
if (e.isFolder) {
|
||||
model.insertNodeInto(e, node, min(node.folderCount, dropLocation.childIndex))
|
||||
} else {
|
||||
model.insertNodeInto(e, node, min(node.childCount, dropLocation.childIndex))
|
||||
}
|
||||
}
|
||||
|
||||
selectionPath = TreePath(model.getPathToRoot(e))
|
||||
}
|
||||
|
||||
// 先展开最顶级的
|
||||
expandPath(TreePath(model.getPathToRoot(node)))
|
||||
|
||||
for (child in node.getAllChildren()) {
|
||||
if (expanded.contains(child.id)) {
|
||||
expandPath(TreePath(model.getPathToRoot(child)))
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun newFolder(newNode: SimpleTreeNode<*>): Boolean {
|
||||
val lastNode = lastSelectedPathComponent
|
||||
if (lastNode !is SimpleTreeNode<*>) return false
|
||||
return newNode(newNode, lastNode.folderCount)
|
||||
}
|
||||
|
||||
protected open fun newFile(newNode: SimpleTreeNode<*>): Boolean {
|
||||
val lastNode = lastSelectedPathComponent
|
||||
if (lastNode !is SimpleTreeNode<*>) return false
|
||||
return newNode(newNode, lastNode.childCount)
|
||||
}
|
||||
|
||||
private fun newNode(newNode: SimpleTreeNode<*>, index: Int): Boolean {
|
||||
val lastNode = lastSelectedPathComponent
|
||||
if (lastNode !is SimpleTreeNode<*>) return false
|
||||
model.insertNodeInto(newNode, lastNode, index)
|
||||
selectionPath = TreePath(model.getPathToRoot(newNode))
|
||||
startEditingAtPath(selectionPath)
|
||||
return true
|
||||
}
|
||||
|
||||
open fun getLastSelectedPathNode(): SimpleTreeNode<*>? {
|
||||
return lastSelectedPathComponent as? SimpleTreeNode<*>
|
||||
}
|
||||
|
||||
|
||||
protected open fun showContextmenu(evt: MouseEvent) {
|
||||
|
||||
}
|
||||
|
||||
protected open fun onRenamed(node: SimpleTreeNode<*>, text: String) {}
|
||||
|
||||
open fun refreshNode(node: SimpleTreeNode<*> = model.root) {
|
||||
val state = TreeUtils.saveExpansionState(tree)
|
||||
val rows = selectionRows
|
||||
|
||||
model.reload(node)
|
||||
|
||||
TreeUtils.loadExpansionState(tree, state)
|
||||
|
||||
super.setSelectionRows(rows)
|
||||
}
|
||||
|
||||
/**
|
||||
* 包含孙子
|
||||
*/
|
||||
open fun getSelectionSimpleTreeNodes(include: Boolean = false): List<SimpleTreeNode<*>> {
|
||||
val paths = selectionPaths ?: return emptyList()
|
||||
if (paths.isEmpty()) return emptyList()
|
||||
val nodes = mutableListOf<SimpleTreeNode<*>>()
|
||||
val parents = paths.mapNotNull { it.lastPathComponent }
|
||||
.filterIsInstance<SimpleTreeNode<*>>().toMutableList()
|
||||
|
||||
if (include) {
|
||||
while (parents.isNotEmpty()) {
|
||||
val node = parents.removeFirst()
|
||||
nodes.add(node)
|
||||
parents.addAll(node.children().toList().filterIsInstance<SimpleTreeNode<*>>())
|
||||
}
|
||||
}
|
||||
|
||||
return if (include) nodes else parents
|
||||
}
|
||||
|
||||
protected open fun isCellEditable(e: EventObject?): Boolean {
|
||||
return getLastSelectedPathNode() != model.root
|
||||
}
|
||||
|
||||
protected open fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>) {
|
||||
|
||||
}
|
||||
|
||||
private class MoveNodeTransferable(val nodes: List<SimpleTreeNode<*>>) : Transferable {
|
||||
companion object {
|
||||
val dataFlavor =
|
||||
DataFlavor("${DataFlavor.javaJVMLocalObjectMimeType};class=${MoveNodeTransferable::class.java.name}")
|
||||
}
|
||||
|
||||
|
||||
override fun getTransferDataFlavors(): Array<DataFlavor> {
|
||||
return arrayOf(dataFlavor)
|
||||
}
|
||||
|
||||
override fun isDataFlavorSupported(flavor: DataFlavor?): Boolean {
|
||||
return dataFlavor == flavor
|
||||
}
|
||||
|
||||
override fun getTransferData(flavor: DataFlavor?): Any {
|
||||
if (flavor == dataFlavor) {
|
||||
return nodes
|
||||
}
|
||||
throw UnsupportedFlavorException(flavor)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
11
src/main/kotlin/app/termora/SimpleTreeModel.kt
Normal file
11
src/main/kotlin/app/termora/SimpleTreeModel.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package app.termora
|
||||
|
||||
import javax.swing.tree.DefaultTreeModel
|
||||
|
||||
abstract class SimpleTreeModel<T>(root: SimpleTreeNode<T>) : DefaultTreeModel(root) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun getRoot(): SimpleTreeNode<T> {
|
||||
return super.getRoot() as SimpleTreeNode<T>
|
||||
}
|
||||
|
||||
}
|
||||
37
src/main/kotlin/app/termora/SimpleTreeNode.kt
Normal file
37
src/main/kotlin/app/termora/SimpleTreeNode.kt
Normal file
@@ -0,0 +1,37 @@
|
||||
package app.termora
|
||||
|
||||
import javax.swing.Icon
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
|
||||
abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open var data: T
|
||||
get() = userObject as T
|
||||
set(value) = setUserObject(value)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun getParent(): SimpleTreeNode<T>? {
|
||||
return super.getParent() as SimpleTreeNode<T>?
|
||||
}
|
||||
|
||||
open val folderCount: Int get() = 0
|
||||
|
||||
open fun getIcon(selected: Boolean, expanded: Boolean, hasFocus: Boolean): Icon? {
|
||||
return null
|
||||
}
|
||||
|
||||
open val isFolder get() = false
|
||||
|
||||
abstract val id: String
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open fun getAllChildren(): List<SimpleTreeNode<T>> {
|
||||
val children = mutableListOf<SimpleTreeNode<T>>()
|
||||
for (child in children()) {
|
||||
val c = child as? SimpleTreeNode<T> ?: continue
|
||||
children.add(c)
|
||||
children.addAll(c.getAllChildren())
|
||||
}
|
||||
return children
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||
import app.termora.terminal.TerminalSize
|
||||
import app.termora.x11.X11ChannelFactory
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import com.formdev.flatlaf.util.FontUtils
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
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.auth.password.UserAuthPasswordFactory
|
||||
import org.apache.sshd.client.channel.ChannelShell
|
||||
import org.apache.sshd.client.channel.ClientChannelEvent
|
||||
import org.apache.sshd.client.config.hosts.HostConfigEntry
|
||||
@@ -15,40 +23,72 @@ 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.ClientProxyConnector
|
||||
import org.apache.sshd.client.session.ClientSession
|
||||
import org.apache.sshd.client.session.ClientSessionImpl
|
||||
import org.apache.sshd.client.session.SessionFactory
|
||||
import org.apache.sshd.common.AttributeRepository
|
||||
import org.apache.sshd.common.SshConstants
|
||||
import org.apache.sshd.common.SshException
|
||||
import org.apache.sshd.common.channel.ChannelFactory
|
||||
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
||||
import org.apache.sshd.common.channel.PtyChannelConfigurationHolder
|
||||
import org.apache.sshd.common.cipher.CipherNone
|
||||
import org.apache.sshd.common.compression.BuiltinCompressions
|
||||
import org.apache.sshd.common.config.keys.KeyRandomArt
|
||||
import org.apache.sshd.common.config.keys.KeyUtils
|
||||
import org.apache.sshd.common.future.CloseFuture
|
||||
import org.apache.sshd.common.future.SshFutureListener
|
||||
import org.apache.sshd.common.global.KeepAliveHandler
|
||||
import org.apache.sshd.common.io.IoConnectFuture
|
||||
import org.apache.sshd.common.io.IoConnector
|
||||
import org.apache.sshd.common.io.IoServiceEventListener
|
||||
import org.apache.sshd.common.io.IoSession
|
||||
import org.apache.sshd.common.kex.BuiltinDHFactories
|
||||
import org.apache.sshd.common.keyprovider.KeyIdentityProvider
|
||||
import org.apache.sshd.common.session.Session
|
||||
import org.apache.sshd.common.session.SessionListener
|
||||
import org.apache.sshd.common.signature.BuiltinSignatures
|
||||
import org.apache.sshd.common.util.net.SshdSocketAddress
|
||||
import org.apache.sshd.core.CoreModuleProperties
|
||||
import org.apache.sshd.server.forward.AcceptAllForwardingFilter
|
||||
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.agent.JGitSshAgentFactory
|
||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.PageantConnector
|
||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocketConnector
|
||||
import org.eclipse.jgit.internal.transport.sshd.proxy.AbstractClientProxyConnector
|
||||
import org.eclipse.jgit.internal.transport.sshd.proxy.HttpClientConnector
|
||||
import org.eclipse.jgit.internal.transport.sshd.proxy.Socks5ClientConnector
|
||||
import org.eclipse.jgit.transport.CredentialsProvider
|
||||
import org.eclipse.jgit.transport.SshConstants.IDENTITY_AGENT
|
||||
import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
|
||||
import org.eclipse.jgit.transport.sshd.ProxyData
|
||||
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Font
|
||||
import java.awt.Window
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.net.InetSocketAddress
|
||||
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.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingUtilities
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import javax.swing.*
|
||||
import kotlin.math.max
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
object SshClients {
|
||||
|
||||
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
|
||||
|
||||
private val timeout = Duration.ofSeconds(30)
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
|
||||
|
||||
/**
|
||||
@@ -71,6 +111,12 @@ object SshClients {
|
||||
env.putAll(host.options.envs())
|
||||
|
||||
val channel = session.createShellChannel(configuration, env)
|
||||
if (host.options.enableX11Forwarding) {
|
||||
if (channel is app.termora.x11.ChannelShell) {
|
||||
channel.xForwarding = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!channel.open().verify(timeout).await()) {
|
||||
throw SshException("Failed to open Shell")
|
||||
}
|
||||
@@ -111,16 +157,16 @@ object SshClients {
|
||||
* 打开一个会话
|
||||
*/
|
||||
fun openSession(host: Host, client: SshClient): ClientSession {
|
||||
|
||||
val h = hostManager.getHost(host.id) ?: host
|
||||
|
||||
// 如果没有跳板机直接连接
|
||||
if (host.options.jumpHosts.isEmpty()) {
|
||||
return doOpenSession(host, client)
|
||||
if (h.options.jumpHosts.isEmpty()) {
|
||||
return doOpenSession(h, client)
|
||||
}
|
||||
|
||||
val jumpHosts = mutableListOf<Host>()
|
||||
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
||||
for (jumpHostId in host.options.jumpHosts) {
|
||||
for (jumpHostId in h.options.jumpHosts) {
|
||||
val e = hosts[jumpHostId]
|
||||
if (e == null) {
|
||||
if (log.isWarnEnabled) {
|
||||
@@ -132,7 +178,7 @@ object SshClients {
|
||||
}
|
||||
|
||||
// 最后一跳是目标机器
|
||||
jumpHosts.add(host)
|
||||
jumpHosts.add(h)
|
||||
|
||||
val sessions = mutableListOf<ClientSession>()
|
||||
for (i in 0 until jumpHosts.size) {
|
||||
@@ -151,7 +197,8 @@ object SshClients {
|
||||
log.info("jump host: ${currentHost.host}:${currentHost.port} , next host: ${nextHost.host}:${nextHost.port} , local address: ${address.hostName}:${address.port}")
|
||||
}
|
||||
// 映射完毕之后修改Host和端口
|
||||
jumpHosts[i + 1] = nextHost.copy(host = address.hostName, port = address.port, updateDate = System.currentTimeMillis())
|
||||
jumpHosts[i + 1] =
|
||||
nextHost.copy(host = address.hostName, port = address.port, updateDate = System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,19 +224,53 @@ object SshClients {
|
||||
entry.username = host.username
|
||||
entry.hostName = host.host
|
||||
entry.setProperty("Middleware", middleware.toString())
|
||||
entry.setProperty("Host", host.id)
|
||||
|
||||
val session = client.connect(entry)
|
||||
.verify(timeout).session
|
||||
// 设置代理
|
||||
// configureProxy(entry, host, client)
|
||||
|
||||
// ssh-agent
|
||||
if (host.authentication.type == AuthenticationType.SSHAgent) {
|
||||
if (host.authentication.password.isNotBlank())
|
||||
entry.setProperty(IDENTITY_AGENT, host.authentication.password)
|
||||
else if (SystemInfo.isWindows)
|
||||
entry.setProperty(IDENTITY_AGENT, PageantConnector.DESCRIPTOR.identityAgent)
|
||||
else
|
||||
entry.setProperty(IDENTITY_AGENT, UnixDomainSocketConnector.DESCRIPTOR.identityAgent)
|
||||
}
|
||||
|
||||
val session = client.connect(entry).verify(timeout).session
|
||||
if (host.authentication.type == AuthenticationType.Password) {
|
||||
session.addPasswordIdentity(host.authentication.password)
|
||||
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||
session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password)
|
||||
}
|
||||
|
||||
val verifyTimeout = Duration.ofSeconds(timeout.seconds * 5)
|
||||
if (!session.auth().verify(verifyTimeout).await(verifyTimeout)) {
|
||||
if (host.options.enableX11Forwarding) {
|
||||
val segments = host.options.x11Forwarding.split(":")
|
||||
if (segments.size == 2) {
|
||||
val x11Host = segments[0]
|
||||
val x11Port = segments[1].toIntOrNull()
|
||||
if (x11Port != null) {
|
||||
CoreModuleProperties.X11_BIND_HOST.set(session, x11Host)
|
||||
CoreModuleProperties.X11_BASE_PORT.set(session, 6000 + x11Port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!session.auth().verify(timeout).await(timeout)) {
|
||||
throw SshException("Authentication failed")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e !is SshException || e.disconnectCode != SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) throw e
|
||||
val owner = client.properties["owner"] as Window? ?: throw e
|
||||
val authentication = ask(host, owner) ?: throw e
|
||||
if (authentication.type == AuthenticationType.No) throw e
|
||||
return doOpenSession(host.copy(authentication = authentication), client)
|
||||
}
|
||||
|
||||
session.setAttribute(HOST_KEY, host)
|
||||
|
||||
return session
|
||||
}
|
||||
@@ -230,17 +311,27 @@ object SshClients {
|
||||
return sshdSocketAddress
|
||||
}
|
||||
|
||||
fun openClient(host: Host, owner: Window): SshClient {
|
||||
val h = hostManager.getHost(host.id) ?: host
|
||||
val client = openClient(h)
|
||||
client.userInteraction = TerminalUserInteraction(owner)
|
||||
client.serverKeyVerifier = DialogServerKeyVerifier(owner)
|
||||
client.properties["owner"] = owner
|
||||
return client
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开一个客户端
|
||||
*/
|
||||
fun openClient(host: Host): SshClient {
|
||||
val builder = ClientBuilder.builder()
|
||||
builder.globalRequestHandlers(listOf(KeepAliveHandler.INSTANCE))
|
||||
.factory { JGitSshClient() }
|
||||
.factory { MyJGitSshClient() }
|
||||
|
||||
val keyExchangeFactories = ClientBuilder.setUpDefaultKeyExchanges(true).toMutableList()
|
||||
|
||||
// https://github.com/TermoraDev/termora/issues/123
|
||||
@Suppress("DEPRECATION")
|
||||
keyExchangeFactories.addAll(
|
||||
listOf(
|
||||
DHGClient.newFactory(BuiltinDHFactories.dhg1),
|
||||
@@ -250,6 +341,24 @@ object SshClients {
|
||||
)
|
||||
builder.keyExchangeFactories(keyExchangeFactories)
|
||||
|
||||
val compressionFactories = ClientBuilder.setUpDefaultCompressionFactories(true).toMutableList()
|
||||
for (compression in listOf(
|
||||
BuiltinCompressions.none,
|
||||
BuiltinCompressions.zlib,
|
||||
BuiltinCompressions.delayedZlib
|
||||
)) {
|
||||
if (compressionFactories.contains(compression)) continue
|
||||
compressionFactories.add(compression)
|
||||
}
|
||||
builder.compressionFactories(compressionFactories)
|
||||
|
||||
val signatureFactories = ClientBuilder.setUpDefaultSignatureFactories(true).toMutableList()
|
||||
for (signature in BuiltinSignatures.entries) {
|
||||
if (signatureFactories.contains(signature)) continue
|
||||
signatureFactories.add(signature)
|
||||
}
|
||||
builder.signatureFactories(signatureFactories)
|
||||
|
||||
if (host.tunnelings.isEmpty() && host.options.jumpHosts.isEmpty()) {
|
||||
builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE)
|
||||
} else {
|
||||
@@ -258,43 +367,71 @@ object SshClients {
|
||||
|
||||
builder.hostConfigEntryResolver(HostConfigEntryResolver.EMPTY)
|
||||
|
||||
val channelFactories = mutableListOf<ChannelFactory>()
|
||||
channelFactories.addAll(ClientBuilder.DEFAULT_CHANNEL_FACTORIES)
|
||||
channelFactories.add(X11ChannelFactory.INSTANCE)
|
||||
builder.channelFactories(channelFactories)
|
||||
|
||||
val sshClient = builder.build() as JGitSshClient
|
||||
|
||||
// https://github.com/TermoraDev/termora/issues/180
|
||||
// JGit 会尝试读取本地的私钥或缓存的私钥
|
||||
sshClient.keyIdentityProvider = KeyIdentityProvider { mutableListOf() }
|
||||
|
||||
// 设置优先级
|
||||
if (host.authentication.type == AuthenticationType.PublicKey || host.authentication.type == AuthenticationType.SSHAgent) {
|
||||
if (host.authentication.type == AuthenticationType.SSHAgent) {
|
||||
// ssh-agent
|
||||
sshClient.agentFactory = JGitSshAgentFactory(ConnectorFactory.getDefault(), null)
|
||||
}
|
||||
CoreModuleProperties.PREFERRED_AUTHS.set(
|
||||
sshClient,
|
||||
listOf(
|
||||
UserAuthPasswordFactory.PUBLIC_KEY,
|
||||
UserAuthPasswordFactory.PASSWORD,
|
||||
UserAuthPasswordFactory.KB_INTERACTIVE
|
||||
).joinToString(",")
|
||||
)
|
||||
} else {
|
||||
CoreModuleProperties.PREFERRED_AUTHS.set(
|
||||
sshClient,
|
||||
listOf(
|
||||
UserAuthPasswordFactory.PASSWORD,
|
||||
UserAuthPasswordFactory.PUBLIC_KEY,
|
||||
UserAuthPasswordFactory.KB_INTERACTIVE
|
||||
).joinToString(",")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val heartbeatInterval = max(host.options.heartbeatInterval, 3)
|
||||
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
|
||||
CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true)
|
||||
|
||||
sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) }
|
||||
|
||||
if (host.proxy.type != ProxyType.No) {
|
||||
sshClient.setProxyDatabase {
|
||||
if (host.proxy.authenticationType == AuthenticationType.No) ProxyData(
|
||||
Proxy(
|
||||
if (host.proxy.type == ProxyType.SOCKS5) Proxy.Type.SOCKS else Proxy.Type.HTTP,
|
||||
InetSocketAddress(host.proxy.host, host.proxy.port)
|
||||
)
|
||||
)
|
||||
else
|
||||
ProxyData(
|
||||
Proxy(
|
||||
if (host.proxy.type == ProxyType.SOCKS5) Proxy.Type.SOCKS else Proxy.Type.HTTP,
|
||||
InetSocketAddress(host.proxy.host, host.proxy.port)
|
||||
),
|
||||
host.proxy.username,
|
||||
host.proxy.password.toCharArray(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sshClient.start()
|
||||
return sshClient
|
||||
}
|
||||
}
|
||||
|
||||
private fun ask(host: Host, owner: Window): Authentication? {
|
||||
val ref = AtomicReference<Authentication>(null)
|
||||
SwingUtilities.invokeAndWait {
|
||||
val dialog = RequestAuthenticationDialog(owner, host)
|
||||
dialog.setLocationRelativeTo(owner)
|
||||
val authentication = dialog.getAuthentication().apply { ref.set(this) }
|
||||
// save
|
||||
if (dialog.isRemembered()) {
|
||||
hostManager.addHost(
|
||||
host.copy(
|
||||
authentication = authentication,
|
||||
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return ref.get()
|
||||
}
|
||||
|
||||
private class MyDialogServerKeyVerifier(private val owner: Window) : ServerKeyVerifier, ModifiedServerKeyAcceptor {
|
||||
override fun verifyServerKey(
|
||||
@@ -302,32 +439,9 @@ private class MyDialogServerKeyVerifier(private val owner: Window) : ServerKeyVe
|
||||
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?,
|
||||
@@ -336,30 +450,73 @@ private class MyDialogServerKeyVerifier(private val owner: Window) : ServerKeyVe
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
SwingUtilities.invokeAndWait { result.set(ask(remoteAddress, expected, actual) == JOptionPane.OK_OPTION) }
|
||||
return result.get()
|
||||
}
|
||||
|
||||
private fun ask(
|
||||
remoteAddress: SocketAddress?,
|
||||
expected: PublicKey?,
|
||||
actual: PublicKey?
|
||||
): Int {
|
||||
val formMargin = "7dlu"
|
||||
val layout = FormLayout(
|
||||
"default:grow",
|
||||
"pref, 12dlu, pref, 4dlu, pref, 2dlu, pref, $formMargin, pref, $formMargin, pref, pref, 12dlu, pref"
|
||||
)
|
||||
|
||||
val errorColor = if (FlatLaf.isLafDark()) UIManager.getColor("Component.warning.focusedBorderColor") else
|
||||
UIManager.getColor("Component.error.focusedBorderColor")
|
||||
val font = FontUtils.getCompositeFont("JetBrains Mono", Font.PLAIN, 12)
|
||||
val artBox = Box.createHorizontalBox()
|
||||
artBox.add(Box.createHorizontalGlue())
|
||||
val expectedBox = Box.createVerticalBox()
|
||||
for (line in KeyRandomArt(expected).toString().lines()) {
|
||||
val label = JLabel(line)
|
||||
label.font = font
|
||||
expectedBox.add(label)
|
||||
}
|
||||
artBox.add(expectedBox)
|
||||
artBox.add(Box.createHorizontalGlue())
|
||||
val actualBox = Box.createVerticalBox()
|
||||
for (line in KeyRandomArt(actual).toString().lines()) {
|
||||
val label = JLabel(line)
|
||||
label.foreground = errorColor
|
||||
label.font = font
|
||||
actualBox.add(label)
|
||||
}
|
||||
artBox.add(actualBox)
|
||||
artBox.add(Box.createHorizontalGlue())
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
|
||||
// @formatter:off
|
||||
val address = remoteAddress.toString().replace("/", StringUtils.EMPTY)
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
.add("<html><b>${I18n.getString("termora.host.modified-server-key.title", address)}</b></html>").xy(1, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.host.modified-server-key.thumbprint")}:").xy(1, rows).apply { rows += step }
|
||||
.add(" ${I18n.getString("termora.host.modified-server-key.expected")}: ${KeyUtils.getFingerPrint(expected)}").xy(1, rows).apply { rows += step }
|
||||
.add("<html> ${I18n.getString("termora.host.modified-server-key.actual")}: <font color=rgb(${errorColor.red},${errorColor.green},${errorColor.blue})>${KeyUtils.getFingerPrint(actual)}</font></html>").xy(1, rows).apply { rows += step }
|
||||
.addSeparator(StringUtils.EMPTY).xy(1, rows).apply { rows += step }
|
||||
.add(artBox).xy(1, rows).apply { rows += step }
|
||||
.addSeparator(StringUtils.EMPTY).xy(1, rows).apply { rows += 1 }
|
||||
.add(I18n.getString("termora.host.modified-server-key.are-you-sure")).xy(1, rows).apply { rows += step }
|
||||
.build()
|
||||
// @formatter:on
|
||||
|
||||
return OptionPane.showConfirmDialog(
|
||||
owner,
|
||||
panel,
|
||||
"SSH Security Warning",
|
||||
messageType = JOptionPane.WARNING_MESSAGE,
|
||||
optionType = JOptionPane.OK_CANCEL_OPTION
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class DialogServerKeyVerifier(
|
||||
private class DialogServerKeyVerifier(
|
||||
owner: Window,
|
||||
) : KnownHostsServerKeyVerifier(
|
||||
MyDialogServerKeyVerifier(owner),
|
||||
@@ -377,10 +534,171 @@ class DialogServerKeyVerifier(
|
||||
knownHosts: Collection<HostEntryPair?>?
|
||||
): KnownHostEntry? {
|
||||
if (clientSession is JGitClientSession) {
|
||||
if (SshClients.isMiddleware(clientSession)) {
|
||||
if (isMiddleware(clientSession)) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return super.updateKnownHostsFile(clientSession, remoteAddress, serverKey, file, knownHosts)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private class MyJGitSshClient : JGitSshClient() {
|
||||
companion object {
|
||||
private val HOST_CONFIG_ENTRY: AttributeRepository.AttributeKey<HostConfigEntry> by lazy {
|
||||
JGitSshClient::class.java.getDeclaredField("HOST_CONFIG_ENTRY").apply { isAccessible = true }
|
||||
.get(null) as AttributeRepository.AttributeKey<HostConfigEntry>
|
||||
}
|
||||
private const val CLIENT_PROXY_CONNECTOR = "ClientProxyConnectorId"
|
||||
}
|
||||
|
||||
private val sshClient = this
|
||||
private val clientProxyConnectors = ConcurrentHashMap<String, ClientProxyConnector>()
|
||||
|
||||
|
||||
override fun createConnector(): IoConnector {
|
||||
return MyIoConnector(this, super.createConnector())
|
||||
}
|
||||
|
||||
override fun createSessionFactory(): SessionFactory {
|
||||
return object : SessionFactory(sshClient) {
|
||||
override fun doCreateSession(ioSession: IoSession): ClientSessionImpl {
|
||||
return object : JGitClientSession(sshClient, ioSession) {
|
||||
override fun getClientProxyConnector(): ClientProxyConnector? {
|
||||
val entry = getAttribute(HOST_CONFIG_ENTRY) ?: return null
|
||||
val clientProxyConnectorId = entry.getProperty(CLIENT_PROXY_CONNECTOR) ?: return null
|
||||
val clientProxyConnector = sshClient.clientProxyConnectors[clientProxyConnectorId]
|
||||
|
||||
if (clientProxyConnector != null) {
|
||||
addSessionListener(object : SessionListener {
|
||||
override fun sessionClosed(session: Session) {
|
||||
clientProxyConnectors.remove(clientProxyConnectorId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return clientProxyConnector
|
||||
}
|
||||
|
||||
override fun createShellChannel(
|
||||
ptyConfig: PtyChannelConfigurationHolder?,
|
||||
env: MutableMap<String, *>?
|
||||
): ChannelShell {
|
||||
if (inCipher is CipherNone || outCipher is CipherNone)
|
||||
throw IllegalStateException("Interactive channels are not supported with none cipher")
|
||||
val channel = app.termora.x11.ChannelShell(ptyConfig, env)
|
||||
val id = connectionService.registerChannel(channel)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("createShellChannel({}) created id={} - PTY={}", this, id, ptyConfig)
|
||||
}
|
||||
return channel
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setClientProxyConnector(proxyConnector: ClientProxyConnector?) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
private class MyIoConnector(private val sshClient: MyJGitSshClient, private val ioConnector: IoConnector) :
|
||||
IoConnector {
|
||||
override fun close(immediately: Boolean): CloseFuture {
|
||||
return ioConnector.close(immediately)
|
||||
}
|
||||
|
||||
override fun addCloseFutureListener(listener: SshFutureListener<CloseFuture>?) {
|
||||
return ioConnector.addCloseFutureListener(listener)
|
||||
}
|
||||
|
||||
override fun removeCloseFutureListener(listener: SshFutureListener<CloseFuture>?) {
|
||||
return ioConnector.removeCloseFutureListener(listener)
|
||||
}
|
||||
|
||||
override fun isClosed(): Boolean {
|
||||
return ioConnector.isClosed
|
||||
}
|
||||
|
||||
override fun isClosing(): Boolean {
|
||||
return ioConnector.isClosing
|
||||
}
|
||||
|
||||
override fun getIoServiceEventListener(): IoServiceEventListener {
|
||||
return ioConnector.ioServiceEventListener
|
||||
}
|
||||
|
||||
override fun setIoServiceEventListener(listener: IoServiceEventListener?) {
|
||||
return ioConnector.setIoServiceEventListener(listener)
|
||||
}
|
||||
|
||||
override fun getManagedSessions(): MutableMap<Long, IoSession> {
|
||||
return ioConnector.managedSessions
|
||||
}
|
||||
|
||||
override fun connect(
|
||||
targetAddress: SocketAddress,
|
||||
context: AttributeRepository?,
|
||||
localAddress: SocketAddress?
|
||||
): IoConnectFuture {
|
||||
var tAddress = targetAddress
|
||||
val entry = context?.getAttribute(HOST_CONFIG_ENTRY)
|
||||
if (entry != null) {
|
||||
val host = hostManager.getHost(entry.getProperty("Host") ?: StringUtils.EMPTY)
|
||||
if (host != null) {
|
||||
tAddress = configureProxy(entry, host, tAddress)
|
||||
}
|
||||
}
|
||||
return ioConnector.connect(tAddress, context, localAddress)
|
||||
}
|
||||
|
||||
private fun configureProxy(
|
||||
entry: HostConfigEntry,
|
||||
host: Host,
|
||||
targetAddress: SocketAddress
|
||||
): SocketAddress {
|
||||
if (host.proxy.type == ProxyType.No) return targetAddress
|
||||
val address = targetAddress as? InetSocketAddress ?: return targetAddress
|
||||
if (address.hostString == (SshdSocketAddress.LOCALHOST_IPV4)) return targetAddress
|
||||
|
||||
// 获取代理连接器
|
||||
val clientProxyConnector = getClientProxyConnector(host, address) ?: return targetAddress
|
||||
|
||||
val id = UUID.randomUUID().toSimpleString()
|
||||
entry.setProperty(CLIENT_PROXY_CONNECTOR, id)
|
||||
sshClient.clientProxyConnectors[id] = clientProxyConnector
|
||||
|
||||
return InetSocketAddress(host.proxy.host, host.proxy.port)
|
||||
}
|
||||
|
||||
private fun getClientProxyConnector(
|
||||
host: Host,
|
||||
remoteAddress: InetSocketAddress
|
||||
): AbstractClientProxyConnector? {
|
||||
if (host.proxy.type == ProxyType.HTTP) {
|
||||
return HttpClientConnector(
|
||||
InetSocketAddress(host.proxy.host, host.proxy.port),
|
||||
remoteAddress,
|
||||
host.proxy.username.ifBlank { null },
|
||||
if (host.proxy.password.isBlank()) null else host.proxy.password.toCharArray()
|
||||
)
|
||||
} else if (host.proxy.type == ProxyType.SOCKS5) {
|
||||
return Socks5ClientConnector(
|
||||
InetSocketAddress(host.proxy.host, host.proxy.port),
|
||||
remoteAddress,
|
||||
host.proxy.username.ifBlank { null },
|
||||
if (host.proxy.password.isBlank()) null else host.proxy.password.toCharArray()
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ class TerminalFactory private constructor() : Disposable {
|
||||
private val terminals = mutableListOf<Terminal>()
|
||||
|
||||
companion object {
|
||||
fun getInstance(scope: WindowScope): TerminalFactory {
|
||||
return scope.getOrCreate(TerminalFactory::class) { TerminalFactory() }
|
||||
fun getInstance(): TerminalFactory {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(TerminalFactory::class) { TerminalFactory() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.actions.MultipleAction
|
||||
import app.termora.highlight.KeywordHighlightPaintListener
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.PtyConnector
|
||||
import app.termora.terminal.Terminal
|
||||
import app.termora.terminal.panel.TerminalHyperlinkPaintListener
|
||||
import app.termora.terminal.panel.TerminalPanel
|
||||
import app.termora.terminal.panel.TerminalWriter
|
||||
import kotlinx.coroutines.*
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.event.ComponentEvent
|
||||
import java.awt.event.ComponentListener
|
||||
import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@@ -17,16 +26,9 @@ class TerminalPanelFactory : Disposable {
|
||||
|
||||
companion object {
|
||||
|
||||
private val Factory = DataKey(TerminalPanelFactory::class)
|
||||
|
||||
fun getInstance(scope: Scope): TerminalPanelFactory {
|
||||
return scope.getOrCreate(TerminalPanelFactory::class) { TerminalPanelFactory() }
|
||||
}
|
||||
|
||||
fun getAllTerminalPanel(): Array<TerminalPanel> {
|
||||
return ApplicationScope.forApplicationScope().windowScopes()
|
||||
.map { getInstance(it) }
|
||||
.flatMap { it.terminalPanels }.toTypedArray()
|
||||
fun getInstance(): TerminalPanelFactory {
|
||||
return ApplicationScope.forApplicationScope()
|
||||
.getOrCreate(TerminalPanelFactory::class) { TerminalPanelFactory() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,17 +39,19 @@ class TerminalPanelFactory : Disposable {
|
||||
|
||||
|
||||
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
||||
val terminalPanel = TerminalPanel(terminal, ptyConnector)
|
||||
val writer = MyTerminalWriter(ptyConnector)
|
||||
val terminalPanel = TerminalPanel(terminal, writer)
|
||||
|
||||
// processDeviceStatusReport
|
||||
terminal.getTerminalModel().setData(DataKey.TerminalWriter, writer)
|
||||
|
||||
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
||||
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
||||
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
||||
terminal.getTerminalModel().setData(Factory, this)
|
||||
|
||||
Disposer.register(terminalPanel, object : Disposable {
|
||||
override fun dispose() {
|
||||
if (terminal.getTerminalModel().hasData(Factory)) {
|
||||
terminal.getTerminalModel().getData(Factory).removeTerminalPanel(terminalPanel)
|
||||
}
|
||||
removeTerminalPanel(terminalPanel)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -75,13 +79,12 @@ class TerminalPanelFactory : Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
fun removeTerminalPanel(terminalPanel: TerminalPanel) {
|
||||
private fun removeTerminalPanel(terminalPanel: TerminalPanel) {
|
||||
terminalPanels.remove(terminalPanel)
|
||||
}
|
||||
|
||||
fun addTerminalPanel(terminalPanel: TerminalPanel) {
|
||||
private fun addTerminalPanel(terminalPanel: TerminalPanel) {
|
||||
terminalPanels.add(terminalPanel)
|
||||
terminalPanel.terminal.getTerminalModel().setData(Factory, this)
|
||||
}
|
||||
|
||||
private class Painter : Disposable {
|
||||
@@ -91,16 +94,13 @@ class TerminalPanelFactory : Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
init {
|
||||
coroutineScope.launch {
|
||||
while (coroutineScope.isActive) {
|
||||
delay(500.milliseconds)
|
||||
SwingUtilities.invokeLater {
|
||||
ApplicationScope.forApplicationScope().windowScopes()
|
||||
.map { getInstance(it) }.forEach { it.repaintAll() }
|
||||
}
|
||||
SwingUtilities.invokeLater { TerminalPanelFactory.getInstance().repaintAll() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,4 +110,58 @@ class TerminalPanelFactory : Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
private class MyTerminalWriter(private val ptyConnector: PtyConnector) : TerminalWriter {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(MyTerminalWriter::class.java)
|
||||
}
|
||||
|
||||
private lateinit var evt: AnActionEvent
|
||||
|
||||
override fun onMounted(c: JComponent) {
|
||||
evt = AnActionEvent(c, StringUtils.EMPTY, EventObject(c))
|
||||
}
|
||||
|
||||
override fun write(request: TerminalWriter.WriteRequest) {
|
||||
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("write: ${String(request.buffer, getCharset())}")
|
||||
}
|
||||
|
||||
val windowScope = evt.getData(DataProviders.WindowScope)
|
||||
if (windowScope == null) {
|
||||
ptyConnector.write(request.buffer)
|
||||
return
|
||||
}
|
||||
|
||||
val multipleAction = MultipleAction.getInstance(windowScope)
|
||||
if (!multipleAction.isSelected) {
|
||||
ptyConnector.write(request.buffer)
|
||||
return
|
||||
}
|
||||
|
||||
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager)
|
||||
if (terminalTabbedManager == null) {
|
||||
ptyConnector.write(request.buffer)
|
||||
return
|
||||
}
|
||||
|
||||
for (tab in terminalTabbedManager.getTerminalTabs()) {
|
||||
val writer = tab.getData(DataProviders.TerminalWriter) ?: continue
|
||||
if (writer is MyTerminalWriter) {
|
||||
writer.ptyConnector.write(request.buffer)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun resize(rows: Int, cols: Int) {
|
||||
ptyConnector.resize(rows, cols)
|
||||
}
|
||||
|
||||
override fun getCharset(): Charset {
|
||||
return ptyConnector.getCharset()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.actions.DataProvider
|
||||
import java.beans.PropertyChangeListener
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComponent
|
||||
|
||||
interface TerminalTab : Disposable {
|
||||
interface TerminalTab : Disposable, DataProvider {
|
||||
|
||||
/**
|
||||
* 标题
|
||||
@@ -42,6 +43,11 @@ interface TerminalTab : Disposable {
|
||||
*/
|
||||
fun canClose(): Boolean = true
|
||||
|
||||
/**
|
||||
* 返回 true 表示可以关闭
|
||||
*/
|
||||
fun willBeClose(): Boolean = true
|
||||
|
||||
/**
|
||||
* 是否可以克隆
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,6 @@ import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
|
||||
import app.termora.findeverywhere.FindEverywhereProvider
|
||||
import app.termora.findeverywhere.FindEverywhereResult
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.transport.TransportPanel
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
@@ -78,21 +77,14 @@ class TerminalTabbed(
|
||||
tabs[oldIndex].onLostFocus()
|
||||
}
|
||||
|
||||
tabbedPane.getComponentAt(newIndex).requestFocusInWindow()
|
||||
|
||||
if (newIndex >= 0 && tabs.size > newIndex) {
|
||||
tabs[newIndex].onGrabFocus()
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater { tabbedPane.getComponentAt(newIndex).requestFocusInWindow() }
|
||||
|
||||
}
|
||||
|
||||
// 选择变动
|
||||
tabbedPane.addChangeListener {
|
||||
if (tabbedPane.selectedIndex >= 0) {
|
||||
val c = tabbedPane.getComponentAt(tabbedPane.selectedIndex)
|
||||
c.requestFocusInWindow()
|
||||
}
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
tabbedPane.addMouseListener(object : MouseAdapter() {
|
||||
@@ -128,7 +120,7 @@ class TerminalTabbed(
|
||||
val results = mutableListOf<FindEverywhereResult>()
|
||||
for (i in 0 until tabbedPane.tabCount) {
|
||||
val c = tabbedPane.getComponentAt(i)
|
||||
if (c is WelcomePanel || c is TransportPanel) {
|
||||
if (c is JComponent && c.getClientProperty(FindEverywhereProvider.SKIP_FIND_EVERYWHERE) != null) {
|
||||
continue
|
||||
}
|
||||
results.add(
|
||||
@@ -162,7 +154,7 @@ class TerminalTabbed(
|
||||
val tab = tabs[index]
|
||||
|
||||
if (disposable) {
|
||||
if (!tab.canClose()) {
|
||||
if (!tab.willBeClose()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -198,12 +190,11 @@ class TerminalTabbed(
|
||||
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
|
||||
rename.addActionListener {
|
||||
if (tabIndex > 0) {
|
||||
val dialog = InputDialog(
|
||||
val text = OptionPane.showInputDialog(
|
||||
SwingUtilities.getWindowAncestor(this),
|
||||
title = rename.text,
|
||||
text = tabbedPane.getTitleAt(tabIndex),
|
||||
value = tabbedPane.getTitleAt(tabIndex)
|
||||
)
|
||||
val text = dialog.getText()
|
||||
if (!text.isNullOrBlank()) {
|
||||
tabbedPane.setTitleAt(tabIndex, text)
|
||||
c.putClientProperty(titleProperty, text)
|
||||
@@ -222,6 +213,21 @@ class TerminalTabbed(
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑
|
||||
val edit = popupMenu.add(I18n.getString("termora.keymgr.edit"))
|
||||
edit.addActionListener(object : AnAction() {
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
if (tab is HostTerminalTab) {
|
||||
val host = hostManager.getHost(tab.host.id) ?: return
|
||||
val dialog = HostDialog(evt.window, host)
|
||||
dialog.setLocationRelativeTo(evt.window)
|
||||
dialog.isVisible = true
|
||||
hostManager.addHost(dialog.host ?: return)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 在新窗口中打开
|
||||
val openInNewWindow = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.open-in-new-window"))
|
||||
openInNewWindow.addActionListener(object : AnAction() {
|
||||
@@ -283,6 +289,7 @@ class TerminalTabbed(
|
||||
close.isEnabled = tab.canClose()
|
||||
rename.isEnabled = close.isEnabled
|
||||
clone.isEnabled = close.isEnabled
|
||||
edit.isEnabled = tab is HostTerminalTab && tab.host.id != "local"
|
||||
openInNewWindow.isEnabled = close.isEnabled
|
||||
|
||||
// 如果不允许克隆
|
||||
@@ -334,6 +341,13 @@ class TerminalTabbed(
|
||||
Disposer.register(this, tab)
|
||||
}
|
||||
|
||||
override fun refreshTerminalTabs() {
|
||||
for (i in 0 until tabbedPane.tabCount) {
|
||||
tabbedPane.setTabClosable(i, tabs[i].canClose())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun openSFTPPtyTab(tab: HostTerminalTab, openHostAction: Action, evt: EventObject) {
|
||||
if (!SFTPPtyTerminalTab.canSupports) {
|
||||
OptionPane.showMessageDialog(
|
||||
|
||||
@@ -7,4 +7,5 @@ interface TerminalTabbedManager {
|
||||
fun getTerminalTabs(): List<TerminalTab>
|
||||
fun setSelectedTerminalTab(tab: TerminalTab)
|
||||
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true)
|
||||
fun refreshTerminalTabs()
|
||||
}
|
||||
@@ -4,23 +4,28 @@ package app.termora
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviderSupport
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.sftp.SFTPTab
|
||||
import app.termora.terminal.DataKey
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import com.formdev.flatlaf.ui.FlatRootPaneUI
|
||||
import com.formdev.flatlaf.ui.FlatTitlePane
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jetbrains.JBR
|
||||
import java.awt.Dimension
|
||||
import java.awt.Insets
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import java.awt.*
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.MouseListener
|
||||
import java.awt.event.MouseMotionListener
|
||||
import java.util.*
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.Box
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JFrame
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.SwingUtilities.isEventDispatchThread
|
||||
import javax.swing.UIManager
|
||||
import kotlin.math.max
|
||||
|
||||
|
||||
fun assertEventDispatchThread() {
|
||||
if (!isEventDispatchThread()) throw WrongThreadException("AWT EventQueue")
|
||||
@@ -32,14 +37,13 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
|
||||
private val id = UUID.randomUUID().toString()
|
||||
private val windowScope = ApplicationScope.forWindowScope(this)
|
||||
private val titleBar = LogicCustomTitleBar.createCustomTitleBar(this)
|
||||
private val tabbedPane = MyTabbedPane()
|
||||
private val toolbar = TermoraToolBar(titleBar, tabbedPane)
|
||||
private val toolbar = TermoraToolBar(windowScope, this, tabbedPane)
|
||||
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane)
|
||||
private val isWindowDecorationsSupported by lazy { JBR.isWindowDecorationsSupported() }
|
||||
private val dataProviderSupport = DataProviderSupport()
|
||||
private val welcomePanel = WelcomePanel(windowScope)
|
||||
private val sftp get() = Database.getDatabase().sftp
|
||||
private var notifyListeners = emptyArray<NotifyListener>()
|
||||
|
||||
|
||||
init {
|
||||
@@ -48,44 +52,144 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
forceHitTest()
|
||||
|
||||
// macos 需要判断是否全部删除
|
||||
// 当 Tab 为 0 的时候,需要加一个边距,避开控制栏
|
||||
if (SystemInfo.isMacOS && isWindowDecorationsSupported) {
|
||||
tabbedPane.addChangeListener {
|
||||
tabbedPane.leadingComponent = if (tabbedPane.tabCount == 0) {
|
||||
Box.createHorizontalStrut(titleBar.leftInset.toInt())
|
||||
} else {
|
||||
null
|
||||
if (SystemInfo.isLinux) {
|
||||
val mouseAdapter = object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
getMouseHandler()?.mouseClicked(e)
|
||||
}
|
||||
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
getMouseHandler()?.mousePressed(e)
|
||||
}
|
||||
|
||||
override fun mouseDragged(e: MouseEvent) {
|
||||
getMouseMotionListener()?.mouseDragged(
|
||||
MouseEvent(
|
||||
e.component,
|
||||
e.id,
|
||||
e.`when`,
|
||||
e.modifiersEx,
|
||||
e.x,
|
||||
e.y,
|
||||
e.clickCount,
|
||||
e.isPopupTrigger,
|
||||
e.button
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getMouseHandler(): MouseListener? {
|
||||
return getHandler() as? MouseListener
|
||||
}
|
||||
|
||||
private fun getMouseMotionListener(): MouseMotionListener? {
|
||||
return getHandler() as? MouseMotionListener
|
||||
}
|
||||
|
||||
private fun getHandler(): Any? {
|
||||
val titlePane = getTitlePane() ?: return null
|
||||
val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null
|
||||
handlerField.isAccessible = true
|
||||
return handlerField.get(titlePane)
|
||||
}
|
||||
|
||||
private fun getTitlePane(): FlatTitlePane? {
|
||||
val ui = rootPane.ui as? FlatRootPaneUI ?: return null
|
||||
val titlePaneField = ui.javaClass.getDeclaredField("titlePane")
|
||||
titlePaneField.isAccessible = true
|
||||
return titlePaneField.get(ui) as? FlatTitlePane
|
||||
}
|
||||
}
|
||||
toolbar.getJToolBar().addMouseListener(mouseAdapter)
|
||||
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
||||
}
|
||||
|
||||
/// force hit
|
||||
if (SystemInfo.isMacOS) {
|
||||
if (JBR.isWindowDecorationsSupported()) {
|
||||
val height = UIManager.getInt("TabbedPane.tabHeight") + tabbedPane.tabAreaInsets.top
|
||||
val customTitleBar = JBR.getWindowDecorations().createCustomTitleBar()
|
||||
customTitleBar.height = height.toFloat()
|
||||
|
||||
val mouseAdapter = object : MouseAdapter() {
|
||||
|
||||
private fun hit(e: MouseEvent) {
|
||||
if (e.source == tabbedPane) {
|
||||
val index = tabbedPane.indexAtLocation(e.x, e.y)
|
||||
if (index >= 0) {
|
||||
return
|
||||
}
|
||||
}
|
||||
customTitleBar.forceHitTest(false)
|
||||
}
|
||||
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
hit(e)
|
||||
}
|
||||
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
hit(e)
|
||||
}
|
||||
|
||||
override fun mouseReleased(e: MouseEvent) {
|
||||
hit(e)
|
||||
}
|
||||
|
||||
override fun mouseEntered(e: MouseEvent) {
|
||||
hit(e)
|
||||
}
|
||||
|
||||
override fun mouseDragged(e: MouseEvent) {
|
||||
hit(e)
|
||||
}
|
||||
|
||||
override fun mouseMoved(e: MouseEvent) {
|
||||
hit(e)
|
||||
}
|
||||
}
|
||||
|
||||
terminalTabbed.addMouseListener(mouseAdapter)
|
||||
terminalTabbed.addMouseMotionListener(mouseAdapter)
|
||||
|
||||
// 监听主题变化 需要动态修改控制栏颜色
|
||||
if (SystemInfo.isWindows && isWindowDecorationsSupported) {
|
||||
ThemeManager.getInstance().addThemeChangeListener(object : ThemeChangeListener {
|
||||
override fun onChanged() {
|
||||
titleBar.putProperty("controls.dark", FlatLaf.isLafDark())
|
||||
}
|
||||
})
|
||||
}
|
||||
tabbedPane.addMouseListener(mouseAdapter)
|
||||
tabbedPane.addMouseMotionListener(mouseAdapter)
|
||||
|
||||
toolbar.getJToolBar().addMouseListener(mouseAdapter)
|
||||
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
||||
|
||||
JBR.getWindowDecorations().setCustomTitleBar(this, customTitleBar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun initView() {
|
||||
if (isWindowDecorationsSupported) {
|
||||
titleBar.height = UIManager.getInt("TabbedPane.tabHeight").toFloat()
|
||||
titleBar.putProperty("controls.dark", FlatLaf.isLafDark())
|
||||
JBR.getWindowDecorations().setCustomTitleBar(this, titleBar)
|
||||
|
||||
// macOS 要避开左边的控制栏
|
||||
if (SystemInfo.isMacOS) {
|
||||
tabbedPane.tabAreaInsets = Insets(0, 76, 0, 0)
|
||||
} else if (SystemInfo.isWindows) {
|
||||
// Windows 10 会有1像素误差
|
||||
tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0)
|
||||
} else if (SystemInfo.isLinux) {
|
||||
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
|
||||
}
|
||||
|
||||
if (SystemInfo.isLinux) {
|
||||
val height = UIManager.getInt("TabbedPane.tabHeight") + tabbedPane.tabAreaInsets.top
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, UIManager.getInt("TabbedPane.tabHeight"))
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICON, false)
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, height)
|
||||
} else if (SystemInfo.isMacOS) {
|
||||
rootPane.putClientProperty("apple.awt.windowTitleVisible", false)
|
||||
rootPane.putClientProperty("apple.awt.fullWindowContent", true)
|
||||
rootPane.putClientProperty("apple.awt.transparentTitleBar", true)
|
||||
rootPane.putClientProperty(
|
||||
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING,
|
||||
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING_MEDIUM
|
||||
)
|
||||
}
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
@@ -101,88 +205,26 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
terminalTabbed.addTerminalTab(welcomePanel)
|
||||
|
||||
// 下一次事件循环检测是否固定 SFTP
|
||||
SwingUtilities.invokeLater {
|
||||
if (sftp.pinTab) {
|
||||
terminalTabbed.addTerminalTab(SFTPTerminalTab(), false)
|
||||
SwingUtilities.invokeLater {
|
||||
terminalTabbed.addTerminalTab(SFTPTab(), false)
|
||||
}
|
||||
}
|
||||
|
||||
// macOS 要避开左边的控制栏
|
||||
if (SystemInfo.isMacOS) {
|
||||
val left = max(titleBar.leftInset.toInt(), 76)
|
||||
if (tabbedPane.tabCount == 0) {
|
||||
tabbedPane.leadingComponent = Box.createHorizontalStrut(left)
|
||||
} else {
|
||||
tabbedPane.tabAreaInsets = Insets(0, left, 0, 0)
|
||||
}
|
||||
}
|
||||
val glassPane = GlassPane()
|
||||
rootPane.glassPane = glassPane
|
||||
glassPane.isOpaque = false
|
||||
glassPane.isVisible = true
|
||||
|
||||
|
||||
Disposer.register(windowScope, terminalTabbed)
|
||||
add(terminalTabbed)
|
||||
add(terminalTabbed, BorderLayout.CENTER)
|
||||
|
||||
dataProviderSupport.addData(DataProviders.TabbedPane, tabbedPane)
|
||||
dataProviderSupport.addData(DataProviders.TermoraFrame, this)
|
||||
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
|
||||
}
|
||||
|
||||
|
||||
private fun forceHitTest() {
|
||||
val mouseAdapter = object : MouseAdapter() {
|
||||
|
||||
private fun hit(e: MouseEvent) {
|
||||
if (e.source == tabbedPane) {
|
||||
val index = tabbedPane.indexAtLocation(e.x, e.y)
|
||||
if (index >= 0) {
|
||||
return
|
||||
}
|
||||
}
|
||||
titleBar.forceHitTest(false)
|
||||
}
|
||||
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
hit(e)
|
||||
}
|
||||
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (e.source == toolbar.getJToolBar()) {
|
||||
if (!isWindowDecorationsSupported && SwingUtilities.isLeftMouseButton(e)) {
|
||||
if (JBR.isWindowMoveSupported()) {
|
||||
JBR.getWindowMove().startMovingTogetherWithMouse(this@TermoraFrame, e.button)
|
||||
}
|
||||
}
|
||||
}
|
||||
hit(e)
|
||||
}
|
||||
|
||||
override fun mouseReleased(e: MouseEvent) {
|
||||
hit(e)
|
||||
}
|
||||
|
||||
override fun mouseEntered(e: MouseEvent) {
|
||||
hit(e)
|
||||
}
|
||||
|
||||
override fun mouseDragged(e: MouseEvent) {
|
||||
|
||||
hit(e)
|
||||
}
|
||||
|
||||
override fun mouseMoved(e: MouseEvent) {
|
||||
hit(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
terminalTabbed.addMouseListener(mouseAdapter)
|
||||
terminalTabbed.addMouseMotionListener(mouseAdapter)
|
||||
|
||||
tabbedPane.addMouseListener(mouseAdapter)
|
||||
tabbedPane.addMouseMotionListener(mouseAdapter)
|
||||
|
||||
toolbar.getJToolBar().addMouseListener(mouseAdapter)
|
||||
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
||||
}
|
||||
|
||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||
return dataProviderSupport.getData(dataKey)
|
||||
?: terminalTabbed.getData(dataKey)
|
||||
@@ -203,5 +245,31 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
return id.hashCode()
|
||||
}
|
||||
|
||||
fun addNotifyListener(listener: NotifyListener) {
|
||||
notifyListeners += listener
|
||||
}
|
||||
|
||||
fun removeNotifyListener(listener: NotifyListener) {
|
||||
notifyListeners = ArrayUtils.removeElements(notifyListeners, listener)
|
||||
}
|
||||
|
||||
override fun addNotify() {
|
||||
super.addNotify()
|
||||
notifyListeners.forEach { it.addNotify() }
|
||||
}
|
||||
|
||||
|
||||
private class GlassPane : JComponent() {
|
||||
override fun paintComponent(g: Graphics) {
|
||||
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
|
||||
val g2d = g as Graphics2D
|
||||
g2d.composite = AlphaComposite.getInstance(
|
||||
AlphaComposite.SRC_OVER,
|
||||
if (FlatLaf.isLafDark()) 0.2f else 0.1f
|
||||
)
|
||||
g2d.drawImage(img, 0, 0, width, height, null)
|
||||
g2d.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,31 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.native.osx.NativeMacLibrary
|
||||
import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.sun.jna.Pointer
|
||||
import com.sun.jna.platform.win32.User32
|
||||
import com.sun.jna.platform.win32.WinDef
|
||||
import com.sun.jna.platform.win32.WinUser.*
|
||||
import de.jangassen.jfa.ThreadUtils
|
||||
import de.jangassen.jfa.foundation.Foundation
|
||||
import de.jangassen.jfa.foundation.ID
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Frame
|
||||
import java.awt.Window
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.swing.JFrame
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.UIManager
|
||||
import javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE
|
||||
import kotlin.math.max
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class TermoraFrameManager {
|
||||
|
||||
class TermoraFrameManager : Disposable {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(TermoraFrameManager::class.java)
|
||||
@@ -20,16 +37,43 @@ class TermoraFrameManager {
|
||||
}
|
||||
|
||||
private val frames = mutableListOf<TermoraFrame>()
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
private val isDisposed = AtomicBoolean(false)
|
||||
private val isBackgroundRunning get() = Database.getDatabase().appearance.backgroundRunning
|
||||
|
||||
fun createWindow(): TermoraFrame {
|
||||
val frame = TermoraFrame()
|
||||
registerCloseCallback(frame)
|
||||
val frame = TermoraFrame().apply { registerCloseCallback(this) }
|
||||
frame.title = if (SystemInfo.isLinux) null else Application.getName()
|
||||
frame.defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
||||
|
||||
val rectangle = getFrameRectangle() ?: FrameRectangle(-1, -1, 1280, 800, 0)
|
||||
if (rectangle.isMaximized) {
|
||||
frame.setSize(1280, 800)
|
||||
frame.setLocationRelativeTo(null)
|
||||
frames.add(frame)
|
||||
return frame
|
||||
frame.extendedState = rectangle.s
|
||||
} else {
|
||||
// 控制最小
|
||||
frame.setSize(
|
||||
max(rectangle.w, UIManager.getInt("Dialog.width") - 150),
|
||||
max(rectangle.h, UIManager.getInt("Dialog.height") - 100)
|
||||
)
|
||||
if (rectangle.x == -1 && rectangle.y == -1) {
|
||||
frame.setLocationRelativeTo(null)
|
||||
} else {
|
||||
frame.setLocation(max(rectangle.x, 0), max(rectangle.y, 0))
|
||||
}
|
||||
}
|
||||
|
||||
frame.addNotifyListener(object : NotifyListener {
|
||||
private val opacity get() = Database.getDatabase().appearance.opacity
|
||||
override fun addNotify() {
|
||||
val opacity = this.opacity
|
||||
if (opacity >= 1.0) return
|
||||
setOpacity(frame, opacity)
|
||||
}
|
||||
})
|
||||
|
||||
return frame.apply { frames.add(this) }
|
||||
}
|
||||
|
||||
fun getWindows(): Array<TermoraFrame> {
|
||||
@@ -38,9 +82,13 @@ class TermoraFrameManager {
|
||||
|
||||
|
||||
private fun registerCloseCallback(window: TermoraFrame) {
|
||||
val manager = this
|
||||
window.addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
|
||||
// 存储位置信息
|
||||
saveFrameRectangle(window)
|
||||
|
||||
// 删除
|
||||
frames.remove(window)
|
||||
|
||||
@@ -50,31 +98,73 @@ class TermoraFrameManager {
|
||||
Disposer.dispose(windowScope)
|
||||
|
||||
val windowScopes = ApplicationScope.windowScopes()
|
||||
if (windowScopes.isNotEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果已经没有 Window 域了,那么就可以退出程序了
|
||||
if (windowScopes.isEmpty()) {
|
||||
this@TermoraFrameManager.dispose()
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
Disposer.dispose(manager)
|
||||
} else if (SystemInfo.isMacOS) {
|
||||
// 如果 macOS 开启了后台运行,那么尽管所有窗口都没了,也不会退出
|
||||
if (isBackgroundRunning) {
|
||||
return
|
||||
}
|
||||
Disposer.dispose(manager)
|
||||
}
|
||||
}
|
||||
|
||||
override fun windowClosing(e: WindowEvent) {
|
||||
if (ApplicationScope.windowScopes().size == 1) {
|
||||
if (OptionPane.showConfirmDialog(
|
||||
if (ApplicationScope.windowScopes().size != 1) {
|
||||
window.dispose()
|
||||
return
|
||||
}
|
||||
|
||||
// 如果 Windows 开启了后台运行,那么最小化
|
||||
if (SystemInfo.isWindows && isBackgroundRunning) {
|
||||
// 最小化
|
||||
window.extendedState = window.extendedState or JFrame.ICONIFIED
|
||||
// 隐藏
|
||||
window.isVisible = false
|
||||
return
|
||||
}
|
||||
|
||||
// 如果 macOS 已经开启了后台运行,那么直接销毁,因为会有一个进程驻守
|
||||
if (SystemInfo.isMacOS && isBackgroundRunning) {
|
||||
window.dispose()
|
||||
return
|
||||
}
|
||||
|
||||
val option = OptionPane.showConfirmDialog(
|
||||
window,
|
||||
I18n.getString("termora.quit-confirm", Application.getName()),
|
||||
optionType = JOptionPane.YES_NO_OPTION,
|
||||
) == JOptionPane.YES_OPTION
|
||||
) {
|
||||
window.dispose()
|
||||
}
|
||||
} else {
|
||||
)
|
||||
if (option == JOptionPane.YES_OPTION) {
|
||||
window.dispose()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun dispose() {
|
||||
fun tick() {
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
val windows = getWindows()
|
||||
if (windows.isEmpty()) return
|
||||
for (window in windows) {
|
||||
if (window.extendedState and JFrame.ICONIFIED == JFrame.ICONIFIED) {
|
||||
window.extendedState = window.extendedState and JFrame.ICONIFIED.inv()
|
||||
}
|
||||
window.isVisible = true
|
||||
}
|
||||
windows.last().toFront()
|
||||
} else {
|
||||
SwingUtilities.invokeLater { tick() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
if (isDisposed.compareAndSet(false, true)) {
|
||||
Disposer.dispose(ApplicationScope.forApplicationScope())
|
||||
|
||||
try {
|
||||
@@ -84,7 +174,56 @@ class TermoraFrameManager {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
private fun saveFrameRectangle(frame: TermoraFrame) {
|
||||
properties.putString("TermoraFrame.x", frame.x.toString())
|
||||
properties.putString("TermoraFrame.y", frame.y.toString())
|
||||
properties.putString("TermoraFrame.width", frame.width.toString())
|
||||
properties.putString("TermoraFrame.height", frame.height.toString())
|
||||
properties.putString("TermoraFrame.extendedState", frame.extendedState.toString())
|
||||
}
|
||||
|
||||
private fun getFrameRectangle(): FrameRectangle? {
|
||||
val x = properties.getString("TermoraFrame.x")?.toIntOrNull() ?: return null
|
||||
val y = properties.getString("TermoraFrame.y")?.toIntOrNull() ?: return null
|
||||
val w = properties.getString("TermoraFrame.width")?.toIntOrNull() ?: return null
|
||||
val h = properties.getString("TermoraFrame.height")?.toIntOrNull() ?: return null
|
||||
val s = properties.getString("TermoraFrame.extendedState")?.toIntOrNull() ?: return null
|
||||
return FrameRectangle(x, y, w, h, s)
|
||||
}
|
||||
|
||||
fun setOpacity(opacity: Double) {
|
||||
if (opacity < 0 || opacity > 1 || SystemInfo.isLinux) return
|
||||
for (window in getWindows()) {
|
||||
setOpacity(window, opacity)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setOpacity(window: Window, opacity: Double) {
|
||||
if (SystemInfo.isMacOS) {
|
||||
val nsWindow = ID(NativeMacLibrary.getNSWindow(window) ?: return)
|
||||
ThreadUtils.dispatch_async {
|
||||
Foundation.invoke(nsWindow, "setOpaque:", false)
|
||||
Foundation.invoke(nsWindow, "setAlphaValue:", opacity)
|
||||
}
|
||||
} else if (SystemInfo.isWindows) {
|
||||
val alpha = ((opacity * 255).toInt() and 0xFF).toByte()
|
||||
val hwnd = WinDef.HWND(Pointer.createConstant(FlatNativeWindowsLibrary.getHWND(window)))
|
||||
val exStyle = User32.INSTANCE.GetWindowLong(hwnd, User32.GWL_EXSTYLE)
|
||||
if (exStyle and WS_EX_LAYERED == 0) {
|
||||
User32.INSTANCE.SetWindowLong(hwnd, GWL_EXSTYLE, exStyle or WS_EX_LAYERED)
|
||||
}
|
||||
User32.INSTANCE.SetLayeredWindowAttributes(hwnd, 0, alpha, LWA_ALPHA)
|
||||
}
|
||||
}
|
||||
|
||||
private data class FrameRectangle(
|
||||
val x: Int, val y: Int, val w: Int, val h: Int, val s: Int
|
||||
) {
|
||||
val isMaximized get() = (s and Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.actions.ActionManager
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.SettingsAction
|
||||
import app.termora.actions.*
|
||||
import app.termora.findeverywhere.FindEverywhereAction
|
||||
import app.termora.snippet.SnippetAction
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jetbrains.WindowDecorations
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.action.ActionContainerFactory
|
||||
import java.awt.Insets
|
||||
import java.awt.Rectangle
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import javax.swing.Box
|
||||
@@ -26,7 +24,8 @@ data class ToolBarAction(
|
||||
)
|
||||
|
||||
class TermoraToolBar(
|
||||
private val titleBar: WindowDecorations.CustomTitleBar,
|
||||
private val windowScope: WindowScope,
|
||||
private val frame: TermoraFrame,
|
||||
private val tabbedPane: FlatTabbedPane
|
||||
) {
|
||||
private val properties by lazy { Database.getDatabase().properties }
|
||||
@@ -42,12 +41,13 @@ class TermoraToolBar(
|
||||
*/
|
||||
fun getAllActions(): List<ToolBarAction> {
|
||||
return listOf(
|
||||
ToolBarAction(SnippetAction.SNIPPET, true),
|
||||
ToolBarAction(Actions.SFTP, true),
|
||||
ToolBarAction(Actions.TERMINAL_LOGGER, true),
|
||||
ToolBarAction(Actions.MACRO, true),
|
||||
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
|
||||
ToolBarAction(Actions.KEY_MANAGER, true),
|
||||
ToolBarAction(Actions.MULTIPLE, true),
|
||||
ToolBarAction(MultipleAction.MULTIPLE, true),
|
||||
ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
|
||||
ToolBarAction(SettingsAction.SETTING, true),
|
||||
)
|
||||
@@ -124,8 +124,13 @@ class TermoraToolBar(
|
||||
// 获取显示的Action,如果不是 false 那么就是显示出来
|
||||
for (action in getActions()) {
|
||||
if (action.visible) {
|
||||
actionManager.getAction(action.id)?.let {
|
||||
toolbar.add(actionContainerFactory.createButton(it))
|
||||
val ac = actionManager.getAction(action.id)
|
||||
if (ac == null) {
|
||||
if (action.id == MultipleAction.MULTIPLE) {
|
||||
toolbar.add(actionContainerFactory.createButton(MultipleAction.getInstance(windowScope)))
|
||||
}
|
||||
} else {
|
||||
toolbar.add(actionContainerFactory.createButton(ac))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,14 +155,11 @@ class TermoraToolBar(
|
||||
}
|
||||
|
||||
fun adjust() {
|
||||
if (SystemInfo.isMacOS) {
|
||||
val left = titleBar.leftInset.toInt()
|
||||
if (tabbedPane.tabAreaInsets.left != left) {
|
||||
tabbedPane.tabAreaInsets = Insets(0, left, 0, 0)
|
||||
}
|
||||
} else if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
|
||||
val right = titleBar.rightInset.toInt()
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
val rectangle =
|
||||
frame.rootPane.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
||||
as? Rectangle ?: return
|
||||
val right = rectangle.width
|
||||
val toolbar = this@MyToolBar
|
||||
for (i in 0 until toolbar.componentCount) {
|
||||
val c = toolbar.getComponent(i)
|
||||
|
||||
@@ -51,7 +51,7 @@ class OutlineTextArea : FlatTextArea() {
|
||||
}
|
||||
}
|
||||
|
||||
class OutlineComboBox<T> : JComboBox<T>() {
|
||||
class OutlineComboBox<T> : FlatComboBox<T>() {
|
||||
init {
|
||||
addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
@@ -146,7 +146,7 @@ open class EmailFormattedTextField(var maxLength: Int = Int.MAX_VALUE) : Outline
|
||||
}
|
||||
|
||||
|
||||
abstract class NumberSpinner(
|
||||
open class NumberSpinner(
|
||||
value: Int,
|
||||
minimum: Int,
|
||||
maximum: Int,
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.formdev.flatlaf.FlatLaf
|
||||
import com.formdev.flatlaf.extras.FlatAnimatedLafChange
|
||||
import com.jthemedetecor.OsThemeDetector
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
@@ -76,8 +75,7 @@ class ThemeManager private constructor() {
|
||||
|
||||
|
||||
init {
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
OsThemeDetector.getDetector().registerListener(object : Consumer<Boolean> {
|
||||
override fun accept(isDark: Boolean) {
|
||||
if (!appearance.followSystem) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package app.termora
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import javax.swing.JTree
|
||||
import javax.swing.tree.TreeModel
|
||||
import javax.swing.tree.TreeNode
|
||||
|
||||
object TreeUtils {
|
||||
/**
|
||||
@@ -31,16 +31,6 @@ object TreeUtils {
|
||||
return nodes
|
||||
}
|
||||
|
||||
fun parents(node: TreeNode): List<Any> {
|
||||
val parents = mutableListOf<Any>()
|
||||
var p = node.parent
|
||||
while (p != null) {
|
||||
parents.add(p)
|
||||
p = p.parent
|
||||
}
|
||||
return parents
|
||||
}
|
||||
|
||||
fun saveExpansionState(tree: JTree): String {
|
||||
val rows = mutableListOf<Int>()
|
||||
for (i in 0 until tree.rowCount) {
|
||||
@@ -63,15 +53,15 @@ object TreeUtils {
|
||||
}
|
||||
}
|
||||
|
||||
fun expandAll(tree: JTree) {
|
||||
var j = tree.rowCount
|
||||
var i = 0
|
||||
while (i < j) {
|
||||
tree.expandRow(i)
|
||||
i += 1
|
||||
j = tree.rowCount
|
||||
fun saveSelectionRows(tree: JTree): String {
|
||||
return tree.selectionRows?.joinToString(",") ?: StringUtils.EMPTY
|
||||
}
|
||||
|
||||
fun loadSelectionRows(tree: JTree, state: String) {
|
||||
if (state.isBlank()) return
|
||||
for (row in state.split(",").mapNotNull { it.toIntOrNull() }) {
|
||||
tree.addSelectionRow(row)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -60,7 +60,6 @@ class UpdaterManager private constructor() {
|
||||
val isSelf get() = this == self
|
||||
}
|
||||
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
var lastVersion = LatestVersion.self
|
||||
|
||||
fun fetchLatestVersion(): LatestVersion {
|
||||
@@ -146,12 +145,4 @@ class UpdaterManager private constructor() {
|
||||
return LatestVersion.self
|
||||
}
|
||||
|
||||
|
||||
fun isIgnored(version: String): Boolean {
|
||||
return properties.getString("ignored.version.$version", "false").toBoolean()
|
||||
}
|
||||
|
||||
fun ignore(version: String) {
|
||||
properties.putString("ignored.version.$version", "true")
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,10 @@ import com.formdev.flatlaf.extras.components.FlatTextField
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.event.*
|
||||
import javax.swing.*
|
||||
import javax.swing.event.DocumentEvent
|
||||
import kotlin.math.max
|
||||
@@ -32,6 +32,7 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
||||
private var fullContent = properties.getString("WelcomeFullContent", "false").toBoolean()
|
||||
private val dataProviderSupport = DataProviderSupport()
|
||||
private val hostTreeModel = hostTree.model as NewHostTreeModel
|
||||
private var lastFocused: Component? = null
|
||||
private val filterableHostTreeModel = FilterableHostTreeModel(hostTree) {
|
||||
searchTextField.text.isBlank()
|
||||
}
|
||||
@@ -44,6 +45,7 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
||||
|
||||
private fun initView() {
|
||||
putClientProperty(FlatClientProperties.TABBED_PANE_TAB_CLOSABLE, false)
|
||||
putClientProperty(FindEverywhereProvider.SKIP_FIND_EVERYWHERE, true)
|
||||
|
||||
val panel = JPanel(BorderLayout())
|
||||
panel.add(createSearchPanel(), BorderLayout.NORTH)
|
||||
@@ -215,6 +217,26 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
searchTextField.addKeyListener(object : KeyAdapter() {
|
||||
private val event = ActionEvent(hostTree, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY)
|
||||
private val openHostAction get() = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
|
||||
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
if (e.keyCode == KeyEvent.VK_DOWN || e.keyCode == KeyEvent.VK_ENTER || e.keyCode == KeyEvent.VK_UP) {
|
||||
when (e.keyCode) {
|
||||
KeyEvent.VK_UP -> hostTree.actionMap.get("selectPrevious")?.actionPerformed(event)
|
||||
KeyEvent.VK_DOWN -> hostTree.actionMap.get("selectNext")?.actionPerformed(event)
|
||||
else -> {
|
||||
for (node in hostTree.getSelectionSimpleTreeNodes(true)) {
|
||||
openHostAction?.actionPerformed(OpenHostActionEvent(hostTree, node.host, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
e.consume()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun perform() {
|
||||
@@ -258,6 +280,14 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onLostFocus() {
|
||||
lastFocused = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||
}
|
||||
|
||||
override fun onGrabFocus() {
|
||||
SwingUtilities.invokeLater { lastFocused?.requestFocusInWindow() }
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
properties.putString("WelcomeFullContent", fullContent.toString())
|
||||
properties.putString("Welcome.HostTree.state", TreeUtils.saveExpansionState(hostTree))
|
||||
|
||||
@@ -6,8 +6,9 @@ import app.termora.findeverywhere.FindEverywhereAction
|
||||
import app.termora.highlight.KeywordHighlightAction
|
||||
import app.termora.keymgr.KeyManagerAction
|
||||
import app.termora.macro.MacroAction
|
||||
import app.termora.sftp.SFTPAction
|
||||
import app.termora.snippet.SnippetAction
|
||||
import app.termora.tlog.TerminalLoggerAction
|
||||
import app.termora.transport.SFTPAction
|
||||
import javax.swing.Action
|
||||
|
||||
class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
||||
@@ -28,12 +29,12 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
||||
addAction(NewWindowAction.NEW_WINDOW, NewWindowAction())
|
||||
addAction(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
|
||||
|
||||
addAction(Actions.MULTIPLE, MultipleAction())
|
||||
addAction(Actions.APP_UPDATE, AppUpdateAction.getInstance())
|
||||
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())
|
||||
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
|
||||
addAction(Actions.SFTP, SFTPAction())
|
||||
addAction(SFTPCommandAction.SFTP_COMMAND, SFTPCommandAction())
|
||||
addAction(SnippetAction.SNIPPET, SnippetAction.getInstance())
|
||||
addAction(Actions.MACRO, MacroAction())
|
||||
addAction(Actions.KEY_MANAGER, KeyManagerAction())
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ class AppUpdateAction private constructor() : AnAction(
|
||||
StringUtils.EMPTY,
|
||||
Icons.ideUpdate
|
||||
) {
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Swing)
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(AppUpdateAction::class.java)
|
||||
@@ -47,6 +48,7 @@ class AppUpdateAction private constructor() : AnAction(
|
||||
}
|
||||
|
||||
private val updaterManager get() = UpdaterManager.getInstance()
|
||||
private var isRemindMeNextTime = false
|
||||
|
||||
init {
|
||||
isEnabled = false
|
||||
@@ -58,18 +60,22 @@ class AppUpdateAction private constructor() : AnAction(
|
||||
}
|
||||
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun scheduleUpdate() {
|
||||
fixedRateTimer(
|
||||
name = "check-update-timer",
|
||||
initialDelay = 3.minutes.inWholeMilliseconds,
|
||||
period = 5.hours.inWholeMilliseconds, daemon = true
|
||||
) {
|
||||
GlobalScope.launch(Dispatchers.IO) { supervisorScope { launch { checkUpdate() } } }
|
||||
if (!isRemindMeNextTime) {
|
||||
coroutineScope.launch(Dispatchers.IO) { checkUpdate() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkUpdate() {
|
||||
if (Application.isUnknownVersion()) {
|
||||
return
|
||||
}
|
||||
|
||||
val latestVersion = updaterManager.fetchLatestVersion()
|
||||
if (latestVersion.isSelf) {
|
||||
@@ -82,10 +88,6 @@ class AppUpdateAction private constructor() : AnAction(
|
||||
return
|
||||
}
|
||||
|
||||
if (updaterManager.isIgnored(latestVersion.version)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
downloadLatestPkg(latestVersion)
|
||||
} catch (e: Exception) {
|
||||
@@ -194,7 +196,7 @@ class AppUpdateAction private constructor() : AnAction(
|
||||
return
|
||||
} else if (option == JOptionPane.NO_OPTION) {
|
||||
isEnabled = false
|
||||
updaterManager.ignore(lastVersion.version)
|
||||
isRemindMeNextTime = true
|
||||
} else if (option == JOptionPane.YES_OPTION) {
|
||||
updateSelf(lastVersion)
|
||||
}
|
||||
@@ -221,7 +223,10 @@ class AppUpdateAction private constructor() : AnAction(
|
||||
// 没有安装过 则打开安装向导
|
||||
else listOf(file.absolutePath)
|
||||
|
||||
println(commands)
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("restart {}", commands.joinToString(StringUtils.SPACE))
|
||||
}
|
||||
|
||||
TermoraRestarter.getInstance().scheduleRestart(owner, commands)
|
||||
|
||||
}
|
||||
|
||||
@@ -17,5 +17,5 @@ interface DataProvider {
|
||||
/**
|
||||
* 数据提供
|
||||
*/
|
||||
fun <T : Any> getData(dataKey: DataKey<T>): T?
|
||||
fun <T : Any> getData(dataKey: DataKey<T>): T? = null
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import app.termora.terminal.DataKey
|
||||
object DataProviders {
|
||||
val TerminalPanel = DataKey(app.termora.terminal.panel.TerminalPanel::class)
|
||||
val Terminal = DataKey(app.termora.terminal.Terminal::class)
|
||||
val PtyConnector = DataKey(app.termora.terminal.PtyConnector::class)
|
||||
val TerminalWriter get() = DataKey.TerminalWriter
|
||||
|
||||
val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
|
||||
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.TerminalPanelFactory
|
||||
import app.termora.WindowScope
|
||||
|
||||
class MultipleAction : AnAction(
|
||||
class MultipleAction private constructor() : AnAction(
|
||||
I18n.getString("termora.tools.multiple"),
|
||||
Icons.vcs
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* 将命令发送到多个会话
|
||||
*/
|
||||
const val MULTIPLE = "MultipleAction"
|
||||
|
||||
fun getInstance(windowScope: WindowScope): MultipleAction {
|
||||
return windowScope.getOrCreate(MultipleAction::class) { MultipleAction() }
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
setStateAction()
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
ApplicationScope.windowScopes().map { TerminalPanelFactory.getInstance(it) }
|
||||
.forEach { it.repaintAll() }
|
||||
TerminalPanelFactory.getInstance().repaintAll()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,19 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.*
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import javax.swing.JOptionPane
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class OpenHostAction : AnAction() {
|
||||
companion object {
|
||||
@@ -26,10 +39,70 @@ class OpenHostAction : AnAction() {
|
||||
Protocol.SSH -> SSHTerminalTab(windowScope, evt.host)
|
||||
Protocol.Serial -> SerialTerminalTab(windowScope, evt.host)
|
||||
Protocol.SFTPPty -> SFTPPtyTerminalTab(windowScope, evt.host)
|
||||
Protocol.RDP -> openRDP(windowScope, evt.host)
|
||||
else -> LocalTerminalTab(windowScope, evt.host)
|
||||
}
|
||||
|
||||
if (tab is TerminalTab) {
|
||||
terminalTabbedManager.addTerminalTab(tab)
|
||||
if (tab is PtyHostTerminalTab) {
|
||||
tab.start()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun openRDP(windowScope: WindowScope, host: Host) {
|
||||
if (SystemInfo.isLinux) {
|
||||
OptionPane.showMessageDialog(
|
||||
windowScope.window,
|
||||
"Linux cannot connect to Windows Remote Server, Supported only for macOS and Windows",
|
||||
messageType = JOptionPane.WARNING_MESSAGE
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (SystemInfo.isMacOS) {
|
||||
if (!FileUtils.getFile("/Applications/Windows App.app").exists()) {
|
||||
val option = OptionPane.showConfirmDialog(
|
||||
windowScope.window,
|
||||
"If you want to connect to a Windows Remote Server, You have to install the Windows App",
|
||||
optionType = JOptionPane.OK_CANCEL_OPTION
|
||||
)
|
||||
if (option == JOptionPane.OK_OPTION) {
|
||||
Application.browse(URI.create("https://apps.apple.com/app/windows-app/id1295203466"))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val sb = StringBuilder()
|
||||
sb.append("full address:s:").append(host.host).append(':').append(host.port).appendLine()
|
||||
sb.append("username:s:").append(host.username).appendLine()
|
||||
|
||||
val file = FileUtils.getFile(Application.getTemporaryDir(), UUID.randomUUID().toSimpleString() + ".rdp")
|
||||
file.outputStream().use { IOUtils.write(sb.toString(), it, Charsets.UTF_8) }
|
||||
|
||||
if (host.authentication.type == AuthenticationType.Password) {
|
||||
val systemClipboard = windowScope.window.toolkit.systemClipboard
|
||||
val password = host.authentication.password
|
||||
systemClipboard.setContents(StringSelection(password), null)
|
||||
// clear password
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
delay(30.seconds)
|
||||
if (systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||
if (systemClipboard.getData(DataFlavor.stringFlavor) == password) {
|
||||
systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (SystemInfo.isMacOS) {
|
||||
ProcessBuilder("open", file.absolutePath).start()
|
||||
} else if (SystemInfo.isWindows) {
|
||||
ProcessBuilder("mstsc", file.absolutePath).start()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ class OpenLocalTerminalAction : AnAction(
|
||||
OpenHostActionEvent(
|
||||
evt.source,
|
||||
Host(
|
||||
id = "local",
|
||||
name = name,
|
||||
protocol = Protocol.Local
|
||||
),
|
||||
|
||||
@@ -32,7 +32,6 @@ class TerminalCopyAction : AnAction() {
|
||||
}
|
||||
|
||||
systemClipboard.setContents(StringSelection(text), null)
|
||||
terminalPanel.toast(I18n.getString("termora.terminal.copied"))
|
||||
if (log.isTraceEnabled) {
|
||||
log.trace("Copy to clipboard. {}", text)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.Database
|
||||
import app.termora.TerminalPanelFactory
|
||||
|
||||
@@ -13,10 +12,8 @@ abstract class TerminalZoomAction : AnAction() {
|
||||
evt.getData(DataProviders.TerminalPanel) ?: return
|
||||
|
||||
if (zoom()) {
|
||||
ApplicationScope.windowScopes().forEach {
|
||||
TerminalPanelFactory.getInstance(it)
|
||||
TerminalPanelFactory.getInstance()
|
||||
.fireResize()
|
||||
}
|
||||
evt.consume()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.jdesktop.swingx.action.AbstractActionExt
|
||||
import java.awt.event.ActionEvent
|
||||
import javax.swing.Action
|
||||
import javax.swing.Icon
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
open class ActionFindEverywhereResult(private val action: Action) : FindEverywhereResult {
|
||||
private val isState: Boolean
|
||||
@@ -26,7 +27,7 @@ open class ActionFindEverywhereResult(private val action: Action) : FindEverywhe
|
||||
if (isState) {
|
||||
action.putValue(Action.SELECTED_KEY, !isSelected)
|
||||
}
|
||||
action.actionPerformed(e)
|
||||
SwingUtilities.invokeLater { action.actionPerformed(e) }
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
|
||||
@@ -3,12 +3,11 @@ package app.termora.findeverywhere
|
||||
import app.termora.DialogWrapper
|
||||
import app.termora.DynamicColor
|
||||
import app.termora.I18n
|
||||
import app.termora.WindowScope
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.macro.MacroFindEverywhereProvider
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatTextField
|
||||
import com.jetbrains.JBR
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Insets
|
||||
@@ -18,7 +17,7 @@ import javax.swing.*
|
||||
import javax.swing.event.DocumentEvent
|
||||
import javax.swing.event.DocumentListener
|
||||
|
||||
class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
||||
class FindEverywhere(owner: Window, windowScope: WindowScope) : DialogWrapper(owner) {
|
||||
private val searchTextField = FlatTextField()
|
||||
private val model = DefaultListModel<FindEverywhereResult>()
|
||||
private val resultList = FindEverywhereXList(model)
|
||||
@@ -26,7 +25,7 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
||||
private val providers = mutableListOf<FindEverywhereProvider>(
|
||||
BasicFilterFindEverywhereProvider(QuickCommandFindEverywhereProvider()),
|
||||
BasicFilterFindEverywhereProvider(SettingsFindEverywhereProvider()),
|
||||
BasicFilterFindEverywhereProvider(QuickActionsFindEverywhereProvider()),
|
||||
BasicFilterFindEverywhereProvider(QuickActionsFindEverywhereProvider(windowScope)),
|
||||
BasicFilterFindEverywhereProvider(MacroFindEverywhereProvider()),
|
||||
)
|
||||
|
||||
@@ -44,17 +43,10 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
||||
minimumSize = Dimension(size.width / 2, size.height / 2)
|
||||
isModal = false
|
||||
lostFocusDispose = true
|
||||
controlsVisible = false
|
||||
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
|
||||
setLocationRelativeTo(null)
|
||||
|
||||
// 不支持装饰,铺满
|
||||
if (!JBR.isWindowDecorationsSupported()) {
|
||||
rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_CLOSE, false)
|
||||
}
|
||||
|
||||
rootPane.background = DynamicColor("desktop")
|
||||
val desktopBackground = DynamicColor("desktop")
|
||||
centerPanel.background = DynamicColor("desktop")
|
||||
centerPanel.border = BorderFactory.createEmptyBorder(12, 12, 12, 12)
|
||||
|
||||
@@ -69,7 +61,7 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
||||
resultList.isRolloverEnabled = false
|
||||
resultList.selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
resultList.border = BorderFactory.createEmptyBorder(5, 0, 0, 0)
|
||||
resultList.background = rootPane.background
|
||||
resultList.background = desktopBackground
|
||||
|
||||
|
||||
val scrollPane = JScrollPane(resultList)
|
||||
@@ -225,5 +217,11 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
||||
super.setVisible(visible)
|
||||
}
|
||||
|
||||
override fun addNotify() {
|
||||
super.addNotify()
|
||||
|
||||
controlsVisible = false
|
||||
fullWindowContent = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class FindEverywhereAction : AnAction(StringUtils.EMPTY, Icons.find) {
|
||||
return
|
||||
}
|
||||
|
||||
val dialog = FindEverywhere(owner)
|
||||
val dialog = FindEverywhere(owner, scope)
|
||||
for (provider in FindEverywhereProvider.getFindEverywhereProviders(scope)) {
|
||||
dialog.registerProvider(provider)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import app.termora.Scope
|
||||
interface FindEverywhereProvider {
|
||||
|
||||
companion object {
|
||||
|
||||
const val SKIP_FIND_EVERYWHERE = "SKIP_FIND_EVERYWHERE"
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun getFindEverywhereProviders(scope: Scope): MutableList<FindEverywhereProvider> {
|
||||
var list = scope.getAnyOrNull("FindEverywhereProviders")
|
||||
|
||||
@@ -2,21 +2,32 @@ package app.termora.findeverywhere
|
||||
|
||||
import app.termora.Actions
|
||||
import app.termora.I18n
|
||||
import app.termora.WindowScope
|
||||
import app.termora.actions.MultipleAction
|
||||
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
|
||||
class QuickActionsFindEverywhereProvider : FindEverywhereProvider {
|
||||
class QuickActionsFindEverywhereProvider(private val windowScope: WindowScope) : FindEverywhereProvider {
|
||||
private val actions = listOf(
|
||||
Actions.KEY_MANAGER,
|
||||
Actions.KEYWORD_HIGHLIGHT,
|
||||
Actions.MULTIPLE,
|
||||
MultipleAction.MULTIPLE,
|
||||
)
|
||||
|
||||
override fun find(pattern: String): List<FindEverywhereResult> {
|
||||
val actionManager = ActionManager.getInstance()
|
||||
return actions
|
||||
.mapNotNull { actionManager.getAction(it) }
|
||||
.map { ActionFindEverywhereResult(it) }
|
||||
val results = ArrayList<FindEverywhereResult>()
|
||||
for (action in actions) {
|
||||
val ac = actionManager.getAction(action)
|
||||
if (ac == null) {
|
||||
if (action == MultipleAction.MULTIPLE) {
|
||||
results.add(ActionFindEverywhereResult(MultipleAction.getInstance(windowScope)))
|
||||
}
|
||||
} else {
|
||||
results.add(ActionFindEverywhereResult(ac))
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.actions.NewHostAction
|
||||
import app.termora.actions.OpenLocalTerminalAction
|
||||
import app.termora.snippet.SnippetAction
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import javax.swing.Icon
|
||||
@@ -21,6 +22,11 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
|
||||
list.add(ActionFindEverywhereResult(it))
|
||||
}
|
||||
|
||||
// Snippet
|
||||
actionManager.getAction(SnippetAction.SNIPPET)?.let {
|
||||
list.add(ActionFindEverywhereResult(it))
|
||||
}
|
||||
|
||||
// SFTP
|
||||
actionManager.getAction(Actions.SFTP)?.let {
|
||||
list.add(ActionFindEverywhereResult(it))
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package app.termora.highlight
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.DialogWrapper
|
||||
import app.termora.TerminalFactory
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
@@ -31,7 +30,7 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val panel = JPanel(GridLayout(2, 8, 4, 4))
|
||||
val colorPalette = TerminalFactory.getInstance(ApplicationScope.forWindowScope(this))
|
||||
val colorPalette = TerminalFactory.getInstance()
|
||||
.createTerminal().getTerminalModel().getColorPalette()
|
||||
for (i in 1..16) {
|
||||
val c = JPanel()
|
||||
@@ -52,6 +51,7 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
|
||||
val customBtn = JButton("Custom")
|
||||
customBtn.addActionListener {
|
||||
val dialog = MyColorPickerDialog(this)
|
||||
dialog.setLocationRelativeTo(this)
|
||||
dialog.colorPicker.color = defaultColor
|
||||
dialog.isVisible = true
|
||||
val color = dialog.color
|
||||
|
||||
@@ -24,6 +24,11 @@ data class KeywordHighlight(
|
||||
*/
|
||||
val matchCase: Boolean = false,
|
||||
|
||||
/**
|
||||
* 是否是正则表达式
|
||||
*/
|
||||
val regex: Boolean = false,
|
||||
|
||||
/**
|
||||
* 0 是取前景色
|
||||
*/
|
||||
@@ -62,5 +67,10 @@ data class KeywordHighlight(
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
val sort: Long = System.currentTimeMillis()
|
||||
val sort: Long = System.currentTimeMillis(),
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
val updateDate: Long = System.currentTimeMillis(),
|
||||
)
|
||||
@@ -20,10 +20,8 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
||||
private val model = KeywordHighlightTableModel()
|
||||
private val table = FlatTable()
|
||||
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
|
||||
private val colorPalette by lazy {
|
||||
TerminalFactory.getInstance(ApplicationScope.forWindowScope(this)).createTerminal().getTerminalModel()
|
||||
.getColorPalette()
|
||||
}
|
||||
private val terminal by lazy { TerminalFactory.getInstance().createTerminal() }
|
||||
private val colorPalette by lazy { terminal.getTerminalModel().getColorPalette() }
|
||||
|
||||
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
|
||||
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
|
||||
@@ -130,6 +128,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
||||
|
||||
addBtn.addActionListener {
|
||||
val dialog = NewKeywordHighlightDialog(this, colorPalette)
|
||||
dialog.setLocationRelativeTo(this)
|
||||
dialog.isVisible = true
|
||||
val keywordHighlight = dialog.keywordHighlight
|
||||
if (keywordHighlight != null) {
|
||||
@@ -143,6 +142,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
||||
if (row > -1) {
|
||||
var keywordHighlight = model.getKeywordHighlight(row)
|
||||
val dialog = NewKeywordHighlightDialog(this, colorPalette)
|
||||
dialog.setLocationRelativeTo(this)
|
||||
dialog.keywordTextField.text = keywordHighlight.keyword
|
||||
dialog.descriptionTextField.text = keywordHighlight.description
|
||||
|
||||
@@ -176,6 +176,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
||||
dialog.underlineCheckBox.isSelected = keywordHighlight.underline
|
||||
dialog.lineThroughCheckBox.isSelected = keywordHighlight.lineThrough
|
||||
dialog.matchCaseBtn.isSelected = keywordHighlight.matchCase
|
||||
dialog.regexBtn.isSelected = keywordHighlight.regex
|
||||
|
||||
dialog.isVisible = true
|
||||
|
||||
@@ -201,7 +202,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
||||
for (row in rows) {
|
||||
val id = model.getKeywordHighlight(row).id
|
||||
keywordHighlightManager.removeKeywordHighlight(id)
|
||||
model.removeRow(row)
|
||||
model.fireTableRowsDeleted(row, row)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,6 +212,12 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
||||
editBtn.isEnabled = table.selectedRowCount > 0
|
||||
deleteBtn.isEnabled = editBtn.isEnabled
|
||||
}
|
||||
|
||||
Disposer.register(disposable, object : Disposable {
|
||||
override fun dispose() {
|
||||
terminal.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package app.termora.highlight
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.TerminalPanelFactory
|
||||
import app.termora.Database
|
||||
import app.termora.DeleteDataManager
|
||||
import app.termora.TerminalPanelFactory
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
class KeywordHighlightManager private constructor() {
|
||||
@@ -27,7 +28,7 @@ class KeywordHighlightManager private constructor() {
|
||||
fun addKeywordHighlight(keywordHighlight: KeywordHighlight) {
|
||||
database.addKeywordHighlight(keywordHighlight)
|
||||
keywordHighlights[keywordHighlight.id] = keywordHighlight
|
||||
ApplicationScope.windowScopes().forEach { TerminalPanelFactory.getInstance(it).repaintAll() }
|
||||
TerminalPanelFactory.getInstance().repaintAll()
|
||||
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Keyword highlighter added. {}", keywordHighlight)
|
||||
@@ -37,7 +38,8 @@ class KeywordHighlightManager private constructor() {
|
||||
fun removeKeywordHighlight(id: String) {
|
||||
database.removeKeywordHighlight(id)
|
||||
keywordHighlights.remove(id)
|
||||
ApplicationScope.windowScopes().forEach { TerminalPanelFactory.getInstance(it).repaintAll() }
|
||||
TerminalPanelFactory.getInstance().repaintAll()
|
||||
DeleteDataManager.getInstance().removeKeywordHighlight(id)
|
||||
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Keyword highlighter removed. {}", id)
|
||||
|
||||
@@ -5,6 +5,7 @@ import app.termora.terminal.*
|
||||
import app.termora.terminal.panel.TerminalDisplay
|
||||
import app.termora.terminal.panel.TerminalPaintListener
|
||||
import app.termora.terminal.panel.TerminalPanel
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Graphics
|
||||
import kotlin.math.min
|
||||
import kotlin.random.Random
|
||||
@@ -18,9 +19,10 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
|
||||
}
|
||||
|
||||
private val tag = Random.nextInt()
|
||||
private val log = LoggerFactory.getLogger(KeywordHighlightPaintListener::class.java)
|
||||
}
|
||||
|
||||
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
|
||||
private val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
||||
|
||||
override fun before(
|
||||
offset: Int,
|
||||
@@ -36,7 +38,8 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
|
||||
}
|
||||
|
||||
val document = terminal.getDocument()
|
||||
val kinds = SubstrFinder(object : Iterator<TerminalLine> {
|
||||
val kinds = mutableListOf<FindKind>()
|
||||
val iterator = object : Iterator<TerminalLine> {
|
||||
private var index = offset + 1
|
||||
private val maxCount = min(index + count, document.getLineCount())
|
||||
override fun hasNext(): Boolean {
|
||||
@@ -46,8 +49,24 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
|
||||
override fun next(): TerminalLine {
|
||||
return document.getLine(index++)
|
||||
}
|
||||
}
|
||||
|
||||
}, CharArraySubstr(highlight.keyword.toCharArray())).find(!highlight.matchCase)
|
||||
if (highlight.regex) {
|
||||
try {
|
||||
val regex = if (highlight.matchCase)
|
||||
highlight.keyword.toRegex()
|
||||
else highlight.keyword.toRegex(RegexOption.IGNORE_CASE)
|
||||
RegexFinder(regex, iterator).find()
|
||||
.apply { kinds.addAll(this) }
|
||||
} catch (e: Exception) {
|
||||
if (log.isDebugEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SubstrFinder(iterator, CharArraySubstr(highlight.keyword.toCharArray())).find(!highlight.matchCase)
|
||||
.apply { kinds.addAll(this) }
|
||||
}
|
||||
|
||||
for (kind in kinds) {
|
||||
terminal.getMarkupModel().addHighlighter(
|
||||
@@ -77,6 +96,74 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
|
||||
terminal.getMarkupModel().removeAllHighlighters(tag)
|
||||
}
|
||||
|
||||
private class RegexFinder(
|
||||
private val regex: Regex,
|
||||
private val iterator: Iterator<TerminalLine>
|
||||
) {
|
||||
private data class Coords(val row: Int, val col: Int)
|
||||
private data class MatchResultWithCoords(
|
||||
val match: String,
|
||||
val coords: List<Coords>
|
||||
)
|
||||
|
||||
fun find(): List<FindKind> {
|
||||
|
||||
val lines = mutableListOf<TerminalLine>()
|
||||
val kinds = mutableListOf<FindKind>()
|
||||
|
||||
for ((index, line) in iterator.withIndex()) {
|
||||
|
||||
lines.add(line)
|
||||
if (line.wrapped) continue
|
||||
|
||||
val data = mutableListOf<MutableList<Char>>()
|
||||
for (e in lines) {
|
||||
data.add(mutableListOf())
|
||||
for (c in e.chars()) {
|
||||
if (c.first.isNull) break
|
||||
data.last().add(c.first)
|
||||
}
|
||||
}
|
||||
|
||||
lines.clear()
|
||||
|
||||
val resultWithCoords = findMatchesWithCoords(data)
|
||||
if (resultWithCoords.isEmpty()) continue
|
||||
val offset = index - data.size + 1
|
||||
|
||||
for (e in resultWithCoords) {
|
||||
val coords = e.coords
|
||||
if (coords.isEmpty()) continue
|
||||
kinds.add(
|
||||
FindKind(
|
||||
startPosition = Position(coords.first().row + offset + 1, coords.first().col + 1),
|
||||
endPosition = Position(coords.last().row + offset + 1, coords.last().col + 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return kinds
|
||||
}
|
||||
|
||||
private fun findMatchesWithCoords(data: List<List<Char>>): List<MatchResultWithCoords> {
|
||||
val flatChars = StringBuilder()
|
||||
val indexMap = mutableListOf<Coords>()
|
||||
|
||||
// 拉平成字符串,并记录每个字符的位置
|
||||
for ((rowIndex, row) in data.withIndex()) {
|
||||
for ((colIndex, char) in row.withIndex()) {
|
||||
flatChars.append(char)
|
||||
indexMap.add(Coords(rowIndex, colIndex))
|
||||
}
|
||||
}
|
||||
|
||||
return regex.findAll(flatChars.toString())
|
||||
.map { MatchResultWithCoords(it.value, indexMap.subList(it.range.first, it.range.last + 1)) }
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class KeywordHighlightHighlighter(
|
||||
range: HighlighterRange, terminal: Terminal,
|
||||
@@ -94,3 +181,5 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package app.termora.highlight
|
||||
|
||||
import app.termora.DialogWrapper
|
||||
import app.termora.DynamicColor
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.Database
|
||||
import app.termora.*
|
||||
import app.termora.terminal.ColorPalette
|
||||
import app.termora.terminal.TerminalColor
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
@@ -46,6 +42,7 @@ class NewKeywordHighlightDialog(
|
||||
I18n.getString("termora.highlight.background-color")
|
||||
)
|
||||
val matchCaseBtn = JToggleButton(Icons.matchCase)
|
||||
val regexBtn = JToggleButton(Icons.regex)
|
||||
|
||||
|
||||
private val textColorRevert = JButton(Icons.revert)
|
||||
@@ -85,6 +82,7 @@ class NewKeywordHighlightDialog(
|
||||
|
||||
val box = FlatToolBar()
|
||||
box.add(matchCaseBtn)
|
||||
box.add(regexBtn)
|
||||
keywordTextField.trailingComponent = box
|
||||
|
||||
repaintKeywordHighlightView()
|
||||
@@ -187,6 +185,7 @@ class NewKeywordHighlightDialog(
|
||||
}
|
||||
|
||||
private fun createColorPanel(color: Color, title: String): ColorPanel {
|
||||
val owner = this
|
||||
val arc = UIManager.getInt("Component.arc")
|
||||
val lineBorder = FlatLineBorder(Insets(1, 1, 1, 1), DynamicColor.BorderColor, 1f, arc)
|
||||
val colorPanel = ColorPanel(color)
|
||||
@@ -195,7 +194,8 @@ class NewKeywordHighlightDialog(
|
||||
colorPanel.addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (SwingUtilities.isLeftMouseButton(e)) {
|
||||
val dialog = ChooseColorTemplateDialog(this@NewKeywordHighlightDialog, title)
|
||||
val dialog = ChooseColorTemplateDialog(owner, title)
|
||||
dialog.setLocationRelativeTo(owner)
|
||||
dialog.defaultColor = colorPanel.color
|
||||
dialog.isVisible = true
|
||||
colorPanel.color = dialog.color ?: return
|
||||
@@ -218,6 +218,7 @@ class NewKeywordHighlightDialog(
|
||||
keyword = keywordTextField.text,
|
||||
description = descriptionTextField.text,
|
||||
matchCase = matchCaseBtn.isSelected,
|
||||
regex = regexBtn.isSelected,
|
||||
textColor = if (textColor.colorIndex != -1) textColor.colorIndex else textColor.color.toRGB(),
|
||||
backgroundColor = if (backgroundColor.colorIndex != -1) backgroundColor.colorIndex else backgroundColor.color.toRGB(),
|
||||
bold = boldCheckBox.isSelected,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora.keymap
|
||||
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.KeyStroke
|
||||
@@ -23,7 +24,14 @@ class KeyShortcut(val keyStroke: KeyStroke) : Shortcut() {
|
||||
text = text.replace("MINUS", "-")
|
||||
}
|
||||
|
||||
return text.toCharArray().joinToString(" + ")
|
||||
text = text.toCharArray().joinToString(" + ")
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
text = text.replace("⇧", "Shift")
|
||||
text = text.replace("⌃", "Ctrl")
|
||||
text = text.replace("⌥", "Alt")
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package app.termora.keymap
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.*
|
||||
import javax.swing.KeyStroke
|
||||
|
||||
@@ -12,6 +11,10 @@ open class Keymap(
|
||||
*/
|
||||
private val parent: Keymap?,
|
||||
val isReadonly: Boolean = false,
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
var updateDate: Long = 0L,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -23,7 +26,8 @@ open class Keymap(
|
||||
val shortcuts = mutableListOf<Keymap>()
|
||||
val name = json["name"]?.jsonPrimitive?.content ?: return null
|
||||
val readonly = json["readonly"]?.jsonPrimitive?.booleanOrNull ?: return null
|
||||
val keymap = Keymap(name, null, readonly)
|
||||
val updateDate = json["updateDate"]?.jsonPrimitive?.longOrNull ?: 0
|
||||
val keymap = Keymap(name, null, readonly, updateDate)
|
||||
|
||||
for (shortcut in (json["shortcuts"]?.jsonArray ?: emptyList()).map { it.jsonObject }) {
|
||||
val keyStroke = shortcut["keyStroke"]?.jsonPrimitive?.contentOrNull ?: continue
|
||||
@@ -40,6 +44,9 @@ open class Keymap(
|
||||
}
|
||||
}
|
||||
|
||||
// 最后设置修改时间
|
||||
keymap.updateDate = updateDate
|
||||
|
||||
shortcuts.add(keymap)
|
||||
return keymap
|
||||
}
|
||||
@@ -51,6 +58,7 @@ open class Keymap(
|
||||
val actionIds = shortcuts.getOrPut(shortcut) { mutableListOf() }
|
||||
actionIds.removeIf { it == actionId }
|
||||
actionIds.add(actionId)
|
||||
updateDate = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
open fun removeAllActionShortcuts(actionId: Any) {
|
||||
@@ -62,6 +70,7 @@ open class Keymap(
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
updateDate = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
open fun getShortcut(actionId: Any): List<Shortcut> {
|
||||
@@ -102,6 +111,7 @@ open class Keymap(
|
||||
return buildJsonObject {
|
||||
put("name", name)
|
||||
put("readonly", isReadonly)
|
||||
put("updateDate", updateDate)
|
||||
parent?.let { put("parent", it.name) }
|
||||
put("shortcuts", buildJsonArray {
|
||||
for (entry in shortcuts.entries) {
|
||||
|
||||
@@ -89,6 +89,7 @@ class KeymapManager private constructor() : Disposable {
|
||||
fun removeKeymap(name: String) {
|
||||
keymaps.remove(name)
|
||||
database.removeKeymap(name)
|
||||
DeleteDataManager.getInstance().removeKeymap(name)
|
||||
}
|
||||
|
||||
private inner class KeymapKeyEventDispatcher : KeyEventDispatcher {
|
||||
|
||||
@@ -119,10 +119,11 @@ class KeymapPanel : JPanel(BorderLayout()) {
|
||||
val keymap = getCurrentKeymap()
|
||||
val index = keymapComboBox.selectedIndex
|
||||
if (keymap != null && !keymap.isReadonly && index >= 0) {
|
||||
val text = InputDialog(
|
||||
val text = OptionPane.showInputDialog(
|
||||
SwingUtilities.getWindowAncestor(this@KeymapPanel),
|
||||
title = renameBtn.toolTipText, text = keymap.name
|
||||
).getText()
|
||||
title = renameBtn.toolTipText,
|
||||
value = keymap.name
|
||||
)
|
||||
if (!text.isNullOrBlank()) {
|
||||
if (text != keymap.name) {
|
||||
keymapManager.removeKeymap(keymap.name)
|
||||
|
||||
@@ -2,6 +2,7 @@ package app.termora.keymgr
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.Database
|
||||
import app.termora.DeleteDataManager
|
||||
|
||||
class KeyManager private constructor() {
|
||||
companion object {
|
||||
@@ -29,6 +30,7 @@ class KeyManager private constructor() {
|
||||
fun removeOhKeyPair(id: String) {
|
||||
keyPairs.removeIf { it.id == id }
|
||||
database.removeKeyPair(id)
|
||||
DeleteDataManager.getInstance().removeKeyPair(id)
|
||||
}
|
||||
|
||||
fun getOhKeyPairs(): List<OhKeyPair> {
|
||||
@@ -39,9 +41,4 @@ class KeyManager private constructor() {
|
||||
return keyPairs.findLast { it.id == id }
|
||||
}
|
||||
|
||||
fun removeAll() {
|
||||
keyPairs.clear()
|
||||
database.removeAllKeyPair()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
package app.termora.keymgr
|
||||
|
||||
import app.termora.*
|
||||
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 com.formdev.flatlaf.extras.components.FlatComboBox
|
||||
import com.formdev.flatlaf.extras.components.FlatTable
|
||||
@@ -14,7 +12,6 @@ import com.formdev.flatlaf.ui.FlatTextBorder
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import org.apache.commons.codec.binary.Base64
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.io.file.PathUtils
|
||||
@@ -34,7 +31,6 @@ import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Files
|
||||
import java.security.KeyPair
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
import java.util.*
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
@@ -104,7 +100,9 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
||||
|
||||
private fun initEvents() {
|
||||
generateBtn.addActionListener {
|
||||
val dialog = GenerateKeyDialog(SwingUtilities.getWindowAncestor(this))
|
||||
val owner = SwingUtilities.getWindowAncestor(this)
|
||||
val dialog = GenerateKeyDialog(owner)
|
||||
dialog.setLocationRelativeTo(owner)
|
||||
dialog.isVisible = true
|
||||
if (dialog.ohKeyPair != OhKeyPair.empty) {
|
||||
val keyPair = dialog.ohKeyPair
|
||||
@@ -143,12 +141,14 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
||||
editBtn.addActionListener {
|
||||
val row = keyPairTable.selectedRow
|
||||
if (row >= 0) {
|
||||
val owner = SwingUtilities.getWindowAncestor(this)
|
||||
var ohKeyPair = keyPairTableModel.getOhKeyPair(row)
|
||||
val dialog = GenerateKeyDialog(
|
||||
SwingUtilities.getWindowAncestor(this),
|
||||
owner,
|
||||
ohKeyPair,
|
||||
true
|
||||
)
|
||||
dialog.setLocationRelativeTo(owner)
|
||||
dialog.title = ohKeyPair.name
|
||||
dialog.isVisible = true
|
||||
ohKeyPair = dialog.ohKeyPair
|
||||
@@ -196,7 +196,6 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -220,7 +219,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
||||
return
|
||||
}
|
||||
|
||||
SSHCopyIdDialog(owner, windowScope, hosts, publicKeys).start()
|
||||
SSHCopyIdDialog(owner, hosts, publicKeys).start()
|
||||
}
|
||||
|
||||
private fun exportKeyPairs(file: File, keyPairs: List<OhKeyPair>) {
|
||||
@@ -311,15 +310,9 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
||||
nameTextField.text = ohKeyPair.name
|
||||
remarkTextField.text = ohKeyPair.remark
|
||||
val baos = ByteArrayOutputStream()
|
||||
if (ohKeyPair.type == "RSA") {
|
||||
val keyPair = OhKeyPairKeyPairProvider.generateKeyPair(ohKeyPair)
|
||||
OpenSSHKeyPairResourceWriter.INSTANCE
|
||||
.writePublicKey(RSA.generatePublic(ohKeyPair.publicKey.decodeBase64()), null, baos)
|
||||
} else if (ohKeyPair.type == "ED25519") {
|
||||
OpenSSHKeyPairResourceWriter.INSTANCE.writePublicKey(
|
||||
EdDSAPublicKey(X509EncodedKeySpec(ohKeyPair.publicKey.decodeBase64())),
|
||||
null, baos
|
||||
)
|
||||
}
|
||||
.writePublicKey(keyPair.public, null, baos)
|
||||
publicKeyTextArea.text = baos.toString()
|
||||
savePublicKeyBtn.isEnabled = true
|
||||
} else {
|
||||
@@ -344,7 +337,6 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
||||
|
||||
pack()
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 300, size.height)
|
||||
setLocationRelativeTo(null)
|
||||
|
||||
}
|
||||
|
||||
@@ -356,7 +348,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
return FormBuilder.create().layout(layout).padding("0dlu, $formMargin, $formMargin, $formMargin")
|
||||
return FormBuilder.create().layout(layout).padding("2dlu, $formMargin, $formMargin, $formMargin")
|
||||
.add("${I18n.getString("termora.keymgr.table.type")}:").xy(1, rows)
|
||||
.add(typeComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.keymgr.table.length")}:").xy(1, rows)
|
||||
@@ -514,7 +506,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
return FormBuilder.create().layout(layout).padding("0dlu, $formMargin, $formMargin, $formMargin")
|
||||
return FormBuilder.create().layout(layout).padding("2dlu, $formMargin, $formMargin, $formMargin")
|
||||
.add("File:").xy(1, rows)
|
||||
.add(fileTextField).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.keymgr.table.type")}:").xy(1, rows)
|
||||
@@ -589,8 +581,10 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
||||
try {
|
||||
val provider = FileKeyPairProvider(file.toPath())
|
||||
provider.passwordFinder = FilePasswordProvider { _, _, _ ->
|
||||
val dialog = InputDialog(owner = this@ImportKeyDialog, title = "Password")
|
||||
dialog.getText() ?: String()
|
||||
OptionPane.showInputDialog(
|
||||
SwingUtilities.getWindowAncestor(this),
|
||||
title = I18n.getString("termora.new-host.general.password"),
|
||||
) ?: String()
|
||||
}
|
||||
val keyPair = provider.loadKeys(null).firstOrNull()
|
||||
?: throw IllegalStateException("Failed to load the key file")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user