mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
93 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 |
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
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ certs/
|
|||||||
!gradle/wrapper/gradle-wrapper.jar
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
!**/src/main/**/build/
|
!**/src/main/**/build/
|
||||||
!**/src/test/**/build/
|
!**/src/test/**/build/
|
||||||
|
.vs
|
||||||
|
|
||||||
### IntelliJ IDEA ###
|
### IntelliJ IDEA ###
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
- Compatible with Windows, macOS, and Linux
|
- Compatible with Windows, macOS, and Linux
|
||||||
- Zmodem protocol support
|
- Zmodem protocol support
|
||||||
- SSH port forwarding & Jump hosts
|
- SSH port forwarding & Jump hosts
|
||||||
|
- Support for X11 and SSH-Agent
|
||||||
- Terminal log
|
- Terminal log
|
||||||
- Configuration synchronization via [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
- Configuration synchronization via [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
||||||
- Macro support (record and replay scripts)
|
- Macro support (record and replay scripts)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
- 支持 Windows、macOS、Linux 平台
|
- 支持 Windows、macOS、Linux 平台
|
||||||
- 支持 Zmodem 协议
|
- 支持 Zmodem 协议
|
||||||
- 支持 SSH 端口转发和跳板机
|
- 支持 SSH 端口转发和跳板机
|
||||||
|
- 支持 X11 和 SSH-Agent
|
||||||
- 终端日志记录
|
- 终端日志记录
|
||||||
- 支持配置同步到 [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
- 支持配置同步到 [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
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/java-annotations/blob/master/LICENSE.txt
|
https://github.com/JetBrains/java-annotations/blob/master/LICENSE.txt
|
||||||
|
|
||||||
bip39-lib-jvm 1.0.8
|
kotlin-bip39
|
||||||
MIT License
|
MIT License
|
||||||
https://github.com/Electric-Coin-Company/kotlin-bip39/blob/main/LICENSE
|
https://github.com/Electric-Coin-Company/kotlin-bip39/blob/main/LICENSE
|
||||||
|
|
||||||
colorpicker 2.0.1
|
colorpicker
|
||||||
BSD 3-Clause "New" or "Revised" License
|
BSD 3-Clause "New" or "Revised" License
|
||||||
https://github.com/dheid/colorpicker/blob/main/LICENSE
|
https://github.com/dheid/colorpicker/blob/main/LICENSE
|
||||||
|
|
||||||
commonmark 0.24.0
|
commonmark
|
||||||
BSD 2-Clause "Simplified" License
|
BSD 2-Clause "Simplified" License
|
||||||
https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt
|
https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt
|
||||||
|
|
||||||
commons-codec 1.18.0
|
commons-codec
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-codec/blob/master/LICENSE.txt
|
https://github.com/apache/commons-codec/blob/master/LICENSE.txt
|
||||||
|
|
||||||
commons-compress 1.27.1
|
commons-compress
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-compress/blob/master/LICENSE.txt
|
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
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-io/blob/master/LICENSE.txt
|
https://github.com/apache/commons-io/blob/master/LICENSE.txt
|
||||||
|
|
||||||
commons-lang3 3.17.0
|
commons-lang3
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-lang/blob/master/LICENSE.txt
|
https://github.com/apache/commons-lang/blob/master/LICENSE.txt
|
||||||
|
|
||||||
commons-net 3.11.1
|
commons-net
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-net/blob/master/LICENSE.txt
|
https://github.com/apache/commons-net/blob/master/LICENSE.txt
|
||||||
|
|
||||||
commons-text 1.13.0
|
commons-text
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-text/blob/master/LICENSE.txt
|
https://github.com/apache/commons-text/blob/master/LICENSE.txt
|
||||||
|
|
||||||
commons-csv 1.13.0
|
commons-csv
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-csv/blob/master/LICENSE.txt
|
https://github.com/apache/commons-csv/blob/master/LICENSE.txt
|
||||||
|
|
||||||
ini4j 0.5.5-2
|
ini4j
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
http://www.apache.org/licenses/LICENSE-2.0.txt
|
http://www.apache.org/licenses/LICENSE-2.0.txt
|
||||||
|
|
||||||
eddsa 0.3.0
|
eddsa
|
||||||
Creative Commons Zero v1.0 Universal
|
Creative Commons Zero v1.0 Universal
|
||||||
https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt
|
https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt
|
||||||
|
|
||||||
flatlaf 3.5.4
|
flatlaf
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
|
|
||||||
flatlaf 3.5.4-no-natives
|
flatlaf-no-natives
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
|
|
||||||
flatlaf-extras 3.5.4
|
flatlaf-extras
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
|
|
||||||
flatlaf-swingx 3.5.4
|
flatlaf-swingx
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
|
|
||||||
JavaEWAH 1.2.3
|
JavaEWAH
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/lemire/javaewah/blob/master/LICENSE
|
https://github.com/lemire/javaewah/blob/master/LICENSE
|
||||||
|
|
||||||
jbr-api 17.1.10.1
|
jbr-api
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/JetBrainsRuntimeApi/blob/main/LICENSE
|
https://github.com/JetBrains/JetBrainsRuntimeApi/blob/main/LICENSE
|
||||||
|
|
||||||
jcl-over-slf4j 1.7.36
|
jcl-over-slf4j
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0.txt
|
https://www.apache.org/licenses/LICENSE-2.0.txt
|
||||||
|
|
||||||
jfa 1.2.0
|
jfa
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/0x4a616e/jfa/blob/main/LICENSE
|
https://github.com/0x4a616e/jfa/blob/main/LICENSE
|
||||||
|
|
||||||
jgoodies-common 1.8.1
|
jgoodies-common
|
||||||
BSD-2-Clause License
|
BSD-2-Clause License
|
||||||
http://www.opensource.org/licenses/bsd-license.html
|
http://www.opensource.org/licenses/bsd-license.html
|
||||||
|
|
||||||
jgoodies-forms 1.9.0
|
jgoodies-forms
|
||||||
BSD-2-Clause License
|
BSD-2-Clause License
|
||||||
http://www.opensource.org/licenses/bsd-license.html
|
http://www.opensource.org/licenses/bsd-license.html
|
||||||
|
|
||||||
jna 5.16.0
|
jna
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/java-native-access/jna/blob/master/AL2.0
|
https://github.com/java-native-access/jna/blob/master/AL2.0
|
||||||
|
|
||||||
jna-platform 5.16.0
|
jna-platform
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/java-native-access/jna/blob/master/AL2.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
|
BSD 3-Clause "New" or "Revised" License
|
||||||
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
||||||
|
|
||||||
jnafilechooser-win32 1.1.2
|
jnafilechooser-win32
|
||||||
BSD 3-Clause "New" or "Revised" License
|
BSD 3-Clause "New" or "Revised" License
|
||||||
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
||||||
|
|
||||||
jsvg 1.4.0
|
jsvg
|
||||||
MIT License
|
MIT License
|
||||||
https://github.com/weisJ/jsvg/blob/master/LICENSE
|
https://github.com/weisJ/jsvg/blob/master/LICENSE
|
||||||
|
|
||||||
jSystemThemeDetector 3.9.1
|
jSystemThemeDetector
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/Dansoftowner/jSystemThemeDetector/blob/master/LICENSE
|
https://github.com/Dansoftowner/jSystemThemeDetector/blob/master/LICENSE
|
||||||
|
|
||||||
kotlin-logging 1.7.9
|
kotlin-logging
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/oshai/kotlin-logging/blob/master/LICENSE
|
https://github.com/oshai/kotlin-logging/blob/master/LICENSE
|
||||||
|
|
||||||
kotlin-stdlib 2.1.10
|
kotlin-stdlib
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
kotlin-stdlib-jdk7 1.9.10
|
kotlin-stdlib-jdk7
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
kotlin-stdlib-jdk8 1.9.10
|
kotlin-stdlib-jdk8
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
kotlin-stdlib-jdk8 1.9.10
|
kotlin-stdlib-jdk8
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
restart4j 0.0.1
|
restart4j
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/hstyi/restart4j/blob/main/LICENSE
|
https://github.com/hstyi/restart4j/blob/main/LICENSE
|
||||||
|
|
||||||
kotlinx-coroutines-core-jvm 1.10.1
|
kotlinx-coroutines-core
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
kotlinx-coroutines-swing 1.10.1
|
kotlinx-coroutines-swing
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/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
|
Apache License 2.0
|
||||||
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
||||||
|
|
||||||
kotlinx-serialization-json-jvm 1.8.0
|
logging-interceptor
|
||||||
Apache License 2.0
|
|
||||||
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
|
||||||
|
|
||||||
logging-interceptor 4.12.0
|
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
okhttp 4.12.0
|
okhttp
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
okio-jvm 3.6.0
|
okio-jvm
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/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
|
Eclipse Distribution License
|
||||||
https://www.eclipse.org/org/documents/edl-v10.php
|
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
|
Eclipse Distribution License
|
||||||
https://www.eclipse.org/org/documents/edl-v10.php
|
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
|
MIT License
|
||||||
https://github.com/oshi/oshi/blob/master/LICENSE
|
https://github.com/oshi/oshi/blob/master/LICENSE
|
||||||
|
|
||||||
pty4j 0.13.2
|
pty4j
|
||||||
Eclipse Public License 1.0
|
Eclipse Public License 1.0
|
||||||
https://github.com/JetBrains/pty4j/blob/master/LICENSE
|
https://github.com/JetBrains/pty4j/blob/master/LICENSE
|
||||||
|
|
||||||
slf4j-api 2.0.16
|
slf4j-api
|
||||||
MIT License
|
MIT License
|
||||||
https://github.com/qos-ch/slf4j/blob/master/LICENSE.txt
|
https://github.com/qos-ch/slf4j/blob/master/LICENSE.txt
|
||||||
|
|
||||||
slf4j-tinylog 2.7.0
|
slf4j-tinylog
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||||
|
|
||||||
sshd-common 2.14.0
|
sshd-common
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
sshd-core 2.14.0
|
sshd-core
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
sshd-osgi 2.14.0
|
sshd-osgi
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
sshd-sftp 2.14.0
|
sshd-sftp
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/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
|
GNU LESSER GENERAL PUBLIC LICENSE v3
|
||||||
https://www.gnu.org/licenses/lgpl-3.0
|
https://www.gnu.org/licenses/lgpl-3.0
|
||||||
|
|
||||||
tinylog-api 2.7.0
|
tinylog-api
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||||
|
|
||||||
tinylog-impl 2.7.0
|
tinylog-impl
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||||
|
|
||||||
versioncompare 1.4.1
|
versioncompare
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/G00fY2/version-compare/blob/main/LICENSE
|
https://github.com/G00fY2/version-compare/blob/main/LICENSE
|
||||||
|
|
||||||
xodus-compress 2.0.1
|
xodus-compress
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
xodus-environment 2.0.1
|
xodus-environment
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
xodus-openAPI 2.0.1
|
xodus-openAPI
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
xodus-utils 2.0.1
|
xodus-utils
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
xodus-vfs 2.0.1
|
xodus-vfs
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
@@ -246,7 +250,7 @@ jediterm
|
|||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
|
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
|
||||||
|
|
||||||
mixpanel-java 1.5.3
|
mixpanel-java
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/mixpanel/mixpanel-java/blob/master/LICENSE
|
https://github.com/mixpanel/mixpanel-java/blob/master/LICENSE
|
||||||
|
|
||||||
@@ -254,6 +258,6 @@ json-20231013
|
|||||||
Public Domain.
|
Public Domain.
|
||||||
https://github.com/stleary/JSON-java/blob/master/LICENSE
|
https://github.com/stleary/JSON-java/blob/master/LICENSE
|
||||||
|
|
||||||
jSerialComm 2.11.0
|
jSerialComm
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/Fazecast/jSerialComm/blob/master/LICENSE-APACHE-2.0
|
https://github.com/Fazecast/jSerialComm/blob/master/LICENSE-APACHE-2.0
|
||||||
144
build.gradle.kts
144
build.gradle.kts
@@ -14,13 +14,14 @@ plugins {
|
|||||||
java
|
java
|
||||||
idea
|
idea
|
||||||
application
|
application
|
||||||
|
`maven-publish`
|
||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
alias(libs.plugins.kotlinx.serialization)
|
alias(libs.plugins.kotlinx.serialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
group = "app.termora"
|
group = "app.termora"
|
||||||
version = "1.0.11"
|
version = "1.0.15"
|
||||||
|
|
||||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||||
@@ -56,64 +57,67 @@ dependencies {
|
|||||||
|
|
||||||
// implementation(platform(libs.koin.bom))
|
// implementation(platform(libs.koin.bom))
|
||||||
// implementation(libs.koin.core)
|
// implementation(libs.koin.core)
|
||||||
implementation(libs.slf4j.api)
|
api(libs.slf4j.api)
|
||||||
implementation(libs.pty4j)
|
api(libs.pty4j)
|
||||||
implementation(libs.slf4j.tinylog)
|
api(libs.slf4j.tinylog)
|
||||||
implementation(libs.tinylog.impl)
|
api(libs.tinylog.impl)
|
||||||
implementation(libs.commons.codec)
|
api(libs.commons.codec)
|
||||||
implementation(libs.commons.io)
|
api(libs.commons.io)
|
||||||
implementation(libs.commons.lang3)
|
api(libs.commons.lang3)
|
||||||
implementation(libs.commons.csv)
|
api(libs.commons.csv)
|
||||||
implementation(libs.commons.net)
|
api(libs.commons.net)
|
||||||
implementation(libs.commons.text)
|
api(libs.commons.text)
|
||||||
implementation(libs.commons.compress)
|
api(libs.commons.compress)
|
||||||
implementation(libs.kotlinx.coroutines.swing)
|
api(libs.commons.vfs2) { exclude(group = "*", module = "*") }
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
api(libs.kotlinx.coroutines.swing)
|
||||||
|
api(libs.kotlinx.coroutines.core)
|
||||||
|
|
||||||
implementation(libs.flatlaf) {
|
api(libs.flatlaf) {
|
||||||
artifact {
|
artifact {
|
||||||
if (useNoNativesFlatLaf) {
|
if (useNoNativesFlatLaf) {
|
||||||
classifier = "no-natives"
|
classifier = "no-natives"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
implementation(libs.flatlaf.extras) {
|
api(libs.flatlaf.extras) {
|
||||||
if (useNoNativesFlatLaf) {
|
if (useNoNativesFlatLaf) {
|
||||||
exclude(group = "com.formdev", module = "flatlaf")
|
exclude(group = "com.formdev", module = "flatlaf")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
implementation(libs.flatlaf.swingx) {
|
api(libs.flatlaf.swingx) {
|
||||||
if (useNoNativesFlatLaf) {
|
if (useNoNativesFlatLaf) {
|
||||||
exclude(group = "com.formdev", module = "flatlaf")
|
exclude(group = "com.formdev", module = "flatlaf")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation(libs.kotlinx.serialization.json)
|
api(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.swingx)
|
api(libs.swingx)
|
||||||
implementation(libs.jgoodies.forms)
|
api(libs.jgoodies.forms)
|
||||||
implementation(libs.jna)
|
api(libs.jna)
|
||||||
implementation(libs.jna.platform)
|
api(libs.jna.platform)
|
||||||
implementation(libs.versioncompare)
|
api(libs.versioncompare)
|
||||||
implementation(libs.oshi.core)
|
api(libs.oshi.core)
|
||||||
implementation(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
api(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
||||||
implementation(libs.jfa) { exclude(group = "*", module = "*") }
|
api(libs.jfa) { exclude(group = "*", module = "*") }
|
||||||
implementation(libs.jbr.api)
|
api(libs.jbr.api)
|
||||||
implementation(libs.okhttp)
|
api(libs.okhttp)
|
||||||
implementation(libs.okhttp.logging)
|
api(libs.okhttp.logging)
|
||||||
implementation(libs.sshd.core)
|
api(libs.sshd.core)
|
||||||
implementation(libs.commonmark)
|
api(libs.commonmark)
|
||||||
implementation(libs.jgit)
|
api(libs.jgit)
|
||||||
implementation(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
api(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
||||||
implementation(libs.jnafilechooser)
|
api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
||||||
implementation(libs.xodus.vfs)
|
api(libs.eddsa)
|
||||||
implementation(libs.xodus.openAPI)
|
api(libs.jnafilechooser)
|
||||||
implementation(libs.xodus.environment)
|
api(libs.xodus.vfs)
|
||||||
implementation(libs.bip39)
|
api(libs.xodus.openAPI)
|
||||||
implementation(libs.colorpicker)
|
api(libs.xodus.environment)
|
||||||
implementation(libs.mixpanel)
|
api(libs.bip39)
|
||||||
implementation(libs.jSerialComm)
|
api(libs.colorpicker)
|
||||||
implementation(libs.ini4j)
|
api(libs.mixpanel)
|
||||||
implementation(libs.restart4j)
|
api(libs.jSerialComm)
|
||||||
|
api(libs.ini4j)
|
||||||
|
api(libs.restart4j)
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
@@ -144,6 +148,37 @@ application {
|
|||||||
mainClass = "app.termora.MainKt"
|
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 {
|
tasks.test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
@@ -455,33 +490,26 @@ tasks.register("dist") {
|
|||||||
|
|
||||||
tasks.register("check-license") {
|
tasks.register("check-license") {
|
||||||
doLast {
|
doLast {
|
||||||
val thirdParty = mutableMapOf<String, String>()
|
|
||||||
val iterator = File(projectDir, "THIRDPARTY").readLines().iterator()
|
val iterator = File(projectDir, "THIRDPARTY").readLines().iterator()
|
||||||
val thirdPartyNames = mutableSetOf<String>()
|
val thirdPartyNames = mutableSetOf<String>()
|
||||||
|
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
val nameWithVersion = iterator.next()
|
val name = iterator.next()
|
||||||
if (nameWithVersion.isBlank()) {
|
if (name.isBlank()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore license name
|
// ignore license name
|
||||||
iterator.next()
|
iterator.next()
|
||||||
|
// ignore license url
|
||||||
|
iterator.next()
|
||||||
|
|
||||||
val license = iterator.next()
|
thirdPartyNames.add(name)
|
||||||
thirdParty[nameWithVersion.replace(StringUtils.SPACE, "-")] = license
|
|
||||||
thirdPartyNames.add(nameWithVersion.split(StringUtils.SPACE).first())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (file in configurations.runtimeClasspath.get()) {
|
for (dependency in configurations.runtimeClasspath.get().allDependencies) {
|
||||||
val name = file.nameWithoutExtension
|
if (!thirdPartyNames.contains(dependency.name)) {
|
||||||
if (!thirdParty.containsKey(name)) {
|
throw GradleException("${dependency.name} No license found")
|
||||||
if (logger.isWarnEnabled) {
|
|
||||||
logger.warn("$name does not exist in third-party")
|
|
||||||
}
|
|
||||||
if (!thirdPartyNames.contains(name)) {
|
|
||||||
throw GradleException("$name No license found")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,46 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "2.1.10"
|
kotlin = "2.1.21"
|
||||||
slf4j = "2.0.16"
|
slf4j = "2.0.17"
|
||||||
pty4j = "0.13.2"
|
pty4j = "0.13.4"
|
||||||
tinylog = "2.7.0"
|
tinylog = "2.7.0"
|
||||||
kotlinx-coroutines = "1.10.1"
|
kotlinx-coroutines = "1.10.2"
|
||||||
flatlaf = "3.5.4"
|
flatlaf = "3.6"
|
||||||
trove4j = "1.0.20200330"
|
kotlinx-serialization-json = "1.8.1"
|
||||||
kotlinx-serialization-json = "1.8.0"
|
|
||||||
commons-codec = "1.18.0"
|
commons-codec = "1.18.0"
|
||||||
commons-lang3 = "3.17.0"
|
commons-lang3 = "3.17.0"
|
||||||
commons-csv = "1.13.0"
|
commons-csv = "1.14.0"
|
||||||
commons-net = "3.11.1"
|
commons-net = "3.11.1"
|
||||||
commons-text = "1.13.0"
|
commons-text = "1.13.1"
|
||||||
commons-compress = "1.27.1"
|
commons-compress = "1.27.1"
|
||||||
koin-bom = "4.0.0"
|
commons-vfs2="2.10.0"
|
||||||
swingx = "1.6.5-1"
|
swingx = "1.6.5-1"
|
||||||
jgoodies-forms = "1.9.0"
|
jgoodies-forms = "1.9.0"
|
||||||
jfa = "1.2.0"
|
jfa = "1.2.0"
|
||||||
oshi = "6.6.5"
|
oshi = "6.8.1"
|
||||||
versioncompare = "1.4.1"
|
versioncompare = "1.4.1"
|
||||||
jna = "5.16.0"
|
jna = "5.17.0"
|
||||||
jSystemThemeDetector = "3.9.1"
|
jSystemThemeDetector = "3.9.1"
|
||||||
commons-io = "2.18.0"
|
commons-io = "2.19.0"
|
||||||
jbr-api = "17.1.10.1"
|
jbr-api = "17.1.10.1"
|
||||||
leveldb = "0.12"
|
hutool = "5.8.37"
|
||||||
guava = "33.3.1-jre"
|
jsch = "0.2.26"
|
||||||
credential-secure-storage = "1.0.3"
|
|
||||||
hutool = "5.8.34"
|
|
||||||
jsch = "0.2.21"
|
|
||||||
okhttp = "4.12.0"
|
okhttp = "4.12.0"
|
||||||
bcprov = "1.79"
|
|
||||||
sshj = "0.39.0"
|
sshj = "0.39.0"
|
||||||
sshd-core = "2.14.0"
|
sshd-core = "2.15.0"
|
||||||
jgit = "7.1.0.202411261347-r"
|
jgit = "7.2.0.202503040940-r"
|
||||||
commonmark = "0.24.0"
|
commonmark = "0.24.0"
|
||||||
jnafilechooser = "1.1.2"
|
jnafilechooser = "1.1.2"
|
||||||
xodus = "2.0.1"
|
xodus = "2.0.1"
|
||||||
bip39 = "1.0.8"
|
bip39 = "1.0.9"
|
||||||
colorpicker = "2.0.1"
|
colorpicker = "2.0.1"
|
||||||
rhino = "1.7.15"
|
rhino = "1.8.0"
|
||||||
delight-rhino-sandbox = "0.0.17"
|
delight-rhino-sandbox = "0.0.17"
|
||||||
testcontainers = "1.20.4"
|
testcontainers = "1.21.0"
|
||||||
mixpanel = "1.5.3"
|
mixpanel = "1.5.3"
|
||||||
jSerialComm = "2.11.0"
|
jSerialComm = "2.11.0"
|
||||||
ini4j = "0.5.5-2"
|
ini4j = "0.5.5-2"
|
||||||
restart4j = "0.0.1"
|
restart4j = "0.0.1"
|
||||||
|
eddsa = "0.3.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||||
@@ -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-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-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-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" }
|
pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" }
|
||||||
ini4j = { module = "org.jetbrains.intellij.deps:ini4j", version.ref = "ini4j" }
|
ini4j = { module = "org.jetbrains.intellij.deps:ini4j", version.ref = "ini4j" }
|
||||||
flatlaf = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
|
flatlaf = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
|
||||||
flatlaf-extras = { group = "com.formdev", name = "flatlaf-extras", 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-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "testcontainers" }
|
||||||
testcontainers = { module = "org.testcontainers:testcontainers" }
|
testcontainers = { module = "org.testcontainers:testcontainers" }
|
||||||
koin-core = { module = "io.insert-koin:koin-core" }
|
|
||||||
swingx = { module = "org.swinglabs.swingx:swingx-all", version.ref = "swingx" }
|
swingx = { module = "org.swinglabs.swingx:swingx-all", version.ref = "swingx" }
|
||||||
jgoodies-forms = { module = "com.jgoodies:jgoodies-forms", version.ref = "jgoodies-forms" }
|
jgoodies-forms = { module = "com.jgoodies:jgoodies-forms", version.ref = "jgoodies-forms" }
|
||||||
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
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" }
|
restart4j = { module = "com.github.hstyi:restart4j", version.ref = "restart4j" }
|
||||||
jbr-api = { module = "com.jetbrains:jbr-api", version.ref = "jbr-api" }
|
jbr-api = { module = "com.jetbrains:jbr-api", version.ref = "jbr-api" }
|
||||||
flatlaf-swingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
|
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" }
|
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" }
|
jsch = { module = "com.github.mwiede:jsch", version.ref = "jsch" }
|
||||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", 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" }
|
sshj = { module = "com.hierynomus:sshj", version.ref = "sshj" }
|
||||||
sshd-core = { module = "org.apache.sshd:sshd-core", version.ref = "sshd-core" }
|
sshd-core = { module = "org.apache.sshd:sshd-core", version.ref = "sshd-core" }
|
||||||
jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" }
|
jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" }
|
||||||
commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
|
commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
|
||||||
jgit-sshd = { module = "org.eclipse.jgit:org.eclipse.jgit.ssh.apache", version.ref = "jgit" }
|
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-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-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" }
|
xodus-vfs = { module = "org.jetbrains.xodus:xodus-vfs", version.ref = "xodus" }
|
||||||
jnafilechooser = { module = "com.github.steos.jnafilechooser:jnafilechooser-api", version.ref = "jnafilechooser" }
|
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" }
|
rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
||||||
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
|
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
|
||||||
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
|
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
|
||||||
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
|
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
|
||||||
jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "jSerialComm" }
|
jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "jSerialComm" }
|
||||||
|
eddsa = { module = "net.i2p.crypto:eddsa", version.ref = "eddsa" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
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"
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import static com.formdev.flatlaf.util.UIScale.scale;
|
|||||||
/**
|
/**
|
||||||
* 如果要升级 FlatLaf 需要检查是否兼容
|
* 如果要升级 FlatLaf 需要检查是否兼容
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public class MyFlatTabbedPaneUI extends FlatTabbedPaneUI {
|
public class MyFlatTabbedPaneUI extends FlatTabbedPaneUI {
|
||||||
@Override
|
@Override
|
||||||
protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {
|
protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package app.termora
|
|||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jthemedetecor.util.OsInfo
|
import com.jthemedetecor.util.OsInfo
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -123,19 +122,18 @@ object Application {
|
|||||||
return "Termora"
|
return "Termora"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("OPT_IN_USAGE")
|
|
||||||
fun browse(uri: URI, async: Boolean = true) {
|
fun browse(uri: URI, async: Boolean = true) {
|
||||||
// https://github.com/TermoraDev/termora/issues/178
|
// https://github.com/TermoraDev/termora/issues/178
|
||||||
if (SystemInfo.isWindows && uri.scheme == "file") {
|
if (SystemInfo.isWindows && uri.scheme == "file") {
|
||||||
if (async) {
|
if (async) {
|
||||||
GlobalScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
swingCoroutineScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
||||||
} else {
|
} else {
|
||||||
tryBrowse(uri)
|
tryBrowse(uri)
|
||||||
}
|
}
|
||||||
} else if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
} else if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||||
Desktop.getDesktop().browse(uri)
|
Desktop.getDesktop().browse(uri)
|
||||||
} else if (async) {
|
} else if (async) {
|
||||||
GlobalScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
swingCoroutineScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
||||||
} else {
|
} else {
|
||||||
tryBrowse(uri)
|
tryBrowse(uri)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app.termora
|
|||||||
|
|
||||||
import app.termora.actions.ActionManager
|
import app.termora.actions.ActionManager
|
||||||
import app.termora.keymap.KeymapManager
|
import app.termora.keymap.KeymapManager
|
||||||
|
import app.termora.vfs2.sftp.MySftpFileProvider
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.FlatSystemProperties
|
import com.formdev.flatlaf.FlatSystemProperties
|
||||||
import com.formdev.flatlaf.extras.FlatDesktop
|
import com.formdev.flatlaf.extras.FlatDesktop
|
||||||
@@ -12,16 +13,29 @@ import com.jthemedetecor.OsThemeDetector
|
|||||||
import com.mixpanel.mixpanelapi.ClientDelivery
|
import com.mixpanel.mixpanelapi.ClientDelivery
|
||||||
import com.mixpanel.mixpanelapi.MessageBuilder
|
import com.mixpanel.mixpanelapi.MessageBuilder
|
||||||
import com.mixpanel.mixpanelapi.MixpanelAPI
|
import com.mixpanel.mixpanelapi.MixpanelAPI
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.apache.commons.lang3.LocaleUtils
|
import org.apache.commons.lang3.LocaleUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
|
import org.apache.commons.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.json.JSONObject
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
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.*
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import javax.imageio.ImageIO
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
@@ -44,11 +58,20 @@ class ApplicationRunner {
|
|||||||
// 统计
|
// 统计
|
||||||
val enableAnalytics = measureTimeMillis { enableAnalytics() }
|
val enableAnalytics = measureTimeMillis { enableAnalytics() }
|
||||||
|
|
||||||
// init ActionManager、KeymapManager
|
// init ActionManager、KeymapManager、VFS
|
||||||
@Suppress("OPT_IN_USAGE")
|
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
ActionManager.getInstance()
|
ActionManager.getInstance()
|
||||||
KeymapManager.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
|
// 设置 LAF
|
||||||
@@ -79,9 +102,8 @@ class ApplicationRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("OPT_IN_USAGE")
|
|
||||||
private fun clearTemporary() {
|
private fun clearTemporary() {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
// 启动时清除
|
// 启动时清除
|
||||||
FileUtils.cleanDirectory(Application.getTemporaryDir())
|
FileUtils.cleanDirectory(Application.getTemporaryDir())
|
||||||
}
|
}
|
||||||
@@ -101,15 +123,66 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
TermoraFrameManager.getInstance().createWindow().isVisible = true
|
TermoraFrameManager.getInstance().createWindow().isVisible = true
|
||||||
|
|
||||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
if (SystemInfo.isMacOS) {
|
||||||
SwingUtilities.invokeLater { FlatDesktop.setQuitHandler { quitHandler() } }
|
SwingUtilities.invokeLater {
|
||||||
|
|
||||||
|
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() {
|
private fun quitHandler() {
|
||||||
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
val windows = TermoraFrameManager.getInstance().getWindows()
|
||||||
frame.dispose()
|
|
||||||
|
for (frame in windows) {
|
||||||
|
frame.dispatchEvent(WindowEvent(frame, WindowEvent.WINDOW_CLOSED))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Disposer.dispose(TermoraFrameManager.getInstance())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadSettings() {
|
private fun loadSettings() {
|
||||||
@@ -191,7 +264,35 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
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() {
|
private fun printSystemInfo() {
|
||||||
@@ -232,13 +333,12 @@ class ApplicationRunner {
|
|||||||
/**
|
/**
|
||||||
* 统计 https://mixpanel.com
|
* 统计 https://mixpanel.com
|
||||||
*/
|
*/
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
private fun enableAnalytics() {
|
private fun enableAnalytics() {
|
||||||
if (Application.isUnknownVersion()) {
|
if (Application.isUnknownVersion()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val properties = JSONObject()
|
val properties = JSONObject()
|
||||||
properties.put("os", SystemUtils.OS_NAME)
|
properties.put("os", SystemUtils.OS_NAME)
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,12 +6,12 @@ import app.termora.keymap.Keymap
|
|||||||
import app.termora.keymgr.OhKeyPair
|
import app.termora.keymgr.OhKeyPair
|
||||||
import app.termora.macro.Macro
|
import app.termora.macro.Macro
|
||||||
import app.termora.snippet.Snippet
|
import app.termora.snippet.Snippet
|
||||||
|
import app.termora.sync.SyncManager
|
||||||
import app.termora.sync.SyncType
|
import app.termora.sync.SyncType
|
||||||
import app.termora.terminal.CursorStyle
|
import app.termora.terminal.CursorStyle
|
||||||
import jetbrains.exodus.bindings.StringBinding
|
import jetbrains.exodus.bindings.StringBinding
|
||||||
import jetbrains.exodus.env.*
|
import jetbrains.exodus.env.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
@@ -30,6 +30,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
|
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
|
||||||
private const val MACRO_STORE = "Macro"
|
private const val MACRO_STORE = "Macro"
|
||||||
private const val KEY_PAIR_STORE = "KeyPair"
|
private const val KEY_PAIR_STORE = "KeyPair"
|
||||||
|
private const val DELETED_DATA_STORE = "DeletedData"
|
||||||
private val log = LoggerFactory.getLogger(Database::class.java)
|
private val log = LoggerFactory.getLogger(Database::class.java)
|
||||||
|
|
||||||
|
|
||||||
@@ -142,6 +143,37 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeHost(id: String) {
|
||||||
|
env.executeInTransaction {
|
||||||
|
delete(it, HOST_STORE, id)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
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) {
|
fun addSnippet(snippet: Snippet) {
|
||||||
var text = ohMyJson.encodeToString(snippet)
|
var text = ohMyJson.encodeToString(snippet)
|
||||||
if (doorman.isWorking()) {
|
if (doorman.isWorking()) {
|
||||||
@@ -155,6 +187,14 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeSnippet(id: String) {
|
||||||
|
env.executeInTransaction {
|
||||||
|
delete(it, SNIPPET_STORE, id)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Removed snippet: $id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getSnippets(): Collection<Snippet> {
|
fun getSnippets(): Collection<Snippet> {
|
||||||
val isWorking = doorman.isWorking()
|
val isWorking = doorman.isWorking()
|
||||||
@@ -249,6 +289,18 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
val k = StringBinding.stringToEntry(key)
|
val k = StringBinding.stringToEntry(key)
|
||||||
val v = StringBinding.stringToEntry(value)
|
val v = StringBinding.stringToEntry(value)
|
||||||
store.put(tx, k, v)
|
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) {
|
private fun delete(tx: Transaction, name: String, key: String) {
|
||||||
@@ -308,8 +360,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
private val properties = Collections.synchronizedMap(mutableMapOf<String, String>())
|
private val properties = Collections.synchronizedMap(mutableMapOf<String, String>())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@Suppress("OPT_IN_USAGE")
|
swingCoroutineScope.launch(Dispatchers.IO) { properties.putAll(getProperties()) }
|
||||||
GlobalScope.launch(Dispatchers.IO) { properties.putAll(getProperties()) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun getString(key: String): String? {
|
protected open fun getString(key: String): String? {
|
||||||
@@ -381,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) :
|
protected inner class LongPropertyDelegate(defaultValue: Long) :
|
||||||
PropertyDelegate<Long>(defaultValue) {
|
PropertyDelegate<Long>(defaultValue) {
|
||||||
@@ -465,6 +523,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
*/
|
*/
|
||||||
var beep by BooleanPropertyDelegate(true)
|
var beep by BooleanPropertyDelegate(true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 超链接
|
||||||
|
*/
|
||||||
|
var hyperlink by BooleanPropertyDelegate(true)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 光标闪烁
|
* 光标闪烁
|
||||||
*/
|
*/
|
||||||
@@ -580,6 +643,16 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
var darkTheme by StringPropertyDelegate("Dark")
|
var darkTheme by StringPropertyDelegate("Dark")
|
||||||
var lightTheme by StringPropertyDelegate("Light")
|
var lightTheme by StringPropertyDelegate("Light")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 允许后台运行,也就是托盘
|
||||||
|
*/
|
||||||
|
var backgroundRunning by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 背景图片的地址
|
||||||
|
*/
|
||||||
|
var backgroundImage by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 语言
|
* 语言
|
||||||
*/
|
*/
|
||||||
@@ -587,6 +660,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
I18n.containsLanguage(Locale.getDefault()) ?: Locale.US.toString()
|
I18n.containsLanguage(Locale.getDefault()) ?: Locale.US.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 透明度
|
||||||
|
*/
|
||||||
|
var opacity by DoublePropertyDelegate(1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -662,6 +740,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
* 最后同步时间
|
* 最后同步时间
|
||||||
*/
|
*/
|
||||||
var lastSyncTime by LongPropertyDelegate(0L)
|
var lastSyncTime by LongPropertyDelegate(0L)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步策略,为空就是默认手动
|
||||||
|
*/
|
||||||
|
var policy by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -222,7 +222,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
doCancelAction()
|
SwingUtilities.invokeLater { doCancelAction() }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
|
||||||
|
@Suppress("CascadeIf")
|
||||||
class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
||||||
init {
|
init {
|
||||||
generalOption.portTextField.value = host.port
|
generalOption.portTextField.value = host.port
|
||||||
@@ -13,6 +16,8 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
|||||||
generalOption.passwordTextField.text = host.authentication.password
|
generalOption.passwordTextField.text = host.authentication.password
|
||||||
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||||
generalOption.publicKeyComboBox.selectedItem = host.authentication.password
|
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
|
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||||
@@ -28,6 +33,8 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
|||||||
terminalOption.heartbeatIntervalTextField.value = host.options.heartbeatInterval
|
terminalOption.heartbeatIntervalTextField.value = host.options.heartbeatInterval
|
||||||
|
|
||||||
tunnelingOption.tunnelings.addAll(host.tunnelings)
|
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()) {
|
if (host.options.jumpHosts.isNotEmpty()) {
|
||||||
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ enum class Protocol {
|
|||||||
SSH,
|
SSH,
|
||||||
Local,
|
Local,
|
||||||
Serial,
|
Serial,
|
||||||
|
RDP,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 交互式的 SFTP,此协议只在系统内部交互不应该暴露给用户也不应该持久化
|
* 交互式的 SFTP,此协议只在系统内部交互不应该暴露给用户也不应该持久化
|
||||||
@@ -38,6 +39,7 @@ enum class AuthenticationType {
|
|||||||
No,
|
No,
|
||||||
Password,
|
Password,
|
||||||
PublicKey,
|
PublicKey,
|
||||||
|
SSHAgent,
|
||||||
KeyboardInteractive,
|
KeyboardInteractive,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +139,16 @@ data class Options(
|
|||||||
* SFTP 默认目录
|
* SFTP 默认目录
|
||||||
*/
|
*/
|
||||||
val sftpDefaultDirectory: String = StringUtils.EMPTY,
|
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 {
|
companion object {
|
||||||
val Default = Options()
|
val Default = Options()
|
||||||
@@ -214,6 +226,27 @@ data class EncryptedHost(
|
|||||||
var updateDate: Long = 0L,
|
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
|
@Serializable
|
||||||
data class Host(
|
data class Host(
|
||||||
|
|||||||
@@ -2,15 +2,17 @@ package app.termora
|
|||||||
|
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import org.apache.sshd.client.SshClient
|
import org.apache.sshd.client.SshClient
|
||||||
import org.apache.sshd.client.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
|
import java.util.*
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
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")}...")
|
putValue(NAME, "${I18n.getString("termora.new-host.test-connection")}...")
|
||||||
isEnabled = false
|
isEnabled = false
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
// 因为测试连接的时候从数据库读取会导致失效,所以这里生成随机ID
|
||||||
testConnection(pane.getHost())
|
testConnection(pane.getHost().copy(id = UUID.randomUUID().toSimpleString()))
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
putValue(NAME, I18n.getString("termora.new-host.test-connection"))
|
putValue(NAME, I18n.getString("termora.new-host.test-connection"))
|
||||||
isEnabled = true
|
isEnabled = true
|
||||||
@@ -103,8 +105,7 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
|||||||
var client: SshClient? = null
|
var client: SshClient? = null
|
||||||
var session: ClientSession? = null
|
var session: ClientSession? = null
|
||||||
try {
|
try {
|
||||||
client = SshClients.openClient(host)
|
client = SshClients.openClient(host, this)
|
||||||
client.userInteraction = TerminalUserInteraction(owner)
|
|
||||||
session = SshClients.openSession(host, client)
|
session = SshClients.openSession(host, client)
|
||||||
} finally {
|
} finally {
|
||||||
session?.close()
|
session?.close()
|
||||||
|
|||||||
@@ -16,14 +16,20 @@ class HostManager private constructor() {
|
|||||||
*/
|
*/
|
||||||
fun addHost(host: Host) {
|
fun addHost(host: Host) {
|
||||||
assertEventDispatchThread()
|
assertEventDispatchThread()
|
||||||
database.addHost(host)
|
|
||||||
if (host.deleted) {
|
if (host.deleted) {
|
||||||
hosts.entries.removeIf { it.value.id == host.id || it.value.parentId == host.id }
|
removeHost(host.id)
|
||||||
} else {
|
} else {
|
||||||
|
database.addHost(host)
|
||||||
hosts[host.id] = 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.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.apache.commons.lang3.StringUtils
|
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.*
|
||||||
import java.awt.event.*
|
import java.awt.event.*
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
@@ -21,7 +24,7 @@ import javax.swing.*
|
|||||||
import javax.swing.table.DefaultTableCellRenderer
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
import javax.swing.table.DefaultTableModel
|
import javax.swing.table.DefaultTableModel
|
||||||
|
|
||||||
|
@Suppress("CascadeIf")
|
||||||
open class HostOptionsPane : OptionsPane() {
|
open class HostOptionsPane : OptionsPane() {
|
||||||
protected val tunnelingOption = TunnelingOption()
|
protected val tunnelingOption = TunnelingOption()
|
||||||
protected val generalOption = GeneralOption()
|
protected val generalOption = GeneralOption()
|
||||||
@@ -52,18 +55,23 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
val port = (generalOption.portTextField.value ?: 22) as Int
|
val port = (generalOption.portTextField.value ?: 22) as Int
|
||||||
var authentication = Authentication.No
|
var authentication = Authentication.No
|
||||||
var proxy = Proxy.No
|
var proxy = Proxy.No
|
||||||
|
val authenticationType = generalOption.authenticationTypeComboBox.selectedItem as AuthenticationType
|
||||||
|
|
||||||
|
if (authenticationType == AuthenticationType.Password) {
|
||||||
if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.Password) {
|
|
||||||
authentication = authentication.copy(
|
authentication = authentication.copy(
|
||||||
type = AuthenticationType.Password,
|
type = authenticationType,
|
||||||
password = String(generalOption.passwordTextField.password)
|
password = String(generalOption.passwordTextField.password)
|
||||||
)
|
)
|
||||||
} else if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
|
} else if (authenticationType == AuthenticationType.PublicKey) {
|
||||||
authentication = authentication.copy(
|
authentication = authentication.copy(
|
||||||
type = AuthenticationType.PublicKey,
|
type = authenticationType,
|
||||||
password = generalOption.publicKeyComboBox.selectedItem?.toString() ?: StringUtils.EMPTY
|
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) {
|
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||||
@@ -94,7 +102,9 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
heartbeatInterval = (terminalOption.heartbeatIntervalTextField.value ?: 30) as Int,
|
heartbeatInterval = (terminalOption.heartbeatIntervalTextField.value ?: 30) as Int,
|
||||||
jumpHosts = jumpHostsOption.jumpHosts.map { it.id },
|
jumpHosts = jumpHostsOption.jumpHosts.map { it.id },
|
||||||
serialComm = serialComm,
|
serialComm = serialComm,
|
||||||
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text
|
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||||
|
enableX11Forwarding = tunnelingOption.x11ForwardingCheckBox.isSelected,
|
||||||
|
x11Forwarding = tunnelingOption.x11ServerTextField.text,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Host(
|
return Host(
|
||||||
@@ -160,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
|
return true
|
||||||
}
|
}
|
||||||
@@ -169,14 +190,18 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
*/
|
*/
|
||||||
private fun validateField(textField: JTextField): Boolean {
|
private fun validateField(textField: JTextField): Boolean {
|
||||||
if (textField.isEnabled && textField.text.isBlank()) {
|
if (textField.isEnabled && textField.text.isBlank()) {
|
||||||
selectOptionJComponent(textField)
|
setOutlineError(textField)
|
||||||
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
|
||||||
textField.requestFocusInWindow()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setOutlineError(textField: JTextField) {
|
||||||
|
selectOptionJComponent(textField)
|
||||||
|
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
|
textField.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 返回 true 表示有错误
|
* 返回 true 表示有错误
|
||||||
*/
|
*/
|
||||||
@@ -200,6 +225,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
private val passwordPanel = JPanel(BorderLayout())
|
private val passwordPanel = JPanel(BorderLayout())
|
||||||
private val chooseKeyBtn = JButton(Icons.greyKey)
|
private val chooseKeyBtn = JButton(Icons.greyKey)
|
||||||
val passwordTextField = OutlinePasswordField(255)
|
val passwordTextField = OutlinePasswordField(255)
|
||||||
|
val sshAgentComboBox = OutlineComboBox<String>()
|
||||||
val publicKeyComboBox = OutlineComboBox<String>()
|
val publicKeyComboBox = OutlineComboBox<String>()
|
||||||
val remarkTextArea = FixedLengthTextArea(512)
|
val remarkTextArea = FixedLengthTextArea(512)
|
||||||
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||||
@@ -215,6 +241,10 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
publicKeyComboBox.isEditable = false
|
publicKeyComboBox.isEditable = false
|
||||||
chooseKeyBtn.isFocusable = false
|
chooseKeyBtn.isFocusable = false
|
||||||
|
|
||||||
|
// 只有 Windows 允许修改
|
||||||
|
sshAgentComboBox.isEditable = SystemInfo.isWindows
|
||||||
|
sshAgentComboBox.isEnabled = SystemInfo.isWindows
|
||||||
|
|
||||||
protocolTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
protocolTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
override fun getListCellRendererComponent(
|
override fun getListCellRendererComponent(
|
||||||
list: JList<*>?,
|
list: JList<*>?,
|
||||||
@@ -290,10 +320,22 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
protocolTypeComboBox.addItem(Protocol.SSH)
|
protocolTypeComboBox.addItem(Protocol.SSH)
|
||||||
protocolTypeComboBox.addItem(Protocol.Local)
|
protocolTypeComboBox.addItem(Protocol.Local)
|
||||||
protocolTypeComboBox.addItem(Protocol.Serial)
|
protocolTypeComboBox.addItem(Protocol.Serial)
|
||||||
|
protocolTypeComboBox.addItem(Protocol.RDP)
|
||||||
|
|
||||||
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||||
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||||
authenticationTypeComboBox.addItem(AuthenticationType.PublicKey)
|
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
|
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
|
||||||
|
|
||||||
@@ -457,6 +499,8 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
.add(chooseKeyBtn).xy(3, 1)
|
.add(chooseKeyBtn).xy(3, 1)
|
||||||
.build(), BorderLayout.CENTER
|
.build(), BorderLayout.CENTER
|
||||||
)
|
)
|
||||||
|
} else if (authenticationTypeComboBox.selectedItem == AuthenticationType.SSHAgent) {
|
||||||
|
passwordPanel.add(sshAgentComboBox, BorderLayout.CENTER)
|
||||||
} else {
|
} else {
|
||||||
passwordPanel.add(passwordTextField, BorderLayout.CENTER)
|
passwordPanel.add(passwordTextField, BorderLayout.CENTER)
|
||||||
}
|
}
|
||||||
@@ -722,6 +766,8 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
protected inner class TunnelingOption : JPanel(BorderLayout()), Option {
|
protected inner class TunnelingOption : JPanel(BorderLayout()), Option {
|
||||||
val tunnelings = mutableListOf<Tunneling>()
|
val tunnelings = mutableListOf<Tunneling>()
|
||||||
|
val x11ForwardingCheckBox = JCheckBox("X DISPLAY:")
|
||||||
|
val x11ServerTextField = OutlineTextField(255)
|
||||||
|
|
||||||
private val model = object : DefaultTableModel() {
|
private val model = object : DefaultTableModel() {
|
||||||
override fun getRowCount(): Int {
|
override fun getRowCount(): Int {
|
||||||
@@ -796,13 +842,36 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
box.add(Box.createHorizontalStrut(4))
|
box.add(Box.createHorizontalStrut(4))
|
||||||
box.add(deleteBtn)
|
box.add(deleteBtn)
|
||||||
|
|
||||||
add(JLabel("TCP/IP Forwarding:"), BorderLayout.NORTH)
|
x11ForwardingCheckBox.isFocusable = false
|
||||||
add(scrollPane, BorderLayout.CENTER)
|
|
||||||
add(box, BorderLayout.SOUTH)
|
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() {
|
private fun initEvents() {
|
||||||
|
x11ForwardingCheckBox.addChangeListener { x11ServerTextField.isEnabled = x11ForwardingCheckBox.isSelected }
|
||||||
|
|
||||||
addBtn.addActionListener(object : AbstractAction() {
|
addBtn.addActionListener(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent?) {
|
override fun actionPerformed(e: ActionEvent?) {
|
||||||
val dialog = PortForwardingDialog(SwingUtilities.getWindowAncestor(this@HostOptionsPane))
|
val dialog = PortForwardingDialog(SwingUtilities.getWindowAncestor(this@HostOptionsPane))
|
||||||
@@ -1054,8 +1123,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
addComponentListener(object : ComponentAdapter() {
|
addComponentListener(object : ComponentAdapter() {
|
||||||
override fun componentShown(e: ComponentEvent) {
|
override fun componentShown(e: ComponentEvent) {
|
||||||
removeComponentListener(this)
|
removeComponentListener(this)
|
||||||
@Suppress("OPT_IN_USAGE")
|
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
for (commPort in SerialPort.getCommPorts()) {
|
for (commPort in SerialPort.getCommPorts()) {
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
serialPortComboBox.addItem(commPort.systemPortName)
|
serialPortComboBox.addItem(commPort.systemPortName)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import app.termora.actions.DataProviders
|
|||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import java.beans.PropertyChangeEvent
|
import java.beans.PropertyChangeEvent
|
||||||
@@ -19,7 +20,7 @@ abstract class HostTerminalTab(
|
|||||||
val Host = DataKey(app.termora.Host::class)
|
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 val terminalModel get() = terminal.getTerminalModel()
|
||||||
protected var unread = false
|
protected var unread = false
|
||||||
set(value) {
|
set(value) {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class HostTreeNode(host: Host) : SimpleTreeNode<Host>(host) {
|
|||||||
return when (host.protocol) {
|
return when (host.protocol) {
|
||||||
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||||
Protocol.Serial -> if (selected && hasFocus) Icons.plugin.dark else Icons.plugin
|
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
|
else -> if (selected && hasFocus) Icons.terminal.dark else Icons.terminal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ object Icons {
|
|||||||
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
|
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
|
||||||
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
|
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
|
||||||
val close by lazy { DynamicIcon("icons/close.svg", "icons/close_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 openInNewWindow by lazy { DynamicIcon("icons/openInNewWindow.svg", "icons/openInNewWindow_dark.svg") }
|
||||||
val openInToolWindow by lazy { DynamicIcon("icons/openInToolWindow.svg", "icons/openInToolWindow_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 searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
|
||||||
@@ -63,6 +64,7 @@ object Icons {
|
|||||||
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
|
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
|
||||||
val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") }
|
val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") }
|
||||||
val microsoft by lazy { DynamicIcon("icons/microsoft.svg", "icons/microsoft_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 tencent by lazy { DynamicIcon("icons/tencent.svg") }
|
||||||
val google by lazy { DynamicIcon("icons/google-small.svg") }
|
val google by lazy { DynamicIcon("icons/google-small.svg") }
|
||||||
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }
|
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }
|
||||||
@@ -93,6 +95,7 @@ object Icons {
|
|||||||
val sortedSet by lazy { DynamicIcon("icons/sortedSet.svg", "icons/sortedSet_dark.svg") }
|
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 colorPicker by lazy { DynamicIcon("icons/colorPicker.svg", "icons/colorPicker_dark.svg") }
|
||||||
val folder by lazy { DynamicIcon("icons/folder.svg", "icons/folder_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 listFiles by lazy { DynamicIcon("icons/listFiles.svg", "icons/listFiles_dark.svg") }
|
||||||
val left by lazy { DynamicIcon("icons/left.svg", "icons/left_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 right by lazy { DynamicIcon("icons/right.svg", "icons/right_dark.svg") }
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
|
import app.termora.terminal.PtyConnectorDelegate
|
||||||
|
import app.termora.terminal.PtyProcessConnector
|
||||||
import org.apache.commons.io.Charsets
|
import org.apache.commons.io.Charsets
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
||||||
class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
||||||
PtyHostTerminalTab(windowScope, host) {
|
PtyHostTerminalTab(windowScope, host) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(LocalTerminalTab::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun openPtyConnector(): PtyConnector {
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
val winSize = terminalPanel.winSize()
|
val winSize = terminalPanel.winSize()
|
||||||
val ptyConnector = PtyConnectorFactory.getInstance().createPtyConnector(
|
val ptyConnector = PtyConnectorFactory.getInstance().createPtyConnector(
|
||||||
@@ -18,4 +28,42 @@ class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
return ptyConnector
|
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,11 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
import com.formdev.flatlaf.ui.FlatRootPaneUI
|
|
||||||
import com.formdev.flatlaf.ui.FlatTitlePane
|
|
||||||
|
|
||||||
class MyFlatRootPaneUI : FlatRootPaneUI() {
|
|
||||||
|
|
||||||
fun getTitlePane(): FlatTitlePane? {
|
|
||||||
return super.titlePane
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@ import java.awt.event.*
|
|||||||
import java.awt.image.BufferedImage
|
import java.awt.image.BufferedImage
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.plaf.TabbedPaneUI
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
class MyTabbedPane : FlatTabbedPane() {
|
class MyTabbedPane : FlatTabbedPane() {
|
||||||
@@ -21,18 +20,12 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
private val owner
|
private val owner
|
||||||
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||||
.getData(DataProviders.TermoraFrame) as TermoraFrame
|
.getData(DataProviders.TermoraFrame) as TermoraFrame
|
||||||
private val myUI = MyFlatTabbedPaneUI()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
isFocusable = false
|
isFocusable = false
|
||||||
super.setUI(myUI)
|
|
||||||
initEvents()
|
initEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setUI(ui: TabbedPaneUI?) {
|
|
||||||
super.setUI(myUI)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateUI() {
|
override fun updateUI() {
|
||||||
styleMap = mapOf(
|
styleMap = mapOf(
|
||||||
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground"),
|
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground"),
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import java.io.*
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
import javax.swing.event.PopupMenuEvent
|
||||||
|
import javax.swing.event.PopupMenuListener
|
||||||
import javax.swing.filechooser.FileNameExtensionFilter
|
import javax.swing.filechooser.FileNameExtensionFilter
|
||||||
import javax.swing.tree.TreePath
|
import javax.swing.tree.TreePath
|
||||||
import javax.swing.tree.TreeSelectionModel
|
import javax.swing.tree.TreeSelectionModel
|
||||||
@@ -35,6 +37,7 @@ import javax.xml.parsers.DocumentBuilderFactory
|
|||||||
import javax.xml.xpath.XPathConstants
|
import javax.xml.xpath.XPathConstants
|
||||||
import javax.xml.xpath.XPathFactory
|
import javax.xml.xpath.XPathFactory
|
||||||
|
|
||||||
|
@Suppress("CascadeIf")
|
||||||
class NewHostTree : SimpleTree() {
|
class NewHostTree : SimpleTree() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -50,7 +53,7 @@ class NewHostTree : SimpleTree() {
|
|||||||
private var isShowMoreInfo
|
private var isShowMoreInfo
|
||||||
get() = properties.getString("HostTree.showMoreInfo", "false").toBoolean()
|
get() = properties.getString("HostTree.showMoreInfo", "false").toBoolean()
|
||||||
set(value) = properties.putString("HostTree.showMoreInfo", value.toString())
|
set(value) = properties.putString("HostTree.showMoreInfo", value.toString())
|
||||||
|
private var isPopupMenu = false
|
||||||
override val model = NewHostTreeModel()
|
override val model = NewHostTreeModel()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,7 +98,7 @@ class NewHostTree : SimpleTree() {
|
|||||||
// 是否显示更多信息
|
// 是否显示更多信息
|
||||||
if (isShowMoreInfo) {
|
if (isShowMoreInfo) {
|
||||||
val color = if (sel) {
|
val color = if (sel) {
|
||||||
if (tree.hasFocus()) {
|
if (tree.hasFocus() || isPopupMenu) {
|
||||||
UIManager.getColor("textHighlightText")
|
UIManager.getColor("textHighlightText")
|
||||||
} else {
|
} else {
|
||||||
this.foreground
|
this.foreground
|
||||||
@@ -108,20 +111,20 @@ class NewHostTree : SimpleTree() {
|
|||||||
"""<font color=rgb(${color.red},${color.green},${color.blue})>${it}</font>"""
|
"""<font color=rgb(${color.red},${color.green},${color.blue})>${it}</font>"""
|
||||||
}
|
}
|
||||||
|
|
||||||
if (host.protocol == Protocol.SSH) {
|
// @formatter:off
|
||||||
text =
|
if (host.protocol == Protocol.SSH || host.protocol == Protocol.RDP) {
|
||||||
"<html>${host.name} ${fontTag.apply("${host.username}@${host.host}")}</html>"
|
text = "<html>${host.name} ${fontTag.apply("${host.username}@${host.host}")}</html>"
|
||||||
} else if (host.protocol == Protocol.Serial) {
|
} else if (host.protocol == Protocol.Serial) {
|
||||||
text =
|
text = "<html>${host.name} ${fontTag.apply(host.options.serialComm.port)}</html>"
|
||||||
"<html>${host.name} ${fontTag.apply(host.options.serialComm.port)}</html>"
|
|
||||||
} else if (host.protocol == Protocol.Folder) {
|
} 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)
|
val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus)
|
||||||
|
|
||||||
icon = node.getIcon(sel, expanded, hasFocus)
|
icon = node.getIcon(sel, expanded, tree.hasFocus() || isPopupMenu)
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -135,6 +138,9 @@ class NewHostTree : SimpleTree() {
|
|||||||
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||||
val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return
|
val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return
|
||||||
if (lastNode.host.protocol != Protocol.Folder) {
|
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))
|
openHostAction?.actionPerformed(OpenHostActionEvent(e.source, lastNode.host, e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,6 +333,21 @@ class NewHostTree : SimpleTree() {
|
|||||||
openWithSFTP.isEnabled = fullNodes.map { it.host }.any { it.protocol == Protocol.SSH }
|
openWithSFTP.isEnabled = fullNodes.map { it.host }.any { it.protocol == Protocol.SSH }
|
||||||
openWithSFTPCommand.isEnabled = openWithSFTP.isEnabled
|
openWithSFTPCommand.isEnabled = openWithSFTP.isEnabled
|
||||||
openWith.isEnabled = openWith.menuComponents.any { it is JMenuItem && it.isEnabled }
|
openWith.isEnabled = openWith.menuComponents.any { it is JMenuItem && it.isEnabled }
|
||||||
|
|
||||||
|
popupMenu.addPopupMenuListener(object : PopupMenuListener {
|
||||||
|
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
||||||
|
isPopupMenu = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {
|
||||||
|
isPopupMenu = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popupMenuCanceled(e: PopupMenuEvent?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
popupMenu.show(this, evt.x, evt.y)
|
popupMenu.show(this, evt.x, evt.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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()
|
||||||
|
}
|
||||||
@@ -5,7 +5,9 @@ import com.formdev.flatlaf.FlatClientProperties
|
|||||||
import com.formdev.flatlaf.extras.components.FlatTextPane
|
import com.formdev.flatlaf.extras.components.FlatTextPane
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jetbrains.JBR
|
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 kotlinx.coroutines.swing.Swing
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
@@ -20,6 +22,8 @@ import kotlin.time.Duration
|
|||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
object OptionPane {
|
object OptionPane {
|
||||||
|
private val coroutineScope = swingCoroutineScope
|
||||||
|
|
||||||
fun showConfirmDialog(
|
fun showConfirmDialog(
|
||||||
parentComponent: Component?,
|
parentComponent: Component?,
|
||||||
message: Any,
|
message: Any,
|
||||||
@@ -29,6 +33,7 @@ object OptionPane {
|
|||||||
icon: Icon? = null,
|
icon: Icon? = null,
|
||||||
options: Array<Any>? = null,
|
options: Array<Any>? = null,
|
||||||
initialValue: Any? = null,
|
initialValue: Any? = null,
|
||||||
|
customizeDialog: (JDialog) -> Unit = {},
|
||||||
): Int {
|
): Int {
|
||||||
|
|
||||||
val panel = if (message is JComponent) {
|
val panel = if (message is JComponent) {
|
||||||
@@ -47,6 +52,9 @@ object OptionPane {
|
|||||||
override fun selectInitialValue() {
|
override fun selectInitialValue() {
|
||||||
super.selectInitialValue()
|
super.selectInitialValue()
|
||||||
if (message is JComponent) {
|
if (message is JComponent) {
|
||||||
|
if (message.getClientProperty("SKIP_requestFocusInWindow") == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
message.requestFocusInWindow()
|
message.requestFocusInWindow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,6 +66,7 @@ object OptionPane {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
dialog.setLocationRelativeTo(parentComponent)
|
dialog.setLocationRelativeTo(parentComponent)
|
||||||
|
customizeDialog.invoke(dialog)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
dialog.dispose()
|
dialog.dispose()
|
||||||
val selectedValue = pane.value
|
val selectedValue = pane.value
|
||||||
@@ -99,9 +108,8 @@ object OptionPane {
|
|||||||
val dialog = initDialog(pane.createDialog(parentComponent, title))
|
val dialog = initDialog(pane.createDialog(parentComponent, title))
|
||||||
if (duration.inWholeMilliseconds > 0) {
|
if (duration.inWholeMilliseconds > 0) {
|
||||||
dialog.addWindowListener(object : WindowAdapter() {
|
dialog.addWindowListener(object : WindowAdapter() {
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
override fun windowOpened(e: WindowEvent) {
|
override fun windowOpened(e: WindowEvent) {
|
||||||
GlobalScope.launch(Dispatchers.Swing) {
|
coroutineScope.launch(Dispatchers.Swing) {
|
||||||
delay(duration.inWholeMilliseconds)
|
delay(duration.inWholeMilliseconds)
|
||||||
if (dialog.isVisible) {
|
if (dialog.isVisible) {
|
||||||
dialog.isVisible = false
|
dialog.isVisible = false
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import kotlin.math.max
|
|||||||
class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(owner) {
|
class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(owner) {
|
||||||
|
|
||||||
private val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
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 passwordPanel = JPanel(BorderLayout())
|
||||||
private val passwordPasswordField = OutlinePasswordField()
|
private val passwordPasswordField = OutlinePasswordField()
|
||||||
private val usernameTextField = OutlineTextField()
|
private val usernameTextField = OutlineTextField()
|
||||||
@@ -34,8 +34,8 @@ class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(own
|
|||||||
pack()
|
pack()
|
||||||
|
|
||||||
size = Dimension(max(380, size.width), size.height)
|
size = Dimension(max(380, size.width), size.height)
|
||||||
|
preferredSize = size
|
||||||
setLocationRelativeTo(null)
|
minimumSize = size
|
||||||
|
|
||||||
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
override fun getListCellRendererComponent(
|
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
|
usernameTextField.text = host.username
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
|||||||
private var lastPasswordReporterDataListener: PasswordReporterDataListener? = null
|
private var lastPasswordReporterDataListener: PasswordReporterDataListener? = null
|
||||||
private val sftpCommand get() = Database.getDatabase().sftp.sftpCommand
|
private val sftpCommand get() = Database.getDatabase().sftp.sftpCommand
|
||||||
private val defaultDirectory get() = Database.getDatabase().sftp.defaultDirectory
|
private val defaultDirectory get() = Database.getDatabase().sftp.defaultDirectory
|
||||||
|
private val owner get() = SwingUtilities.getWindowAncestor(terminalPanel)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
terminalPanel.dropFiles = true
|
terminalPanel.dropFiles = true
|
||||||
@@ -67,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 }
|
val sshSession = SshClients.openSession(host, sshClient).apply { sshSession = this }
|
||||||
|
|
||||||
// 打开通道
|
// 打开通道
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import app.termora.actions.AnActionEvent
|
|||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.actions.TabReconnectAction
|
import app.termora.actions.TabReconnectAction
|
||||||
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
||||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
|
||||||
import app.termora.keymap.KeyShortcut
|
import app.termora.keymap.KeyShortcut
|
||||||
import app.termora.keymap.KeymapManager
|
import app.termora.keymap.KeymapManager
|
||||||
import app.termora.terminal.ControlCharacters
|
import app.termora.terminal.ControlCharacters
|
||||||
@@ -53,6 +52,7 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
terminalPanel.dropFiles = false
|
terminalPanel.dropFiles = false
|
||||||
|
terminalPanel.dataProviderSupport.addData(DataProviders.TerminalTab, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getJComponent(): JComponent {
|
override fun getJComponent(): JComponent {
|
||||||
@@ -89,35 +89,8 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
terminal.write("SSH client is opening...\r\n")
|
terminal.write("SSH client is opening...\r\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
var host =
|
|
||||||
this.host.copy(authentication = this.host.authentication.copy(), updateDate = System.currentTimeMillis())
|
|
||||||
val owner = SwingUtilities.getWindowAncestor(terminalPanel)
|
val owner = SwingUtilities.getWindowAncestor(terminalPanel)
|
||||||
val client = SshClients.openClient(host).also { sshClient = it }
|
val client = SshClients.openClient(host, owner).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 sessionListener = MySessionListener()
|
val sessionListener = MySessionListener()
|
||||||
val channelListener = MyChannelListener()
|
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 {
|
private inner class MySessionListener : SessionListener, Disposable {
|
||||||
override fun sessionEvent(session: Session, event: Event) {
|
override fun sessionEvent(session: Session, event: Event) {
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package app.termora
|
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 org.slf4j.LoggerFactory
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
@@ -8,6 +13,8 @@ import javax.swing.JPopupMenu
|
|||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
val swingCoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Swing)
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
open class Scope(
|
open class Scope(
|
||||||
private val beans: MutableMap<KClass<*>, Any> = ConcurrentHashMap(),
|
private val beans: MutableMap<KClass<*>, Any> = ConcurrentHashMap(),
|
||||||
@@ -144,6 +151,7 @@ class ApplicationScope private constructor() : Scope() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun windowScopes(): List<WindowScope> {
|
fun windowScopes(): List<WindowScope> {
|
||||||
|
if (scopes.isEmpty()) return emptyList()
|
||||||
return scopes.values.toList()
|
return scopes.values.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +159,7 @@ class ApplicationScope private constructor() : Scope() {
|
|||||||
if (log.isInfoEnabled) {
|
if (log.isInfoEnabled) {
|
||||||
log.info("ApplicationScope disposed")
|
log.info("ApplicationScope disposed")
|
||||||
}
|
}
|
||||||
|
swingCoroutineScope.cancel()
|
||||||
super.dispose()
|
super.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ import app.termora.native.FileChooser
|
|||||||
import app.termora.sftp.SFTPTab
|
import app.termora.sftp.SFTPTab
|
||||||
import app.termora.snippet.Snippet
|
import app.termora.snippet.Snippet
|
||||||
import app.termora.snippet.SnippetManager
|
import app.termora.snippet.SnippetManager
|
||||||
import app.termora.sync.SyncConfig
|
import app.termora.sync.*
|
||||||
import app.termora.sync.SyncRange
|
|
||||||
import app.termora.sync.SyncType
|
|
||||||
import app.termora.sync.SyncerProvider
|
|
||||||
import app.termora.terminal.CursorStyle
|
import app.termora.terminal.CursorStyle
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.terminal.panel.FloatingToolbarPanel
|
import app.termora.terminal.panel.FloatingToolbarPanel
|
||||||
@@ -36,10 +33,14 @@ import com.jgoodies.forms.builder.FormBuilder
|
|||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
import com.jthemedetecor.OsThemeDetector
|
import com.jthemedetecor.OsThemeDetector
|
||||||
import com.sun.jna.LastErrorException
|
import com.sun.jna.LastErrorException
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import org.apache.commons.codec.binary.Base64
|
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.io.IOUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
@@ -58,12 +59,14 @@ import java.awt.event.ItemListener
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.function.Consumer
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
import javax.swing.JSpinner.NumberEditor
|
||||||
import javax.swing.event.DocumentEvent
|
import javax.swing.event.DocumentEvent
|
||||||
import javax.swing.event.PopupMenuEvent
|
import javax.swing.event.PopupMenuEvent
|
||||||
import javax.swing.event.PopupMenuListener
|
import javax.swing.event.PopupMenuListener
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
|
|
||||||
|
|
||||||
class SettingsOptionsPane : OptionsPane() {
|
class SettingsOptionsPane : OptionsPane() {
|
||||||
@@ -79,7 +82,6 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java)
|
private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java)
|
||||||
private val localShells by lazy { loadShells() }
|
private val localShells by lazy { loadShells() }
|
||||||
var pulled = false
|
|
||||||
|
|
||||||
private fun loadShells(): List<String> {
|
private fun loadShells(): List<String> {
|
||||||
val shells = mutableListOf<String>()
|
val shells = mutableListOf<String>()
|
||||||
@@ -129,9 +131,15 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
val themeManager = ThemeManager.getInstance()
|
val themeManager = ThemeManager.getInstance()
|
||||||
val themeComboBox = FlatComboBox<String>()
|
val themeComboBox = FlatComboBox<String>()
|
||||||
val languageComboBox = FlatComboBox<String>()
|
val languageComboBox = FlatComboBox<String>()
|
||||||
|
val backgroundComBoBox = YesOrNoComboBox()
|
||||||
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
|
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
|
||||||
val preferredThemeBtn = JButton(Icons.settings)
|
val preferredThemeBtn = JButton(Icons.settings)
|
||||||
|
val opacitySpinner = NumberSpinner(100, 0, 100)
|
||||||
|
val backgroundImageTextField = OutlineTextField()
|
||||||
|
|
||||||
private val appearance get() = database.appearance
|
private val appearance get() = database.appearance
|
||||||
|
private val backgroundButton = JButton(Icons.folder)
|
||||||
|
private val backgroundClearButton = FlatButton()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -140,8 +148,38 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
private fun initView() {
|
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
|
followSystemCheckBox.isSelected = appearance.followSystem
|
||||||
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
|
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
|
||||||
|
backgroundComBoBox.selectedItem = appearance.backgroundRunning
|
||||||
|
|
||||||
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
||||||
themeManager.themes.keys.forEach { themeComboBox.addItem(it) }
|
themeManager.themes.keys.forEach { themeComboBox.addItem(it) }
|
||||||
@@ -178,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 {
|
followSystemCheckBox.addActionListener {
|
||||||
appearance.followSystem = followSystemCheckBox.isSelected
|
appearance.followSystem = followSystemCheckBox.isSelected
|
||||||
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
||||||
@@ -207,6 +259,46 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preferredThemeBtn.addActionListener { showPreferredThemeContextmenu() }
|
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 {
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
@@ -276,7 +368,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private fun getFormPanel(): JPanel {
|
private fun getFormPanel(): JPanel {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $formMargin, default:grow, $formMargin, default, default:grow",
|
"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()
|
val box = FlatToolBar()
|
||||||
box.add(followSystemCheckBox)
|
box.add(followSystemCheckBox)
|
||||||
@@ -285,7 +377,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
var rows = 1
|
var rows = 1
|
||||||
val step = 2
|
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("${I18n.getString("termora.settings.appearance.theme")}:").xy(1, rows)
|
||||||
.add(themeComboBox).xy(3, rows)
|
.add(themeComboBox).xy(3, rows)
|
||||||
.add(box).xy(5, rows).apply { rows += step }
|
.add(box).xy(5, rows).apply { rows += step }
|
||||||
@@ -296,7 +388,22 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
Application.browse(URI.create("https://github.com/TermoraDev/termora/tree/main/src/main/resources/i18n"))
|
Application.browse(URI.create("https://github.com/TermoraDev/termora/tree/main/src/main/resources/i18n"))
|
||||||
}
|
}
|
||||||
})).xy(5, rows).apply { rows += step }
|
})).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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -315,6 +422,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private val selectCopyComboBox = YesOrNoComboBox()
|
private val selectCopyComboBox = YesOrNoComboBox()
|
||||||
private val autoCloseTabComboBox = YesOrNoComboBox()
|
private val autoCloseTabComboBox = YesOrNoComboBox()
|
||||||
private val floatingToolbarComboBox = YesOrNoComboBox()
|
private val floatingToolbarComboBox = YesOrNoComboBox()
|
||||||
|
private val hyperlinkComboBox = YesOrNoComboBox()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -392,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 ->
|
cursorBlinkComboBox.addItemListener { e ->
|
||||||
if (e.stateChange == ItemEvent.SELECTED) {
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
terminalSetting.cursorBlink = cursorBlinkComboBox.selectedItem as Boolean
|
terminalSetting.cursorBlink = cursorBlinkComboBox.selectedItem as Boolean
|
||||||
@@ -408,8 +523,8 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fireFontChanged() {
|
private fun fireFontChanged() {
|
||||||
TerminalPanelFactory.getInstance()
|
TerminalPanelFactory.getInstance()
|
||||||
.fireResize()
|
.fireResize()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
@@ -470,7 +585,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
shellComboBox.selectedItem = terminalSetting.localShell
|
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 {
|
FontUtils.getAllFonts().forEach {
|
||||||
if (!fonts.contains(it.family)) {
|
if (!fonts.contains(it.family)) {
|
||||||
fonts.addLast(it.family)
|
fonts.addLast(it.family)
|
||||||
@@ -484,6 +599,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
fontComboBox.selectedItem = terminalSetting.font
|
fontComboBox.selectedItem = terminalSetting.font
|
||||||
debugComboBox.selectedItem = terminalSetting.debug
|
debugComboBox.selectedItem = terminalSetting.debug
|
||||||
beepComboBox.selectedItem = terminalSetting.beep
|
beepComboBox.selectedItem = terminalSetting.beep
|
||||||
|
hyperlinkComboBox.selectedItem = terminalSetting.hyperlink
|
||||||
cursorBlinkComboBox.selectedItem = terminalSetting.cursorBlink
|
cursorBlinkComboBox.selectedItem = terminalSetting.cursorBlink
|
||||||
cursorStyleComboBox.selectedItem = terminalSetting.cursor
|
cursorStyleComboBox.selectedItem = terminalSetting.cursor
|
||||||
selectCopyComboBox.selectedItem = terminalSetting.selectCopy
|
selectCopyComboBox.selectedItem = terminalSetting.selectCopy
|
||||||
@@ -506,7 +622,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private fun getCenterComponent(): JComponent {
|
private fun getCenterComponent(): JComponent {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow",
|
"left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow",
|
||||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $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)
|
val beepBtn = JButton(Icons.run)
|
||||||
@@ -529,6 +645,8 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
.add("${I18n.getString("termora.settings.terminal.beep")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.beep")}:").xy(1, rows)
|
||||||
.add(beepComboBox).xy(3, rows)
|
.add(beepComboBox).xy(3, rows)
|
||||||
.add(beepBtn).xy(5, rows).apply { rows += step }
|
.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("${I18n.getString("termora.settings.terminal.select-copy")}:").xy(1, rows)
|
||||||
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
|
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
|
||||||
@@ -553,11 +671,11 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
val typeComboBox = FlatComboBox<SyncType>()
|
val typeComboBox = FlatComboBox<SyncType>()
|
||||||
val tokenTextField = OutlinePasswordField(255)
|
val tokenTextField = OutlinePasswordField(255)
|
||||||
val gistTextField = OutlineTextField(255)
|
val gistTextField = OutlineTextField(255)
|
||||||
|
val policyComboBox = JComboBox<SyncPolicy>()
|
||||||
val domainTextField = OutlineTextField(255)
|
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 exportConfigButton = JButton(I18n.getString("termora.settings.sync.export"), Icons.export)
|
||||||
val importConfigButton = JButton(I18n.getString("termora.settings.sync.import"), Icons.import)
|
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 lastSyncTimeLabel = JLabel()
|
||||||
val sync get() = database.sync
|
val sync get() = database.sync
|
||||||
val hostsCheckBox = JCheckBox(I18n.getString("termora.welcome.my-hosts"))
|
val hostsCheckBox = JCheckBox(I18n.getString("termora.welcome.my-hosts"))
|
||||||
@@ -575,19 +693,23 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
add(getCenterComponent(), BorderLayout.CENTER)
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
downloadConfigButton.addActionListener {
|
syncConfigButton.addActionListener(object : AbstractAction() {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
pushOrPull(false)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
swingCoroutineScope.launch(Dispatchers.IO) { sync() }
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
uploadConfigButton.addActionListener {
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
pushOrPull(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
typeComboBox.addItemListener {
|
typeComboBox.addItemListener {
|
||||||
if (it.stateChange == ItemEvent.SELECTED) {
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
@@ -606,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() {
|
tokenTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
override fun changedUpdate(e: DocumentEvent) {
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
sync.token = String(tokenTextField.password)
|
sync.token = String(tokenTextField.password)
|
||||||
@@ -626,6 +754,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
visitGistBtn.addActionListener {
|
visitGistBtn.addActionListener {
|
||||||
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
||||||
if (domainTextField.text.isNotBlank()) {
|
if (domainTextField.text.isNotBlank()) {
|
||||||
@@ -672,17 +801,47 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
private fun refreshButtons() {
|
||||||
sync.rangeKeyPairs = keysCheckBox.isSelected
|
sync.rangeKeyPairs = keysCheckBox.isSelected
|
||||||
sync.rangeHosts = hostsCheckBox.isSelected
|
sync.rangeHosts = hostsCheckBox.isSelected
|
||||||
sync.rangeSnippets = snippetsCheckBox.isSelected
|
sync.rangeSnippets = snippetsCheckBox.isSelected
|
||||||
sync.rangeKeywordHighlights = keywordHighlightsCheckBox.isSelected
|
sync.rangeKeywordHighlights = keywordHighlightsCheckBox.isSelected
|
||||||
|
|
||||||
downloadConfigButton.isEnabled = keysCheckBox.isSelected || hostsCheckBox.isSelected
|
syncConfigButton.isEnabled = keysCheckBox.isSelected || hostsCheckBox.isSelected
|
||||||
|| keywordHighlightsCheckBox.isSelected
|
|| keywordHighlightsCheckBox.isSelected
|
||||||
uploadConfigButton.isEnabled = downloadConfigButton.isEnabled
|
exportConfigButton.isEnabled = syncConfigButton.isEnabled
|
||||||
exportConfigButton.isEnabled = downloadConfigButton.isEnabled
|
importConfigButton.isEnabled = syncConfigButton.isEnabled
|
||||||
importConfigButton.isEnabled = downloadConfigButton.isEnabled
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun export() {
|
private fun export() {
|
||||||
@@ -1008,8 +1167,11 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true 同步成功
|
||||||
|
*/
|
||||||
@Suppress("DuplicatedCode")
|
@Suppress("DuplicatedCode")
|
||||||
private suspend fun pushOrPull(push: Boolean) {
|
private suspend fun pushOrPull(push: Boolean): Boolean {
|
||||||
|
|
||||||
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
||||||
if (domainTextField.text.isBlank()) {
|
if (domainTextField.text.isBlank()) {
|
||||||
@@ -1017,7 +1179,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
domainTextField.outline = "error"
|
domainTextField.outline = "error"
|
||||||
domainTextField.requestFocusInWindow()
|
domainTextField.requestFocusInWindow()
|
||||||
}
|
}
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1026,7 +1188,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
tokenTextField.outline = "error"
|
tokenTextField.outline = "error"
|
||||||
tokenTextField.requestFocusInWindow()
|
tokenTextField.requestFocusInWindow()
|
||||||
}
|
}
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gistTextField.text.isBlank() && !push) {
|
if (gistTextField.text.isBlank() && !push) {
|
||||||
@@ -1034,39 +1196,13 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
gistTextField.outline = "error"
|
gistTextField.outline = "error"
|
||||||
gistTextField.requestFocusInWindow()
|
gistTextField.requestFocusInWindow()
|
||||||
}
|
}
|
||||||
return
|
return false
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 没有拉取过 && 是推送 && 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
exportConfigButton.isEnabled = false
|
exportConfigButton.isEnabled = false
|
||||||
importConfigButton.isEnabled = false
|
importConfigButton.isEnabled = false
|
||||||
downloadConfigButton.isEnabled = false
|
syncConfigButton.isEnabled = false
|
||||||
uploadConfigButton.isEnabled = false
|
|
||||||
typeComboBox.isEnabled = false
|
typeComboBox.isEnabled = false
|
||||||
gistTextField.isEnabled = false
|
gistTextField.isEnabled = false
|
||||||
tokenTextField.isEnabled = false
|
tokenTextField.isEnabled = false
|
||||||
@@ -1077,19 +1213,14 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
hostsCheckBox.isEnabled = false
|
hostsCheckBox.isEnabled = false
|
||||||
snippetsCheckBox.isEnabled = false
|
snippetsCheckBox.isEnabled = false
|
||||||
domainTextField.isEnabled = false
|
domainTextField.isEnabled = false
|
||||||
|
syncConfigButton.text = "${I18n.getString("termora.settings.sync")}..."
|
||||||
if (push) {
|
|
||||||
uploadConfigButton.text = "${I18n.getString("termora.settings.sync.push")}..."
|
|
||||||
} else {
|
|
||||||
downloadConfigButton.text = "${I18n.getString("termora.settings.sync.pull")}..."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val syncConfig = getSyncConfig()
|
val syncConfig = getSyncConfig()
|
||||||
|
|
||||||
// sync
|
// sync
|
||||||
val syncResult = kotlin.runCatching {
|
val syncResult = kotlin.runCatching {
|
||||||
val syncer = SyncerProvider.getInstance().getSyncer(syncConfig.type)
|
val syncer = SyncManager.getInstance()
|
||||||
if (push) {
|
if (push) {
|
||||||
syncer.push(syncConfig)
|
syncer.push(syncConfig)
|
||||||
} else {
|
} else {
|
||||||
@@ -1099,10 +1230,9 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
// 恢复状态
|
// 恢复状态
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
downloadConfigButton.isEnabled = true
|
syncConfigButton.isEnabled = true
|
||||||
exportConfigButton.isEnabled = true
|
exportConfigButton.isEnabled = true
|
||||||
importConfigButton.isEnabled = true
|
importConfigButton.isEnabled = true
|
||||||
uploadConfigButton.isEnabled = true
|
|
||||||
keysCheckBox.isEnabled = true
|
keysCheckBox.isEnabled = true
|
||||||
hostsCheckBox.isEnabled = true
|
hostsCheckBox.isEnabled = true
|
||||||
snippetsCheckBox.isEnabled = true
|
snippetsCheckBox.isEnabled = true
|
||||||
@@ -1113,11 +1243,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
tokenTextField.isEnabled = true
|
tokenTextField.isEnabled = true
|
||||||
domainTextField.isEnabled = true
|
domainTextField.isEnabled = true
|
||||||
keywordHighlightsCheckBox.isEnabled = true
|
keywordHighlightsCheckBox.isEnabled = true
|
||||||
if (push) {
|
syncConfigButton.text = I18n.getString("termora.settings.sync")
|
||||||
uploadConfigButton.text = I18n.getString("termora.settings.sync.push")
|
|
||||||
} else {
|
|
||||||
downloadConfigButton.text = I18n.getString("termora.settings.sync.pull")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果失败,提示错误
|
// 如果失败,提示错误
|
||||||
@@ -1137,10 +1263,8 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
OptionPane.showMessageDialog(owner, message, messageType = JOptionPane.ERROR_MESSAGE)
|
OptionPane.showMessageDialog(owner, message, messageType = JOptionPane.ERROR_MESSAGE)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// pulled
|
|
||||||
if (!pulled) pulled = !push
|
|
||||||
|
|
||||||
|
} else {
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
sync.lastSyncTime = now
|
sync.lastSyncTime = now
|
||||||
@@ -1149,14 +1273,10 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
if (push && gistTextField.text.isBlank()) {
|
if (push && gistTextField.text.isBlank()) {
|
||||||
gistTextField.text = syncResult.map { it.config }.getOrDefault(syncConfig).gistId
|
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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1166,6 +1286,9 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
typeComboBox.addItem(SyncType.Gitee)
|
typeComboBox.addItem(SyncType.Gitee)
|
||||||
typeComboBox.addItem(SyncType.WebDAV)
|
typeComboBox.addItem(SyncType.WebDAV)
|
||||||
|
|
||||||
|
policyComboBox.addItem(SyncPolicy.Manual)
|
||||||
|
policyComboBox.addItem(SyncPolicy.OnChange)
|
||||||
|
|
||||||
hostsCheckBox.isFocusable = false
|
hostsCheckBox.isFocusable = false
|
||||||
snippetsCheckBox.isFocusable = false
|
snippetsCheckBox.isFocusable = false
|
||||||
keysCheckBox.isFocusable = false
|
keysCheckBox.isFocusable = false
|
||||||
@@ -1180,6 +1303,12 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
macrosCheckBox.isSelected = sync.rangeMacros
|
macrosCheckBox.isSelected = sync.rangeMacros
|
||||||
keymapCheckBox.isSelected = sync.rangeKeymap
|
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
|
typeComboBox.selectedItem = sync.type
|
||||||
gistTextField.text = sync.gist
|
gistTextField.text = sync.gist
|
||||||
tokenTextField.text = sync.token
|
tokenTextField.text = sync.token
|
||||||
@@ -1227,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
|
val lastSyncTime = sync.lastSyncTime
|
||||||
lastSyncTimeLabel.text = "${I18n.getString("termora.settings.sync.last-sync-time")}: ${
|
lastSyncTimeLabel.text = "${I18n.getString("termora.settings.sync.last-sync-time")}: ${
|
||||||
@@ -1237,6 +1383,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
refreshButtons()
|
refreshButtons()
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(isSelected: Boolean): Icon {
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
@@ -1254,7 +1401,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private fun getCenterComponent(): JComponent {
|
private fun getCenterComponent(): JComponent {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $formMargin, default:grow, 30dlu",
|
"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()
|
val rangeBox = FormBuilder.create()
|
||||||
@@ -1304,20 +1451,26 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
gistTextField.trailingComponent = visitGistBtn
|
gistTextField.trailingComponent = visitGistBtn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val syncPolicyBox = Box.createHorizontalBox()
|
||||||
|
syncPolicyBox.add(policyComboBox)
|
||||||
|
syncPolicyBox.add(Box.createHorizontalGlue())
|
||||||
|
syncPolicyBox.add(Box.createHorizontalGlue())
|
||||||
|
|
||||||
builder.add("${tokenText}:").xy(1, rows)
|
builder.add("${tokenText}:").xy(1, rows)
|
||||||
.add(if (isWebDAV) gistTextField else tokenTextField).xy(3, rows).apply { rows += step }
|
.add(if (isWebDAV) gistTextField else tokenTextField).xy(3, rows).apply { rows += step }
|
||||||
.add("${gistText}:").xy(1, rows)
|
.add("${gistText}:").xy(1, rows)
|
||||||
.add(if (isWebDAV) tokenTextField else gistTextField).xy(3, rows).apply { rows += step }
|
.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("${I18n.getString("termora.settings.sync.range")}:").xy(1, rows)
|
||||||
.add(rangeBox).xy(3, rows).apply { rows += step }
|
.add(rangeBox).xy(3, rows).apply { rows += step }
|
||||||
// Sync buttons
|
// Sync buttons
|
||||||
.add(
|
.add(
|
||||||
FormBuilder.create()
|
FormBuilder.create()
|
||||||
.layout(FormLayout("pref, 2dlu, pref, 2dlu, pref, 2dlu, pref", "pref"))
|
.layout(FormLayout("pref, 2dlu, pref, 2dlu, pref", "pref"))
|
||||||
.add(uploadConfigButton).xy(1, 1)
|
.add(syncConfigButton).xy(1, 1)
|
||||||
.add(downloadConfigButton).xy(3, 1)
|
.add(exportConfigButton).xy(3, 1)
|
||||||
.add(exportConfigButton).xy(5, 1)
|
.add(importConfigButton).xy(5, 1)
|
||||||
.add(importConfigButton).xy(7, 1)
|
|
||||||
.build()
|
.build()
|
||||||
).xy(3, rows, "center, fill").apply { rows += step }
|
).xy(3, rows, "center, fill").apply { rows += step }
|
||||||
.add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step }
|
.add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step }
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ open class SimpleTree : JXTree() {
|
|||||||
): Component {
|
): Component {
|
||||||
val node = value as SimpleTreeNode<*>
|
val node = value as SimpleTreeNode<*>
|
||||||
val c = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus)
|
val c = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus)
|
||||||
icon = node.getIcon(sel, expanded, hasFocus)
|
icon = node.getIcon(sel, expanded, tree.hasFocus())
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -197,6 +197,7 @@ open class SimpleTree : JXTree() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun importData(support: TransferSupport): Boolean {
|
override fun importData(support: TransferSupport): Boolean {
|
||||||
|
if (!support.isDrop) return false
|
||||||
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
|
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
|
||||||
val node = dropLocation.path.lastPathComponent as? SimpleTreeNode<*> ?: return false
|
val node = dropLocation.path.lastPathComponent as? SimpleTreeNode<*> ?: return false
|
||||||
val nodes = (support.transferable.getTransferData(MoveNodeTransferable.dataFlavor) as? List<*>)
|
val nodes = (support.transferable.getTransferData(MoveNodeTransferable.dataFlavor) as? List<*>)
|
||||||
@@ -277,7 +278,7 @@ open class SimpleTree : JXTree() {
|
|||||||
|
|
||||||
protected open fun onRenamed(node: SimpleTreeNode<*>, text: String) {}
|
protected open fun onRenamed(node: SimpleTreeNode<*>, text: String) {}
|
||||||
|
|
||||||
protected open fun refreshNode(node: SimpleTreeNode<*>) {
|
open fun refreshNode(node: SimpleTreeNode<*> = model.root) {
|
||||||
val state = TreeUtils.saveExpansionState(tree)
|
val state = TreeUtils.saveExpansionState(tree)
|
||||||
val rows = selectionRows
|
val rows = selectionRows
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,17 @@ package app.termora
|
|||||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||||
import app.termora.terminal.TerminalSize
|
import app.termora.terminal.TerminalSize
|
||||||
import kotlinx.coroutines.Dispatchers
|
import app.termora.x11.X11ChannelFactory
|
||||||
import kotlinx.coroutines.swing.Swing
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import kotlinx.coroutines.withContext
|
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.io.IOUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.sshd.client.ClientBuilder
|
import org.apache.sshd.client.ClientBuilder
|
||||||
import org.apache.sshd.client.SshClient
|
import org.apache.sshd.client.SshClient
|
||||||
|
import org.apache.sshd.client.auth.password.UserAuthPasswordFactory
|
||||||
import org.apache.sshd.client.channel.ChannelShell
|
import org.apache.sshd.client.channel.ChannelShell
|
||||||
import org.apache.sshd.client.channel.ClientChannelEvent
|
import org.apache.sshd.client.channel.ClientChannelEvent
|
||||||
import org.apache.sshd.client.config.hosts.HostConfigEntry
|
import org.apache.sshd.client.config.hosts.HostConfigEntry
|
||||||
@@ -19,44 +23,72 @@ import org.apache.sshd.client.kex.DHGClient
|
|||||||
import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier
|
import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier
|
||||||
import org.apache.sshd.client.keyverifier.ModifiedServerKeyAcceptor
|
import org.apache.sshd.client.keyverifier.ModifiedServerKeyAcceptor
|
||||||
import org.apache.sshd.client.keyverifier.ServerKeyVerifier
|
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.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.AttributeRepository
|
||||||
|
import org.apache.sshd.common.SshConstants
|
||||||
import org.apache.sshd.common.SshException
|
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.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.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.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.kex.BuiltinDHFactories
|
||||||
import org.apache.sshd.common.keyprovider.KeyIdentityProvider
|
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.common.util.net.SshdSocketAddress
|
||||||
import org.apache.sshd.core.CoreModuleProperties
|
import org.apache.sshd.core.CoreModuleProperties
|
||||||
import org.apache.sshd.server.forward.AcceptAllForwardingFilter
|
import org.apache.sshd.server.forward.AcceptAllForwardingFilter
|
||||||
import org.apache.sshd.server.forward.RejectAllForwardingFilter
|
import org.apache.sshd.server.forward.RejectAllForwardingFilter
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession
|
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient
|
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.CredentialsProvider
|
||||||
|
import org.eclipse.jgit.transport.SshConstants.IDENTITY_AGENT
|
||||||
import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
|
import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
|
||||||
import org.eclipse.jgit.transport.sshd.ProxyData
|
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Font
|
||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Proxy
|
|
||||||
import java.net.SocketAddress
|
import java.net.SocketAddress
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.swing.JOptionPane
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.*
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@Suppress("CascadeIf")
|
||||||
object SshClients {
|
object SshClients {
|
||||||
|
|
||||||
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
|
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
|
||||||
|
|
||||||
private val timeout = Duration.ofSeconds(30)
|
private val timeout = Duration.ofSeconds(30)
|
||||||
|
private val hostManager get() = HostManager.getInstance()
|
||||||
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
|
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,6 +111,12 @@ object SshClients {
|
|||||||
env.putAll(host.options.envs())
|
env.putAll(host.options.envs())
|
||||||
|
|
||||||
val channel = session.createShellChannel(configuration, env)
|
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()) {
|
if (!channel.open().verify(timeout).await()) {
|
||||||
throw SshException("Failed to open Shell")
|
throw SshException("Failed to open Shell")
|
||||||
}
|
}
|
||||||
@@ -119,16 +157,16 @@ object SshClients {
|
|||||||
* 打开一个会话
|
* 打开一个会话
|
||||||
*/
|
*/
|
||||||
fun openSession(host: Host, client: SshClient): ClientSession {
|
fun openSession(host: Host, client: SshClient): ClientSession {
|
||||||
|
val h = hostManager.getHost(host.id) ?: host
|
||||||
|
|
||||||
// 如果没有跳板机直接连接
|
// 如果没有跳板机直接连接
|
||||||
if (host.options.jumpHosts.isEmpty()) {
|
if (h.options.jumpHosts.isEmpty()) {
|
||||||
return doOpenSession(host, client)
|
return doOpenSession(h, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
val jumpHosts = mutableListOf<Host>()
|
val jumpHosts = mutableListOf<Host>()
|
||||||
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
||||||
for (jumpHostId in host.options.jumpHosts) {
|
for (jumpHostId in h.options.jumpHosts) {
|
||||||
val e = hosts[jumpHostId]
|
val e = hosts[jumpHostId]
|
||||||
if (e == null) {
|
if (e == null) {
|
||||||
if (log.isWarnEnabled) {
|
if (log.isWarnEnabled) {
|
||||||
@@ -140,7 +178,7 @@ object SshClients {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 最后一跳是目标机器
|
// 最后一跳是目标机器
|
||||||
jumpHosts.add(host)
|
jumpHosts.add(h)
|
||||||
|
|
||||||
val sessions = mutableListOf<ClientSession>()
|
val sessions = mutableListOf<ClientSession>()
|
||||||
for (i in 0 until jumpHosts.size) {
|
for (i in 0 until jumpHosts.size) {
|
||||||
@@ -186,18 +224,50 @@ object SshClients {
|
|||||||
entry.username = host.username
|
entry.username = host.username
|
||||||
entry.hostName = host.host
|
entry.hostName = host.host
|
||||||
entry.setProperty("Middleware", middleware.toString())
|
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) {
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
session.addPasswordIdentity(host.authentication.password)
|
session.addPasswordIdentity(host.authentication.password)
|
||||||
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||||
session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password)
|
session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password)
|
||||||
}
|
}
|
||||||
|
|
||||||
val verifyTimeout = Duration.ofSeconds(timeout.seconds * 5)
|
if (host.options.enableX11Forwarding) {
|
||||||
if (!session.auth().verify(verifyTimeout).await(verifyTimeout)) {
|
val segments = host.options.x11Forwarding.split(":")
|
||||||
throw SshException("Authentication failed")
|
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)
|
session.setAttribute(HOST_KEY, host)
|
||||||
@@ -241,27 +311,13 @@ object SshClients {
|
|||||||
return sshdSocketAddress
|
return sshdSocketAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun openClient(host: Host, owner: Window): Pair<SshClient, Host> {
|
fun openClient(host: Host, owner: Window): SshClient {
|
||||||
val client = openClient(host)
|
val h = hostManager.getHost(host.id) ?: host
|
||||||
var myHost = host
|
val client = openClient(h)
|
||||||
withContext(Dispatchers.Swing) {
|
client.userInteraction = TerminalUserInteraction(owner)
|
||||||
client.userInteraction = TerminalUserInteraction(owner)
|
client.serverKeyVerifier = DialogServerKeyVerifier(owner)
|
||||||
client.serverKeyVerifier = DialogServerKeyVerifier(owner)
|
client.properties["owner"] = owner
|
||||||
// 弹出授权框
|
return client
|
||||||
if (host.authentication.type == AuthenticationType.No) {
|
|
||||||
val dialog = RequestAuthenticationDialog(owner, host)
|
|
||||||
val authentication = dialog.getAuthentication()
|
|
||||||
myHost = myHost.copy(
|
|
||||||
authentication = authentication,
|
|
||||||
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
|
||||||
)
|
|
||||||
// save
|
|
||||||
if (dialog.isRemembered()) {
|
|
||||||
HostManager.getInstance().addHost(myHost)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return client to myHost
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -270,11 +326,12 @@ object SshClients {
|
|||||||
fun openClient(host: Host): SshClient {
|
fun openClient(host: Host): SshClient {
|
||||||
val builder = ClientBuilder.builder()
|
val builder = ClientBuilder.builder()
|
||||||
builder.globalRequestHandlers(listOf(KeepAliveHandler.INSTANCE))
|
builder.globalRequestHandlers(listOf(KeepAliveHandler.INSTANCE))
|
||||||
.factory { JGitSshClient() }
|
.factory { MyJGitSshClient() }
|
||||||
|
|
||||||
val keyExchangeFactories = ClientBuilder.setUpDefaultKeyExchanges(true).toMutableList()
|
val keyExchangeFactories = ClientBuilder.setUpDefaultKeyExchanges(true).toMutableList()
|
||||||
|
|
||||||
// https://github.com/TermoraDev/termora/issues/123
|
// https://github.com/TermoraDev/termora/issues/123
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
keyExchangeFactories.addAll(
|
keyExchangeFactories.addAll(
|
||||||
listOf(
|
listOf(
|
||||||
DHGClient.newFactory(BuiltinDHFactories.dhg1),
|
DHGClient.newFactory(BuiltinDHFactories.dhg1),
|
||||||
@@ -284,6 +341,24 @@ object SshClients {
|
|||||||
)
|
)
|
||||||
builder.keyExchangeFactories(keyExchangeFactories)
|
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()) {
|
if (host.tunnelings.isEmpty() && host.options.jumpHosts.isEmpty()) {
|
||||||
builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE)
|
builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE)
|
||||||
} else {
|
} else {
|
||||||
@@ -292,106 +367,338 @@ object SshClients {
|
|||||||
|
|
||||||
builder.hostConfigEntryResolver(HostConfigEntryResolver.EMPTY)
|
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
|
val sshClient = builder.build() as JGitSshClient
|
||||||
|
|
||||||
// https://github.com/TermoraDev/termora/issues/180
|
// https://github.com/TermoraDev/termora/issues/180
|
||||||
// JGit 会尝试读取本地的私钥或缓存的私钥
|
// JGit 会尝试读取本地的私钥或缓存的私钥
|
||||||
sshClient.keyIdentityProvider = KeyIdentityProvider { mutableListOf() }
|
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)
|
val heartbeatInterval = max(host.options.heartbeatInterval, 3)
|
||||||
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
|
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
|
||||||
CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true)
|
CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true)
|
||||||
|
|
||||||
sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) }
|
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()
|
sshClient.start()
|
||||||
return sshClient
|
return sshClient
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private class MyDialogServerKeyVerifier(private val owner: Window) : ServerKeyVerifier, ModifiedServerKeyAcceptor {
|
|
||||||
override fun verifyServerKey(
|
|
||||||
clientSession: ClientSession,
|
|
||||||
remoteAddress: SocketAddress,
|
|
||||||
serverKey: PublicKey
|
|
||||||
): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun acceptModifiedServerKey(
|
|
||||||
clientSession: ClientSession?,
|
|
||||||
remoteAddress: SocketAddress?,
|
|
||||||
entry: KnownHostEntry?,
|
|
||||||
expected: PublicKey?,
|
|
||||||
actual: PublicKey?
|
|
||||||
): Boolean {
|
|
||||||
val result = AtomicBoolean(false)
|
|
||||||
|
|
||||||
|
private fun ask(host: Host, owner: Window): Authentication? {
|
||||||
|
val ref = AtomicReference<Authentication>(null)
|
||||||
SwingUtilities.invokeAndWait {
|
SwingUtilities.invokeAndWait {
|
||||||
result.set(
|
val dialog = RequestAuthenticationDialog(owner, host)
|
||||||
OptionPane.showConfirmDialog(
|
dialog.setLocationRelativeTo(owner)
|
||||||
parentComponent = owner,
|
val authentication = dialog.getAuthentication().apply { ref.set(this) }
|
||||||
message = I18n.getString(
|
// save
|
||||||
"termora.host.modified-server-key",
|
if (dialog.isRemembered()) {
|
||||||
remoteAddress.toString().replace("/", StringUtils.EMPTY),
|
hostManager.addHost(
|
||||||
KeyUtils.getKeyType(expected),
|
host.copy(
|
||||||
KeyUtils.getFingerPrint(expected),
|
authentication = authentication,
|
||||||
KeyUtils.getKeyType(actual),
|
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
||||||
KeyUtils.getFingerPrint(actual),
|
)
|
||||||
),
|
)
|
||||||
optionType = JOptionPane.OK_CANCEL_OPTION,
|
|
||||||
messageType = JOptionPane.WARNING_MESSAGE,
|
|
||||||
) == JOptionPane.OK_OPTION
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DialogServerKeyVerifier(
|
|
||||||
owner: Window,
|
|
||||||
) : KnownHostsServerKeyVerifier(
|
|
||||||
MyDialogServerKeyVerifier(owner),
|
|
||||||
Paths.get(Application.getBaseDataDir().absolutePath, "known_hosts")
|
|
||||||
) {
|
|
||||||
init {
|
|
||||||
modifiedServerKeyAcceptor = delegateVerifier as ModifiedServerKeyAcceptor
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateKnownHostsFile(
|
|
||||||
clientSession: ClientSession?,
|
|
||||||
remoteAddress: SocketAddress?,
|
|
||||||
serverKey: PublicKey?,
|
|
||||||
file: Path?,
|
|
||||||
knownHosts: Collection<HostEntryPair?>?
|
|
||||||
): KnownHostEntry? {
|
|
||||||
if (clientSession is JGitClientSession) {
|
|
||||||
if (SshClients.isMiddleware(clientSession)) {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return super.updateKnownHostsFile(clientSession, remoteAddress, serverKey, file, knownHosts)
|
return ref.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class MyDialogServerKeyVerifier(private val owner: Window) : ServerKeyVerifier, ModifiedServerKeyAcceptor {
|
||||||
|
override fun verifyServerKey(
|
||||||
|
clientSession: ClientSession,
|
||||||
|
remoteAddress: SocketAddress,
|
||||||
|
serverKey: PublicKey
|
||||||
|
): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun acceptModifiedServerKey(
|
||||||
|
clientSession: ClientSession?,
|
||||||
|
remoteAddress: SocketAddress?,
|
||||||
|
entry: KnownHostEntry?,
|
||||||
|
expected: PublicKey?,
|
||||||
|
actual: PublicKey?
|
||||||
|
): Boolean {
|
||||||
|
val result = AtomicBoolean(false)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DialogServerKeyVerifier(
|
||||||
|
owner: Window,
|
||||||
|
) : KnownHostsServerKeyVerifier(
|
||||||
|
MyDialogServerKeyVerifier(owner),
|
||||||
|
Paths.get(Application.getBaseDataDir().absolutePath, "known_hosts")
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
modifiedServerKeyAcceptor = delegateVerifier as ModifiedServerKeyAcceptor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateKnownHostsFile(
|
||||||
|
clientSession: ClientSession?,
|
||||||
|
remoteAddress: SocketAddress?,
|
||||||
|
serverKey: PublicKey?,
|
||||||
|
file: Path?,
|
||||||
|
knownHosts: Collection<HostEntryPair?>?
|
||||||
|
): KnownHostEntry? {
|
||||||
|
if (clientSession is JGitClientSession) {
|
||||||
|
if (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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import app.termora.actions.AnActionEvent
|
|||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.actions.MultipleAction
|
import app.termora.actions.MultipleAction
|
||||||
import app.termora.highlight.KeywordHighlightPaintListener
|
import app.termora.highlight.KeywordHighlightPaintListener
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
import app.termora.terminal.Terminal
|
import app.termora.terminal.Terminal
|
||||||
import app.termora.terminal.panel.TerminalHyperlinkPaintListener
|
import app.termora.terminal.panel.TerminalHyperlinkPaintListener
|
||||||
@@ -40,6 +41,10 @@ class TerminalPanelFactory : Disposable {
|
|||||||
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
||||||
val writer = MyTerminalWriter(ptyConnector)
|
val writer = MyTerminalWriter(ptyConnector)
|
||||||
val terminalPanel = TerminalPanel(terminal, writer)
|
val terminalPanel = TerminalPanel(terminal, writer)
|
||||||
|
|
||||||
|
// processDeviceStatusReport
|
||||||
|
terminal.getTerminalModel().setData(DataKey.TerminalWriter, writer)
|
||||||
|
|
||||||
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
||||||
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
||||||
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
||||||
@@ -89,7 +94,7 @@ class TerminalPanelFactory : Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ interface TerminalTab : Disposable, DataProvider {
|
|||||||
*/
|
*/
|
||||||
fun canClose(): Boolean = true
|
fun canClose(): Boolean = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示可以关闭
|
||||||
|
*/
|
||||||
fun willBeClose(): Boolean = true
|
fun willBeClose(): Boolean = true
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import app.termora.actions.DataProviders
|
|||||||
import app.termora.sftp.SFTPTab
|
import app.termora.sftp.SFTPTab
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
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.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jetbrains.JBR
|
import com.jetbrains.JBR
|
||||||
import java.awt.BorderLayout
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
import java.awt.Dimension
|
import java.awt.*
|
||||||
import java.awt.Insets
|
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.awt.event.MouseListener
|
import java.awt.event.MouseListener
|
||||||
@@ -24,6 +26,7 @@ import javax.swing.SwingUtilities
|
|||||||
import javax.swing.SwingUtilities.isEventDispatchThread
|
import javax.swing.SwingUtilities.isEventDispatchThread
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
|
|
||||||
|
|
||||||
fun assertEventDispatchThread() {
|
fun assertEventDispatchThread() {
|
||||||
if (!isEventDispatchThread()) throw WrongThreadException("AWT EventQueue")
|
if (!isEventDispatchThread()) throw WrongThreadException("AWT EventQueue")
|
||||||
}
|
}
|
||||||
@@ -40,7 +43,7 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
private val dataProviderSupport = DataProviderSupport()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
private val welcomePanel = WelcomePanel(windowScope)
|
private val welcomePanel = WelcomePanel(windowScope)
|
||||||
private val sftp get() = Database.getDatabase().sftp
|
private val sftp get() = Database.getDatabase().sftp
|
||||||
private val myUI = MyFlatRootPaneUI()
|
private var notifyListeners = emptyArray<NotifyListener>()
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -60,10 +63,9 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun mouseDragged(e: MouseEvent) {
|
override fun mouseDragged(e: MouseEvent) {
|
||||||
val mouseLayer = getMouseLayer() ?: return
|
|
||||||
getMouseMotionListener()?.mouseDragged(
|
getMouseMotionListener()?.mouseDragged(
|
||||||
MouseEvent(
|
MouseEvent(
|
||||||
mouseLayer,
|
e.component,
|
||||||
e.id,
|
e.id,
|
||||||
e.`when`,
|
e.`when`,
|
||||||
e.modifiersEx,
|
e.modifiersEx,
|
||||||
@@ -84,19 +86,19 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
return getHandler() as? MouseMotionListener
|
return getHandler() as? MouseMotionListener
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMouseLayer(): JComponent? {
|
|
||||||
val titlePane = myUI.getTitlePane() ?: return null
|
|
||||||
val handlerField = titlePane.javaClass.getDeclaredField("mouseLayer") ?: return null
|
|
||||||
handlerField.isAccessible = true
|
|
||||||
return handlerField.get(titlePane) as? JComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getHandler(): Any? {
|
private fun getHandler(): Any? {
|
||||||
val titlePane = myUI.getTitlePane() ?: return null
|
val titlePane = getTitlePane() ?: return null
|
||||||
val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null
|
val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null
|
||||||
handlerField.isAccessible = true
|
handlerField.isAccessible = true
|
||||||
return handlerField.get(titlePane)
|
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().addMouseListener(mouseAdapter)
|
||||||
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
||||||
@@ -170,7 +172,6 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
// Windows 10 会有1像素误差
|
// Windows 10 会有1像素误差
|
||||||
tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0)
|
tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0)
|
||||||
} else if (SystemInfo.isLinux) {
|
} else if (SystemInfo.isLinux) {
|
||||||
rootPane.setUI(myUI)
|
|
||||||
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
|
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +211,11 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val glassPane = GlassPane()
|
||||||
|
rootPane.glassPane = glassPane
|
||||||
|
glassPane.isOpaque = false
|
||||||
|
glassPane.isVisible = true
|
||||||
|
|
||||||
|
|
||||||
Disposer.register(windowScope, terminalTabbed)
|
Disposer.register(windowScope, terminalTabbed)
|
||||||
add(terminalTabbed, BorderLayout.CENTER)
|
add(terminalTabbed, BorderLayout.CENTER)
|
||||||
@@ -239,4 +245,31 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
return id.hashCode()
|
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,10 +1,21 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.native.osx.NativeMacLibrary
|
||||||
|
import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
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 org.slf4j.LoggerFactory
|
||||||
import java.awt.Frame
|
import java.awt.Frame
|
||||||
|
import java.awt.Window
|
||||||
import java.awt.event.WindowAdapter
|
import java.awt.event.WindowAdapter
|
||||||
import java.awt.event.WindowEvent
|
import java.awt.event.WindowEvent
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.swing.JFrame
|
import javax.swing.JFrame
|
||||||
import javax.swing.JOptionPane
|
import javax.swing.JOptionPane
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
@@ -13,7 +24,8 @@ import javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE
|
|||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class TermoraFrameManager {
|
|
||||||
|
class TermoraFrameManager : Disposable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(TermoraFrameManager::class.java)
|
private val log = LoggerFactory.getLogger(TermoraFrameManager::class.java)
|
||||||
@@ -26,6 +38,8 @@ class TermoraFrameManager {
|
|||||||
|
|
||||||
private val frames = mutableListOf<TermoraFrame>()
|
private val frames = mutableListOf<TermoraFrame>()
|
||||||
private val properties get() = Database.getDatabase().properties
|
private val properties get() = Database.getDatabase().properties
|
||||||
|
private val isDisposed = AtomicBoolean(false)
|
||||||
|
private val isBackgroundRunning get() = Database.getDatabase().appearance.backgroundRunning
|
||||||
|
|
||||||
fun createWindow(): TermoraFrame {
|
fun createWindow(): TermoraFrame {
|
||||||
val frame = TermoraFrame().apply { registerCloseCallback(this) }
|
val frame = TermoraFrame().apply { registerCloseCallback(this) }
|
||||||
@@ -50,6 +64,15 @@ class TermoraFrameManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) }
|
return frame.apply { frames.add(this) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +82,7 @@ class TermoraFrameManager {
|
|||||||
|
|
||||||
|
|
||||||
private fun registerCloseCallback(window: TermoraFrame) {
|
private fun registerCloseCallback(window: TermoraFrame) {
|
||||||
|
val manager = this
|
||||||
window.addWindowListener(object : WindowAdapter() {
|
window.addWindowListener(object : WindowAdapter() {
|
||||||
override fun windowClosed(e: WindowEvent) {
|
override fun windowClosed(e: WindowEvent) {
|
||||||
|
|
||||||
@@ -74,24 +98,49 @@ class TermoraFrameManager {
|
|||||||
Disposer.dispose(windowScope)
|
Disposer.dispose(windowScope)
|
||||||
|
|
||||||
val windowScopes = ApplicationScope.windowScopes()
|
val windowScopes = ApplicationScope.windowScopes()
|
||||||
|
if (windowScopes.isNotEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 如果已经没有 Window 域了,那么就可以退出程序了
|
// 如果已经没有 Window 域了,那么就可以退出程序了
|
||||||
if (windowScopes.isEmpty()) {
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
this@TermoraFrameManager.dispose()
|
Disposer.dispose(manager)
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
// 如果 macOS 开启了后台运行,那么尽管所有窗口都没了,也不会退出
|
||||||
|
if (isBackgroundRunning) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Disposer.dispose(manager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun windowClosing(e: WindowEvent) {
|
override fun windowClosing(e: WindowEvent) {
|
||||||
if (ApplicationScope.windowScopes().size == 1) {
|
if (ApplicationScope.windowScopes().size != 1) {
|
||||||
if (OptionPane.showConfirmDialog(
|
window.dispose()
|
||||||
window,
|
return
|
||||||
I18n.getString("termora.quit-confirm", Application.getName()),
|
}
|
||||||
optionType = JOptionPane.YES_NO_OPTION,
|
|
||||||
) == JOptionPane.YES_OPTION
|
// 如果 Windows 开启了后台运行,那么最小化
|
||||||
) {
|
if (SystemInfo.isWindows && isBackgroundRunning) {
|
||||||
window.dispose()
|
// 最小化
|
||||||
}
|
window.extendedState = window.extendedState or JFrame.ICONIFIED
|
||||||
} else {
|
// 隐藏
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
if (option == JOptionPane.YES_OPTION) {
|
||||||
window.dispose()
|
window.dispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +155,7 @@ class TermoraFrameManager {
|
|||||||
if (window.extendedState and JFrame.ICONIFIED == JFrame.ICONIFIED) {
|
if (window.extendedState and JFrame.ICONIFIED == JFrame.ICONIFIED) {
|
||||||
window.extendedState = window.extendedState and JFrame.ICONIFIED.inv()
|
window.extendedState = window.extendedState and JFrame.ICONIFIED.inv()
|
||||||
}
|
}
|
||||||
|
window.isVisible = true
|
||||||
}
|
}
|
||||||
windows.last().toFront()
|
windows.last().toFront()
|
||||||
} else {
|
} else {
|
||||||
@@ -113,14 +163,16 @@ class TermoraFrameManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dispose() {
|
override fun dispose() {
|
||||||
Disposer.dispose(ApplicationScope.forApplicationScope())
|
if (isDisposed.compareAndSet(false, true)) {
|
||||||
|
Disposer.dispose(ApplicationScope.forApplicationScope())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Disposer.getTree().assertIsEmpty(true)
|
Disposer.getTree().assertIsEmpty(true)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (log.isErrorEnabled) {
|
if (log.isErrorEnabled) {
|
||||||
log.error(e.message, e)
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +196,31 @@ class TermoraFrameManager {
|
|||||||
return FrameRectangle(x, y, w, h, s)
|
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(
|
private data class FrameRectangle(
|
||||||
val x: Int, val y: Int, val w: Int, val h: Int, val s: Int
|
val x: Int, val y: Int, val w: Int, val h: Int, val s: Int
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class OutlineTextArea : FlatTextArea() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class OutlineComboBox<T> : JComboBox<T>() {
|
class OutlineComboBox<T> : FlatComboBox<T>() {
|
||||||
init {
|
init {
|
||||||
addItemListener {
|
addItemListener {
|
||||||
if (it.stateChange == ItemEvent.SELECTED) {
|
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,
|
value: Int,
|
||||||
minimum: Int,
|
minimum: Int,
|
||||||
maximum: Int,
|
maximum: Int,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import com.formdev.flatlaf.FlatLaf
|
|||||||
import com.formdev.flatlaf.extras.FlatAnimatedLafChange
|
import com.formdev.flatlaf.extras.FlatAnimatedLafChange
|
||||||
import com.jthemedetecor.OsThemeDetector
|
import com.jthemedetecor.OsThemeDetector
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -76,8 +75,7 @@ class ThemeManager private constructor() {
|
|||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@Suppress("OPT_IN_USAGE")
|
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
OsThemeDetector.getDetector().registerListener(object : Consumer<Boolean> {
|
OsThemeDetector.getDetector().registerListener(object : Consumer<Boolean> {
|
||||||
override fun accept(isDark: Boolean) {
|
override fun accept(isDark: Boolean) {
|
||||||
if (!appearance.followSystem) {
|
if (!appearance.followSystem) {
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ import java.awt.BorderLayout
|
|||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.KeyboardFocusManager
|
import java.awt.KeyboardFocusManager
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.*
|
||||||
import java.awt.event.ComponentAdapter
|
|
||||||
import java.awt.event.ComponentEvent
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.event.DocumentEvent
|
import javax.swing.event.DocumentEvent
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
@@ -219,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() {
|
private fun perform() {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class AppUpdateAction private constructor() : AnAction(
|
|||||||
StringUtils.EMPTY,
|
StringUtils.EMPTY,
|
||||||
Icons.ideUpdate
|
Icons.ideUpdate
|
||||||
) {
|
) {
|
||||||
|
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Swing)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(AppUpdateAction::class.java)
|
private val log = LoggerFactory.getLogger(AppUpdateAction::class.java)
|
||||||
@@ -59,7 +60,6 @@ class AppUpdateAction private constructor() : AnAction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
private fun scheduleUpdate() {
|
private fun scheduleUpdate() {
|
||||||
fixedRateTimer(
|
fixedRateTimer(
|
||||||
name = "check-update-timer",
|
name = "check-update-timer",
|
||||||
@@ -67,7 +67,7 @@ class AppUpdateAction private constructor() : AnAction(
|
|||||||
period = 5.hours.inWholeMilliseconds, daemon = true
|
period = 5.hours.inWholeMilliseconds, daemon = true
|
||||||
) {
|
) {
|
||||||
if (!isRemindMeNextTime) {
|
if (!isRemindMeNextTime) {
|
||||||
GlobalScope.launch(Dispatchers.IO) { supervisorScope { launch { checkUpdate() } } }
|
coroutineScope.launch(Dispatchers.IO) { checkUpdate() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
import app.termora.*
|
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() {
|
class OpenHostAction : AnAction() {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -26,10 +39,70 @@ class OpenHostAction : AnAction() {
|
|||||||
Protocol.SSH -> SSHTerminalTab(windowScope, evt.host)
|
Protocol.SSH -> SSHTerminalTab(windowScope, evt.host)
|
||||||
Protocol.Serial -> SerialTerminalTab(windowScope, evt.host)
|
Protocol.Serial -> SerialTerminalTab(windowScope, evt.host)
|
||||||
Protocol.SFTPPty -> SFTPPtyTerminalTab(windowScope, evt.host)
|
Protocol.SFTPPty -> SFTPPtyTerminalTab(windowScope, evt.host)
|
||||||
|
Protocol.RDP -> openRDP(windowScope, evt.host)
|
||||||
else -> LocalTerminalTab(windowScope, evt.host)
|
else -> LocalTerminalTab(windowScope, evt.host)
|
||||||
}
|
}
|
||||||
|
|
||||||
terminalTabbedManager.addTerminalTab(tab)
|
if (tab is TerminalTab) {
|
||||||
tab.start()
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,6 +51,7 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
|
|||||||
val customBtn = JButton("Custom")
|
val customBtn = JButton("Custom")
|
||||||
customBtn.addActionListener {
|
customBtn.addActionListener {
|
||||||
val dialog = MyColorPickerDialog(this)
|
val dialog = MyColorPickerDialog(this)
|
||||||
|
dialog.setLocationRelativeTo(this)
|
||||||
dialog.colorPicker.color = defaultColor
|
dialog.colorPicker.color = defaultColor
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
val color = dialog.color
|
val color = dialog.color
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ data class KeywordHighlight(
|
|||||||
*/
|
*/
|
||||||
val matchCase: Boolean = false,
|
val matchCase: Boolean = false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否是正则表达式
|
||||||
|
*/
|
||||||
|
val regex: Boolean = false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 0 是取前景色
|
* 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 model = KeywordHighlightTableModel()
|
||||||
private val table = FlatTable()
|
private val table = FlatTable()
|
||||||
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
|
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
|
||||||
private val colorPalette by lazy {
|
private val terminal by lazy { TerminalFactory.getInstance().createTerminal() }
|
||||||
TerminalFactory.getInstance().createTerminal().getTerminalModel()
|
private val colorPalette by lazy { terminal.getTerminalModel().getColorPalette() }
|
||||||
.getColorPalette()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
|
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
|
||||||
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
|
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
|
||||||
@@ -130,6 +128,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
|
|
||||||
addBtn.addActionListener {
|
addBtn.addActionListener {
|
||||||
val dialog = NewKeywordHighlightDialog(this, colorPalette)
|
val dialog = NewKeywordHighlightDialog(this, colorPalette)
|
||||||
|
dialog.setLocationRelativeTo(this)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
val keywordHighlight = dialog.keywordHighlight
|
val keywordHighlight = dialog.keywordHighlight
|
||||||
if (keywordHighlight != null) {
|
if (keywordHighlight != null) {
|
||||||
@@ -143,6 +142,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
if (row > -1) {
|
if (row > -1) {
|
||||||
var keywordHighlight = model.getKeywordHighlight(row)
|
var keywordHighlight = model.getKeywordHighlight(row)
|
||||||
val dialog = NewKeywordHighlightDialog(this, colorPalette)
|
val dialog = NewKeywordHighlightDialog(this, colorPalette)
|
||||||
|
dialog.setLocationRelativeTo(this)
|
||||||
dialog.keywordTextField.text = keywordHighlight.keyword
|
dialog.keywordTextField.text = keywordHighlight.keyword
|
||||||
dialog.descriptionTextField.text = keywordHighlight.description
|
dialog.descriptionTextField.text = keywordHighlight.description
|
||||||
|
|
||||||
@@ -176,6 +176,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
dialog.underlineCheckBox.isSelected = keywordHighlight.underline
|
dialog.underlineCheckBox.isSelected = keywordHighlight.underline
|
||||||
dialog.lineThroughCheckBox.isSelected = keywordHighlight.lineThrough
|
dialog.lineThroughCheckBox.isSelected = keywordHighlight.lineThrough
|
||||||
dialog.matchCaseBtn.isSelected = keywordHighlight.matchCase
|
dialog.matchCaseBtn.isSelected = keywordHighlight.matchCase
|
||||||
|
dialog.regexBtn.isSelected = keywordHighlight.regex
|
||||||
|
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
|
|
||||||
@@ -201,7 +202,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
for (row in rows) {
|
for (row in rows) {
|
||||||
val id = model.getKeywordHighlight(row).id
|
val id = model.getKeywordHighlight(row).id
|
||||||
keywordHighlightManager.removeKeywordHighlight(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
|
editBtn.isEnabled = table.selectedRowCount > 0
|
||||||
deleteBtn.isEnabled = editBtn.isEnabled
|
deleteBtn.isEnabled = editBtn.isEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Disposer.register(disposable, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
terminal.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createCenterPanel(): JComponent {
|
override fun createCenterPanel(): JComponent {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app.termora.highlight
|
|||||||
|
|
||||||
import app.termora.ApplicationScope
|
import app.termora.ApplicationScope
|
||||||
import app.termora.Database
|
import app.termora.Database
|
||||||
|
import app.termora.DeleteDataManager
|
||||||
import app.termora.TerminalPanelFactory
|
import app.termora.TerminalPanelFactory
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ class KeywordHighlightManager private constructor() {
|
|||||||
database.removeKeywordHighlight(id)
|
database.removeKeywordHighlight(id)
|
||||||
keywordHighlights.remove(id)
|
keywordHighlights.remove(id)
|
||||||
TerminalPanelFactory.getInstance().repaintAll()
|
TerminalPanelFactory.getInstance().repaintAll()
|
||||||
|
DeleteDataManager.getInstance().removeKeywordHighlight(id)
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Keyword highlighter removed. {}", id)
|
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.TerminalDisplay
|
||||||
import app.termora.terminal.panel.TerminalPaintListener
|
import app.termora.terminal.panel.TerminalPaintListener
|
||||||
import app.termora.terminal.panel.TerminalPanel
|
import app.termora.terminal.panel.TerminalPanel
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.Graphics
|
import java.awt.Graphics
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
@@ -18,9 +19,10 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val tag = Random.nextInt()
|
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(
|
override fun before(
|
||||||
offset: Int,
|
offset: Int,
|
||||||
@@ -36,7 +38,8 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
|
|||||||
}
|
}
|
||||||
|
|
||||||
val document = terminal.getDocument()
|
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 var index = offset + 1
|
||||||
private val maxCount = min(index + count, document.getLineCount())
|
private val maxCount = min(index + count, document.getLineCount())
|
||||||
override fun hasNext(): Boolean {
|
override fun hasNext(): Boolean {
|
||||||
@@ -46,8 +49,24 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
|
|||||||
override fun next(): TerminalLine {
|
override fun next(): TerminalLine {
|
||||||
return document.getLine(index++)
|
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) {
|
for (kind in kinds) {
|
||||||
terminal.getMarkupModel().addHighlighter(
|
terminal.getMarkupModel().addHighlighter(
|
||||||
@@ -77,6 +96,74 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
|
|||||||
terminal.getMarkupModel().removeAllHighlighters(tag)
|
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(
|
private class KeywordHighlightHighlighter(
|
||||||
range: HighlighterRange, terminal: Terminal,
|
range: HighlighterRange, terminal: Terminal,
|
||||||
@@ -94,3 +181,5 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
package app.termora.highlight
|
package app.termora.highlight
|
||||||
|
|
||||||
import app.termora.DialogWrapper
|
import app.termora.*
|
||||||
import app.termora.DynamicColor
|
|
||||||
import app.termora.I18n
|
|
||||||
import app.termora.Icons
|
|
||||||
import app.termora.Database
|
|
||||||
import app.termora.terminal.ColorPalette
|
import app.termora.terminal.ColorPalette
|
||||||
import app.termora.terminal.TerminalColor
|
import app.termora.terminal.TerminalColor
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
@@ -46,6 +42,7 @@ class NewKeywordHighlightDialog(
|
|||||||
I18n.getString("termora.highlight.background-color")
|
I18n.getString("termora.highlight.background-color")
|
||||||
)
|
)
|
||||||
val matchCaseBtn = JToggleButton(Icons.matchCase)
|
val matchCaseBtn = JToggleButton(Icons.matchCase)
|
||||||
|
val regexBtn = JToggleButton(Icons.regex)
|
||||||
|
|
||||||
|
|
||||||
private val textColorRevert = JButton(Icons.revert)
|
private val textColorRevert = JButton(Icons.revert)
|
||||||
@@ -85,6 +82,7 @@ class NewKeywordHighlightDialog(
|
|||||||
|
|
||||||
val box = FlatToolBar()
|
val box = FlatToolBar()
|
||||||
box.add(matchCaseBtn)
|
box.add(matchCaseBtn)
|
||||||
|
box.add(regexBtn)
|
||||||
keywordTextField.trailingComponent = box
|
keywordTextField.trailingComponent = box
|
||||||
|
|
||||||
repaintKeywordHighlightView()
|
repaintKeywordHighlightView()
|
||||||
@@ -187,6 +185,7 @@ class NewKeywordHighlightDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createColorPanel(color: Color, title: String): ColorPanel {
|
private fun createColorPanel(color: Color, title: String): ColorPanel {
|
||||||
|
val owner = this
|
||||||
val arc = UIManager.getInt("Component.arc")
|
val arc = UIManager.getInt("Component.arc")
|
||||||
val lineBorder = FlatLineBorder(Insets(1, 1, 1, 1), DynamicColor.BorderColor, 1f, arc)
|
val lineBorder = FlatLineBorder(Insets(1, 1, 1, 1), DynamicColor.BorderColor, 1f, arc)
|
||||||
val colorPanel = ColorPanel(color)
|
val colorPanel = ColorPanel(color)
|
||||||
@@ -195,7 +194,8 @@ class NewKeywordHighlightDialog(
|
|||||||
colorPanel.addMouseListener(object : MouseAdapter() {
|
colorPanel.addMouseListener(object : MouseAdapter() {
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
if (SwingUtilities.isLeftMouseButton(e)) {
|
if (SwingUtilities.isLeftMouseButton(e)) {
|
||||||
val dialog = ChooseColorTemplateDialog(this@NewKeywordHighlightDialog, title)
|
val dialog = ChooseColorTemplateDialog(owner, title)
|
||||||
|
dialog.setLocationRelativeTo(owner)
|
||||||
dialog.defaultColor = colorPanel.color
|
dialog.defaultColor = colorPanel.color
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
colorPanel.color = dialog.color ?: return
|
colorPanel.color = dialog.color ?: return
|
||||||
@@ -218,6 +218,7 @@ class NewKeywordHighlightDialog(
|
|||||||
keyword = keywordTextField.text,
|
keyword = keywordTextField.text,
|
||||||
description = descriptionTextField.text,
|
description = descriptionTextField.text,
|
||||||
matchCase = matchCaseBtn.isSelected,
|
matchCase = matchCaseBtn.isSelected,
|
||||||
|
regex = regexBtn.isSelected,
|
||||||
textColor = if (textColor.colorIndex != -1) textColor.colorIndex else textColor.color.toRGB(),
|
textColor = if (textColor.colorIndex != -1) textColor.colorIndex else textColor.color.toRGB(),
|
||||||
backgroundColor = if (backgroundColor.colorIndex != -1) backgroundColor.colorIndex else backgroundColor.color.toRGB(),
|
backgroundColor = if (backgroundColor.colorIndex != -1) backgroundColor.colorIndex else backgroundColor.color.toRGB(),
|
||||||
bold = boldCheckBox.isSelected,
|
bold = boldCheckBox.isSelected,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.keymap
|
package app.termora.keymap
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.event.KeyEvent
|
import java.awt.event.KeyEvent
|
||||||
import javax.swing.KeyStroke
|
import javax.swing.KeyStroke
|
||||||
@@ -23,7 +24,14 @@ class KeyShortcut(val keyStroke: KeyStroke) : Shortcut() {
|
|||||||
text = text.replace("MINUS", "-")
|
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
|
package app.termora.keymap
|
||||||
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import javax.swing.KeyStroke
|
import javax.swing.KeyStroke
|
||||||
|
|
||||||
@@ -12,6 +11,10 @@ open class Keymap(
|
|||||||
*/
|
*/
|
||||||
private val parent: Keymap?,
|
private val parent: Keymap?,
|
||||||
val isReadonly: Boolean = false,
|
val isReadonly: Boolean = false,
|
||||||
|
/**
|
||||||
|
* 修改时间
|
||||||
|
*/
|
||||||
|
var updateDate: Long = 0L,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -23,7 +26,8 @@ open class Keymap(
|
|||||||
val shortcuts = mutableListOf<Keymap>()
|
val shortcuts = mutableListOf<Keymap>()
|
||||||
val name = json["name"]?.jsonPrimitive?.content ?: return null
|
val name = json["name"]?.jsonPrimitive?.content ?: return null
|
||||||
val readonly = json["readonly"]?.jsonPrimitive?.booleanOrNull ?: 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 }) {
|
for (shortcut in (json["shortcuts"]?.jsonArray ?: emptyList()).map { it.jsonObject }) {
|
||||||
val keyStroke = shortcut["keyStroke"]?.jsonPrimitive?.contentOrNull ?: continue
|
val keyStroke = shortcut["keyStroke"]?.jsonPrimitive?.contentOrNull ?: continue
|
||||||
@@ -40,6 +44,9 @@ open class Keymap(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 最后设置修改时间
|
||||||
|
keymap.updateDate = updateDate
|
||||||
|
|
||||||
shortcuts.add(keymap)
|
shortcuts.add(keymap)
|
||||||
return keymap
|
return keymap
|
||||||
}
|
}
|
||||||
@@ -51,6 +58,7 @@ open class Keymap(
|
|||||||
val actionIds = shortcuts.getOrPut(shortcut) { mutableListOf() }
|
val actionIds = shortcuts.getOrPut(shortcut) { mutableListOf() }
|
||||||
actionIds.removeIf { it == actionId }
|
actionIds.removeIf { it == actionId }
|
||||||
actionIds.add(actionId)
|
actionIds.add(actionId)
|
||||||
|
updateDate = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun removeAllActionShortcuts(actionId: Any) {
|
open fun removeAllActionShortcuts(actionId: Any) {
|
||||||
@@ -62,6 +70,7 @@ open class Keymap(
|
|||||||
iterator.remove()
|
iterator.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateDate = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getShortcut(actionId: Any): List<Shortcut> {
|
open fun getShortcut(actionId: Any): List<Shortcut> {
|
||||||
@@ -102,6 +111,7 @@ open class Keymap(
|
|||||||
return buildJsonObject {
|
return buildJsonObject {
|
||||||
put("name", name)
|
put("name", name)
|
||||||
put("readonly", isReadonly)
|
put("readonly", isReadonly)
|
||||||
|
put("updateDate", updateDate)
|
||||||
parent?.let { put("parent", it.name) }
|
parent?.let { put("parent", it.name) }
|
||||||
put("shortcuts", buildJsonArray {
|
put("shortcuts", buildJsonArray {
|
||||||
for (entry in shortcuts.entries) {
|
for (entry in shortcuts.entries) {
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
fun removeKeymap(name: String) {
|
fun removeKeymap(name: String) {
|
||||||
keymaps.remove(name)
|
keymaps.remove(name)
|
||||||
database.removeKeymap(name)
|
database.removeKeymap(name)
|
||||||
|
DeleteDataManager.getInstance().removeKeymap(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class KeymapKeyEventDispatcher : KeyEventDispatcher {
|
private inner class KeymapKeyEventDispatcher : KeyEventDispatcher {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app.termora.keymgr
|
|||||||
|
|
||||||
import app.termora.ApplicationScope
|
import app.termora.ApplicationScope
|
||||||
import app.termora.Database
|
import app.termora.Database
|
||||||
|
import app.termora.DeleteDataManager
|
||||||
|
|
||||||
class KeyManager private constructor() {
|
class KeyManager private constructor() {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -29,6 +30,7 @@ class KeyManager private constructor() {
|
|||||||
fun removeOhKeyPair(id: String) {
|
fun removeOhKeyPair(id: String) {
|
||||||
keyPairs.removeIf { it.id == id }
|
keyPairs.removeIf { it.id == id }
|
||||||
database.removeKeyPair(id)
|
database.removeKeyPair(id)
|
||||||
|
DeleteDataManager.getInstance().removeKeyPair(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOhKeyPairs(): List<OhKeyPair> {
|
fun getOhKeyPairs(): List<OhKeyPair> {
|
||||||
@@ -39,9 +41,4 @@ class KeyManager private constructor() {
|
|||||||
return keyPairs.findLast { it.id == id }
|
return keyPairs.findLast { it.id == id }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAll() {
|
|
||||||
keyPairs.clear()
|
|
||||||
database.removeAllKeyPair()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.termora.keymgr
|
package app.termora.keymgr
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.AES.decodeBase64
|
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.native.FileChooser
|
import app.termora.native.FileChooser
|
||||||
@@ -13,7 +12,6 @@ import com.formdev.flatlaf.ui.FlatTextBorder
|
|||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
|
||||||
import org.apache.commons.codec.binary.Base64
|
import org.apache.commons.codec.binary.Base64
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.apache.commons.io.file.PathUtils
|
import org.apache.commons.io.file.PathUtils
|
||||||
@@ -33,7 +31,6 @@ import java.io.File
|
|||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.spec.X509EncodedKeySpec
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
@@ -313,15 +310,9 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
nameTextField.text = ohKeyPair.name
|
nameTextField.text = ohKeyPair.name
|
||||||
remarkTextField.text = ohKeyPair.remark
|
remarkTextField.text = ohKeyPair.remark
|
||||||
val baos = ByteArrayOutputStream()
|
val baos = ByteArrayOutputStream()
|
||||||
if (ohKeyPair.type == "RSA") {
|
val keyPair = OhKeyPairKeyPairProvider.generateKeyPair(ohKeyPair)
|
||||||
OpenSSHKeyPairResourceWriter.INSTANCE
|
OpenSSHKeyPairResourceWriter.INSTANCE
|
||||||
.writePublicKey(RSA.generatePublic(ohKeyPair.publicKey.decodeBase64()), null, baos)
|
.writePublicKey(keyPair.public, null, baos)
|
||||||
} else if (ohKeyPair.type == "ED25519") {
|
|
||||||
OpenSSHKeyPairResourceWriter.INSTANCE.writePublicKey(
|
|
||||||
EdDSAPublicKey(X509EncodedKeySpec(ohKeyPair.publicKey.decodeBase64())),
|
|
||||||
null, baos
|
|
||||||
)
|
|
||||||
}
|
|
||||||
publicKeyTextArea.text = baos.toString()
|
publicKeyTextArea.text = baos.toString()
|
||||||
savePublicKeyBtn.isEnabled = true
|
savePublicKeyBtn.isEnabled = true
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ data class OhKeyPair(
|
|||||||
val remark: String,
|
val remark: String,
|
||||||
val length: Int,
|
val length: Int,
|
||||||
val sort: Long,
|
val sort: Long,
|
||||||
|
val updateDate: Long = System.currentTimeMillis(),
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val empty = OhKeyPair(String(), String(), String(), String(), String(), String(), 0, 0)
|
val empty = OhKeyPair(String(), String(), String(), String(), String(), String(), 0, 0)
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ package app.termora.keymgr
|
|||||||
|
|
||||||
import app.termora.AES.decodeBase64
|
import app.termora.AES.decodeBase64
|
||||||
import app.termora.RSA
|
import app.termora.RSA
|
||||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
|
||||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
|
||||||
import org.apache.sshd.common.keyprovider.AbstractResourceKeyPairProvider
|
import org.apache.sshd.common.keyprovider.AbstractResourceKeyPairProvider
|
||||||
import org.apache.sshd.common.session.SessionContext
|
import org.apache.sshd.common.session.SessionContext
|
||||||
|
import org.apache.sshd.common.util.security.eddsa.Ed25519PublicKeyDecoder
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.security.Key
|
import java.security.Key
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
@@ -25,7 +24,7 @@ class OhKeyPairKeyPairProvider(private val id: String) : AbstractResourceKeyPair
|
|||||||
val publicKey = cache.getOrPut(ohKeyPair.publicKey) {
|
val publicKey = cache.getOrPut(ohKeyPair.publicKey) {
|
||||||
when (ohKeyPair.type) {
|
when (ohKeyPair.type) {
|
||||||
"RSA" -> RSA.generatePublic(ohKeyPair.publicKey.decodeBase64())
|
"RSA" -> RSA.generatePublic(ohKeyPair.publicKey.decodeBase64())
|
||||||
"ED25519" -> EdDSAPublicKey(X509EncodedKeySpec(ohKeyPair.publicKey.decodeBase64()))
|
"ED25519" -> Ed25519PublicKeyDecoder.INSTANCE.generatePublicKey((X509EncodedKeySpec(ohKeyPair.publicKey.decodeBase64())))
|
||||||
else -> throw UnsupportedOperationException("${ohKeyPair.type} is not supported")
|
else -> throw UnsupportedOperationException("${ohKeyPair.type} is not supported")
|
||||||
}
|
}
|
||||||
} as PublicKey
|
} as PublicKey
|
||||||
@@ -33,7 +32,7 @@ class OhKeyPairKeyPairProvider(private val id: String) : AbstractResourceKeyPair
|
|||||||
val privateKey = cache.getOrPut(ohKeyPair.privateKey) {
|
val privateKey = cache.getOrPut(ohKeyPair.privateKey) {
|
||||||
when (ohKeyPair.type) {
|
when (ohKeyPair.type) {
|
||||||
"RSA" -> RSA.generatePrivate(ohKeyPair.privateKey.decodeBase64())
|
"RSA" -> RSA.generatePrivate(ohKeyPair.privateKey.decodeBase64())
|
||||||
"ED25519" -> EdDSAPrivateKey(PKCS8EncodedKeySpec(ohKeyPair.privateKey.decodeBase64()))
|
"ED25519" -> Ed25519PublicKeyDecoder.INSTANCE.generatePrivateKey(PKCS8EncodedKeySpec(ohKeyPair.privateKey.decodeBase64()))
|
||||||
else -> throw UnsupportedOperationException("${ohKeyPair.type} is not supported")
|
else -> throw UnsupportedOperationException("${ohKeyPair.type} is not supported")
|
||||||
}
|
}
|
||||||
} as PrivateKey
|
} as PrivateKey
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class SSHCopyIdDialog(
|
|||||||
terminalPanelFactory.createTerminalPanel(terminal, PtyConnectorDelegate())
|
terminalPanelFactory.createTerminalPanel(terminal, PtyConnectorDelegate())
|
||||||
.apply { enableFloatingToolbar = false }
|
.apply { enableFloatingToolbar = false }
|
||||||
}
|
}
|
||||||
private val coroutineScope = CoroutineScope(Job() + Dispatchers.IO)
|
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
size = Dimension(UIManager.getInt("Dialog.width") - 100, UIManager.getInt("Dialog.height") - 100)
|
size = Dimension(UIManager.getInt("Dialog.width") - 100, UIManager.getInt("Dialog.height") - 100)
|
||||||
@@ -144,7 +144,7 @@ class SSHCopyIdDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val client = SshClients.openClient(host).apply { myClient = this }
|
val client = SshClients.openClient(host, this).apply { myClient = this }
|
||||||
client.userInteraction = TerminalUserInteraction(owner)
|
client.userInteraction = TerminalUserInteraction(owner)
|
||||||
val session = SshClients.openSession(host, client).apply { mySession = this }
|
val session = SshClients.openSession(host, client).apply { mySession = this }
|
||||||
val channel =
|
val channel =
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ data class Macro(
|
|||||||
* 越大越靠前
|
* 越大越靠前
|
||||||
*/
|
*/
|
||||||
val sort: Long = System.currentTimeMillis(),
|
val sort: Long = System.currentTimeMillis(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
val updateDate: Long = System.currentTimeMillis(),
|
||||||
) {
|
) {
|
||||||
val macroByteArray by lazy { macro.decodeBase64() }
|
val macroByteArray by lazy { macro.decodeBase64() }
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package app.termora.macro
|
|||||||
|
|
||||||
import app.termora.ApplicationScope
|
import app.termora.ApplicationScope
|
||||||
import app.termora.Database
|
import app.termora.Database
|
||||||
|
import app.termora.DeleteDataManager
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,6 +39,7 @@ class MacroManager private constructor() {
|
|||||||
fun removeMacro(id: String) {
|
fun removeMacro(id: String) {
|
||||||
database.removeMacro(id)
|
database.removeMacro(id)
|
||||||
macros.remove(id)
|
macros.remove(id)
|
||||||
|
DeleteDataManager.getInstance().removeMacro(id)
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Removed macro $id")
|
log.debug("Removed macro $id")
|
||||||
|
|||||||
9
src/main/kotlin/app/termora/sftp/FileSystemProvider.kt
Normal file
9
src/main/kotlin/app/termora/sftp/FileSystemProvider.kt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package app.termora.sftp
|
||||||
|
|
||||||
|
import org.apache.commons.vfs2.FileSystem
|
||||||
|
|
||||||
|
|
||||||
|
interface FileSystemProvider {
|
||||||
|
fun getFileSystem(): FileSystem
|
||||||
|
fun setFileSystem(fileSystem: FileSystem)
|
||||||
|
}
|
||||||
@@ -4,7 +4,12 @@ import app.termora.Icons
|
|||||||
import app.termora.assertEventDispatchThread
|
import app.termora.assertEventDispatchThread
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.FlatTextField
|
import com.formdev.flatlaf.extras.components.FlatTextField
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.SystemUtils
|
||||||
|
import org.apache.commons.vfs2.FileObject
|
||||||
|
import org.apache.commons.vfs2.VFS
|
||||||
|
import org.apache.commons.vfs2.provider.local.LocalFileSystem
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
@@ -14,8 +19,7 @@ import java.awt.event.ActionEvent
|
|||||||
import java.awt.event.ActionListener
|
import java.awt.event.ActionListener
|
||||||
import java.awt.event.ItemEvent
|
import java.awt.event.ItemEvent
|
||||||
import java.awt.event.ItemListener
|
import java.awt.event.ItemListener
|
||||||
import java.nio.file.FileSystem
|
import java.nio.file.FileSystems
|
||||||
import java.nio.file.Path
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.event.PopupMenuEvent
|
import javax.swing.event.PopupMenuEvent
|
||||||
import javax.swing.event.PopupMenuListener
|
import javax.swing.event.PopupMenuListener
|
||||||
@@ -23,8 +27,8 @@ import javax.swing.filechooser.FileSystemView
|
|||||||
import kotlin.io.path.absolutePathString
|
import kotlin.io.path.absolutePathString
|
||||||
|
|
||||||
class FileSystemViewNav(
|
class FileSystemViewNav(
|
||||||
private val fileSystem: FileSystem,
|
private val fileSystemProvider: FileSystemProvider,
|
||||||
private val homeDirectory: Path
|
private val homeDirectory: FileObject
|
||||||
) : JPanel(BorderLayout()) {
|
) : JPanel(BorderLayout()) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -38,7 +42,7 @@ class FileSystemViewNav(
|
|||||||
private val history = linkedSetOf<String>()
|
private val history = linkedSetOf<String>()
|
||||||
private val layeredPane = LayeredPane()
|
private val layeredPane = LayeredPane()
|
||||||
private val downBtn = JButton(Icons.chevronDown)
|
private val downBtn = JButton(Icons.chevronDown)
|
||||||
private val comboBox = object : JComboBox<Path>() {
|
private val comboBox = object : JComboBox<FileObject>() {
|
||||||
override fun getLocationOnScreen(): Point {
|
override fun getLocationOnScreen(): Point {
|
||||||
val point = super.getLocationOnScreen()
|
val point = super.getLocationOnScreen()
|
||||||
point.y -= 1
|
point.y -= 1
|
||||||
@@ -80,7 +84,7 @@ class FileSystemViewNav(
|
|||||||
): Component {
|
): Component {
|
||||||
val c = super.getListCellRendererComponent(
|
val c = super.getListCellRendererComponent(
|
||||||
list,
|
list,
|
||||||
value,
|
if (value is FileObject) formatDisplayPath(value) else value.toString(),
|
||||||
index,
|
index,
|
||||||
isSelected,
|
isSelected,
|
||||||
cellHasFocus
|
cellHasFocus
|
||||||
@@ -99,12 +103,12 @@ class FileSystemViewNav(
|
|||||||
add(layeredPane, BorderLayout.CENTER)
|
add(layeredPane, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
|
||||||
if (fileSystem.isWindows()) {
|
if (SystemInfo.isWindows && fileSystemProvider.getFileSystem() is LocalFileSystem) {
|
||||||
try {
|
try {
|
||||||
for (root in fileSystemView.roots) {
|
for (root in fileSystemView.roots) {
|
||||||
history.add(root.absolutePath)
|
history.add(root.absolutePath)
|
||||||
}
|
}
|
||||||
for (rootDirectory in fileSystem.rootDirectories) {
|
for (rootDirectory in FileSystems.getDefault().rootDirectories) {
|
||||||
history.add(rootDirectory.absolutePathString())
|
history.add(rootDirectory.absolutePathString())
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -115,12 +119,16 @@ class FileSystemViewNav(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun formatDisplayPath(file: FileObject): String {
|
||||||
|
return file.absolutePathString()
|
||||||
|
}
|
||||||
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
|
|
||||||
val itemListener = ItemListener { e ->
|
val itemListener = ItemListener { e ->
|
||||||
if (e.stateChange == ItemEvent.SELECTED) {
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
val item = comboBox.selectedItem
|
val item = comboBox.selectedItem
|
||||||
if (item is Path) {
|
if (item is FileObject) {
|
||||||
changeSelectedPath(item)
|
changeSelectedPath(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,8 +174,17 @@ class FileSystemViewNav(
|
|||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
val name = textField.text.trim()
|
val name = textField.text.trim()
|
||||||
if (name.isBlank()) return
|
if (name.isBlank()) return
|
||||||
|
val fileSystem = fileSystemProvider.getFileSystem()
|
||||||
try {
|
try {
|
||||||
changeSelectedPath(fileSystem.getPath(name))
|
if (fileSystem is LocalFileSystem && SystemUtils.IS_OS_WINDOWS) {
|
||||||
|
val file = VFS.getManager().resolveFile("file://${name}")
|
||||||
|
if (!StringUtils.equals(file.fileSystem.rootURI, fileSystemProvider.getFileSystem().rootURI)) {
|
||||||
|
fileSystemProvider.setFileSystem(file.fileSystem)
|
||||||
|
}
|
||||||
|
changeSelectedPath(file)
|
||||||
|
} else {
|
||||||
|
changeSelectedPath(fileSystem.resolveFile(name))
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (log.isErrorEnabled) {
|
if (log.isErrorEnabled) {
|
||||||
log.error(e.message, e)
|
log.error(e.message, e)
|
||||||
@@ -180,9 +197,14 @@ class FileSystemViewNav(
|
|||||||
private fun showComboBoxPopup() {
|
private fun showComboBoxPopup() {
|
||||||
|
|
||||||
comboBox.removeAllItems()
|
comboBox.removeAllItems()
|
||||||
|
val fileSystem = fileSystemProvider.getFileSystem()
|
||||||
|
|
||||||
for (text in history) {
|
for (text in history) {
|
||||||
val path = fileSystem.getPath(text)
|
val path = if (SystemInfo.isWindows && fileSystem is LocalFileSystem) {
|
||||||
|
VFS.getManager().resolveFile("file://${text}")
|
||||||
|
} else {
|
||||||
|
fileSystem.resolveFile(text)
|
||||||
|
}
|
||||||
comboBox.addItem(path)
|
comboBox.addItem(path)
|
||||||
if (text == textField.text) {
|
if (text == textField.text) {
|
||||||
comboBox.selectedItem = path
|
comboBox.selectedItem = path
|
||||||
@@ -218,15 +240,22 @@ class FileSystemViewNav(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSelectedPath(): Path {
|
fun getSelectedPath(): FileObject {
|
||||||
return textField.getClientProperty(PATH) as Path
|
return textField.getClientProperty(PATH) as FileObject
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeSelectedPath(path: Path) {
|
fun changeSelectedPath(file: FileObject) {
|
||||||
assertEventDispatchThread()
|
assertEventDispatchThread()
|
||||||
|
|
||||||
textField.text = path.absolutePathString()
|
textField.text = formatDisplayPath(file)
|
||||||
textField.putClientProperty(PATH, path)
|
textField.putClientProperty(PATH, file)
|
||||||
|
|
||||||
|
val fileSystem = fileSystemProvider.getFileSystem()
|
||||||
|
if (SystemInfo.isWindows && fileSystem is LocalFileSystem) {
|
||||||
|
if (!StringUtils.equals(fileSystem.rootURI, file.fileSystem.rootURI)) {
|
||||||
|
fileSystemProvider.setFileSystem(file.fileSystem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (listener in listenerList.getListeners(ActionListener::class.java)) {
|
for (listener in listenerList.getListeners(ActionListener::class.java)) {
|
||||||
listener.actionPerformed(ActionEvent(this, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
listener.actionPerformed(ActionEvent(this, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
||||||
|
|||||||
@@ -3,35 +3,38 @@ package app.termora.sftp
|
|||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
|
import app.termora.vfs2.sftp.MySftpFileSystem
|
||||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
import kotlinx.coroutines.*
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
import org.apache.commons.vfs2.FileObject
|
||||||
|
import org.apache.commons.vfs2.FileSystem
|
||||||
|
import org.apache.commons.vfs2.VFS
|
||||||
|
import org.apache.commons.vfs2.provider.local.LocalFileSystem
|
||||||
import org.jdesktop.swingx.JXBusyLabel
|
import org.jdesktop.swingx.JXBusyLabel
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.event.*
|
import java.awt.event.*
|
||||||
import java.nio.file.FileSystem
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.StandardCopyOption
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import kotlin.io.path.absolutePathString
|
|
||||||
import kotlin.io.path.name
|
|
||||||
|
|
||||||
class FileSystemViewPanel(
|
class FileSystemViewPanel(
|
||||||
val host: Host,
|
val host: Host,
|
||||||
val fileSystem: FileSystem,
|
private var fileSystem: FileSystem,
|
||||||
private val transportManager: TransportManager,
|
private val transportManager: TransportManager,
|
||||||
private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
|
private val coroutineScope: CoroutineScope,
|
||||||
) : JPanel(BorderLayout()), Disposable, DataProvider {
|
) : JPanel(BorderLayout()), Disposable, DataProvider, FileSystemProvider {
|
||||||
|
|
||||||
private val properties get() = Database.getDatabase().properties
|
private val properties get() = Database.getDatabase().properties
|
||||||
private val sftp get() = Database.getDatabase().sftp
|
private val sftp get() = Database.getDatabase().sftp
|
||||||
private val table = FileSystemViewTable(fileSystem, transportManager, coroutineScope)
|
private val table = FileSystemViewTable(this, transportManager, coroutineScope)
|
||||||
private val disposed = AtomicBoolean(false)
|
private val disposed = AtomicBoolean(false)
|
||||||
private var nextReloadTicks = emptyArray<Consumer<Unit>>()
|
private var nextReloadTicks = emptyArray<Consumer<Unit>>()
|
||||||
private val isLoading = AtomicBoolean(false)
|
private val isLoading = AtomicBoolean(false)
|
||||||
@@ -39,7 +42,7 @@ class FileSystemViewPanel(
|
|||||||
private val loadingPanel = LoadingPanel()
|
private val loadingPanel = LoadingPanel()
|
||||||
private val layeredPane = LayeredPane()
|
private val layeredPane = LayeredPane()
|
||||||
private val homeDirectory = getHomeDirectory()
|
private val homeDirectory = getHomeDirectory()
|
||||||
private val nav = FileSystemViewNav(fileSystem, homeDirectory)
|
private val nav = FileSystemViewNav(this, homeDirectory)
|
||||||
private var workdir = homeDirectory
|
private var workdir = homeDirectory
|
||||||
private val model get() = table.model as FileSystemViewTableModel
|
private val model get() = table.model as FileSystemViewTableModel
|
||||||
private val showHiddenFilesKey = "termora.transport.host.${host.id}.show-hidden-files"
|
private val showHiddenFilesKey = "termora.transport.host.${host.id}.show-hidden-files"
|
||||||
@@ -100,7 +103,7 @@ class FileSystemViewPanel(
|
|||||||
override fun onTransportChanged(transport: Transport) {
|
override fun onTransportChanged(transport: Transport) {
|
||||||
val path = transport.target.parent ?: return
|
val path = transport.target.parent ?: return
|
||||||
if (path.fileSystem != fileSystem) return
|
if (path.fileSystem != fileSystem) return
|
||||||
if (path.absolutePathString() != workdir.absolutePathString()) return
|
if (path.name.path != workdir.name.path) return
|
||||||
// 立即刷新
|
// 立即刷新
|
||||||
reload(true)
|
reload(true)
|
||||||
}
|
}
|
||||||
@@ -123,19 +126,19 @@ class FileSystemViewPanel(
|
|||||||
|
|
||||||
private fun enterTableSelectionFolder(row: Int = table.selectedRow) {
|
private fun enterTableSelectionFolder(row: Int = table.selectedRow) {
|
||||||
if (row < 0 || isLoading.get()) return
|
if (row < 0 || isLoading.get()) return
|
||||||
val attr = model.getAttr(row)
|
val file = model.getFileObject(row)
|
||||||
if (attr.isFile) return
|
if (file.isFile) return
|
||||||
|
|
||||||
// 当前工作目录
|
// 当前工作目录
|
||||||
val workdir = getWorkdir()
|
val workdir = getWorkdir()
|
||||||
|
|
||||||
// 返回上级之后,选中上级目录
|
// 返回上级之后,选中上级目录
|
||||||
if (attr.name == "..") {
|
if (row == 0 && model.hasParent) {
|
||||||
val workdirName = workdir.name
|
val workdirName = workdir.name
|
||||||
nextReloadTickSelection(workdirName)
|
nextReloadTickSelection(workdirName.baseName)
|
||||||
}
|
}
|
||||||
|
|
||||||
changeWorkdir(attr.path)
|
changeWorkdir(file)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,13 +172,21 @@ class FileSystemViewPanel(
|
|||||||
bookmarkBtn.addActionListener { e ->
|
bookmarkBtn.addActionListener { e ->
|
||||||
if (e.actionCommand.isNullOrBlank()) {
|
if (e.actionCommand.isNullOrBlank()) {
|
||||||
if (bookmarkBtn.isBookmark) {
|
if (bookmarkBtn.isBookmark) {
|
||||||
bookmarkBtn.deleteBookmark(workdir.toString())
|
bookmarkBtn.deleteBookmark(workdir.absolutePathString())
|
||||||
} else {
|
} else {
|
||||||
bookmarkBtn.addBookmark(workdir.toString())
|
bookmarkBtn.addBookmark(workdir.absolutePathString())
|
||||||
}
|
}
|
||||||
bookmarkBtn.isBookmark = !bookmarkBtn.isBookmark
|
bookmarkBtn.isBookmark = !bookmarkBtn.isBookmark
|
||||||
} else {
|
} else {
|
||||||
changeWorkdir(fileSystem.getPath(e.actionCommand))
|
if (fileSystem is LocalFileSystem && SystemUtils.IS_OS_WINDOWS) {
|
||||||
|
val file = VFS.getManager().resolveFile("file://${e.actionCommand}")
|
||||||
|
if (!StringUtils.equals(file.fileSystem.rootURI, fileSystem.rootURI)) {
|
||||||
|
fileSystem = file.fileSystem
|
||||||
|
}
|
||||||
|
changeWorkdir(file)
|
||||||
|
} else {
|
||||||
|
changeWorkdir(fileSystem.resolveFile(e.actionCommand))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,14 +205,12 @@ class FileSystemViewPanel(
|
|||||||
button.addActionListener(object : AbstractAction() {
|
button.addActionListener(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
if (model.rowCount < 1) return
|
if (model.rowCount < 1) return
|
||||||
val attr = model.getAttr(0)
|
if (model.hasParent) enterTableSelectionFolder(0)
|
||||||
if (attr !is FileSystemViewTableModel.ParentAttr) return
|
|
||||||
enterTableSelectionFolder(0)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
addPropertyChangeListener("workdir") {
|
addPropertyChangeListener("workdir") {
|
||||||
button.isEnabled = model.rowCount > 0 && model.getAttr(0) is FileSystemViewTableModel.ParentAttr
|
button.isEnabled = model.rowCount > 0 && model.hasParent
|
||||||
}
|
}
|
||||||
|
|
||||||
return button
|
return button
|
||||||
@@ -211,7 +220,7 @@ class FileSystemViewPanel(
|
|||||||
// 创建成功之后需要修改和选中
|
// 创建成功之后需要修改和选中
|
||||||
registerNextReloadTick {
|
registerNextReloadTick {
|
||||||
for (i in 0 until table.rowCount) {
|
for (i in 0 until table.rowCount) {
|
||||||
if (model.getAttr(i).name == name) {
|
if (model.getFileObject(i).name.baseName == name) {
|
||||||
table.addRowSelectionInterval(i, i)
|
table.addRowSelectionInterval(i, i)
|
||||||
table.scrollRectToVisible(table.getCellRect(i, 0, true))
|
table.scrollRectToVisible(table.getCellRect(i, 0, true))
|
||||||
consumer.accept(i)
|
consumer.accept(i)
|
||||||
@@ -221,18 +230,19 @@ class FileSystemViewPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeWorkdir(workdir: Path) {
|
private fun changeWorkdir(workdir: FileObject) {
|
||||||
assertEventDispatchThread()
|
assertEventDispatchThread()
|
||||||
nav.changeSelectedPath(workdir)
|
nav.changeSelectedPath(workdir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun renameTo(oldPath: Path, newPath: Path) {
|
fun renameTo(oldPath: FileObject, newPath: FileObject) {
|
||||||
|
|
||||||
// 新建文件夹
|
// 新建文件夹
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
|
|
||||||
if (requestLoading()) {
|
if (requestLoading()) {
|
||||||
try {
|
try {
|
||||||
Files.move(oldPath, newPath, StandardCopyOption.ATOMIC_MOVE)
|
oldPath.moveTo(newPath)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
@@ -247,7 +257,7 @@ class FileSystemViewPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建成功之后需要选中
|
// 创建成功之后需要选中
|
||||||
nextReloadTickSelection(newPath.name)
|
nextReloadTickSelection(newPath.name.baseName)
|
||||||
|
|
||||||
// 立即刷新
|
// 立即刷新
|
||||||
reload()
|
reload()
|
||||||
@@ -258,7 +268,7 @@ class FileSystemViewPanel(
|
|||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
if (requestLoading()) {
|
if (requestLoading()) {
|
||||||
try {
|
try {
|
||||||
doNewFolderOrFile(getWorkdir().resolve(name), isFile)
|
doNewFolderOrFile(getWorkdir().resolveFile(name), isFile)
|
||||||
} finally {
|
} finally {
|
||||||
stopLoading()
|
stopLoading()
|
||||||
}
|
}
|
||||||
@@ -273,9 +283,9 @@ class FileSystemViewPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private suspend fun doNewFolderOrFile(path: Path, isFile: Boolean) {
|
private suspend fun doNewFolderOrFile(path: FileObject, isFile: Boolean) {
|
||||||
|
|
||||||
if (Files.exists(path)) {
|
if (path.exists()) {
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
owner,
|
owner,
|
||||||
@@ -288,7 +298,7 @@ class FileSystemViewPanel(
|
|||||||
|
|
||||||
// 创建文件夹
|
// 创建文件夹
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
runCatching { if (isFile) Files.createFile(path) else Files.createDirectories(path) }.onFailure {
|
runCatching { if (isFile) path.createFile() else path.createFolder() }.onFailure {
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
if (it is Exception) {
|
if (it is Exception) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
@@ -329,7 +339,7 @@ class FileSystemViewPanel(
|
|||||||
|
|
||||||
fun reload(rememberSelection: Boolean = false) {
|
fun reload(rememberSelection: Boolean = false) {
|
||||||
if (!requestLoading()) return
|
if (!requestLoading()) return
|
||||||
if (fileSystem.isSFTP()) loadingPanel.start()
|
if (fileSystem is MySftpFileSystem) loadingPanel.start()
|
||||||
val oldWorkdir = workdir
|
val oldWorkdir = workdir
|
||||||
val path = nav.getSelectedPath()
|
val path = nav.getSelectedPath()
|
||||||
|
|
||||||
@@ -338,7 +348,7 @@ class FileSystemViewPanel(
|
|||||||
|
|
||||||
if (rememberSelection) {
|
if (rememberSelection) {
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
table.selectedRows.sortedDescending().map { model.getAttr(it).name }
|
table.selectedRows.sortedDescending().map { model.getFileObject(it).name.baseName }
|
||||||
.forEach { nextReloadTickSelection(it) }
|
.forEach { nextReloadTickSelection(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -347,7 +357,7 @@ class FileSystemViewPanel(
|
|||||||
if (it is Exception) {
|
if (it is Exception) {
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
owner, ExceptionUtils.getMessage(it),
|
owner, ExceptionUtils.getRootCauseMessage(it),
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -367,34 +377,41 @@ class FileSystemViewPanel(
|
|||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
stopLoading()
|
stopLoading()
|
||||||
if (fileSystem.isSFTP()) {
|
if (fileSystem is MySftpFileSystem) {
|
||||||
withContext(Dispatchers.Swing) { loadingPanel.stop() }
|
withContext(Dispatchers.Swing) { loadingPanel.stop() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getHomeDirectory(): Path {
|
private fun getHomeDirectory(): FileObject {
|
||||||
if (fileSystem.isSFTP()) {
|
val fileSystem = this.fileSystem
|
||||||
val fs = fileSystem as SftpFileSystem
|
if (fileSystem is MySftpFileSystem) {
|
||||||
val host = fs.session.getAttribute(SshClients.HOST_KEY) ?: return fs.defaultDir
|
val host = fileSystem.getClientSession().getAttribute(SshClients.HOST_KEY)
|
||||||
|
?: return fileSystem.resolveFile(fileSystem.getDefaultDir())
|
||||||
val defaultDirectory = host.options.sftpDefaultDirectory
|
val defaultDirectory = host.options.sftpDefaultDirectory
|
||||||
if (defaultDirectory.isNotBlank()) {
|
if (defaultDirectory.isNotBlank()) {
|
||||||
return runCatching { fs.getPath(defaultDirectory) }
|
return fileSystem.resolveFile(defaultDirectory)
|
||||||
.getOrElse { fs.defaultDir }
|
|
||||||
}
|
}
|
||||||
return fs.defaultDir
|
return fileSystem.resolveFile(fileSystem.getDefaultDir())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sftp.defaultDirectory.isNotBlank()) {
|
if (sftp.defaultDirectory.isNotBlank()) {
|
||||||
return runCatching { fileSystem.getPath(sftp.defaultDirectory) }
|
val resolveFile = if (fileSystem is LocalFileSystem && SystemInfo.isWindows) {
|
||||||
.getOrElse { fileSystem.getPath(SystemUtils.USER_HOME) }
|
VFS.getManager().resolveFile("file://${sftp.defaultDirectory}")
|
||||||
|
} else {
|
||||||
|
fileSystem.resolveFile("file://${sftp.defaultDirectory}")
|
||||||
|
}
|
||||||
|
if (resolveFile.exists()) {
|
||||||
|
setFileSystem(resolveFile.fileSystem)
|
||||||
|
return resolveFile
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileSystem.getPath(SystemUtils.USER_HOME)
|
return fileSystem.resolveFile("file://${SystemUtils.USER_HOME}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getWorkdir(): Path {
|
fun getWorkdir(): FileObject {
|
||||||
return workdir
|
return workdir
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,6 +439,7 @@ class FileSystemViewPanel(
|
|||||||
child.changeStatus(TransportStatus.Failed)
|
child.changeStatus(TransportStatus.Failed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fileSystem.fileSystemManager.filesCache.clear(fileSystem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,6 +448,14 @@ class FileSystemViewPanel(
|
|||||||
return if (dataKey == SFTPDataProviders.FileSystemViewTable) table as T else null
|
return if (dataKey == SFTPDataProviders.FileSystemViewTable) table as T else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getFileSystem(): FileSystem {
|
||||||
|
return fileSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setFileSystem(fileSystem: FileSystem) {
|
||||||
|
this.fileSystem = fileSystem
|
||||||
|
}
|
||||||
|
|
||||||
private class LoadingPanel : JPanel() {
|
private class LoadingPanel : JPanel() {
|
||||||
private val busyLabel = JXBusyLabel()
|
private val busyLabel = JXBusyLabel()
|
||||||
|
|
||||||
|
|||||||
@@ -3,21 +3,27 @@ package app.termora.sftp
|
|||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.actions.SettingsAction
|
import app.termora.actions.SettingsAction
|
||||||
|
import app.termora.sftp.FileSystemViewTable.AskTransfer.Action
|
||||||
|
import app.termora.vfs2.VFSWalker
|
||||||
|
import app.termora.vfs2.sftp.MySftpFileObject
|
||||||
|
import app.termora.vfs2.sftp.MySftpFileSystem
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import org.apache.commons.io.FileUtils
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import org.apache.sshd.sftp.client.SftpClient
|
import org.apache.commons.vfs2.FileObject
|
||||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
import org.apache.commons.vfs2.VFS
|
||||||
import org.apache.sshd.sftp.client.fs.SftpPath
|
import org.apache.commons.vfs2.provider.local.LocalFileSystem
|
||||||
import org.apache.sshd.sftp.client.fs.SftpPosixFileAttributes
|
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
|
import java.awt.Dimension
|
||||||
import java.awt.Insets
|
import java.awt.Insets
|
||||||
import java.awt.datatransfer.DataFlavor
|
import java.awt.datatransfer.DataFlavor
|
||||||
import java.awt.datatransfer.StringSelection
|
import java.awt.datatransfer.StringSelection
|
||||||
@@ -27,7 +33,10 @@ import java.awt.event.*
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.nio.file.*
|
import java.nio.file.FileVisitResult
|
||||||
|
import java.nio.file.FileVisitor
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
import java.nio.file.attribute.BasicFileAttributes
|
import java.nio.file.attribute.BasicFileAttributes
|
||||||
import java.text.MessageFormat
|
import java.text.MessageFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -36,13 +45,29 @@ import java.util.regex.Pattern
|
|||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.table.DefaultTableCellRenderer
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
import kotlin.collections.ArrayDeque
|
import kotlin.collections.ArrayDeque
|
||||||
import kotlin.io.path.*
|
import kotlin.collections.List
|
||||||
|
import kotlin.collections.all
|
||||||
|
import kotlin.collections.contains
|
||||||
|
import kotlin.collections.filter
|
||||||
|
import kotlin.collections.filterIsInstance
|
||||||
|
import kotlin.collections.find
|
||||||
|
import kotlin.collections.forEach
|
||||||
|
import kotlin.collections.isEmpty
|
||||||
|
import kotlin.collections.isNotEmpty
|
||||||
|
import kotlin.collections.last
|
||||||
|
import kotlin.collections.listOf
|
||||||
|
import kotlin.collections.map
|
||||||
|
import kotlin.collections.mapOf
|
||||||
|
import kotlin.collections.mutableListOf
|
||||||
|
import kotlin.collections.sortedArray
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
import kotlin.math.max
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|
||||||
@Suppress("DuplicatedCode")
|
@Suppress("DuplicatedCode", "CascadeIf")
|
||||||
class FileSystemViewTable(
|
class FileSystemViewTable(
|
||||||
private val fileSystem: FileSystem,
|
private val fileSystemProvider: FileSystemProvider,
|
||||||
private val transportManager: TransportManager,
|
private val transportManager: TransportManager,
|
||||||
private val coroutineScope: CoroutineScope
|
private val coroutineScope: CoroutineScope
|
||||||
) : JTable(), Disposable {
|
) : JTable(), Disposable {
|
||||||
@@ -99,8 +124,8 @@ class FileSystemViewTable(
|
|||||||
): Component {
|
): Component {
|
||||||
foreground = null
|
foreground = null
|
||||||
val c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column)
|
val c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column)
|
||||||
icon = if (column == FileSystemViewTableModel.COLUMN_NAME) model.getAttr(row).icon else null
|
icon = if (column == FileSystemViewTableModel.COLUMN_NAME) model.getFileIcon(row) else null
|
||||||
foreground = if (!isSelected && model.getAttr(row).isHidden)
|
foreground = if (!isSelected && model.getFileObject(row).isHidden)
|
||||||
UIManager.getColor("textInactiveText") else foreground
|
UIManager.getColor("textInactiveText") else foreground
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@@ -131,14 +156,14 @@ class FileSystemViewTable(
|
|||||||
table.requestFocusInWindow()
|
table.requestFocusInWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
showContextMenu(rows, e)
|
showContextMenu(rows.sortedArray(), e)
|
||||||
} else if (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
} else if (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||||
val row = table.selectedRow
|
val row = table.selectedRow
|
||||||
if (row <= 0 || row >= table.rowCount) return
|
if (row <= 0 || row >= table.rowCount) return
|
||||||
val attr = model.getAttr(row)
|
val file = model.getFileObject(row)
|
||||||
if (attr.isDirectory) return
|
if (file.isFolder) return
|
||||||
// 传输
|
// 传输
|
||||||
transfer(arrayOf(attr))
|
transfer(listOf(file))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -150,8 +175,7 @@ class FileSystemViewTable(
|
|||||||
if ((SystemInfo.isMacOS && e.keyCode == KeyEvent.VK_BACK_SPACE) || (e.keyCode == KeyEvent.VK_DELETE)) {
|
if ((SystemInfo.isMacOS && e.keyCode == KeyEvent.VK_BACK_SPACE) || (e.keyCode == KeyEvent.VK_DELETE)) {
|
||||||
val rows = selectedRows
|
val rows = selectedRows
|
||||||
if (rows.contains(0)) return
|
if (rows.contains(0)) return
|
||||||
val attrs = rows.map { model.getAttr(it) }.toTypedArray()
|
val files = rows.map { model.getFileObject(it) }
|
||||||
val files = attrs.map { it.path }.toTypedArray()
|
|
||||||
deletePaths(files, false)
|
deletePaths(files, false)
|
||||||
} else if (!SystemInfo.isMacOS && e.keyCode == KeyEvent.VK_F5) {
|
} else if (!SystemInfo.isMacOS && e.keyCode == KeyEvent.VK_F5) {
|
||||||
fileSystemViewPanel.reload(true)
|
fileSystemViewPanel.reload(true)
|
||||||
@@ -167,13 +191,15 @@ class FileSystemViewTable(
|
|||||||
// 如果不是新增行,并且光标不在第一列,那么不允许
|
// 如果不是新增行,并且光标不在第一列,那么不允许
|
||||||
if (!dropLocation.isInsertRow && dropLocation.column != FileSystemViewTableModel.COLUMN_NAME) return false
|
if (!dropLocation.isInsertRow && dropLocation.column != FileSystemViewTableModel.COLUMN_NAME) return false
|
||||||
// 如果不是新增行,如果在一个文件上,那么不允许
|
// 如果不是新增行,如果在一个文件上,那么不允许
|
||||||
if (!dropLocation.isInsertRow && model.getAttr(dropLocation.row).isFile) return false
|
if (!dropLocation.isInsertRow && model.getFileObject(dropLocation.row).isFile) return false
|
||||||
|
// 如果不是新增行,在 .. 上面,不允许
|
||||||
|
if (!dropLocation.isInsertRow && model.hasParent && dropLocation.row == 0) return false
|
||||||
|
|
||||||
if (support.isDataFlavorSupported(FileSystemTableRowTransferable.dataFlavor)) {
|
if (support.isDataFlavorSupported(FileSystemTableRowTransferable.dataFlavor)) {
|
||||||
val data = support.transferable.getTransferData(FileSystemTableRowTransferable.dataFlavor)
|
val data = support.transferable.getTransferData(FileSystemTableRowTransferable.dataFlavor)
|
||||||
return data is FileSystemTableRowTransferable && data.source != table
|
return data is FileSystemTableRowTransferable && data.source != table
|
||||||
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||||
return !fileSystem.isLocal()
|
return fileSystemProvider.getFileSystem() !is LocalFileSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -184,32 +210,30 @@ class FileSystemViewTable(
|
|||||||
// 如果不是新增行,并且光标不在第一列,那么不允许
|
// 如果不是新增行,并且光标不在第一列,那么不允许
|
||||||
if (!dropLocation.isInsertRow && dropLocation.column != FileSystemViewTableModel.COLUMN_NAME) return false
|
if (!dropLocation.isInsertRow && dropLocation.column != FileSystemViewTableModel.COLUMN_NAME) return false
|
||||||
// 如果不是新增行,如果在一个文件上,那么不允许
|
// 如果不是新增行,如果在一个文件上,那么不允许
|
||||||
if (!dropLocation.isInsertRow && model.getAttr(dropLocation.row).isFile) return false
|
if (!dropLocation.isInsertRow && model.getFileObject(dropLocation.row).isFile) return false
|
||||||
|
|
||||||
var targetWorkdir: Path? = null
|
var targetWorkdir: FileObject? = null
|
||||||
|
|
||||||
// 变更工作目录
|
// 变更工作目录
|
||||||
if (!dropLocation.isInsertRow) {
|
if (!dropLocation.isInsertRow) {
|
||||||
targetWorkdir = model.getAttr(dropLocation.row).path
|
targetWorkdir = model.getFileObject(dropLocation.row)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (support.isDataFlavorSupported(FileSystemTableRowTransferable.dataFlavor)) {
|
if (support.isDataFlavorSupported(FileSystemTableRowTransferable.dataFlavor)) {
|
||||||
val data = support.transferable.getTransferData(FileSystemTableRowTransferable.dataFlavor)
|
val data = support.transferable.getTransferData(FileSystemTableRowTransferable.dataFlavor)
|
||||||
if (data !is FileSystemTableRowTransferable) return false
|
if (data !is FileSystemTableRowTransferable) return false
|
||||||
// 委托源表开始传输
|
// 委托源表开始传输
|
||||||
data.source.transfer(data.attrs.toTypedArray(), false, targetWorkdir)
|
data.source.transfer(data.files, false, targetWorkdir)
|
||||||
return true
|
return true
|
||||||
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||||
val files = support.transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*>
|
val files = support.transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*>
|
||||||
if (files.isEmpty()) return false
|
if (files.isEmpty()) return false
|
||||||
val paths = files.filterIsInstance<File>()
|
val paths = files.filterIsInstance<File>().map { VFS.getManager().resolveFile(it.toURI()) }
|
||||||
.map { FileSystemViewTableModel.Attr(it.toPath()) }
|
|
||||||
.toTypedArray()
|
|
||||||
if (paths.isEmpty()) return false
|
if (paths.isEmpty()) return false
|
||||||
val localTarget = sftpPanel.getLocalTarget()
|
val localTarget = sftpPanel.getLocalTarget()
|
||||||
val table = localTarget.getData(SFTPDataProviders.FileSystemViewTable) ?: return false
|
val table = localTarget.getData(SFTPDataProviders.FileSystemViewTable) ?: return false
|
||||||
// 委托最左侧的本地文件系统传输
|
// 委托最左侧的本地文件系统传输
|
||||||
table.transfer(paths, true, targetWorkdir)
|
table.transfer(paths, true, targetWorkdir, fileSystemViewPanel)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -220,9 +244,9 @@ class FileSystemViewTable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun createTransferable(c: JComponent?): Transferable? {
|
override fun createTransferable(c: JComponent?): Transferable? {
|
||||||
val attrs = table.selectedRows.filter { it != 0 }.map { model.getAttr(it) }
|
val files = table.selectedRows.filter { it != 0 }.map { model.getFileObject(it) }
|
||||||
if (attrs.isEmpty()) return null
|
if (files.isEmpty()) return null
|
||||||
return FileSystemTableRowTransferable(table, attrs)
|
return FileSystemTableRowTransferable(table, files)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +261,7 @@ class FileSystemViewTable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun navigate(row: Int, c: Char): Boolean {
|
private fun navigate(row: Int, c: Char): Boolean {
|
||||||
val name = model.getAttr(row).name
|
val name = model.getFileObject(row).name.baseName
|
||||||
if (name.startsWith(c, true)) {
|
if (name.startsWith(c, true)) {
|
||||||
clearSelection()
|
clearSelection()
|
||||||
addRowSelectionInterval(row, row)
|
addRowSelectionInterval(row, row)
|
||||||
@@ -249,19 +273,10 @@ class FileSystemViewTable(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun dispose() {
|
|
||||||
if (isDisposed.compareAndSet(false, true)) {
|
|
||||||
if (!fileSystem.isSFTP()) {
|
|
||||||
coroutineScope.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showContextMenu(rows: IntArray, e: MouseEvent) {
|
private fun showContextMenu(rows: IntArray, e: MouseEvent) {
|
||||||
val attrs = rows.map { model.getAttr(it) }.toTypedArray()
|
val files = rows.map { model.getFileObject(it) }
|
||||||
val files = attrs.map { it.path }.toTypedArray()
|
|
||||||
val hasParent = rows.contains(0)
|
val hasParent = rows.contains(0)
|
||||||
|
val fileSystem = fileSystemProvider.getFileSystem()
|
||||||
|
|
||||||
val popupMenu = FlatPopupMenu()
|
val popupMenu = FlatPopupMenu()
|
||||||
val newMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.new"))
|
val newMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.new"))
|
||||||
@@ -273,13 +288,13 @@ class FileSystemViewTable(
|
|||||||
val transfer = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.transfer"))
|
val transfer = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.transfer"))
|
||||||
// 编辑
|
// 编辑
|
||||||
val edit = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.edit"))
|
val edit = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.edit"))
|
||||||
edit.isEnabled = fileSystem.isSFTP() && attrs.all { it.isFile }
|
edit.isEnabled = fileSystem is MySftpFileSystem && files.all { it.isFile }
|
||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
// 复制路径
|
// 复制路径
|
||||||
val copyPath = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.copy-path"))
|
val copyPath = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.copy-path"))
|
||||||
|
|
||||||
// 如果是本地,那么支持打开本地路径
|
// 如果是本地,那么支持打开本地路径
|
||||||
if (fileSystem.isLocal()) {
|
if (fileSystem is LocalFileSystem) {
|
||||||
popupMenu.add(
|
popupMenu.add(
|
||||||
I18n.getString(
|
I18n.getString(
|
||||||
"termora.transport.table.contextmenu.open-in-folder",
|
"termora.transport.table.contextmenu.open-in-folder",
|
||||||
@@ -288,7 +303,7 @@ class FileSystemViewTable(
|
|||||||
else I18n.getString("termora.folder")
|
else I18n.getString("termora.folder")
|
||||||
)
|
)
|
||||||
).addActionListener {
|
).addActionListener {
|
||||||
Application.browseInFolder(files.last().toFile())
|
Application.browseInFolder(File(files.last().absolutePathString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -301,18 +316,15 @@ class FileSystemViewTable(
|
|||||||
val delete = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.delete"))
|
val delete = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.delete"))
|
||||||
// rm -rf
|
// rm -rf
|
||||||
val rmrf = popupMenu.add(JMenuItem("rm -rf", Icons.warningIntroduction))
|
val rmrf = popupMenu.add(JMenuItem("rm -rf", Icons.warningIntroduction))
|
||||||
|
|
||||||
// 只有 SFTP 可以
|
// 只有 SFTP 可以
|
||||||
if (!fileSystem.isSFTP()) {
|
rmrf.isVisible = fileSystem is MySftpFileSystem
|
||||||
rmrf.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改权限
|
// 修改权限
|
||||||
val permission = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.change-permissions"))
|
val permission = popupMenu.add(I18n.getString("termora.transport.table.contextmenu.change-permissions"))
|
||||||
permission.isEnabled = false
|
permission.isEnabled = false
|
||||||
|
|
||||||
// 如果是本地系统文件,那么不允许修改权限,用户应该自己修改
|
// 如果是本地系统文件,那么不允许修改权限,用户应该自己修改
|
||||||
if (fileSystem.isSFTP() && rows.isNotEmpty()) {
|
if (fileSystem is MySftpFileSystem && rows.isNotEmpty()) {
|
||||||
permission.isEnabled = true
|
permission.isEnabled = true
|
||||||
}
|
}
|
||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
@@ -354,45 +366,20 @@ class FileSystemViewTable(
|
|||||||
})
|
})
|
||||||
copyPath.addActionListener {
|
copyPath.addActionListener {
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
attrs.forEach { sb.append(it.path.absolutePathString()).appendLine() }
|
files.forEach { sb.append(it.absolutePathString()).appendLine() }
|
||||||
sb.deleteCharAt(sb.length - 1)
|
sb.deleteCharAt(sb.length - 1)
|
||||||
toolkit.systemClipboard.setContents(StringSelection(sb.toString()), null)
|
toolkit.systemClipboard.setContents(StringSelection(sb.toString()), null)
|
||||||
}
|
}
|
||||||
edit.addActionListener { if (files.isNotEmpty()) editFiles(files) }
|
edit.addActionListener { if (files.isNotEmpty()) editFiles(files) }
|
||||||
permission.addActionListener(object : AbstractAction() {
|
permission.addActionListener(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
val last = attrs.last()
|
val last = files.last()
|
||||||
val dialog = PosixFilePermissionDialog(
|
if (last !is MySftpFileObject) return
|
||||||
SwingUtilities.getWindowAncestor(table),
|
changePermission(last)
|
||||||
last.posixFilePermissions
|
|
||||||
)
|
|
||||||
val permissions = dialog.open() ?: return
|
|
||||||
|
|
||||||
if (fileSystemViewPanel.requestLoading()) {
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
|
||||||
val c = runCatching { Files.setPosixFilePermissions(last.path, permissions) }.onFailure {
|
|
||||||
withContext(Dispatchers.Swing) {
|
|
||||||
OptionPane.showMessageDialog(
|
|
||||||
owner,
|
|
||||||
ExceptionUtils.getMessage(it),
|
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop loading
|
|
||||||
fileSystemViewPanel.stopLoading()
|
|
||||||
|
|
||||||
// reload
|
|
||||||
if (c.isSuccess) {
|
|
||||||
fileSystemViewPanel.reload(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
refresh.addActionListener { fileSystemViewPanel.reload() }
|
refresh.addActionListener { fileSystemViewPanel.reload() }
|
||||||
transfer.addActionListener { transfer(attrs) }
|
transfer.addActionListener { transfer(files) }
|
||||||
|
|
||||||
if (rows.isEmpty() || hasParent) {
|
if (rows.isEmpty() || hasParent) {
|
||||||
transfer.isEnabled = false
|
transfer.isEnabled = false
|
||||||
@@ -410,16 +397,90 @@ class FileSystemViewTable(
|
|||||||
popupMenu.show(table, e.x, e.y)
|
popupMenu.show(table, e.x, e.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun changePermission(file: MySftpFileObject) {
|
||||||
|
|
||||||
|
val dialog = PosixFilePermissionDialog(
|
||||||
|
SwingUtilities.getWindowAncestor(table),
|
||||||
|
model.getFilePermissions(file)
|
||||||
|
)
|
||||||
|
val permissions = dialog.open() ?: return
|
||||||
|
val isIncludeSubdirectories = dialog.isIncludeSubdirectories()
|
||||||
|
|
||||||
|
if (fileSystemViewPanel.requestLoading()) {
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
val c = runCatching {
|
||||||
|
file.setPosixFilePermissions(permissions)
|
||||||
|
if (isIncludeSubdirectories && file.isFolder) {
|
||||||
|
file.refresh()
|
||||||
|
VFSWalker.walk(file, object : FileVisitor<FileObject> {
|
||||||
|
override fun preVisitDirectory(
|
||||||
|
dir: FileObject,
|
||||||
|
attrs: BasicFileAttributes
|
||||||
|
): FileVisitResult {
|
||||||
|
dir.refresh()
|
||||||
|
if (dir is MySftpFileObject) {
|
||||||
|
dir.setPosixFilePermissions(permissions)
|
||||||
|
}
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitFile(
|
||||||
|
file: FileObject,
|
||||||
|
attrs: BasicFileAttributes
|
||||||
|
): FileVisitResult {
|
||||||
|
if (file is MySftpFileObject) {
|
||||||
|
file.setPosixFilePermissions(permissions)
|
||||||
|
}
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitFileFailed(
|
||||||
|
file: FileObject,
|
||||||
|
exc: IOException
|
||||||
|
): FileVisitResult {
|
||||||
|
return FileVisitResult.TERMINATE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun postVisitDirectory(
|
||||||
|
dir: FileObject,
|
||||||
|
exc: IOException?
|
||||||
|
): FileVisitResult {
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
ExceptionUtils.getMessage(it),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop loading
|
||||||
|
fileSystemViewPanel.stopLoading()
|
||||||
|
|
||||||
|
// reload
|
||||||
|
if (c.isSuccess) {
|
||||||
|
fileSystemViewPanel.reload(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun renameSelection() {
|
private fun renameSelection() {
|
||||||
val index = selectedRow
|
val index = selectedRow
|
||||||
if (index < 0) return
|
if (index < 0) return
|
||||||
val attr = model.getAttr(index)
|
val file = model.getFileObject(index)
|
||||||
val text = OptionPane.showInputDialog(
|
val text = OptionPane.showInputDialog(
|
||||||
owner,
|
owner,
|
||||||
value = attr.name,
|
value = file.name.baseName,
|
||||||
title = I18n.getString("termora.transport.table.contextmenu.rename")
|
title = I18n.getString("termora.transport.table.contextmenu.rename")
|
||||||
) ?: return
|
) ?: return
|
||||||
if (text.isBlank() || text == attr.name) return
|
if (text.isBlank() || text == file.name.baseName) return
|
||||||
if (model.getPathNames().contains(text)) {
|
if (model.getPathNames().contains(text)) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
owner,
|
owner,
|
||||||
@@ -428,10 +489,11 @@ class FileSystemViewTable(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fileSystemViewPanel.renameTo(attr.path, attr.path.parent.resolve(text))
|
|
||||||
|
fileSystemViewPanel.renameTo(file, file.parent.resolveFile(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun editFiles(files: Array<Path>) {
|
private fun editFiles(files: List<FileObject>) {
|
||||||
if (files.isEmpty()) return
|
if (files.isEmpty()) return
|
||||||
|
|
||||||
if (SystemInfo.isLinux) {
|
if (SystemInfo.isLinux) {
|
||||||
@@ -449,10 +511,11 @@ class FileSystemViewTable(
|
|||||||
|
|
||||||
for (file in files) {
|
for (file in files) {
|
||||||
val dir = Application.createSubTemporaryDir()
|
val dir = Application.createSubTemporaryDir()
|
||||||
val path = Paths.get(dir.absolutePathString(), file.name)
|
val path = Paths.get(dir.absolutePathString(), file.name.baseName)
|
||||||
|
val target = VFS.getManager().resolveFile("file://" + path.absolutePathString())
|
||||||
|
|
||||||
val newTransport = createTransport(file, false, 0L)
|
val newTransport = createTransport(file, false, 0L)
|
||||||
.apply { target = path }
|
.apply { this.target = target }
|
||||||
|
|
||||||
transportManager.addTransportListener(object : TransportListener {
|
transportManager.addTransportListener(object : TransportListener {
|
||||||
override fun onTransportChanged(transport: Transport) {
|
override fun onTransportChanged(transport: Transport) {
|
||||||
@@ -461,7 +524,7 @@ class FileSystemViewTable(
|
|||||||
transportManager.removeTransportListener(this)
|
transportManager.removeTransportListener(this)
|
||||||
if (transport.status != TransportStatus.Done) return
|
if (transport.status != TransportStatus.Done) return
|
||||||
// 监听文件变动
|
// 监听文件变动
|
||||||
listenFileChange(path, file)
|
listenFileChange(target, file)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -470,21 +533,15 @@ class FileSystemViewTable(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun listenFileChange(localPath: Path, remotePath: Path) {
|
private fun listenFileChange(localPath: FileObject, remotePath: FileObject) {
|
||||||
try {
|
try {
|
||||||
|
val p = localPath.absolutePathString()
|
||||||
if (sftp.editCommand.isNotBlank()) {
|
if (sftp.editCommand.isNotBlank()) {
|
||||||
ProcessBuilder(
|
ProcessBuilder(parseCommand(MessageFormat.format(sftp.editCommand, p))).start()
|
||||||
parseCommand(
|
|
||||||
MessageFormat.format(
|
|
||||||
sftp.editCommand,
|
|
||||||
localPath.absolutePathString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).start()
|
|
||||||
} else if (SystemInfo.isMacOS) {
|
} else if (SystemInfo.isMacOS) {
|
||||||
ProcessBuilder("open", "-a", "TextEdit", localPath.absolutePathString()).start()
|
ProcessBuilder("open", "-a", "TextEdit", p).start()
|
||||||
} else if (SystemInfo.isWindows) {
|
} else if (SystemInfo.isWindows) {
|
||||||
ProcessBuilder("notepad", localPath.absolutePathString()).start()
|
ProcessBuilder("notepad", p).start()
|
||||||
} else {
|
} else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -495,13 +552,17 @@ class FileSystemViewTable(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastModifiedTime = localPath.getLastModifiedTime().toMillis()
|
var lastModifiedTime = localPath.content.lastModifiedTime
|
||||||
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
while (coroutineScope.isActive) {
|
while (coroutineScope.isActive) {
|
||||||
try {
|
try {
|
||||||
if (isDisposed.get() || !Files.exists(localPath)) break
|
|
||||||
val nowModifiedTime = localPath.getLastModifiedTime().toMillis()
|
if (isDisposed.get()) break
|
||||||
|
localPath.refresh()
|
||||||
|
if (!localPath.exists()) break
|
||||||
|
|
||||||
|
val nowModifiedTime = localPath.content.lastModifiedTime
|
||||||
if (nowModifiedTime != lastModifiedTime) {
|
if (nowModifiedTime != lastModifiedTime) {
|
||||||
lastModifiedTime = nowModifiedTime
|
lastModifiedTime = nowModifiedTime
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
@@ -556,23 +617,7 @@ class FileSystemViewTable(
|
|||||||
fileSystemViewPanel.newFolderOrFile(text, isFile)
|
fileSystemViewPanel.newFolderOrFile(text, isFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun transfer(
|
private fun deletePaths(paths: List<FileObject>, rm: Boolean = false) {
|
||||||
attrs: Array<FileSystemViewTableModel.Attr>,
|
|
||||||
fromLocalSystem: Boolean = false,
|
|
||||||
targetWorkdir: Path? = null
|
|
||||||
) {
|
|
||||||
coroutineScope.launch {
|
|
||||||
try {
|
|
||||||
doTransfer(attrs, fromLocalSystem, targetWorkdir)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (log.isErrorEnabled) {
|
|
||||||
log.error(e.message, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deletePaths(paths: Array<Path>, rm: Boolean = false) {
|
|
||||||
if (OptionPane.showConfirmDialog(
|
if (OptionPane.showConfirmDialog(
|
||||||
SwingUtilities.getWindowAncestor(this),
|
SwingUtilities.getWindowAncestor(this),
|
||||||
I18n.getString(if (rm) "termora.transport.table.contextmenu.rm-warning" else "termora.transport.table.contextmenu.delete-warning"),
|
I18n.getString(if (rm) "termora.transport.table.contextmenu.rm-warning" else "termora.transport.table.contextmenu.delete-warning"),
|
||||||
@@ -586,10 +631,10 @@ class FileSystemViewTable(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
coroutineScope.launch {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
if (fileSystem.isSFTP()) {
|
if (fileSystemProvider.getFileSystem() is MySftpFileSystem) {
|
||||||
deleteSftpPaths(paths, rm)
|
deleteSftpPaths(paths, rm)
|
||||||
} else {
|
} else {
|
||||||
deleteRecursively(paths)
|
deleteRecursively(paths)
|
||||||
@@ -600,61 +645,215 @@ class FileSystemViewTable(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止加载
|
withContext(Dispatchers.Swing) {
|
||||||
fileSystemViewPanel.stopLoading()
|
// 停止加载
|
||||||
|
fileSystemViewPanel.stopLoading()
|
||||||
// 刷新
|
// 刷新
|
||||||
fileSystemViewPanel.reload()
|
fileSystemViewPanel.reload()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteSftpPaths(paths: Array<Path>, rm: Boolean = false) {
|
private fun deleteSftpPaths(files: List<FileObject>, rm: Boolean = false) {
|
||||||
val fs = this.fileSystem as SftpFileSystem
|
|
||||||
if (rm) {
|
if (rm) {
|
||||||
for (path in paths) {
|
val session = (this.fileSystemProvider.getFileSystem() as MySftpFileSystem).getClientSession()
|
||||||
fs.session.executeRemoteCommand(
|
for (path in files) {
|
||||||
|
session.executeRemoteCommand(
|
||||||
"rm -rf '${path.absolutePathString()}'",
|
"rm -rf '${path.absolutePathString()}'",
|
||||||
OutputStream.nullOutputStream(),
|
OutputStream.nullOutputStream(),
|
||||||
Charsets.UTF_8
|
Charsets.UTF_8
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fs.client.use {
|
deleteRecursively(files)
|
||||||
for (path in paths) {
|
|
||||||
deleteRecursivelySFTP(path as SftpPath, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteRecursively(paths: Array<Path>) {
|
private fun deleteRecursively(files: List<FileObject>) {
|
||||||
for (path in paths) {
|
for (path in files) {
|
||||||
FileUtils.deleteQuietly(path.toFile())
|
path.deleteAll()
|
||||||
|
path.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 优化删除效率,采用一个连接
|
private fun transfer(
|
||||||
*/
|
files: List<FileObject>,
|
||||||
private fun deleteRecursivelySFTP(path: SftpPath, sftpClient: SftpClient) {
|
fromLocalSystem: Boolean = false,
|
||||||
val isDirectory = if (path.attributes != null) path.attributes.isDirectory else path.isDirectory()
|
targetWorkdir: FileObject? = null,
|
||||||
if (isDirectory) {
|
target: FileSystemViewPanel? = null,
|
||||||
for (e in sftpClient.readDir(path.toString())) {
|
) {
|
||||||
if (e.filename == ".." || e.filename == ".") {
|
|
||||||
continue
|
assertEventDispatchThread()
|
||||||
}
|
|
||||||
if (e.attributes.isDirectory) {
|
val target = (target ?: sftpPanel.getTarget(table)) ?: return
|
||||||
deleteRecursivelySFTP(path.resolve(e.filename), sftpClient)
|
val table = target.getData(SFTPDataProviders.FileSystemViewTable) ?: return
|
||||||
} else {
|
var isApplyAll = false
|
||||||
sftpClient.remove(path.resolve(e.filename).toString())
|
var lastAction = Action.Overwrite
|
||||||
|
|
||||||
|
for (file in files) {
|
||||||
|
if (!isApplyAll && (targetWorkdir == null || target.getWorkdir() == targetWorkdir)) {
|
||||||
|
val targetAttr = 0.rangeUntil(table.model.rowCount).map { table.model.getFileObject(it) }
|
||||||
|
.find { it.name.baseName == file.name.baseName }
|
||||||
|
if (targetAttr != null) {
|
||||||
|
val askTransfer = askTransfer(file, targetAttr)
|
||||||
|
if (askTransfer.option != JOptionPane.YES_OPTION) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (askTransfer.action == Action.Skip) {
|
||||||
|
if (askTransfer.applyAll) break
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
lastAction = askTransfer.action
|
||||||
|
isApplyAll = askTransfer.applyAll
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sftpClient.rmdir(path.toString())
|
|
||||||
|
coroutineScope.launch {
|
||||||
|
try {
|
||||||
|
doTransfer(file, lastAction, fromLocalSystem, targetWorkdir, target)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class AskTransfer(
|
||||||
|
val option: Int,
|
||||||
|
val action: Action,
|
||||||
|
val applyAll: Boolean
|
||||||
|
) {
|
||||||
|
enum class Action {
|
||||||
|
Overwrite,
|
||||||
|
Append,
|
||||||
|
Skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun askTransfer(
|
||||||
|
sourceFile: FileObject,
|
||||||
|
targetFile: FileObject
|
||||||
|
): AskTransfer {
|
||||||
|
val formMargin = "7dlu"
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $formMargin, default:grow, 2dlu, left:pref",
|
||||||
|
"pref, 12dlu, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, 16dlu, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
val iconSize = 36
|
||||||
|
|
||||||
|
val targetIcon = if (SystemInfo.isWindows)
|
||||||
|
model.getFileIcon(targetFile, iconSize, iconSize)
|
||||||
|
else if (targetFile.isFolder) {
|
||||||
|
FlatSVGIcon(Icons.folder.name, iconSize, iconSize)
|
||||||
} else {
|
} else {
|
||||||
sftpClient.remove(path.toString())
|
FlatSVGIcon(Icons.file.name, iconSize, iconSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sourceIcon = if (SystemInfo.isWindows)
|
||||||
|
model.getFileIcon(sourceFile, iconSize, iconSize)
|
||||||
|
else if (sourceFile.isFolder) {
|
||||||
|
FlatSVGIcon(Icons.folder.name, iconSize, iconSize)
|
||||||
|
} else {
|
||||||
|
FlatSVGIcon(Icons.file.name, iconSize, iconSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val sourceModified = StringUtils.defaultIfBlank(model.getLastModifiedTime(sourceFile), "-")
|
||||||
|
val targetModified = StringUtils.defaultIfBlank(model.getLastModifiedTime(targetFile), "-")
|
||||||
|
|
||||||
|
val actionsComBoBox = JComboBox<Action>()
|
||||||
|
actionsComBoBox.addItem(Action.Overwrite)
|
||||||
|
actionsComBoBox.addItem(Action.Append)
|
||||||
|
actionsComBoBox.addItem(Action.Skip)
|
||||||
|
actionsComBoBox.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 == Action.Overwrite) {
|
||||||
|
text = I18n.getString("termora.transport.sftp.already-exists.overwrite")
|
||||||
|
} else if (value == Action.Skip) {
|
||||||
|
text = I18n.getString("termora.transport.sftp.already-exists.skip")
|
||||||
|
} else if (value == Action.Append) {
|
||||||
|
text = I18n.getString("termora.transport.sftp.already-exists.append")
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val applyAllCheckbox = JCheckBox(I18n.getString("termora.transport.sftp.already-exists.apply-all"))
|
||||||
|
val box = Box.createHorizontalBox()
|
||||||
|
box.add(actionsComBoBox)
|
||||||
|
box.add(Box.createHorizontalStrut(8))
|
||||||
|
box.add(applyAllCheckbox)
|
||||||
|
box.add(Box.createHorizontalGlue())
|
||||||
|
|
||||||
|
val ttBox = Box.createVerticalBox()
|
||||||
|
ttBox.add(JLabel(I18n.getString("termora.transport.sftp.already-exists.message1")))
|
||||||
|
ttBox.add(JLabel(I18n.getString("termora.transport.sftp.already-exists.message2")))
|
||||||
|
|
||||||
|
val warningIcon = FlatSVGIcon(
|
||||||
|
Icons.warningIntroduction.name,
|
||||||
|
iconSize,
|
||||||
|
iconSize
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
// tip
|
||||||
|
.add(JLabel(warningIcon)).xy(1, rows)
|
||||||
|
.add(ttBox).xyw(3, rows, 3).apply { rows += step }
|
||||||
|
// name
|
||||||
|
.add(JLabel("${I18n.getString("termora.transport.sftp.already-exists.name")}:")).xy(1, rows)
|
||||||
|
.add(sourceFile.name.baseName).xyw(3, rows, 3).apply { rows += step }
|
||||||
|
// separator
|
||||||
|
.addSeparator(StringUtils.EMPTY).xyw(1, rows, 5).apply { rows += step }
|
||||||
|
// Destination
|
||||||
|
.add("${I18n.getString("termora.transport.sftp.already-exists.destination")}:").xy(1, rows)
|
||||||
|
.apply { rows += step }
|
||||||
|
// Folder
|
||||||
|
.add(JLabel(targetIcon)).xy(1, rows, "center, fill")
|
||||||
|
.add(targetModified).xyw(3, rows, 3).apply { rows += step }
|
||||||
|
// Source
|
||||||
|
.add("${I18n.getString("termora.transport.sftp.already-exists.source")}:").xy(1, rows)
|
||||||
|
.apply { rows += step }
|
||||||
|
// Folder
|
||||||
|
.add(JLabel(sourceIcon)).xy(1, rows, "center, fill")
|
||||||
|
.add(sourceModified).xyw(3, rows, 3).apply { rows += step }
|
||||||
|
// separator
|
||||||
|
.addSeparator(StringUtils.EMPTY).xyw(1, rows, 5).apply { rows += step }
|
||||||
|
// name
|
||||||
|
.add(JLabel("${I18n.getString("termora.transport.sftp.already-exists.actions")}:")).xy(1, rows)
|
||||||
|
.add(box).xyw(3, rows, 3).apply { rows += step }
|
||||||
|
.build()
|
||||||
|
panel.putClientProperty("SKIP_requestFocusInWindow", true)
|
||||||
|
|
||||||
|
return AskTransfer(
|
||||||
|
option = OptionPane.showConfirmDialog(
|
||||||
|
owner, panel,
|
||||||
|
messageType = JOptionPane.PLAIN_MESSAGE,
|
||||||
|
optionType = JOptionPane.OK_CANCEL_OPTION,
|
||||||
|
title = sourceFile.name.baseName,
|
||||||
|
initialValue = JOptionPane.YES_OPTION,
|
||||||
|
) {
|
||||||
|
it.size = Dimension(max(UIManager.getInt("Dialog.width") - 220, it.width), it.height)
|
||||||
|
it.setLocationRelativeTo(it.owner)
|
||||||
|
},
|
||||||
|
action = actionsComBoBox.selectedItem as Action,
|
||||||
|
applyAll = applyAllCheckbox.isSelected
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -662,78 +861,82 @@ class FileSystemViewTable(
|
|||||||
* 开始查找所有子,查找到之后立即添加任务,如果添加失败(任意一个)那么立即终止
|
* 开始查找所有子,查找到之后立即添加任务,如果添加失败(任意一个)那么立即终止
|
||||||
*/
|
*/
|
||||||
private fun doTransfer(
|
private fun doTransfer(
|
||||||
attrs: Array<FileSystemViewTableModel.Attr>,
|
file: FileObject,
|
||||||
|
action: Action,
|
||||||
fromLocalSystem: Boolean,
|
fromLocalSystem: Boolean,
|
||||||
targetWorkdir: Path?
|
targetWorkdir: FileObject?,
|
||||||
|
target: FileSystemViewPanel? = null
|
||||||
) {
|
) {
|
||||||
if (attrs.isEmpty()) return
|
|
||||||
val sftpPanel = this.sftpPanel
|
val sftpPanel = this.sftpPanel
|
||||||
val target = sftpPanel.getTarget(table) ?: return
|
val target = (target ?: sftpPanel.getTarget(table)) ?: return
|
||||||
var isTerminate = false
|
|
||||||
|
/**
|
||||||
|
* 定义一个添加器,它可以自动的判断导入/拖拽行为
|
||||||
|
*/
|
||||||
|
val adder = object {
|
||||||
|
fun add(transport: Transport): Boolean {
|
||||||
|
if (action == Action.Append) {
|
||||||
|
transport.mode = StandardOpenOption.APPEND
|
||||||
|
} else {
|
||||||
|
transport.mode = StandardOpenOption.TRUNCATE_EXISTING
|
||||||
|
}
|
||||||
|
return addTransport(
|
||||||
|
sftpPanel,
|
||||||
|
if (fromLocalSystem) file.parent else null,
|
||||||
|
target,
|
||||||
|
targetWorkdir,
|
||||||
|
transport
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.isFile) {
|
||||||
|
adder.add(createTransport(file, false, 0).apply { scanned() })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val queue = ArrayDeque<Transport>()
|
val queue = ArrayDeque<Transport>()
|
||||||
|
var isTerminate = false
|
||||||
|
|
||||||
for (attr in attrs) {
|
try {
|
||||||
|
walk(file, object : FileVisitor<FileObject> {
|
||||||
/**
|
override fun preVisitDirectory(dir: FileObject, attrs: BasicFileAttributes): FileVisitResult {
|
||||||
* 定义一个添加器,它可以自动的判断导入/拖拽行为
|
val transport = createTransport(dir, true, queue.lastOrNull()?.id ?: 0L)
|
||||||
*/
|
.apply { queue.addLast(this) }
|
||||||
val adder = object {
|
if (adder.add(transport)) return FileVisitResult.CONTINUE
|
||||||
fun add(transport: Transport): Boolean {
|
return FileVisitResult.TERMINATE.apply { isTerminate = true }
|
||||||
return addTransport(
|
|
||||||
sftpPanel,
|
|
||||||
if (fromLocalSystem) attr.path.parent else null,
|
|
||||||
target,
|
|
||||||
targetWorkdir,
|
|
||||||
transport
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (attr.isFile) {
|
override fun visitFile(file: FileObject, attrs: BasicFileAttributes): FileVisitResult {
|
||||||
if (!adder.add(createTransport(attr.path, false, 0).apply { scanned() })) {
|
if (queue.isEmpty()) return FileVisitResult.SKIP_SIBLINGS
|
||||||
isTerminate = true
|
val transport = createTransport(file, false, queue.last().id).apply { scanned() }
|
||||||
break
|
if (adder.add(transport)) return FileVisitResult.CONTINUE
|
||||||
|
return FileVisitResult.TERMINATE.apply { isTerminate = true }
|
||||||
}
|
}
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
queue.clear()
|
override fun visitFileFailed(file: FileObject, exc: IOException): FileVisitResult {
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
try {
|
|
||||||
walk(attr.path, object : FileVisitor<Path> {
|
|
||||||
override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
|
|
||||||
val transport = createTransport(dir, true, queue.lastOrNull()?.id ?: 0L)
|
|
||||||
.apply { queue.addLast(this) }
|
|
||||||
if (adder.add(transport)) return FileVisitResult.CONTINUE
|
|
||||||
return FileVisitResult.TERMINATE.apply { isTerminate = true }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
|
|
||||||
if (queue.isEmpty()) return FileVisitResult.SKIP_SIBLINGS
|
|
||||||
val transport = createTransport(file, false, queue.last().id).apply { scanned() }
|
|
||||||
if (adder.add(transport)) return FileVisitResult.CONTINUE
|
|
||||||
return FileVisitResult.TERMINATE.apply { isTerminate = true }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun visitFileFailed(file: Path, exc: IOException): FileVisitResult {
|
|
||||||
return FileVisitResult.CONTINUE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult {
|
|
||||||
// 标记为扫描完毕
|
|
||||||
queue.removeLast().scanned()
|
|
||||||
return FileVisitResult.CONTINUE
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (log.isErrorEnabled) {
|
|
||||||
log.error(e.message, e)
|
|
||||||
}
|
}
|
||||||
isTerminate = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isTerminate) break
|
override fun postVisitDirectory(dir: FileObject, exc: IOException?): FileVisitResult {
|
||||||
|
// 标记为扫描完毕
|
||||||
|
queue.removeLast().scanned()
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
message = ExceptionUtils.getRootCauseMessage(e),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isTerminate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTerminate) {
|
if (isTerminate) {
|
||||||
@@ -742,61 +945,32 @@ class FileSystemViewTable(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun walk(dir: Path, visitor: FileVisitor<Path>) {
|
|
||||||
if (fileSystem is SftpFileSystem) {
|
|
||||||
val attr = SftpPosixFileAttributes(dir, SftpClient.Attributes())
|
|
||||||
fileSystem.client.use { walkSFTP(dir, attr, visitor, it) }
|
|
||||||
} else {
|
|
||||||
Files.walkFileTree(dir, setOf(FileVisitOption.FOLLOW_LINKS), Int.MAX_VALUE, visitor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun walkSFTP(
|
private fun walk(
|
||||||
dir: Path,
|
dir: FileObject,
|
||||||
attr: SftpPosixFileAttributes,
|
visitor: FileVisitor<FileObject>,
|
||||||
visitor: FileVisitor<Path>,
|
|
||||||
client: SftpClient
|
|
||||||
): FileVisitResult {
|
): FileVisitResult {
|
||||||
|
return VFSWalker.walk(dir, visitor)
|
||||||
if (visitor.preVisitDirectory(dir, attr) == FileVisitResult.TERMINATE) {
|
|
||||||
return FileVisitResult.TERMINATE
|
|
||||||
}
|
|
||||||
|
|
||||||
val paths = client.readDir(dir.absolutePathString())
|
|
||||||
for (e in paths) {
|
|
||||||
if (e.filename == ".." || e.filename == ".") continue
|
|
||||||
if (e.attributes.isDirectory) {
|
|
||||||
if (walkSFTP(dir.resolve(e.filename), attr, visitor, client) == FileVisitResult.TERMINATE) {
|
|
||||||
return FileVisitResult.TERMINATE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val result = visitor.visitFile(dir.resolve(e.filename), attr)
|
|
||||||
if (result == FileVisitResult.TERMINATE) {
|
|
||||||
return FileVisitResult.TERMINATE
|
|
||||||
} else if (result == FileVisitResult.SKIP_SUBTREE) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visitor.postVisitDirectory(dir, null) == FileVisitResult.TERMINATE) {
|
|
||||||
return FileVisitResult.TERMINATE
|
|
||||||
}
|
|
||||||
|
|
||||||
return FileVisitResult.CONTINUE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addTransport(
|
private fun addTransport(
|
||||||
sftpPanel: SFTPPanel,
|
sftpPanel: SFTPPanel,
|
||||||
sourceWorkdir: Path?,
|
sourceWorkdir: FileObject?,
|
||||||
target: FileSystemViewPanel,
|
target: FileSystemViewPanel,
|
||||||
targetWorkdir: Path?,
|
targetWorkdir: FileObject?,
|
||||||
transport: Transport
|
transport: Transport
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return sftpPanel.addTransport(table, sourceWorkdir, target, targetWorkdir, transport)
|
return try {
|
||||||
|
sftpPanel.addTransport(table, sourceWorkdir, target, targetWorkdir, transport)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createTransport(source: Path, isDirectory: Boolean, parentId: Long): Transport {
|
private fun createTransport(source: FileObject, isDirectory: Boolean, parentId: Long): Transport {
|
||||||
val transport = Transport(
|
val transport = Transport(
|
||||||
source = source,
|
source = source,
|
||||||
target = source,
|
target = source,
|
||||||
@@ -804,7 +978,7 @@ class FileSystemViewTable(
|
|||||||
isDirectory = isDirectory,
|
isDirectory = isDirectory,
|
||||||
)
|
)
|
||||||
if (transport.isFile) {
|
if (transport.isFile) {
|
||||||
transport.filesize.addAndGet(source.fileSize())
|
transport.filesize.addAndGet(source.content.size)
|
||||||
}
|
}
|
||||||
return transport
|
return transport
|
||||||
}
|
}
|
||||||
@@ -812,7 +986,7 @@ class FileSystemViewTable(
|
|||||||
|
|
||||||
private class FileSystemTableRowTransferable(
|
private class FileSystemTableRowTransferable(
|
||||||
val source: FileSystemViewTable,
|
val source: FileSystemViewTable,
|
||||||
val attrs: List<FileSystemViewTableModel.Attr>
|
val files: List<FileObject>
|
||||||
) : Transferable {
|
) : Transferable {
|
||||||
companion object {
|
companion object {
|
||||||
val dataFlavor = DataFlavor(FileSystemTableRowTransferable::class.java, "TableRowTransferable")
|
val dataFlavor = DataFlavor(FileSystemTableRowTransferable::class.java, "TableRowTransferable")
|
||||||
@@ -835,4 +1009,5 @@ class FileSystemViewTable(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,21 +3,24 @@ package app.termora.sftp
|
|||||||
import app.termora.I18n
|
import app.termora.I18n
|
||||||
import app.termora.NativeStringComparator
|
import app.termora.NativeStringComparator
|
||||||
import app.termora.formatBytes
|
import app.termora.formatBytes
|
||||||
|
import app.termora.vfs2.sftp.MySftpFileObject
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import org.apache.commons.lang3.time.DateFormatUtils
|
import org.apache.commons.lang3.time.DateFormatUtils
|
||||||
import org.apache.sshd.sftp.client.fs.SftpPath
|
import org.apache.commons.vfs2.FileObject
|
||||||
|
import org.apache.commons.vfs2.FileType
|
||||||
|
import org.apache.commons.vfs2.provider.local.LocalFileSystem
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.File
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.attribute.PosixFilePermission
|
import java.nio.file.attribute.PosixFilePermission
|
||||||
import java.nio.file.attribute.PosixFilePermissions
|
import java.nio.file.attribute.PosixFilePermissions
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import javax.swing.Icon
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
import javax.swing.table.DefaultTableModel
|
import javax.swing.table.DefaultTableModel
|
||||||
import kotlin.io.path.*
|
|
||||||
|
|
||||||
class FileSystemViewTableModel : DefaultTableModel() {
|
class FileSystemViewTableModel : DefaultTableModel() {
|
||||||
|
|
||||||
@@ -29,9 +32,10 @@ class FileSystemViewTableModel : DefaultTableModel() {
|
|||||||
const val COLUMN_LAST_MODIFIED_TIME = 3
|
const val COLUMN_LAST_MODIFIED_TIME = 3
|
||||||
const val COLUMN_ATTRS = 4
|
const val COLUMN_ATTRS = 4
|
||||||
const val COLUMN_OWNER = 5
|
const val COLUMN_OWNER = 5
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(FileSystemViewTableModel::class.java)
|
private val log = LoggerFactory.getLogger(FileSystemViewTableModel::class.java)
|
||||||
|
|
||||||
private fun fromSftpPermissions(sftpPermissions: Int): Set<PosixFilePermission> {
|
fun fromSftpPermissions(sftpPermissions: Int): Set<PosixFilePermission> {
|
||||||
val result = mutableSetOf<PosixFilePermission>()
|
val result = mutableSetOf<PosixFilePermission>()
|
||||||
|
|
||||||
// 将十进制权限转换为八进制字符串
|
// 将十进制权限转换为八进制字符串
|
||||||
@@ -68,23 +72,69 @@ class FileSystemViewTableModel : DefaultTableModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getValueAt(row: Int, column: Int): Any {
|
var hasParent: Boolean = false
|
||||||
val attr = getAttr(row)
|
private set
|
||||||
return when (column) {
|
|
||||||
COLUMN_NAME -> attr.name
|
|
||||||
COLUMN_FILE_SIZE -> if (attr.isDirectory) StringUtils.EMPTY else formatBytes(attr.size)
|
|
||||||
COLUMN_TYPE -> attr.type
|
|
||||||
COLUMN_LAST_MODIFIED_TIME -> if (attr.modified > 0) DateFormatUtils.format(
|
|
||||||
Date(attr.modified),
|
|
||||||
"yyyy/MM/dd HH:mm"
|
|
||||||
) else StringUtils.EMPTY
|
|
||||||
|
|
||||||
COLUMN_ATTRS -> attr.permissions
|
override fun getValueAt(row: Int, column: Int): Any {
|
||||||
COLUMN_OWNER -> attr.owner
|
val file = getFileObject(row)
|
||||||
else -> StringUtils.EMPTY
|
val isParentRow = hasParent && row == 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (file.type == FileType.IMAGINARY) return StringUtils.EMPTY
|
||||||
|
return when (column) {
|
||||||
|
COLUMN_NAME -> if (isParentRow) ".." else file.name.baseName
|
||||||
|
COLUMN_FILE_SIZE -> if (isParentRow || file.isFolder) StringUtils.EMPTY else formatBytes(file.content.size)
|
||||||
|
COLUMN_TYPE -> if (isParentRow) StringUtils.EMPTY else getFileType(file)
|
||||||
|
COLUMN_LAST_MODIFIED_TIME -> if (isParentRow) StringUtils.EMPTY else getLastModifiedTime(file)
|
||||||
|
COLUMN_ATTRS -> if (isParentRow) StringUtils.EMPTY else getAttrs(file)
|
||||||
|
COLUMN_OWNER -> StringUtils.EMPTY
|
||||||
|
else -> StringUtils.EMPTY
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (file.fileSystem is LocalFileSystem) {
|
||||||
|
if (ExceptionUtils.getRootCause(e) is java.nio.file.NoSuchFileException) {
|
||||||
|
SwingUtilities.invokeLater { removeRow(row) }
|
||||||
|
return StringUtils.EMPTY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn(e.message, e)
|
||||||
|
}
|
||||||
|
return StringUtils.EMPTY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getFileType(file: FileObject): String {
|
||||||
|
return if (SystemInfo.isWindows) NativeFileIcons.getIcon(file.name.baseName, file.isFile).second
|
||||||
|
else if (file.isSymbolicLink) I18n.getString("termora.transport.table.type.symbolic-link")
|
||||||
|
else NativeFileIcons.getIcon(file.name.baseName, file.isFile).second
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFileIcon(file: FileObject, width: Int = 16, height: Int = 16): Icon {
|
||||||
|
return if (SystemInfo.isWindows) NativeFileIcons.getIcon(file.name.baseName, file.isFile, width, height).first
|
||||||
|
else NativeFileIcons.getIcon(file.name.baseName, file.isFile).first
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFileIcon(row: Int): Icon {
|
||||||
|
return getFileIcon(getFileObject(row))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLastModifiedTime(file: FileObject): String {
|
||||||
|
if (file.content.lastModifiedTime < 1) return "-"
|
||||||
|
return DateFormatUtils.format(Date(file.content.lastModifiedTime), "yyyy/MM/dd HH:mm")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAttrs(file: FileObject): String {
|
||||||
|
if (file.fileSystem is LocalFileSystem) return StringUtils.EMPTY
|
||||||
|
return PosixFilePermissions.toString(getFilePermissions(file))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFilePermissions(file: FileObject): Set<PosixFilePermission> {
|
||||||
|
val permissions = file.content.getAttribute(MySftpFileObject.POSIX_FILE_PERMISSIONS)
|
||||||
|
as Int? ?: return emptySet()
|
||||||
|
return fromSftpPermissions(permissions)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getDataVector(): Vector<Vector<Any>> {
|
override fun getDataVector(): Vector<Vector<Any>> {
|
||||||
return super.getDataVector()
|
return super.getDataVector()
|
||||||
}
|
}
|
||||||
@@ -100,14 +150,18 @@ class FileSystemViewTableModel : DefaultTableModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAttr(row: Int): Attr {
|
fun getFileObject(row: Int): FileObject {
|
||||||
return super.getValueAt(row, 0) as Attr
|
return super.getValueAt(row, 0) as FileObject
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPathNames(): Set<String> {
|
fun getPathNames(): Set<String> {
|
||||||
val names = linkedSetOf<String>()
|
val names = linkedSetOf<String>()
|
||||||
for (i in 0 until rowCount) {
|
for (i in 0 until rowCount) {
|
||||||
names.add(getAttr(i).name)
|
if (hasParent && i == 0) {
|
||||||
|
names.add("..")
|
||||||
|
} else {
|
||||||
|
names.add(getFileObject(i).name.baseName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
@@ -129,144 +183,40 @@ class FileSystemViewTableModel : DefaultTableModel() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun reload(dir: Path, useFileHiding: Boolean) {
|
suspend fun reload(dir: FileObject, useFileHiding: Boolean) {
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Reloading {} , useFileHiding {}", dir, useFileHiding)
|
log.debug("Reloading {} , useFileHiding {}", dir, useFileHiding)
|
||||||
}
|
}
|
||||||
|
|
||||||
val attrs = mutableListOf<Attr>()
|
val files = mutableListOf<FileObject>()
|
||||||
if (dir.parent != null) {
|
|
||||||
attrs.add(ParentAttr(dir.parent))
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Files.list(dir).use { paths ->
|
dir.refresh()
|
||||||
for (path in paths) {
|
for (file in dir.children) {
|
||||||
val attr = if (path is SftpPath) SftpAttr(path) else Attr(path)
|
if (useFileHiding && file.isHidden) continue
|
||||||
if (useFileHiding && attr.isHidden) continue
|
files.add(file)
|
||||||
attrs.add(attr)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs.sortWith(compareBy<Attr> { !it.isDirectory }.thenComparing { a, b ->
|
files.sortWith(compareBy<FileObject> { !it.isFolder }.thenComparing { a, b ->
|
||||||
NativeStringComparator.getInstance().compare(
|
NativeStringComparator.getInstance().compare(
|
||||||
a.name,
|
a.name.baseName,
|
||||||
b.name
|
b.name.baseName
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
hasParent = dir.parent != null
|
||||||
|
if (hasParent) {
|
||||||
|
files.addFirst(dir.parent)
|
||||||
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
while (rowCount > 0) removeRow(0)
|
while (rowCount > 0) removeRow(0)
|
||||||
attrs.forEach { addRow(arrayOf(it)) }
|
files.forEach { addRow(arrayOf(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
open class Attr(val path: Path) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 名称
|
|
||||||
*/
|
|
||||||
open val name by lazy { path.name }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文件类型
|
|
||||||
*/
|
|
||||||
open val type by lazy {
|
|
||||||
if (path.fileSystem.isWindows()) NativeFileIcons.getIcon(name, isFile).second
|
|
||||||
else if (isSymbolicLink) I18n.getString("termora.transport.table.type.symbolic-link")
|
|
||||||
else NativeFileIcons.getIcon(name, isFile).second
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 大小
|
|
||||||
*/
|
|
||||||
open val size by lazy { path.fileSize() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修改时间
|
|
||||||
*/
|
|
||||||
open val modified by lazy { path.getLastModifiedTime().toMillis() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有者
|
|
||||||
*/
|
|
||||||
open val owner by lazy { StringUtils.EMPTY }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取操作系统图标
|
|
||||||
*/
|
|
||||||
open val icon by lazy { NativeFileIcons.getIcon(name, isFile).first }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否是文件夹
|
|
||||||
*/
|
|
||||||
open val isDirectory by lazy { path.isDirectory() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否是文件
|
|
||||||
*/
|
|
||||||
open val isFile by lazy { !isDirectory }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否是文件夹
|
|
||||||
*/
|
|
||||||
open val isHidden by lazy { path.isHidden() }
|
|
||||||
|
|
||||||
open val isSymbolicLink by lazy { path.isSymbolicLink() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取权限
|
|
||||||
*/
|
|
||||||
open val permissions: String by lazy {
|
|
||||||
posixFilePermissions.let {
|
|
||||||
if (it.isNotEmpty()) PosixFilePermissions.toString(
|
|
||||||
it
|
|
||||||
) else StringUtils.EMPTY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
open val posixFilePermissions by lazy { if (path.fileSystem.isUnix()) path.getPosixFilePermissions() else emptySet() }
|
|
||||||
|
|
||||||
open fun toFile(): File {
|
|
||||||
if (path.fileSystem.isSFTP()) {
|
|
||||||
return File(path.absolutePathString())
|
|
||||||
}
|
|
||||||
return path.toFile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ParentAttr(path: Path) : Attr(path) {
|
|
||||||
override val name by lazy { ".." }
|
|
||||||
override val isDirectory = true
|
|
||||||
override val isFile = false
|
|
||||||
override val isHidden = false
|
|
||||||
override val permissions = StringUtils.EMPTY
|
|
||||||
override val modified = 0L
|
|
||||||
override val type = StringUtils.EMPTY
|
|
||||||
override val icon by lazy { NativeFileIcons.getFolderIcon() }
|
|
||||||
override val isSymbolicLink = false
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SftpAttr(sftpPath: SftpPath) : Attr(sftpPath) {
|
|
||||||
private val attributes = sftpPath.attributes
|
|
||||||
|
|
||||||
override val isSymbolicLink = attributes.isSymbolicLink
|
|
||||||
override val isDirectory = if (isSymbolicLink) sftpPath.isDirectory() else attributes.isDirectory
|
|
||||||
override val isHidden = name.startsWith(".")
|
|
||||||
override val size = attributes.size
|
|
||||||
override val owner: String = StringUtils.defaultString(attributes.owner)
|
|
||||||
override val modified = attributes.modifyTime.toMillis()
|
|
||||||
override val permissions: String = PosixFilePermissions.toString(fromSftpPermissions(attributes.permissions))
|
|
||||||
override val posixFilePermissions = fromSftpPermissions(attributes.permissions)
|
|
||||||
|
|
||||||
override fun toFile(): File {
|
|
||||||
return File(path.absolutePathString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.formdev.flatlaf.icons.FlatTreeLeafIcon
|
|||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.apache.commons.io.FilenameUtils
|
import org.apache.commons.io.FilenameUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.eclipse.jgit.util.LRUMap
|
import org.eclipse.jgit.util.LRUMap
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -24,7 +25,7 @@ object NativeFileIcons {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
if (SystemUtils.IS_OS_UNIX) {
|
if (SystemUtils.IS_OS_UNIX) {
|
||||||
cache[SystemUtils.USER_HOME] = Pair(FlatTreeClosedIcon(), I18n.getString("termora.folder"))
|
cache[SystemUtils.USER_HOME] = Pair(folderIcon, I18n.getString("termora.folder"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,35 +37,30 @@ object NativeFileIcons {
|
|||||||
return getIcon(filename, true).first
|
return getIcon(filename, true).first
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getIcon(filename: String, isFile: Boolean = true): Pair<Icon, String> {
|
fun getIcon(filename: String, isFile: Boolean = true, width: Int = 16, height: Int = 16): Pair<Icon, String> {
|
||||||
if (isFile) {
|
val key = if (isFile) FilenameUtils.getExtension(filename) + "." + width + "@" + height
|
||||||
val extension = FilenameUtils.getExtension(filename)
|
else SystemUtils.USER_HOME + "." + width + "@" + height
|
||||||
if (cache.containsKey(extension)) {
|
|
||||||
return cache.getValue(extension)
|
if (cache.containsKey(key)) {
|
||||||
}
|
return cache.getValue(key)
|
||||||
} else {
|
|
||||||
if (cache.containsKey(SystemUtils.USER_HOME)) {
|
|
||||||
return cache.getValue(SystemUtils.USER_HOME)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val isDirectory = !isFile
|
val isDirectory = !isFile
|
||||||
|
|
||||||
if (SystemInfo.isWindows) {
|
if (SystemInfo.isWindows) {
|
||||||
|
|
||||||
val file = if (isDirectory) FileUtils.getFile(SystemUtils.USER_HOME) else
|
val file = if (isDirectory) FileUtils.getFile(SystemUtils.USER_HOME) else
|
||||||
FileUtils.getFile(Application.getTemporaryDir(), "${UUID.randomUUID()}.${filename}")
|
FileUtils.getFile(Application.getTemporaryDir(), "${UUID.randomUUID()}.${filename}")
|
||||||
if (isFile && !file.exists()) {
|
if (isFile && !file.exists()) {
|
||||||
file.createNewFile()
|
file.createNewFile()
|
||||||
}
|
}
|
||||||
val icon = getFileSystemView().getSystemIcon(file, 16, 16)
|
|
||||||
|
val icon = getFileSystemView().getSystemIcon(file, width, height) ?: if (isFile) fileIcon else folderIcon
|
||||||
val description = getFileSystemView().getSystemTypeDescription(file)
|
val description = getFileSystemView().getSystemTypeDescription(file)
|
||||||
|
?: StringUtils.defaultString(file.extension)
|
||||||
val pair = icon to description
|
val pair = icon to description
|
||||||
|
|
||||||
if (isDirectory) {
|
cache[key] = pair
|
||||||
cache[SystemUtils.USER_HOME] = pair
|
|
||||||
} else {
|
|
||||||
cache[FilenameUtils.getExtension(file.name)] = pair
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFile) FileUtils.deleteQuietly(file)
|
if (isFile) FileUtils.deleteQuietly(file)
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class PosixFilePermissionDialog(
|
|||||||
private val otherRead = JCheckBox(I18n.getString("termora.transport.permissions.read"))
|
private val otherRead = JCheckBox(I18n.getString("termora.transport.permissions.read"))
|
||||||
private val otherWrite = JCheckBox(I18n.getString("termora.transport.permissions.write"))
|
private val otherWrite = JCheckBox(I18n.getString("termora.transport.permissions.write"))
|
||||||
private val otherExecute = JCheckBox(I18n.getString("termora.transport.permissions.execute"))
|
private val otherExecute = JCheckBox(I18n.getString("termora.transport.permissions.execute"))
|
||||||
|
private val includeSubFolder = JCheckBox(I18n.getString("termora.transport.permissions.include-subfolder"))
|
||||||
|
|
||||||
private var isCancelled = false
|
private var isCancelled = false
|
||||||
|
|
||||||
@@ -60,13 +61,14 @@ class PosixFilePermissionDialog(
|
|||||||
otherRead.isFocusable = false
|
otherRead.isFocusable = false
|
||||||
otherWrite.isFocusable = false
|
otherWrite.isFocusable = false
|
||||||
otherExecute.isFocusable = false
|
otherExecute.isFocusable = false
|
||||||
|
includeSubFolder.isFocusable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createCenterPanel(): JComponent {
|
override fun createCenterPanel(): JComponent {
|
||||||
val formMargin = "7dlu"
|
val formMargin = "7dlu"
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"default:grow, $formMargin, default:grow, $formMargin, default:grow",
|
"default:grow, $formMargin, default:grow, $formMargin, default:grow",
|
||||||
"pref, $formMargin, pref, $formMargin, pref"
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
)
|
)
|
||||||
|
|
||||||
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
|
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
|
||||||
@@ -95,6 +97,8 @@ class PosixFilePermissionDialog(
|
|||||||
otherBox.border = BorderFactory.createTitledBorder(I18n.getString("termora.transport.permissions.others"))
|
otherBox.border = BorderFactory.createTitledBorder(I18n.getString("termora.transport.permissions.others"))
|
||||||
builder.add(otherBox).xy(5, 3)
|
builder.add(otherBox).xy(5, 3)
|
||||||
|
|
||||||
|
builder.add(includeSubFolder).xyw(1, 5, 5)
|
||||||
|
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +107,10 @@ class PosixFilePermissionDialog(
|
|||||||
super.doCancelAction()
|
super.doCancelAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isIncludeSubdirectories(): Boolean {
|
||||||
|
return includeSubFolder.isSelected
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return 返回空表示取消了
|
* @return 返回空表示取消了
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -56,6 +56,16 @@ class SFTPAction : AnAction("SFTP", Icons.folder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val host = hostManager.getHost(hostId) ?: return
|
val host = hostManager.getHost(hostId) ?: return
|
||||||
|
for (i in 0 until tabbed.tabCount) {
|
||||||
|
val c = tabbed.getComponentAt(i)
|
||||||
|
if (c is SFTPFileSystemViewPanel) {
|
||||||
|
if (c.state == SFTPFileSystemViewPanel.State.Initialized) {
|
||||||
|
c.selectHost(host)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tabbed.addSFTPFileSystemViewPanelTab(host)
|
tabbed.addSFTPFileSystemViewPanelTab(host)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package app.termora.sftp
|
|||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
|
import app.termora.vfs2.sftp.MySftpFileSystemConfigBuilder
|
||||||
import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon
|
import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
@@ -11,10 +12,11 @@ import kotlinx.coroutines.swing.Swing
|
|||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
|
import org.apache.commons.vfs2.FileSystem
|
||||||
|
import org.apache.commons.vfs2.FileSystemOptions
|
||||||
|
import org.apache.commons.vfs2.VFS
|
||||||
import org.apache.sshd.client.SshClient
|
import org.apache.sshd.client.SshClient
|
||||||
import org.apache.sshd.client.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
import org.apache.sshd.sftp.client.SftpClientFactory
|
|
||||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
|
||||||
import org.jdesktop.swingx.JXBusyLabel
|
import org.jdesktop.swingx.JXBusyLabel
|
||||||
import org.jdesktop.swingx.JXHyperlink
|
import org.jdesktop.swingx.JXHyperlink
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -25,6 +27,8 @@ import java.awt.event.MouseAdapter
|
|||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
import javax.swing.event.TreeExpansionEvent
|
||||||
|
import javax.swing.event.TreeExpansionListener
|
||||||
|
|
||||||
class SFTPFileSystemViewPanel(
|
class SFTPFileSystemViewPanel(
|
||||||
var host: Host? = null,
|
var host: Host? = null,
|
||||||
@@ -33,31 +37,30 @@ class SFTPFileSystemViewPanel(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(SFTPFileSystemViewPanel::class.java)
|
private val log = LoggerFactory.getLogger(SFTPFileSystemViewPanel::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
private enum class State {
|
enum class State {
|
||||||
Initialized,
|
Initialized,
|
||||||
Connecting,
|
Connecting,
|
||||||
Connected,
|
Connected,
|
||||||
ConnectFailed,
|
ConnectFailed,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var state = State.Initialized
|
var state = State.Initialized
|
||||||
|
private set
|
||||||
private val cardLayout = CardLayout()
|
private val cardLayout = CardLayout()
|
||||||
private val cardPanel = JPanel(cardLayout)
|
private val cardPanel = JPanel(cardLayout)
|
||||||
|
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
private val connectingPanel = ConnectingPanel()
|
private val connectingPanel = ConnectingPanel()
|
||||||
private val selectHostPanel = SelectHostPanel()
|
private val selectHostPanel = SelectHostPanel()
|
||||||
private val connectFailedPanel = ConnectFailedPanel()
|
private val connectFailedPanel = ConnectFailedPanel()
|
||||||
private val isDisposed = AtomicBoolean(false)
|
private val isDisposed = AtomicBoolean(false)
|
||||||
private val that = this
|
private val that = this
|
||||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
||||||
private val properties get() = Database.getDatabase().properties
|
private val properties get() = Database.getDatabase().properties
|
||||||
|
|
||||||
private var client: SshClient? = null
|
private var client: SshClient? = null
|
||||||
private var session: ClientSession? = null
|
private var session: ClientSession? = null
|
||||||
private var fileSystem: SftpFileSystem? = null
|
|
||||||
private var fileSystemPanel: FileSystemViewPanel? = null
|
private var fileSystemPanel: FileSystemViewPanel? = null
|
||||||
|
|
||||||
|
|
||||||
@@ -111,11 +114,17 @@ class SFTPFileSystemViewPanel(
|
|||||||
|
|
||||||
closeIO()
|
closeIO()
|
||||||
|
|
||||||
|
val mySftpFileSystem: FileSystem
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val (client, host) = SshClients.openClient(thisHost, SwingUtilities.getWindowAncestor(that))
|
val owner = SwingUtilities.getWindowAncestor(that)
|
||||||
this.client = client
|
val client = SshClients.openClient(thisHost, owner).apply { client = this }
|
||||||
val session = SshClients.openSession(host, client).apply { session = this }
|
val session = SshClients.openSession(thisHost, client).apply { session = this }
|
||||||
fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
|
|
||||||
|
val options = FileSystemOptions()
|
||||||
|
MySftpFileSystemConfigBuilder.getInstance()
|
||||||
|
.setClientSession(options, session)
|
||||||
|
mySftpFileSystem = VFS.getManager().resolveFile("sftp:///", options).fileSystem
|
||||||
session.addCloseFutureListener { onClose() }
|
session.addCloseFutureListener { onClose() }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
closeIO()
|
closeIO()
|
||||||
@@ -126,11 +135,10 @@ class SFTPFileSystemViewPanel(
|
|||||||
throw IllegalStateException("Closed")
|
throw IllegalStateException("Closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
val fileSystem = this.fileSystem ?: return
|
|
||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
state = State.Connected
|
state = State.Connected
|
||||||
val fileSystemPanel = FileSystemViewPanel(thisHost, fileSystem, transportManager, coroutineScope)
|
val fileSystemPanel = FileSystemViewPanel(thisHost, mySftpFileSystem, transportManager, coroutineScope)
|
||||||
cardPanel.add(fileSystemPanel, State.Connected.name)
|
cardPanel.add(fileSystemPanel, State.Connected.name)
|
||||||
cardLayout.show(cardPanel, State.Connected.name)
|
cardLayout.show(cardPanel, State.Connected.name)
|
||||||
that.fileSystemPanel = fileSystemPanel
|
that.fileSystemPanel = fileSystemPanel
|
||||||
@@ -157,7 +165,6 @@ class SFTPFileSystemViewPanel(
|
|||||||
fileSystemPanel?.let { Disposer.dispose(it) }
|
fileSystemPanel?.let { Disposer.dispose(it) }
|
||||||
fileSystemPanel = null
|
fileSystemPanel = null
|
||||||
|
|
||||||
runCatching { IOUtils.closeQuietly(fileSystem) }
|
|
||||||
runCatching { IOUtils.closeQuietly(session) }
|
runCatching { IOUtils.closeQuietly(session) }
|
||||||
runCatching { IOUtils.closeQuietly(client) }
|
runCatching { IOUtils.closeQuietly(client) }
|
||||||
|
|
||||||
@@ -279,12 +286,20 @@ class SFTPFileSystemViewPanel(
|
|||||||
val node = tree.getLastSelectedPathNode() ?: return
|
val node = tree.getLastSelectedPathNode() ?: return
|
||||||
if (node.isFolder) return
|
if (node.isFolder) return
|
||||||
val host = node.data as Host
|
val host = node.data as Host
|
||||||
that.setTabTitle(host.name)
|
selectHost(host)
|
||||||
that.host = host
|
|
||||||
that.connect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
tree.addTreeExpansionListener(object : TreeExpansionListener {
|
||||||
|
override fun treeExpanded(event: TreeExpansionEvent) {
|
||||||
|
properties.putString("SFTPTabbed.Tree.state", TreeUtils.saveExpansionState(tree))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun treeCollapsed(event: TreeExpansionEvent) {
|
||||||
|
properties.putString("SFTPTabbed.Tree.state", TreeUtils.saveExpansionState(tree))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
@@ -301,6 +316,12 @@ class SFTPFileSystemViewPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun selectHost(host: Host) {
|
||||||
|
that.setTabTitle(host.name)
|
||||||
|
that.host = host
|
||||||
|
that.connect()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setTabTitle(title: String) {
|
private fun setTabTitle(title: String) {
|
||||||
val tabbed = SwingUtilities.getAncestorOfClass(JTabbedPane::class.java, that)
|
val tabbed = SwingUtilities.getAncestorOfClass(JTabbedPane::class.java, that)
|
||||||
if (tabbed is JTabbedPane) {
|
if (tabbed is JTabbedPane) {
|
||||||
|
|||||||
18
src/main/kotlin/app/termora/sftp/SFTPKit.kt
Normal file
18
src/main/kotlin/app/termora/sftp/SFTPKit.kt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package app.termora.sftp
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.SystemUtils
|
||||||
|
import org.apache.commons.vfs2.FileObject
|
||||||
|
import org.apache.commons.vfs2.provider.local.LocalFile
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
|
fun FileObject.absolutePathString(): String {
|
||||||
|
var text = name.path
|
||||||
|
if (this is LocalFile && SystemUtils.IS_OS_WINDOWS) {
|
||||||
|
text = this.name.toString()
|
||||||
|
text = StringUtils.removeStart(text, "file:///")
|
||||||
|
text = StringUtils.replace(text, "/", File.separator)
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
@@ -5,31 +5,34 @@ import app.termora.actions.DataProvider
|
|||||||
import app.termora.actions.DataProviderSupport
|
import app.termora.actions.DataProviderSupport
|
||||||
import app.termora.findeverywhere.FindEverywhereProvider
|
import app.termora.findeverywhere.FindEverywhereProvider
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import okio.withLock
|
import okio.withLock
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
import org.apache.commons.vfs2.FileObject
|
||||||
|
import org.apache.commons.vfs2.VFS
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.event.ComponentAdapter
|
import java.awt.event.ComponentAdapter
|
||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
import java.nio.file.FileSystem
|
|
||||||
import java.nio.file.FileSystems
|
import java.nio.file.FileSystems
|
||||||
import java.nio.file.Path
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import kotlin.io.path.absolutePathString
|
|
||||||
|
|
||||||
fun FileSystem.isSFTP() = this is SftpFileSystem
|
|
||||||
fun FileSystem.isUnix() = isLocal() && SystemUtils.IS_OS_UNIX
|
|
||||||
fun FileSystem.isWindows() = isLocal() && SystemUtils.IS_OS_WINDOWS
|
|
||||||
fun FileSystem.isLocal() = StringUtils.startsWithAny(javaClass.name, "java", "sun")
|
|
||||||
|
|
||||||
class SFTPPanel : JPanel(BorderLayout()), DataProvider, Disposable {
|
class SFTPPanel : JPanel(BorderLayout()), DataProvider, Disposable {
|
||||||
|
|
||||||
|
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
private val transportTable = TransportTable()
|
private val transportTable = TransportTable()
|
||||||
private val transportManager get() = transportTable.model
|
private val transportManager get() = transportTable.model
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
private val leftComponent = SFTPTabbed(transportManager)
|
private val leftComponent = SFTPTabbed(transportManager)
|
||||||
private val rightComponent = SFTPTabbed(transportManager)
|
private val rightComponent = SFTPTabbed(transportManager)
|
||||||
|
private val localHost = Host(
|
||||||
|
id = "local",
|
||||||
|
name = I18n.getString("termora.transport.local"),
|
||||||
|
protocol = Protocol.Local,
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initViews()
|
initViews()
|
||||||
@@ -87,11 +90,10 @@ class SFTPPanel : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
leftComponent.addTab(
|
leftComponent.addTab(
|
||||||
I18n.getString("termora.transport.local"),
|
I18n.getString("termora.transport.local"),
|
||||||
FileSystemViewPanel(
|
FileSystemViewPanel(
|
||||||
Host(
|
localHost,
|
||||||
id = "local",
|
VFS.getManager().resolveFile("file:///${SystemUtils.USER_HOME}").fileSystem,
|
||||||
name = I18n.getString("termora.transport.local"),
|
transportManager,
|
||||||
protocol = Protocol.Local,
|
coroutineScope
|
||||||
), FileSystems.getDefault(), transportManager
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
leftComponent.setTabClosable(0, false)
|
leftComponent.setTabClosable(0, false)
|
||||||
@@ -125,7 +127,7 @@ class SFTPPanel : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val fs = c.fileSystem
|
val fs = c.getFileSystem()
|
||||||
val root = transportManager.root
|
val root = transportManager.root
|
||||||
|
|
||||||
transportManager.lock.withLock {
|
transportManager.lock.withLock {
|
||||||
@@ -165,9 +167,9 @@ class SFTPPanel : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
*/
|
*/
|
||||||
fun addTransport(
|
fun addTransport(
|
||||||
source: JComponent,
|
source: JComponent,
|
||||||
sourceWorkdir: Path?,
|
sourceWorkdir: FileObject?,
|
||||||
target: FileSystemViewPanel,
|
target: FileSystemViewPanel,
|
||||||
targetWorkdir: Path?,
|
targetWorkdir: FileObject?,
|
||||||
transport: Transport
|
transport: Transport
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
|
||||||
@@ -175,15 +177,12 @@ class SFTPPanel : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
as? FileSystemViewPanel ?: return false
|
as? FileSystemViewPanel ?: return false
|
||||||
val targetPanel = target as? FileSystemViewPanel ?: return false
|
val targetPanel = target as? FileSystemViewPanel ?: return false
|
||||||
if (sourcePanel.isDisposed || targetPanel.isDisposed) return false
|
if (sourcePanel.isDisposed || targetPanel.isDisposed) return false
|
||||||
val myTargetWorkdir = (targetWorkdir ?: targetPanel.getWorkdir()).absolutePathString()
|
val myTargetWorkdir = (targetWorkdir ?: targetPanel.getWorkdir())
|
||||||
val mySourceWorkdir = (sourceWorkdir ?: sourcePanel.getWorkdir()).absolutePathString()
|
val mySourceWorkdir = (sourceWorkdir ?: sourcePanel.getWorkdir())
|
||||||
val targetFileSystem = targetPanel.fileSystem
|
val sourcePath = transport.source
|
||||||
val sourcePath = transport.source.absolutePathString()
|
|
||||||
|
|
||||||
transport.target = targetFileSystem.getPath(
|
val relativeName = mySourceWorkdir.name.getRelativeName(sourcePath.name)
|
||||||
myTargetWorkdir,
|
transport.target = myTargetWorkdir.resolveFile(relativeName)
|
||||||
StringUtils.removeStart(sourcePath, mySourceWorkdir)
|
|
||||||
)
|
|
||||||
|
|
||||||
return transportManager.addTransport(transport)
|
return transportManager.addTransport(transport)
|
||||||
|
|
||||||
@@ -212,4 +211,8 @@ class SFTPPanel : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
return dataProviderSupport.getData(dataKey)
|
return dataProviderSupport.getData(dataKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
coroutineScope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,6 @@ import app.termora.actions.AnAction
|
|||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
import java.awt.Point
|
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
@@ -13,8 +12,6 @@ import javax.swing.JButton
|
|||||||
import javax.swing.JToolBar
|
import javax.swing.JToolBar
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
import kotlin.io.path.absolutePathString
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
@Suppress("DuplicatedCode")
|
@Suppress("DuplicatedCode")
|
||||||
class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPane(), Disposable {
|
class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPane(), Disposable {
|
||||||
@@ -44,23 +41,20 @@ class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPan
|
|||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
addBtn.addActionListener(object : AnAction() {
|
addBtn.addActionListener(object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val dialog = NewHostTreeDialog(SwingUtilities.getWindowAncestor(tabbed))
|
for (i in 0 until tabCount) {
|
||||||
dialog.location = Point(
|
val c = getComponentAt(i)
|
||||||
max(0, addBtn.locationOnScreen.x - dialog.width / 2 + addBtn.width / 2),
|
if (c !is SFTPFileSystemViewPanel) continue
|
||||||
addBtn.locationOnScreen.y + max(tabHeight, addBtn.height)
|
if (c.state != SFTPFileSystemViewPanel.State.Initialized) continue
|
||||||
)
|
selectedIndex = i
|
||||||
dialog.setFilter { it.host.protocol == Protocol.SSH }
|
return
|
||||||
dialog.setTreeName("SFTPTabbed.Tree")
|
|
||||||
dialog.allowMulti = true
|
|
||||||
dialog.isVisible = true
|
|
||||||
|
|
||||||
val hosts = dialog.hosts
|
|
||||||
if (hosts.isEmpty()) return
|
|
||||||
|
|
||||||
for (host in hosts) {
|
|
||||||
addSFTPFileSystemViewPanelTab(host)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加一个新的
|
||||||
|
addTab(
|
||||||
|
I18n.getString("termora.transport.sftp.select-host"),
|
||||||
|
SFTPFileSystemViewPanel(transportManager = transportManager)
|
||||||
|
)
|
||||||
|
selectedIndex = tabCount - 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -6,18 +6,13 @@ import kotlinx.coroutines.isActive
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.apache.commons.net.io.Util
|
import org.apache.commons.net.io.Util
|
||||||
|
import org.apache.commons.vfs2.FileObject
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.nio.file.Files
|
import java.nio.file.StandardOpenOption
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.attribute.BasicFileAttributeView
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
import kotlin.io.path.createDirectories
|
|
||||||
import kotlin.io.path.exists
|
|
||||||
import kotlin.io.path.getLastModifiedTime
|
|
||||||
import kotlin.io.path.name
|
|
||||||
|
|
||||||
enum class TransportStatus {
|
enum class TransportStatus {
|
||||||
Ready,
|
Ready,
|
||||||
@@ -48,12 +43,19 @@ class Transport(
|
|||||||
/**
|
/**
|
||||||
* 源
|
* 源
|
||||||
*/
|
*/
|
||||||
val source: Path,
|
val source: FileObject,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 目标
|
* 目标
|
||||||
*/
|
*/
|
||||||
var target: Path,
|
var target: FileObject,
|
||||||
|
/**
|
||||||
|
* 仅对文件生效,切只有两个选项
|
||||||
|
*
|
||||||
|
* 1. [StandardOpenOption.APPEND]
|
||||||
|
* 2. [StandardOpenOption.TRUNCATE_EXISTING]
|
||||||
|
*/
|
||||||
|
var mode: StandardOpenOption = StandardOpenOption.TRUNCATE_EXISTING
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -154,7 +156,7 @@ class Transport(
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
if (!target.exists()) {
|
if (!target.exists()) {
|
||||||
target.createDirectories()
|
target.createFolder()
|
||||||
}
|
}
|
||||||
} catch (e: FileAlreadyExistsException) {
|
} catch (e: FileAlreadyExistsException) {
|
||||||
if (log.isWarnEnabled) {
|
if (log.isWarnEnabled) {
|
||||||
@@ -169,8 +171,8 @@ class Transport(
|
|||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val input = Files.newInputStream(source)
|
val input = source.content.inputStream
|
||||||
val output = Files.newOutputStream(target)
|
val output = target.content.getOutputStream(mode == StandardOpenOption.APPEND)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@@ -209,8 +211,7 @@ class Transport(
|
|||||||
private fun preserveModificationTime() {
|
private fun preserveModificationTime() {
|
||||||
// 设置修改时间
|
// 设置修改时间
|
||||||
if (isPreserveModificationTime) {
|
if (isPreserveModificationTime) {
|
||||||
Files.getFileAttributeView(target, BasicFileAttributeView::class.java)
|
target.content.lastModifiedTime = source.content.lastModifiedTime
|
||||||
.setTimes(source.getLastModifiedTime(), source.getLastModifiedTime(), null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode
|
|||||||
import org.jdesktop.swingx.treetable.DefaultTreeTableModel
|
import org.jdesktop.swingx.treetable.DefaultTreeTableModel
|
||||||
import org.jdesktop.swingx.treetable.MutableTreeTableNode
|
import org.jdesktop.swingx.treetable.MutableTreeTableNode
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.*
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
import kotlin.collections.ArrayDeque
|
||||||
import kotlin.io.path.name
|
import kotlin.io.path.name
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
@@ -27,7 +29,7 @@ class TransportTableModel(private val coroutineScope: CoroutineScope) :
|
|||||||
|
|
||||||
val lock = ReentrantLock()
|
val lock = ReentrantLock()
|
||||||
|
|
||||||
private val transports = linkedMapOf<Long, TransportTreeTableNode>()
|
private val transports = Collections.synchronizedMap(linkedMapOf<Long, TransportTreeTableNode>())
|
||||||
private val reporter = SpeedReporter(coroutineScope)
|
private val reporter = SpeedReporter(coroutineScope)
|
||||||
private var listeners = emptyArray<TransportListener>()
|
private var listeners = emptyArray<TransportListener>()
|
||||||
private val activeTransports = linkedMapOf<Long, Job>()
|
private val activeTransports = linkedMapOf<Long, Job>()
|
||||||
@@ -104,8 +106,15 @@ class TransportTableModel(private val coroutineScope: CoroutineScope) :
|
|||||||
transports[transport.id] = newNode
|
transports[transport.id] = newNode
|
||||||
|
|
||||||
if ((transports.containsKey(parentId) || p == root) && transports.containsKey(transport.id)) {
|
if ((transports.containsKey(parentId) || p == root) && transports.containsKey(transport.id)) {
|
||||||
// 同步加入节点
|
// 主线程加入节点
|
||||||
SwingUtilities.invokeLater { insertNodeInto(newNode, p, p.childCount) }
|
SwingUtilities.invokeLater {
|
||||||
|
// 因为是异步的,父节点此时可能已经被移除了
|
||||||
|
if (p == root || transports.containsKey(parentId)) {
|
||||||
|
insertNodeInto(newNode, p, p.childCount)
|
||||||
|
} else {
|
||||||
|
removeTransport(transport.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return@withLock true
|
return@withLock true
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ package app.termora.sftp
|
|||||||
import app.termora.I18n
|
import app.termora.I18n
|
||||||
import app.termora.formatBytes
|
import app.termora.formatBytes
|
||||||
import app.termora.formatSeconds
|
import app.termora.formatSeconds
|
||||||
import org.apache.commons.io.file.PathUtils
|
import app.termora.vfs2.sftp.MySftpFileSystem
|
||||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
import app.termora.vfs2.sftp.MySftpFileSystemConfigBuilder
|
||||||
|
import org.apache.commons.vfs2.FileObject
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession
|
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession
|
||||||
import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode
|
import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode
|
||||||
import java.nio.file.Path
|
|
||||||
import kotlin.io.path.absolutePathString
|
|
||||||
|
|
||||||
class TransportTreeTableNode(transport: Transport) : DefaultMutableTreeTableNode(transport) {
|
class TransportTreeTableNode(transport: Transport) : DefaultMutableTreeTableNode(transport) {
|
||||||
val transport get() = userObject as Transport
|
val transport get() = userObject as Transport
|
||||||
@@ -20,7 +19,7 @@ class TransportTreeTableNode(transport: Transport) : DefaultMutableTreeTableNode
|
|||||||
(transport.filesize.get() - transport.transferredFilesize.get()) / speed else 0
|
(transport.filesize.get() - transport.transferredFilesize.get()) / speed else 0
|
||||||
|
|
||||||
return when (column) {
|
return when (column) {
|
||||||
TransportTableModel.COLUMN_NAME -> PathUtils.getFileNameString(transport.source)
|
TransportTableModel.COLUMN_NAME -> transport.source.name.baseName
|
||||||
TransportTableModel.COLUMN_STATUS -> formatStatus(transport)
|
TransportTableModel.COLUMN_STATUS -> formatStatus(transport)
|
||||||
TransportTableModel.COLUMN_SIZE -> size()
|
TransportTableModel.COLUMN_SIZE -> size()
|
||||||
TransportTableModel.COLUMN_SPEED -> if (isProcessing) formatBytes(speed) + "/s" else "-"
|
TransportTableModel.COLUMN_SPEED -> if (isProcessing) formatBytes(speed) + "/s" else "-"
|
||||||
@@ -31,12 +30,14 @@ class TransportTreeTableNode(transport: Transport) : DefaultMutableTreeTableNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatPath(path: Path): String {
|
private fun formatPath(file: FileObject): String {
|
||||||
if (path.fileSystem.isSFTP()) {
|
if (file.fileSystem is MySftpFileSystem) {
|
||||||
val hostname = ((path.fileSystem as SftpFileSystem).session as JGitClientSession).hostConfigEntry.hostName
|
val session = MySftpFileSystemConfigBuilder.getInstance()
|
||||||
return hostname + ":" + path.absolutePathString()
|
.getClientSession(file.fileSystem.fileSystemOptions) as JGitClientSession
|
||||||
|
val hostname = session.hostConfigEntry.hostName
|
||||||
|
return hostname + ":" + file.name.path
|
||||||
}
|
}
|
||||||
return path.toUri().scheme + ":" + path.absolutePathString()
|
return file.name.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatStatus(transport: Transport): String {
|
private fun formatStatus(transport: Transport): String {
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import app.termora.Icons
|
|||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.terminal.ControlCharacters
|
import app.termora.terminal.ControlCharacters
|
||||||
|
import app.termora.terminal.Null
|
||||||
import app.termora.terminal.panel.TerminalWriter
|
import app.termora.terminal.panel.TerminalWriter
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.text.StringEscapeUtils
|
||||||
|
|
||||||
class SnippetAction private constructor() : AnAction(I18n.getString("termora.snippet.title"), Icons.codeSpan) {
|
class SnippetAction private constructor() : AnAction(I18n.getString("termora.snippet.title"), Icons.codeSpan) {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -25,18 +28,30 @@ class SnippetAction private constructor() : AnAction(I18n.getString("termora.sni
|
|||||||
fun runSnippet(snippet: Snippet, writer: TerminalWriter) {
|
fun runSnippet(snippet: Snippet, writer: TerminalWriter) {
|
||||||
if (snippet.type != SnippetType.Snippet) return
|
if (snippet.type != SnippetType.Snippet) return
|
||||||
val map = mapOf(
|
val map = mapOf(
|
||||||
"\\r" to ControlCharacters.CR,
|
"\n" to ControlCharacters.LF,
|
||||||
"\\n" to ControlCharacters.LF,
|
"\r" to ControlCharacters.CR,
|
||||||
"\\t" to ControlCharacters.TAB,
|
"\t" to ControlCharacters.TAB,
|
||||||
|
"\b" to ControlCharacters.BS,
|
||||||
"\\a" to ControlCharacters.BEL,
|
"\\a" to ControlCharacters.BEL,
|
||||||
"\\e" to ControlCharacters.ESC,
|
"\\e" to ControlCharacters.ESC,
|
||||||
"\\b" to ControlCharacters.BS,
|
|
||||||
)
|
)
|
||||||
|
val chars = snippet.snippet.toCharArray()
|
||||||
|
for (i in chars.indices) {
|
||||||
|
val c = chars[i]
|
||||||
|
if (i == 0) continue
|
||||||
|
if (c != '\n') continue
|
||||||
|
if (chars[i - 1] != '\\') continue
|
||||||
|
// 每一行的最后一个 \ 比较特殊,先转成 null 然后再去 unescapeJava
|
||||||
|
chars[i - 1] = Char.Null
|
||||||
|
}
|
||||||
|
|
||||||
var text = snippet.snippet
|
var text = chars.joinToString(StringUtils.EMPTY)
|
||||||
|
text = StringEscapeUtils.unescapeJava(text)
|
||||||
for (e in map.entries) {
|
for (e in map.entries) {
|
||||||
text = text.replace(e.key, e.value.toString())
|
text = text.replace(e.key, e.value.toString())
|
||||||
}
|
}
|
||||||
|
text = text.replace(Char.Null, '\\')
|
||||||
|
|
||||||
writer.write(TerminalWriter.WriteRequest.fromBytes(text.toByteArray(writer.getCharset())))
|
writer.write(TerminalWriter.WriteRequest.fromBytes(text.toByteArray(writer.getCharset())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package app.termora.snippet
|
|||||||
|
|
||||||
import app.termora.ApplicationScope
|
import app.termora.ApplicationScope
|
||||||
import app.termora.Database
|
import app.termora.Database
|
||||||
|
import app.termora.DeleteDataManager
|
||||||
import app.termora.assertEventDispatchThread
|
import app.termora.assertEventDispatchThread
|
||||||
|
|
||||||
|
|
||||||
@@ -20,14 +21,20 @@ class SnippetManager private constructor() {
|
|||||||
*/
|
*/
|
||||||
fun addSnippet(snippet: Snippet) {
|
fun addSnippet(snippet: Snippet) {
|
||||||
assertEventDispatchThread()
|
assertEventDispatchThread()
|
||||||
database.addSnippet(snippet)
|
|
||||||
if (snippet.deleted) {
|
if (snippet.deleted) {
|
||||||
snippets.entries.removeIf { it.value.id == snippet.id || it.value.parentId == snippet.id }
|
removeSnippet(snippet.id)
|
||||||
} else {
|
} else {
|
||||||
|
database.addSnippet(snippet)
|
||||||
snippets[snippet.id] = snippet
|
snippets[snippet.id] = snippet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeSnippet(id: String) {
|
||||||
|
snippets.entries.removeIf { it.value.id == id || it.value.parentId == id }
|
||||||
|
database.removeSnippet(id)
|
||||||
|
DeleteDataManager.getInstance().removeSnippet(id)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 第一次调用从数据库中获取,后续从缓存中获取
|
* 第一次调用从数据库中获取,后续从缓存中获取
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -41,8 +41,10 @@ class SnippetPanel : JPanel(BorderLayout()), Disposable {
|
|||||||
private fun initViews() {
|
private fun initViews() {
|
||||||
val splitPane = JSplitPane()
|
val splitPane = JSplitPane()
|
||||||
splitPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
splitPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||||
|
val scrollPane = JScrollPane(snippetTree)
|
||||||
|
scrollPane.border = BorderFactory.createEmptyBorder()
|
||||||
|
|
||||||
leftPanel.add(snippetTree, BorderLayout.CENTER)
|
leftPanel.add(scrollPane, BorderLayout.CENTER)
|
||||||
leftPanel.border = BorderFactory.createCompoundBorder(
|
leftPanel.border = BorderFactory.createCompoundBorder(
|
||||||
BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor),
|
BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor),
|
||||||
BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
@@ -51,6 +53,7 @@ class SnippetPanel : JPanel(BorderLayout()), Disposable {
|
|||||||
properties.getString("SnippetPanel.LeftPanel.width", "180").toIntOrNull() ?: 180,
|
properties.getString("SnippetPanel.LeftPanel.width", "180").toIntOrNull() ?: 180,
|
||||||
-1
|
-1
|
||||||
)
|
)
|
||||||
|
leftPanel.minimumSize = Dimension(leftPanel.preferredSize.width, leftPanel.preferredSize.height)
|
||||||
|
|
||||||
rightPanel.border = BorderFactory.createCompoundBorder(
|
rightPanel.border = BorderFactory.createCompoundBorder(
|
||||||
BorderFactory.createMatteBorder(0, 1, 0, 0, DynamicColor.BorderColor),
|
BorderFactory.createMatteBorder(0, 1, 0, 0, DynamicColor.BorderColor),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.snippet
|
package app.termora.snippet
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
import app.termora.SimpleTreeModel
|
import app.termora.SimpleTreeModel
|
||||||
import javax.swing.tree.MutableTreeNode
|
import javax.swing.tree.MutableTreeNode
|
||||||
import javax.swing.tree.TreeNode
|
import javax.swing.tree.TreeNode
|
||||||
@@ -8,7 +9,7 @@ class SnippetTreeModel : SimpleTreeModel<Snippet>(
|
|||||||
SnippetTreeNode(
|
SnippetTreeNode(
|
||||||
Snippet(
|
Snippet(
|
||||||
id = "0",
|
id = "0",
|
||||||
name = "全部片段",
|
name = I18n.getString("termora.snippet.title"),
|
||||||
type = SnippetType.Folder
|
type = SnippetType.Folder
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.termora.sync
|
package app.termora.sync
|
||||||
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
|
import app.termora.DeletedData
|
||||||
import app.termora.ResponseException
|
import app.termora.ResponseException
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
@@ -26,46 +27,51 @@ abstract class GitSyncer : SafetySyncer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val gistResponse = parsePullResponse(response, config)
|
val gistResponse = parsePullResponse(response, config)
|
||||||
|
val deletedData = mutableListOf<DeletedData>()
|
||||||
|
|
||||||
|
// DeletedData
|
||||||
|
gistResponse.gists.findLast { it.filename == "DeletedData" }
|
||||||
|
?.let { deletedData.addAll(decodeDeletedData(it.content, config)) }
|
||||||
|
|
||||||
// decode hosts
|
// decode hosts
|
||||||
if (config.ranges.contains(SyncRange.Hosts)) {
|
if (config.ranges.contains(SyncRange.Hosts)) {
|
||||||
gistResponse.gists.findLast { it.filename == "Hosts" }?.let {
|
gistResponse.gists.findLast { it.filename == "Hosts" }?.let {
|
||||||
decodeHosts(it.content, config)
|
decodeHosts(it.content, deletedData.filter { e -> e.type == "Host" }, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode keys
|
// decode keys
|
||||||
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
||||||
gistResponse.gists.findLast { it.filename == "KeyPairs" }?.let {
|
gistResponse.gists.findLast { it.filename == "KeyPairs" }?.let {
|
||||||
decodeKeys(it.content, config)
|
decodeKeys(it.content, deletedData.filter { e -> e.type == "KeyPair" }, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode keyword highlights
|
// decode keyword highlights
|
||||||
if (config.ranges.contains(SyncRange.KeywordHighlights)) {
|
if (config.ranges.contains(SyncRange.KeywordHighlights)) {
|
||||||
gistResponse.gists.findLast { it.filename == "KeywordHighlights" }?.let {
|
gistResponse.gists.findLast { it.filename == "KeywordHighlights" }?.let {
|
||||||
decodeKeywordHighlights(it.content, config)
|
decodeKeywordHighlights(it.content, deletedData.filter { e -> e.type == "KeywordHighlight" }, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode macros
|
// decode macros
|
||||||
if (config.ranges.contains(SyncRange.Macros)) {
|
if (config.ranges.contains(SyncRange.Macros)) {
|
||||||
gistResponse.gists.findLast { it.filename == "Macros" }?.let {
|
gistResponse.gists.findLast { it.filename == "Macros" }?.let {
|
||||||
decodeMacros(it.content, config)
|
decodeMacros(it.content, deletedData.filter { e -> e.type == "Macro" }, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode keymaps
|
// decode keymaps
|
||||||
if (config.ranges.contains(SyncRange.Macros)) {
|
if (config.ranges.contains(SyncRange.Macros)) {
|
||||||
gistResponse.gists.findLast { it.filename == "Keymaps" }?.let {
|
gistResponse.gists.findLast { it.filename == "Keymaps" }?.let {
|
||||||
decodeKeymaps(it.content, config)
|
decodeKeymaps(it.content, deletedData.filter { e -> e.type == "Keymap" }, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode Snippets
|
// decode Snippets
|
||||||
if (config.ranges.contains(SyncRange.Snippets)) {
|
if (config.ranges.contains(SyncRange.Snippets)) {
|
||||||
gistResponse.gists.findLast { it.filename == "Snippets" }?.let {
|
gistResponse.gists.findLast { it.filename == "Snippets" }?.let {
|
||||||
decodeSnippets(it.content, config)
|
decodeSnippets(it.content, deletedData.filter { e -> e.type == "Snippet" }, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +85,11 @@ abstract class GitSyncer : SafetySyncer() {
|
|||||||
|
|
||||||
|
|
||||||
override fun push(config: SyncConfig): GistResponse {
|
override fun push(config: SyncConfig): GistResponse {
|
||||||
|
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("Type: ${config.type} , Gist: ${config.gistId} Push...")
|
||||||
|
}
|
||||||
|
|
||||||
val gistFiles = mutableListOf<GistFile>()
|
val gistFiles = mutableListOf<GistFile>()
|
||||||
// aes key
|
// aes key
|
||||||
val key = ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16)
|
val key = ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16)
|
||||||
@@ -142,9 +153,21 @@ abstract class GitSyncer : SafetySyncer() {
|
|||||||
throw IllegalArgumentException("No gist files found")
|
throw IllegalArgumentException("No gist files found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val deletedData = encodeDeletedData(config)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Push DeletedData: {}", deletedData)
|
||||||
|
}
|
||||||
|
gistFiles.add(GistFile("DeletedData", deletedData))
|
||||||
|
|
||||||
val request = newPushRequestBuilder(gistFiles, config).build()
|
val request = newPushRequestBuilder(gistFiles, config).build()
|
||||||
|
|
||||||
return parsePushResponse(httpClient.newCall(request).execute(), config)
|
try {
|
||||||
|
return parsePushResponse(httpClient.newCall(request).execute(), config)
|
||||||
|
} finally {
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("Type: ${config.type} , Gist: ${config.gistId} Pushed")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun parsePullResponse(response: Response, config: SyncConfig): GistResponse {
|
open fun parsePullResponse(response: Response, config: SyncConfig): GistResponse {
|
||||||
|
|||||||
@@ -34,8 +34,9 @@ abstract class SafetySyncer : Syncer {
|
|||||||
protected val macroManager get() = MacroManager.getInstance()
|
protected val macroManager get() = MacroManager.getInstance()
|
||||||
protected val keymapManager get() = KeymapManager.getInstance()
|
protected val keymapManager get() = KeymapManager.getInstance()
|
||||||
protected val snippetManager get() = SnippetManager.getInstance()
|
protected val snippetManager get() = SnippetManager.getInstance()
|
||||||
|
protected val deleteDataManager get() = DeleteDataManager.getInstance()
|
||||||
|
|
||||||
protected fun decodeHosts(text: String, config: SyncConfig) {
|
protected fun decodeHosts(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
||||||
// aes key
|
// aes key
|
||||||
val key = getKey(config)
|
val key = getKey(config)
|
||||||
val encryptedHosts = ohMyJson.decodeFromString<List<EncryptedHost>>(text)
|
val encryptedHosts = ohMyJson.decodeFromString<List<EncryptedHost>>(text)
|
||||||
@@ -44,9 +45,9 @@ abstract class SafetySyncer : Syncer {
|
|||||||
for (encryptedHost in encryptedHosts) {
|
for (encryptedHost in encryptedHosts) {
|
||||||
val oldHost = hosts[encryptedHost.id]
|
val oldHost = hosts[encryptedHost.id]
|
||||||
|
|
||||||
// 如果一样,则无需配置
|
// 如果本地的修改时间大于云端时间,那么跳过
|
||||||
if (oldHost != null) {
|
if (oldHost != null) {
|
||||||
if (oldHost.updateDate == encryptedHost.updateDate) {
|
if (oldHost.updateDate >= encryptedHost.updateDate) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,7 +84,6 @@ abstract class SafetySyncer : Syncer {
|
|||||||
creatorId = encryptedHost.creatorId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
creatorId = encryptedHost.creatorId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
createDate = encryptedHost.createDate,
|
createDate = encryptedHost.createDate,
|
||||||
updateDate = encryptedHost.updateDate,
|
updateDate = encryptedHost.updateDate,
|
||||||
deleted = encryptedHost.deleted
|
|
||||||
)
|
)
|
||||||
SwingUtilities.invokeLater { hostManager.addHost(host) }
|
SwingUtilities.invokeLater { hostManager.addHost(host) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -93,6 +93,12 @@ abstract class SafetySyncer : Syncer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
deletedData.forEach {
|
||||||
|
hostManager.removeHost(it.id)
|
||||||
|
deleteDataManager.removeHost(it.id, it.deleteDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Decode hosts: {}", text)
|
log.debug("Decode hosts: {}", text)
|
||||||
@@ -120,7 +126,6 @@ abstract class SafetySyncer : Syncer {
|
|||||||
encryptedHost.tunnelings =
|
encryptedHost.tunnelings =
|
||||||
ohMyJson.encodeToString(host.tunnelings).aesCBCEncrypt(key, iv).encodeBase64String()
|
ohMyJson.encodeToString(host.tunnelings).aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
encryptedHost.sort = host.sort
|
encryptedHost.sort = host.sort
|
||||||
encryptedHost.deleted = host.deleted
|
|
||||||
encryptedHost.parentId = host.parentId.aesCBCEncrypt(key, iv).encodeBase64String()
|
encryptedHost.parentId = host.parentId.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
encryptedHost.ownerId = host.ownerId.aesCBCEncrypt(key, iv).encodeBase64String()
|
encryptedHost.ownerId = host.ownerId.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
encryptedHost.creatorId = host.creatorId.aesCBCEncrypt(key, iv).encodeBase64String()
|
encryptedHost.creatorId = host.creatorId.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
@@ -133,7 +138,18 @@ abstract class SafetySyncer : Syncer {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun decodeSnippets(text: String, config: SyncConfig) {
|
protected fun encodeDeletedData(config: SyncConfig): String {
|
||||||
|
return ohMyJson.encodeToString(deleteDataManager.getDeletedData())
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun decodeDeletedData(text: String, config: SyncConfig): List<DeletedData> {
|
||||||
|
val deletedData = ohMyJson.decodeFromString<List<DeletedData>>(text).toMutableList()
|
||||||
|
// 和本地融合
|
||||||
|
deletedData.addAll(deleteDataManager.getDeletedData())
|
||||||
|
return deletedData
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun decodeSnippets(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
||||||
// aes key
|
// aes key
|
||||||
val key = getKey(config)
|
val key = getKey(config)
|
||||||
val encryptedSnippets = ohMyJson.decodeFromString<List<Snippet>>(text)
|
val encryptedSnippets = ohMyJson.decodeFromString<List<Snippet>>(text)
|
||||||
@@ -144,7 +160,7 @@ abstract class SafetySyncer : Syncer {
|
|||||||
|
|
||||||
// 如果一样,则无需配置
|
// 如果一样,则无需配置
|
||||||
if (oldHost != null) {
|
if (oldHost != null) {
|
||||||
if (oldHost.updateDate == encryptedSnippet.updateDate) {
|
if (oldHost.updateDate >= encryptedSnippet.updateDate) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,9 +181,15 @@ abstract class SafetySyncer : Syncer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
deletedData.forEach {
|
||||||
|
snippetManager.removeSnippet(it.id)
|
||||||
|
deleteDataManager.removeSnippet(it.id, it.deleteDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Decode hosts: {}", text)
|
log.debug("Decode Snippets: {}", text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,12 +210,20 @@ abstract class SafetySyncer : Syncer {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun decodeKeys(text: String, config: SyncConfig) {
|
protected fun decodeKeys(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
||||||
// aes key
|
// aes key
|
||||||
val key = getKey(config)
|
val key = getKey(config)
|
||||||
val encryptedKeys = ohMyJson.decodeFromString<List<OhKeyPair>>(text)
|
val encryptedKeys = ohMyJson.decodeFromString<List<OhKeyPair>>(text)
|
||||||
|
val keys = keyManager.getOhKeyPairs().associateBy { it.id }
|
||||||
|
|
||||||
for (encryptedKey in encryptedKeys) {
|
for (encryptedKey in encryptedKeys) {
|
||||||
|
val k = keys[encryptedKey.id]
|
||||||
|
if (k != null) {
|
||||||
|
if (k.updateDate > encryptedKey.updateDate) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// aes iv
|
// aes iv
|
||||||
val iv = getIv(encryptedKey.id)
|
val iv = getIv(encryptedKey.id)
|
||||||
@@ -215,6 +245,13 @@ abstract class SafetySyncer : Syncer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
deletedData.forEach {
|
||||||
|
keyManager.removeOhKeyPair(it.id)
|
||||||
|
deleteDataManager.removeKeyPair(it.id, it.deleteDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Decode keys: {}", text)
|
log.debug("Decode keys: {}", text)
|
||||||
}
|
}
|
||||||
@@ -240,12 +277,20 @@ abstract class SafetySyncer : Syncer {
|
|||||||
return ohMyJson.encodeToString(encryptedKeys)
|
return ohMyJson.encodeToString(encryptedKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun decodeKeywordHighlights(text: String, config: SyncConfig) {
|
protected fun decodeKeywordHighlights(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
||||||
// aes key
|
// aes key
|
||||||
val key = getKey(config)
|
val key = getKey(config)
|
||||||
val encryptedKeywordHighlights = ohMyJson.decodeFromString<List<KeywordHighlight>>(text)
|
val encryptedKeywordHighlights = ohMyJson.decodeFromString<List<KeywordHighlight>>(text)
|
||||||
|
val keywordHighlights = keywordHighlightManager.getKeywordHighlights().associateBy { it.id }
|
||||||
|
|
||||||
for (e in encryptedKeywordHighlights) {
|
for (e in encryptedKeywordHighlights) {
|
||||||
|
val keywordHighlight = keywordHighlights[e.id]
|
||||||
|
if (keywordHighlight != null) {
|
||||||
|
if (keywordHighlight.updateDate >= e.updateDate) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// aes iv
|
// aes iv
|
||||||
val iv = getIv(e.id)
|
val iv = getIv(e.id)
|
||||||
@@ -262,6 +307,13 @@ abstract class SafetySyncer : Syncer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
deletedData.forEach {
|
||||||
|
keywordHighlightManager.removeKeywordHighlight(it.id)
|
||||||
|
deleteDataManager.removeKeywordHighlight(it.id, it.deleteDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Decode KeywordHighlight: {}", text)
|
log.debug("Decode KeywordHighlight: {}", text)
|
||||||
}
|
}
|
||||||
@@ -281,12 +333,19 @@ abstract class SafetySyncer : Syncer {
|
|||||||
return ohMyJson.encodeToString(keywordHighlights)
|
return ohMyJson.encodeToString(keywordHighlights)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun decodeMacros(text: String, config: SyncConfig) {
|
protected fun decodeMacros(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
||||||
// aes key
|
// aes key
|
||||||
val key = getKey(config)
|
val key = getKey(config)
|
||||||
val encryptedMacros = ohMyJson.decodeFromString<List<Macro>>(text)
|
val encryptedMacros = ohMyJson.decodeFromString<List<Macro>>(text)
|
||||||
|
val macros = macroManager.getMacros().associateBy { it.id }
|
||||||
for (e in encryptedMacros) {
|
for (e in encryptedMacros) {
|
||||||
|
val macro = macros[e.id]
|
||||||
|
if (macro != null) {
|
||||||
|
if (macro.updateDate >= e.updateDate) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// aes iv
|
// aes iv
|
||||||
val iv = getIv(e.id)
|
val iv = getIv(e.id)
|
||||||
@@ -303,6 +362,13 @@ abstract class SafetySyncer : Syncer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
deletedData.forEach {
|
||||||
|
macroManager.removeMacro(it.id)
|
||||||
|
deleteDataManager.removeMacro(it.id, it.deleteDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Decode Macros: {}", text)
|
log.debug("Decode Macros: {}", text)
|
||||||
}
|
}
|
||||||
@@ -322,12 +388,27 @@ abstract class SafetySyncer : Syncer {
|
|||||||
return ohMyJson.encodeToString(macros)
|
return ohMyJson.encodeToString(macros)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun decodeKeymaps(text: String, config: SyncConfig) {
|
protected fun decodeKeymaps(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
||||||
|
|
||||||
for (keymap in ohMyJson.decodeFromString<List<JsonObject>>(text).mapNotNull { Keymap.fromJSON(it) }) {
|
val localKeymaps = keymapManager.getKeymaps().associateBy { it.name }
|
||||||
|
val remoteKeymaps = ohMyJson.decodeFromString<List<JsonObject>>(text).mapNotNull { Keymap.fromJSON(it) }
|
||||||
|
for (keymap in remoteKeymaps) {
|
||||||
|
val localKeymap = localKeymaps[keymap.name]
|
||||||
|
if (localKeymap != null) {
|
||||||
|
if (localKeymap.updateDate > keymap.updateDate) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
keymapManager.addKeymap(keymap)
|
keymapManager.addKeymap(keymap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
deletedData.forEach {
|
||||||
|
keymapManager.removeKeymap(it.id)
|
||||||
|
deleteDataManager.removeKeymap(it.id, it.deleteDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Decode Keymaps: {}", text)
|
log.debug("Decode Keymaps: {}", text)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ enum class SyncType {
|
|||||||
WebDAV,
|
WebDAV,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class SyncPolicy {
|
||||||
|
Manual,
|
||||||
|
OnChange,
|
||||||
|
}
|
||||||
|
|
||||||
enum class SyncRange {
|
enum class SyncRange {
|
||||||
Hosts,
|
Hosts,
|
||||||
KeyPairs,
|
KeyPairs,
|
||||||
|
|||||||
175
src/main/kotlin/app/termora/sync/SyncManager.kt
Normal file
175
src/main/kotlin/app/termora/sync/SyncManager.kt
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package app.termora.sync
|
||||||
|
|
||||||
|
import app.termora.ApplicationScope
|
||||||
|
import app.termora.Database
|
||||||
|
import app.termora.Disposable
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@Suppress("DuplicatedCode")
|
||||||
|
class SyncManager private constructor() : Disposable {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(SyncManager::class.java)
|
||||||
|
|
||||||
|
fun getInstance(): SyncManager {
|
||||||
|
return ApplicationScope.forApplicationScope().getOrCreate(SyncManager::class) { SyncManager() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sync get() = Database.getDatabase().sync
|
||||||
|
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
private var job: Job? = null
|
||||||
|
private var disableTrigger = false
|
||||||
|
|
||||||
|
|
||||||
|
private fun trigger() {
|
||||||
|
trigger(getSyncConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun triggerOnChanged() {
|
||||||
|
if (sync.policy == SyncPolicy.OnChange.name) {
|
||||||
|
trigger()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun trigger(config: SyncConfig) {
|
||||||
|
if (disableTrigger) return
|
||||||
|
|
||||||
|
job?.cancel()
|
||||||
|
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("Automatic synchronisation is interrupted")
|
||||||
|
}
|
||||||
|
|
||||||
|
job = coroutineScope.launch {
|
||||||
|
|
||||||
|
// 因为会频繁调用,等待 10 - 30 秒
|
||||||
|
val seconds = Random.nextInt(10, 30)
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("Trigger synchronisation, which will take place after {} seconds", seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(seconds.seconds)
|
||||||
|
|
||||||
|
|
||||||
|
if (!disableTrigger) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("Automatic synchronisation begin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已经开始,设置为 null
|
||||||
|
// 因为同步的时候会修改数据,避免被中断
|
||||||
|
job = null
|
||||||
|
|
||||||
|
sync(config)
|
||||||
|
|
||||||
|
sync.lastSyncTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("Automatic synchronisation end")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sync(config: SyncConfig): SyncResponse {
|
||||||
|
return syncImmediately(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun getSyncConfig(): SyncConfig {
|
||||||
|
val range = mutableSetOf<SyncRange>()
|
||||||
|
if (sync.rangeHosts) {
|
||||||
|
range.add(SyncRange.Hosts)
|
||||||
|
}
|
||||||
|
if (sync.rangeKeyPairs) {
|
||||||
|
range.add(SyncRange.KeyPairs)
|
||||||
|
}
|
||||||
|
if (sync.rangeKeywordHighlights) {
|
||||||
|
range.add(SyncRange.KeywordHighlights)
|
||||||
|
}
|
||||||
|
if (sync.rangeMacros) {
|
||||||
|
range.add(SyncRange.Macros)
|
||||||
|
}
|
||||||
|
if (sync.rangeKeymap) {
|
||||||
|
range.add(SyncRange.Keymap)
|
||||||
|
}
|
||||||
|
if (sync.rangeSnippets) {
|
||||||
|
range.add(SyncRange.Snippets)
|
||||||
|
}
|
||||||
|
return SyncConfig(
|
||||||
|
type = sync.type,
|
||||||
|
token = sync.token,
|
||||||
|
gistId = sync.gist,
|
||||||
|
options = mapOf("domain" to sync.domain),
|
||||||
|
ranges = range
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun syncImmediately(config: SyncConfig): SyncResponse {
|
||||||
|
synchronized(this) {
|
||||||
|
return SyncResponse(pull(config), push(config))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun pull(config: SyncConfig): GistResponse {
|
||||||
|
synchronized(this) {
|
||||||
|
disableTrigger = true
|
||||||
|
try {
|
||||||
|
return SyncerProvider.getInstance().getSyncer(config.type).pull(config)
|
||||||
|
} finally {
|
||||||
|
disableTrigger = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun push(config: SyncConfig): GistResponse {
|
||||||
|
synchronized(this) {
|
||||||
|
try {
|
||||||
|
disableTrigger = true
|
||||||
|
return SyncerProvider.getInstance().getSyncer(config.type).push(config)
|
||||||
|
} finally {
|
||||||
|
disableTrigger = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
coroutineScope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class SyncerProvider private constructor() {
|
||||||
|
companion object {
|
||||||
|
fun getInstance(): SyncerProvider {
|
||||||
|
return ApplicationScope.forApplicationScope().getOrCreate(SyncerProvider::class) { SyncerProvider() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getSyncer(type: SyncType): Syncer {
|
||||||
|
return when (type) {
|
||||||
|
SyncType.GitHub -> GitHubSyncer.getInstance()
|
||||||
|
SyncType.Gitee -> GiteeSyncer.getInstance()
|
||||||
|
SyncType.GitLab -> GitLabSyncer.getInstance()
|
||||||
|
SyncType.WebDAV -> WebDAVSyncer.getInstance()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SyncResponse(val pull: GistResponse, val push: GistResponse)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package app.termora.sync
|
|
||||||
|
|
||||||
import app.termora.ApplicationScope
|
|
||||||
|
|
||||||
class SyncerProvider private constructor() {
|
|
||||||
companion object {
|
|
||||||
fun getInstance(): SyncerProvider {
|
|
||||||
return ApplicationScope.forApplicationScope().getOrCreate(SyncerProvider::class) { SyncerProvider() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun getSyncer(type: SyncType): Syncer {
|
|
||||||
return when (type) {
|
|
||||||
SyncType.GitHub -> GitHubSyncer.getInstance()
|
|
||||||
SyncType.Gitee -> GiteeSyncer.getInstance()
|
|
||||||
SyncType.GitLab -> GitLabSyncer.getInstance()
|
|
||||||
SyncType.WebDAV -> WebDAVSyncer.getInstance()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ package app.termora.sync
|
|||||||
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.ApplicationScope
|
import app.termora.ApplicationScope
|
||||||
|
import app.termora.DeletedData
|
||||||
import app.termora.PBKDF2
|
import app.termora.PBKDF2
|
||||||
import app.termora.ResponseException
|
import app.termora.ResponseException
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
@@ -27,6 +28,9 @@ class WebDAVSyncer private constructor() : SafetySyncer() {
|
|||||||
override fun pull(config: SyncConfig): GistResponse {
|
override fun pull(config: SyncConfig): GistResponse {
|
||||||
val response = httpClient.newCall(newRequestBuilder(config).get().build()).execute()
|
val response = httpClient.newCall(newRequestBuilder(config).get().build()).execute()
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
|
if (response.code == 404) {
|
||||||
|
return GistResponse(config, emptyList())
|
||||||
|
}
|
||||||
throw ResponseException(response.code, response)
|
throw ResponseException(response.code, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,46 +38,48 @@ class WebDAVSyncer private constructor() : SafetySyncer() {
|
|||||||
?: throw ResponseException(response.code, response)
|
?: throw ResponseException(response.code, response)
|
||||||
|
|
||||||
val json = ohMyJson.decodeFromString<JsonObject>(text)
|
val json = ohMyJson.decodeFromString<JsonObject>(text)
|
||||||
|
val deletedData = mutableListOf<DeletedData>()
|
||||||
|
json["DeletedData"]?.jsonPrimitive?.content?.let { deletedData.addAll(decodeDeletedData(it, config)) }
|
||||||
|
|
||||||
// decode hosts
|
// decode hosts
|
||||||
if (config.ranges.contains(SyncRange.Hosts)) {
|
if (config.ranges.contains(SyncRange.Hosts)) {
|
||||||
json["Hosts"]?.jsonPrimitive?.content?.let {
|
json["Hosts"]?.jsonPrimitive?.content?.let {
|
||||||
decodeHosts(it, config)
|
decodeHosts(it, deletedData.filter { e -> e.type == "Host" }, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode KeyPairs
|
// decode KeyPairs
|
||||||
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
||||||
json["KeyPairs"]?.jsonPrimitive?.content?.let {
|
json["KeyPairs"]?.jsonPrimitive?.content?.let {
|
||||||
decodeKeys(it, config)
|
decodeKeys(it, deletedData.filter { e -> e.type == "KeyPair" }, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode Highlights
|
// decode Highlights
|
||||||
if (config.ranges.contains(SyncRange.KeywordHighlights)) {
|
if (config.ranges.contains(SyncRange.KeywordHighlights)) {
|
||||||
json["KeywordHighlights"]?.jsonPrimitive?.content?.let {
|
json["KeywordHighlights"]?.jsonPrimitive?.content?.let {
|
||||||
decodeKeywordHighlights(it, config)
|
decodeKeywordHighlights(it, deletedData.filter { e -> e.type == "KeywordHighlight" }, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode Macros
|
// decode Macros
|
||||||
if (config.ranges.contains(SyncRange.Macros)) {
|
if (config.ranges.contains(SyncRange.Macros)) {
|
||||||
json["Macros"]?.jsonPrimitive?.content?.let {
|
json["Macros"]?.jsonPrimitive?.content?.let {
|
||||||
decodeMacros(it, config)
|
decodeMacros(it, deletedData.filter { e -> e.type == "Macro" }, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode Keymaps
|
// decode Keymaps
|
||||||
if (config.ranges.contains(SyncRange.Keymap)) {
|
if (config.ranges.contains(SyncRange.Keymap)) {
|
||||||
json["Keymaps"]?.jsonPrimitive?.content?.let {
|
json["Keymaps"]?.jsonPrimitive?.content?.let {
|
||||||
decodeKeymaps(it, config)
|
decodeKeymaps(it, deletedData.filter { e -> e.type == "Keymap" }, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode Snippets
|
// decode Snippets
|
||||||
if (config.ranges.contains(SyncRange.Snippets)) {
|
if (config.ranges.contains(SyncRange.Snippets)) {
|
||||||
json["Snippets"]?.jsonPrimitive?.content?.let {
|
json["Snippets"]?.jsonPrimitive?.content?.let {
|
||||||
decodeSnippets(it, config)
|
decodeSnippets(it, deletedData.filter { e -> e.type == "Snippet" }, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +143,13 @@ class WebDAVSyncer private constructor() : SafetySyncer() {
|
|||||||
}
|
}
|
||||||
put("Keymaps", keymapsContent)
|
put("Keymaps", keymapsContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deletedData
|
||||||
|
val deletedData = encodeDeletedData(config)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Push DeletedData: {}", deletedData)
|
||||||
|
}
|
||||||
|
put("DeletedData", deletedData)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = httpClient.newCall(
|
val response = httpClient.newCall(
|
||||||
|
|||||||
@@ -399,6 +399,16 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).
|
||||||
|
'Z' -> {
|
||||||
|
val count = args.toInt(1)
|
||||||
|
val cursorModel = terminal.getCursorModel()
|
||||||
|
for (i in 0 until count) {
|
||||||
|
val x = terminal.getTabulator().previousTab(cursorModel.getPosition().x - 1) + 1
|
||||||
|
terminal.getCursorModel().move(cursorModel.getPosition().y, x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// split
|
// split
|
||||||
';' -> {
|
';' -> {
|
||||||
args.append(ch)
|
args.append(ch)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import java.io.InputStreamReader
|
|||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
class PtyProcessConnector(private val process: PtyProcess, private val charset: Charset = StandardCharsets.UTF_8) :
|
class PtyProcessConnector(val process: PtyProcess, private val charset: Charset = StandardCharsets.UTF_8) :
|
||||||
StreamPtyConnector(process.inputStream, process.outputStream) {
|
StreamPtyConnector(process.inputStream, process.outputStream) {
|
||||||
|
|
||||||
private val reader = InputStreamReader(input)
|
private val reader = InputStreamReader(input)
|
||||||
|
|||||||
@@ -14,11 +14,16 @@ import com.formdev.flatlaf.extras.components.FlatToolBar
|
|||||||
import com.formdev.flatlaf.ui.FlatRoundBorder
|
import com.formdev.flatlaf.ui.FlatRoundBorder
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.event.ActionListener
|
import java.awt.event.ActionListener
|
||||||
|
import java.beans.PropertyChangeEvent
|
||||||
|
import java.beans.PropertyChangeListener
|
||||||
|
import java.util.*
|
||||||
import javax.swing.JButton
|
import javax.swing.JButton
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||||
private val floatingToolbarEnable get() = Database.getDatabase().terminal.floatingToolbar
|
private val floatingToolbarEnable get() = Database.getDatabase().terminal.floatingToolbar
|
||||||
private var closed = false
|
private var closed = false
|
||||||
|
private val anEvent get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@@ -72,6 +77,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initActions()
|
initActions()
|
||||||
|
initEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateUI() {
|
override fun updateUI() {
|
||||||
@@ -123,12 +129,38 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
add(initCloseActionButton())
|
add(initCloseActionButton())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
// 被添加到组件后
|
||||||
|
addPropertyChangeListener("ancestor", object : PropertyChangeListener {
|
||||||
|
override fun propertyChange(evt: PropertyChangeEvent) {
|
||||||
|
removePropertyChangeListener("ancestor", this)
|
||||||
|
SwingUtilities.invokeLater { resumeVisualWindows() }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun resumeVisualWindows() {
|
||||||
|
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
|
||||||
|
if (tab !is SSHTerminalTab) return
|
||||||
|
val terminalPanel = tab.getData(DataProviders.TerminalPanel) ?: return
|
||||||
|
terminalPanel.resumeVisualWindows(tab.host.id, object : DataProvider {
|
||||||
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
|
if (dataKey == DataProviders.TerminalTab) {
|
||||||
|
return tab as T
|
||||||
|
}
|
||||||
|
return super.getData(dataKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun initServerInfoActionButton(): JButton {
|
private fun initServerInfoActionButton(): JButton {
|
||||||
val btn = JButton(Icons.infoOutline)
|
val btn = JButton(Icons.infoOutline)
|
||||||
btn.toolTipText = I18n.getString("termora.visual-window.system-information")
|
btn.toolTipText = I18n.getString("termora.visual-window.system-information")
|
||||||
btn.addActionListener(object : AnAction() {
|
btn.addActionListener(object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
|
||||||
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
||||||
|
|
||||||
if (tab !is SSHTerminalTab) {
|
if (tab !is SSHTerminalTab) {
|
||||||
@@ -156,7 +188,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
btn.toolTipText = I18n.getString("termora.snippet.title")
|
btn.toolTipText = I18n.getString("termora.snippet.title")
|
||||||
btn.addActionListener(object : AnAction() {
|
btn.addActionListener(object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
|
||||||
val writer = tab.getData(DataProviders.TerminalWriter) ?: return
|
val writer = tab.getData(DataProviders.TerminalWriter) ?: return
|
||||||
val dialog = SnippetTreeDialog(evt.window)
|
val dialog = SnippetTreeDialog(evt.window)
|
||||||
dialog.setLocationRelativeTo(btn)
|
dialog.setLocationRelativeTo(btn)
|
||||||
@@ -174,7 +206,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi")
|
btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi")
|
||||||
btn.addActionListener(object : AnAction() {
|
btn.addActionListener(object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
|
||||||
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
||||||
|
|
||||||
if (tab !is SSHTerminalTab) {
|
if (tab !is SSHTerminalTab) {
|
||||||
@@ -233,7 +265,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
|
|
||||||
btn.addActionListener(object : AnAction() {
|
btn.addActionListener(object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
|
||||||
if (tab.canReconnect()) {
|
if (tab.canReconnect()) {
|
||||||
tab.reconnect()
|
tab.reconnect()
|
||||||
}
|
}
|
||||||
@@ -242,8 +274,4 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
return btn
|
return btn
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ class TerminalBlink(terminal: Terminal) : Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val coroutineScope by lazy { CoroutineScope(Dispatchers.IO) }
|
val coroutineScope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 返回 true 表示可以显示某些内容 [TextStyle.blink]
|
* 返回 true 表示可以显示某些内容 [TextStyle.blink]
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ package app.termora.terminal.panel
|
|||||||
import app.termora.Database
|
import app.termora.Database
|
||||||
import app.termora.DynamicColor
|
import app.termora.DynamicColor
|
||||||
import app.termora.assertEventDispatchThread
|
import app.termora.assertEventDispatchThread
|
||||||
|
import app.termora.swingCoroutineScope
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
@@ -484,14 +487,13 @@ class TerminalDisplay(
|
|||||||
g.font = font
|
g.font = font
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
fun toast(text: String, duration: Duration) {
|
fun toast(text: String, duration: Duration) {
|
||||||
if (!terminalPanel.showToast) {
|
if (!terminalPanel.showToast) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val toast = Toast(text)
|
val toast = Toast(text)
|
||||||
GlobalScope.launch(Dispatchers.Swing) {
|
swingCoroutineScope.launch(Dispatchers.Swing) {
|
||||||
delay(duration)
|
delay(duration)
|
||||||
toasts.remove(toast)
|
toasts.remove(toast)
|
||||||
terminalPanel.repaintImmediate()
|
terminalPanel.repaintImmediate()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app.termora.terminal.panel
|
|||||||
|
|
||||||
import app.termora.Application
|
import app.termora.Application
|
||||||
import app.termora.ApplicationScope
|
import app.termora.ApplicationScope
|
||||||
|
import app.termora.Database
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import java.awt.Graphics
|
import java.awt.Graphics
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@@ -16,6 +17,7 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val regex = Regex("https?://\\S*[^.\\s'\",()<>\\[\\]]")
|
private val regex = Regex("https?://\\S*[^.\\s'\",()<>\\[\\]]")
|
||||||
|
private val isEnableHyperlink get() = Database.getDatabase().terminal.hyperlink
|
||||||
|
|
||||||
override fun before(
|
override fun before(
|
||||||
offset: Int,
|
offset: Int,
|
||||||
@@ -25,6 +27,9 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
|
|||||||
terminalDisplay: TerminalDisplay,
|
terminalDisplay: TerminalDisplay,
|
||||||
terminal: Terminal
|
terminal: Terminal
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
if (isEnableHyperlink.not()) return
|
||||||
|
|
||||||
val document = terminal.getDocument()
|
val document = terminal.getDocument()
|
||||||
var startOffset = offset
|
var startOffset = offset
|
||||||
var endOffset = startOffset + count
|
var endOffset = startOffset + count
|
||||||
@@ -91,4 +96,18 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun after(
|
||||||
|
offset: Int,
|
||||||
|
count: Int,
|
||||||
|
g: Graphics,
|
||||||
|
terminalPanel: TerminalPanel,
|
||||||
|
terminalDisplay: TerminalDisplay,
|
||||||
|
terminal: Terminal
|
||||||
|
) {
|
||||||
|
if (isEnableHyperlink.not()) {
|
||||||
|
// 删除之前的
|
||||||
|
terminal.getMarkupModel().removeAllHighlighters(Highlighter.HYPERLINK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
package app.termora.terminal.panel
|
package app.termora.terminal.panel
|
||||||
|
|
||||||
|
import app.termora.Database
|
||||||
import app.termora.Disposable
|
import app.termora.Disposable
|
||||||
import app.termora.Disposer
|
import app.termora.Disposer
|
||||||
|
import app.termora.SSHTerminalTab
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.actions.DataProviderSupport
|
import app.termora.actions.DataProviderSupport
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import app.termora.terminal.panel.vw.VisualWindow
|
import app.termora.terminal.panel.vw.*
|
||||||
import app.termora.terminal.panel.vw.VisualWindowManager
|
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.apache.commons.lang3.ArrayUtils
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
@@ -44,15 +45,15 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
|
|||||||
val SelectCopy = DataKey(Boolean::class)
|
val SelectCopy = DataKey(Boolean::class)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val properties get() = Database.getDatabase().properties
|
||||||
private val terminalBlink = TerminalBlink(terminal)
|
private val terminalBlink = TerminalBlink(terminal)
|
||||||
private val terminalFindPanel = TerminalFindPanel(this, terminal)
|
private val terminalFindPanel = TerminalFindPanel(this, terminal)
|
||||||
private val floatingToolbar = FloatingToolbarPanel()
|
private val floatingToolbar = FloatingToolbarPanel()
|
||||||
private val terminalDisplay = TerminalDisplay(this, terminal, terminalBlink)
|
private val terminalDisplay = TerminalDisplay(this, terminal, terminalBlink)
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
|
||||||
private val layeredPane = TerminalLayeredPane()
|
private val layeredPane = TerminalLayeredPane()
|
||||||
private var visualWindows = emptyArray<VisualWindow>()
|
private var visualWindows = emptyArray<VisualWindow>()
|
||||||
|
|
||||||
val scrollBar = TerminalScrollBar(this@TerminalPanel, terminalFindPanel, terminal)
|
val scrollBar = TerminalScrollBar(this, terminalFindPanel, terminal)
|
||||||
var enableFloatingToolbar = true
|
var enableFloatingToolbar = true
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
@@ -63,6 +64,8 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val dataProviderSupport = DataProviderSupport()
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 键盘事件
|
* 键盘事件
|
||||||
@@ -585,6 +588,37 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
|
|||||||
requestFocusInWindow()
|
requestFocusInWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun resumeVisualWindows(id: String, dataProvider: DataProvider) {
|
||||||
|
val windows = properties.getString("VisualWindow.${id}.store") ?: return
|
||||||
|
for (name in windows.split(",")) {
|
||||||
|
if (name == "NVIDIA-SMI") {
|
||||||
|
addVisualWindow(
|
||||||
|
NvidiaSMIVisualWindow(
|
||||||
|
dataProvider.getData(DataProviders.TerminalTab) as SSHTerminalTab,
|
||||||
|
this
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else if (name == "SystemInformation") {
|
||||||
|
addVisualWindow(
|
||||||
|
SystemInformationVisualWindow(
|
||||||
|
dataProvider.getData(DataProviders.TerminalTab) as SSHTerminalTab,
|
||||||
|
this
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun storeVisualWindows(id: String) {
|
||||||
|
val windows = mutableListOf<String>()
|
||||||
|
for (window in getVisualWindows()) {
|
||||||
|
if (window is Resumeable) {
|
||||||
|
windows.add(window.getWindowName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
properties.putString("VisualWindow.${id}.store", windows.joinToString(","))
|
||||||
|
}
|
||||||
|
|
||||||
override fun getDimension(): Dimension {
|
override fun getDimension(): Dimension {
|
||||||
return Dimension(
|
return Dimension(
|
||||||
terminalDisplay.size.width + padding.left + padding.right,
|
terminalDisplay.size.width + padding.left + padding.right,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app.termora.terminal.panel
|
|||||||
|
|
||||||
import app.termora.keymap.KeyShortcut
|
import app.termora.keymap.KeyShortcut
|
||||||
import app.termora.keymap.KeymapManager
|
import app.termora.keymap.KeymapManager
|
||||||
|
import app.termora.terminal.ControlCharacters
|
||||||
import app.termora.terminal.Terminal
|
import app.termora.terminal.Terminal
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -78,6 +79,8 @@ class TerminalPanelKeyAdapter(
|
|||||||
val encode = terminal.getKeyEncoder().encode(AWTTerminalKeyEvent(e))
|
val encode = terminal.getKeyEncoder().encode(AWTTerminalKeyEvent(e))
|
||||||
if (encode.isNotEmpty()) {
|
if (encode.isNotEmpty()) {
|
||||||
writer.write(TerminalWriter.WriteRequest.fromBytes(encode.toByteArray(writer.getCharset())))
|
writer.write(TerminalWriter.WriteRequest.fromBytes(encode.toByteArray(writer.getCharset())))
|
||||||
|
// scroll to bottom
|
||||||
|
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
||||||
e.consume()
|
e.consume()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +93,8 @@ class TerminalPanelKeyAdapter(
|
|||||||
if (isAltPressedOnly(e) && Character.isDefined(e.keyChar)) {
|
if (isAltPressedOnly(e) && Character.isDefined(e.keyChar)) {
|
||||||
val c = String(charArrayOf(ASCII_ESC, simpleMapKeyCodeToChar(e)))
|
val c = String(charArrayOf(ASCII_ESC, simpleMapKeyCodeToChar(e)))
|
||||||
writer.write(TerminalWriter.WriteRequest.fromBytes(c.toByteArray(writer.getCharset())))
|
writer.write(TerminalWriter.WriteRequest.fromBytes(c.toByteArray(writer.getCharset())))
|
||||||
|
// scroll to bottom
|
||||||
|
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
||||||
e.consume()
|
e.consume()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -99,11 +104,12 @@ class TerminalPanelKeyAdapter(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Character.isISOControl(e.keyChar)) {
|
val keyChar = mapKeyChar(e)
|
||||||
|
if (Character.isISOControl(keyChar)) {
|
||||||
terminal.getSelectionModel().clearSelection()
|
terminal.getSelectionModel().clearSelection()
|
||||||
// 如果不为空表示已经发送过了,所以这里为空的时候再发送
|
// 如果不为空表示已经发送过了,所以这里为空的时候再发送
|
||||||
if (encode.isEmpty()) {
|
if (encode.isEmpty()) {
|
||||||
writer.write(TerminalWriter.WriteRequest.fromBytes("${e.keyChar}".toByteArray(writer.getCharset())))
|
writer.write(TerminalWriter.WriteRequest.fromBytes("$keyChar".toByteArray(writer.getCharset())))
|
||||||
e.consume()
|
e.consume()
|
||||||
}
|
}
|
||||||
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
||||||
@@ -111,6 +117,21 @@ class TerminalPanelKeyAdapter(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun mapKeyChar(e: KeyEvent): Char {
|
||||||
|
if (Character.isISOControl(e.keyChar)) {
|
||||||
|
return e.keyChar
|
||||||
|
}
|
||||||
|
|
||||||
|
val isCtrlPressedOnly = isCtrlPressedOnly(e)
|
||||||
|
|
||||||
|
// https://github.com/TermoraDev/termora/issues/478
|
||||||
|
if (isCtrlPressedOnly && e.keyCode == KeyEvent.VK_OPEN_BRACKET) {
|
||||||
|
return ControlCharacters.ESC
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.keyChar
|
||||||
|
}
|
||||||
|
|
||||||
private fun isCtrlPressedOnly(e: KeyEvent): Boolean {
|
private fun isCtrlPressedOnly(e: KeyEvent): Boolean {
|
||||||
val modifiersEx = e.modifiersEx
|
val modifiersEx = e.modifiersEx
|
||||||
return (modifiersEx and InputEvent.ALT_DOWN_MASK) == 0
|
return (modifiersEx and InputEvent.ALT_DOWN_MASK) == 0
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ abstract class AutoRefreshPanel : JPanel(), Disposable {
|
|||||||
private val log = LoggerFactory.getLogger(AutoRefreshPanel::class.java)
|
private val log = LoggerFactory.getLogger(AutoRefreshPanel::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected val coroutineScope = CoroutineScope(Dispatchers.IO)
|
protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
protected abstract suspend fun refresh(isFirst: Boolean)
|
protected abstract suspend fun refresh(isFirst: Boolean)
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class NvidiaSMIVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWind
|
|||||||
private val percentageBtn by lazy { JButton(if (isPercentage) Icons.text else Icons.percentage) }
|
private val percentageBtn by lazy { JButton(if (isPercentage) Icons.text else Icons.percentage) }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
Disposer.register(tab, this)
|
||||||
initViews()
|
initViews()
|
||||||
initEvents()
|
initEvents()
|
||||||
initVisualWindowPanel()
|
initVisualWindowPanel()
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package app.termora.terminal.panel.vw
|
||||||
|
|
||||||
|
interface Resumeable
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package app.termora.terminal.panel.vw
|
package app.termora.terminal.panel.vw
|
||||||
|
|
||||||
import app.termora.Disposer
|
|
||||||
import app.termora.SSHTerminalTab
|
import app.termora.SSHTerminalTab
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
@@ -11,11 +10,7 @@ abstract class SSHVisualWindow(
|
|||||||
protected val tab: SSHTerminalTab,
|
protected val tab: SSHTerminalTab,
|
||||||
id: String,
|
id: String,
|
||||||
visualWindowManager: VisualWindowManager
|
visualWindowManager: VisualWindowManager
|
||||||
) : VisualWindowPanel(id, visualWindowManager) {
|
) : VisualWindowPanel(id, visualWindowManager), Resumeable {
|
||||||
|
|
||||||
init {
|
|
||||||
Disposer.register(tab, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toggleWindow() {
|
override fun toggleWindow() {
|
||||||
val evt = AnActionEvent(tab.getJComponent(), StringUtils.EMPTY, EventObject(this))
|
val evt = AnActionEvent(tab.getJComponent(), StringUtils.EMPTY, EventObject(this))
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
|||||||
private val systemInformationPanel by lazy { SystemInformationPanel() }
|
private val systemInformationPanel by lazy { SystemInformationPanel() }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
Disposer.register(tab, this)
|
||||||
initViews()
|
initViews()
|
||||||
initEvents()
|
initEvents()
|
||||||
initVisualWindowPanel()
|
initVisualWindowPanel()
|
||||||
@@ -137,7 +138,7 @@ class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
|||||||
private suspend fun refreshCPUAndMem(session: ClientSession) {
|
private suspend fun refreshCPUAndMem(session: ClientSession) {
|
||||||
|
|
||||||
// top
|
// top
|
||||||
var pair = SshClients.execChannel(session, "top -bn1")
|
val pair = SshClients.execChannel(session, "top -bn1")
|
||||||
if (pair.first != 0) {
|
if (pair.first != 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -236,7 +237,7 @@ class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
|||||||
private suspend fun refreshDisk(session: ClientSession) {
|
private suspend fun refreshDisk(session: ClientSession) {
|
||||||
|
|
||||||
// df -h
|
// df -h
|
||||||
var pair = SshClients.execChannel(session, "df -B1")
|
val pair = SshClients.execChannel(session, "df -B1")
|
||||||
if (pair.first != 0) {
|
if (pair.first != 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user