mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
150 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b499667cbb | ||
|
|
1d596e18df | ||
|
|
6f95033009 | ||
|
|
1f08af6575 | ||
|
|
071a091347 | ||
|
|
ca484618c7 | ||
|
|
1f68f8a112 | ||
|
|
0cd5670bd3 | ||
|
|
8e9c6bcb68 | ||
|
|
6c1fa0fc53 | ||
|
|
5145cfa8a5 | ||
|
|
87b1a5e315 | ||
|
|
fa59869f2c | ||
|
|
1ae64fe0db | ||
|
|
f8d363836e | ||
|
|
38dccb1d22 | ||
|
|
3e31a89b92 | ||
|
|
d8f892cc02 | ||
|
|
873deb55aa | ||
|
|
c08712d79b | ||
|
|
61bc905727 | ||
|
|
17859be3c5 | ||
|
|
7a24e34695 | ||
|
|
58638eaad8 | ||
|
|
09d2f2d193 | ||
|
|
9121eff8d8 | ||
|
|
8b090b0526 | ||
|
|
15a0d642ff | ||
|
|
dc4333da21 | ||
|
|
184f6d46dc | ||
|
|
68788905fe | ||
|
|
fc46216a3f | ||
|
|
563143645e | ||
|
|
891ccb901b | ||
|
|
928a866fe7 | ||
|
|
ea25b5b46f | ||
|
|
1de10e6129 | ||
|
|
aaf9c2e8d2 | ||
|
|
b8196b5730 | ||
|
|
0a83e8beb4 | ||
|
|
bdf29b27e7 | ||
|
|
96da7eac41 | ||
|
|
71c0751692 | ||
|
|
442f334af2 | ||
|
|
48302a519f | ||
|
|
c00f759f15 | ||
|
|
1736dd909e | ||
|
|
1f01e368dd | ||
|
|
bfba958b7e | ||
|
|
758121b523 | ||
|
|
06e9a89e82 | ||
|
|
0ba6ac3305 | ||
|
|
993f220b8b | ||
|
|
8755c4ad23 | ||
|
|
77cb102dd6 | ||
|
|
89cfb0b451 | ||
|
|
6bdd83f208 | ||
|
|
8f86057dcc | ||
|
|
a7d7ffa2cc | ||
|
|
d51cbeee13 | ||
|
|
deb2a0151e | ||
|
|
e1c4e9312d | ||
|
|
c7233357bd | ||
|
|
eff8d565d0 | ||
|
|
932db49868 | ||
|
|
4d71c6cd05 | ||
|
|
96133e5abf | ||
|
|
f06e5d7dc1 | ||
|
|
d4b96edccf | ||
|
|
e9876d5b91 | ||
|
|
8b9a78a7bd | ||
|
|
6b48f577e9 | ||
|
|
da9b6c21d6 | ||
|
|
f1f889df14 | ||
|
|
ed65853ebe | ||
|
|
5ffdd219d9 | ||
|
|
4f84d6741c | ||
|
|
2568e7fcc8 | ||
|
|
dddbb49084 | ||
|
|
95846ab135 | ||
|
|
b5207e56c1 | ||
|
|
160771e912 | ||
|
|
0fbe180f3f | ||
|
|
41a0409e9e | ||
|
|
79e59143fb | ||
|
|
54e0f621ce | ||
|
|
4c8944d248 | ||
|
|
64bd95d8a8 | ||
|
|
1d88942e8e | ||
|
|
129e1b149a | ||
|
|
01aac98437 | ||
|
|
f9aaf7143f | ||
|
|
28174483f4 | ||
|
|
46412054c4 | ||
|
|
1ab0d26bab | ||
|
|
d90fb9aa35 | ||
|
|
744e64b359 | ||
|
|
2c5442f1f3 | ||
|
|
054c4701d2 | ||
|
|
54044625ea | ||
|
|
ca82704738 | ||
|
|
e98ec3fa8e | ||
|
|
6a4abf7e50 | ||
|
|
e2a6cceafd | ||
|
|
283404b6b9 | ||
|
|
c714f33a44 | ||
|
|
30fe047e5c | ||
|
|
827d814c7b | ||
|
|
ccb2c6daa0 | ||
|
|
1516d6d81e | ||
|
|
09b3655c4e | ||
|
|
614514c87e | ||
|
|
30cba6720d | ||
|
|
dce6551de2 | ||
|
|
95943cdeec | ||
|
|
18a26ee6bf | ||
|
|
f23aae371a | ||
|
|
757bc1c001 | ||
|
|
a19222dc60 | ||
|
|
24677ca4a6 | ||
|
|
0c5b6f8112 | ||
|
|
7c26e3d08a | ||
|
|
9b84fb4ec8 | ||
|
|
d8ec7b6d4a | ||
|
|
769c0d990b | ||
|
|
3f1ae38b61 | ||
|
|
e10fce21a2 | ||
|
|
a00557bb9d | ||
|
|
e478535ae5 | ||
|
|
7756758738 | ||
|
|
e0ea42faee | ||
|
|
e72c6b77b5 | ||
|
|
bcd3aacd6f | ||
|
|
570b0e08ad | ||
|
|
d703850e87 | ||
|
|
4bb1a411e8 | ||
|
|
9884ed19fa | ||
|
|
1ffaed3f36 | ||
|
|
4cb42953ad | ||
|
|
0248992dc3 | ||
|
|
9bab9db875 | ||
|
|
b283a3ea38 | ||
|
|
98ac2928b4 | ||
|
|
a0a6f43c10 | ||
|
|
0c158acbe0 | ||
|
|
9a97b3a304 | ||
|
|
aef44bd0da | ||
|
|
75c65d9ba8 | ||
|
|
93755db77f | ||
|
|
79d0a9a348 |
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
|
||||||
4
.github/workflows/linux-aarch64.yml
vendored
4
.github/workflows/linux-aarch64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# download jdk
|
# download jdk
|
||||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-aarch64-b895.91.tar.gz
|
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-aarch64-b1034.51.tar.gz
|
||||||
|
|
||||||
# appimagetool
|
# appimagetool
|
||||||
- run: sudo apt install libfuse2
|
- run: sudo apt install libfuse2
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'jdkfile'
|
distribution: 'jdkfile'
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
java-version: '21.0.6'
|
java-version: '21.0.7'
|
||||||
architecture: aarch64
|
architecture: aarch64
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
|
|||||||
4
.github/workflows/linux-x86-64.yml
vendored
4
.github/workflows/linux-x86-64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# download jdk
|
# download jdk
|
||||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-x64-b895.91.tar.gz
|
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-x64-b1034.51.tar.gz
|
||||||
|
|
||||||
# appimagetool
|
# appimagetool
|
||||||
- run: sudo apt install libfuse2
|
- run: sudo apt install libfuse2
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'jdkfile'
|
distribution: 'jdkfile'
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
java-version: '21.0.6'
|
java-version: '21.0.7'
|
||||||
architecture: x64
|
architecture: x64
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
|
|||||||
4
.github/workflows/osx-aarch64.yml
vendored
4
.github/workflows/osx-aarch64.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
|||||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||||
|
|
||||||
# download jdk
|
# download jdk
|
||||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b895.91.tar.gz
|
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-osx-aarch64-b1034.51.tar.gz
|
||||||
|
|
||||||
# install jdk
|
# install jdk
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'jdkfile'
|
distribution: 'jdkfile'
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
java-version: '21.0.6'
|
java-version: '21.0.7'
|
||||||
architecture: aarch64
|
architecture: aarch64
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
|
|||||||
4
.github/workflows/osx-x86-64.yml
vendored
4
.github/workflows/osx-x86-64.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
|||||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||||
|
|
||||||
# download jdk
|
# download jdk
|
||||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b895.91.tar.gz
|
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-osx-x64-b1034.51.tar.gz
|
||||||
|
|
||||||
# install jdk
|
# install jdk
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'jdkfile'
|
distribution: 'jdkfile'
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
java-version: '21.0.6'
|
java-version: '21.0.7'
|
||||||
architecture: x64
|
architecture: x64
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
5
.github/workflows/windows-x86-64.yml
vendored
5
.github/workflows/windows-x86-64.yml
vendored
@@ -21,9 +21,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
run: |
|
run: |
|
||||||
curl -s --output ${{ runner.temp }}\java_package.zip -L https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-windows-x64-b895.91.zip
|
curl -s --output ${{ runner.temp }}\java_package.zip -L https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-windows-x64-b1034.51.zip
|
||||||
unzip -q ${{ runner.temp }}\java_package.zip -d ${{ runner.temp }}\jbr
|
unzip -q ${{ runner.temp }}\java_package.zip -d ${{ runner.temp }}\jbr
|
||||||
echo "JAVA_HOME=${{ runner.temp }}\jbr\jbrsdk-21.0.6-windows-x64-b895.91" >> $env:GITHUB_ENV
|
echo "JAVA_HOME=${{ runner.temp }}\jbr\jbrsdk-21.0.7-windows-x64-b1034.51" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -45,5 +45,4 @@ jobs:
|
|||||||
name: termora-windows-x86-64
|
name: termora-windows-x86-64
|
||||||
path: |
|
path: |
|
||||||
build/distributions/*.zip
|
build/distributions/*.zip
|
||||||
build/distributions/*.msi
|
|
||||||
build/distributions/*.exe
|
build/distributions/*.exe
|
||||||
|
|||||||
2
.github/workflows/winget.yml
vendored
2
.github/workflows/winget.yml
vendored
@@ -10,5 +10,5 @@ jobs:
|
|||||||
if: github.repository == 'TermoraDev/termora'
|
if: github.repository == 'TermoraDev/termora'
|
||||||
with:
|
with:
|
||||||
identifier: TermoraDev.Termora
|
identifier: TermoraDev.Termora
|
||||||
installers-regex: 'x86-64\.exe$' # Only x86-64.exe files
|
installers-regex: '\.exe$'
|
||||||
token: ${{ secrets.WINGET_TOKEN }}
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
|
|||||||
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
|
||||||
172
build.gradle.kts
172
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.10"
|
version = "1.0.17"
|
||||||
|
|
||||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||||
@@ -56,69 +57,71 @@ 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 {
|
||||||
val args = mutableListOf(
|
val args = mutableListOf(
|
||||||
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
|
||||||
"-Xmx2g",
|
"-Xmx2g",
|
||||||
"-XX:+UseZGC",
|
"-XX:+UseZGC",
|
||||||
"-XX:+ZUncommit",
|
"-XX:+ZUncommit",
|
||||||
@@ -127,21 +130,52 @@ application {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
args.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
// macOS NSWindow
|
||||||
|
args.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||||
|
args.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||||
|
args.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||||
|
args.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
|
||||||
args.add("-Dsun.java2d.metal=true")
|
args.add("-Dsun.java2d.metal=true")
|
||||||
args.add("-Dapple.awt.application.appearance=system")
|
args.add("-Dapple.awt.application.appearance=system")
|
||||||
}
|
}
|
||||||
|
|
||||||
args.add("-Dapp-version=${project.version}")
|
args.add("-Dapp-version=${project.version}")
|
||||||
|
|
||||||
if (os.isLinux) {
|
|
||||||
args.add("-Dsun.java2d.opengl=true")
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationDefaultJvmArgs = args
|
applicationDefaultJvmArgs = args
|
||||||
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()
|
||||||
}
|
}
|
||||||
@@ -345,12 +379,13 @@ tasks.register<Exec>("jpackage") {
|
|||||||
options.add("-Dsun.java2d.metal=true")
|
options.add("-Dsun.java2d.metal=true")
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
|
// NSWindow
|
||||||
|
options.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||||
|
options.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||||
|
options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||||
options.add("-Dapple.awt.application.appearance=system")
|
options.add("-Dapple.awt.application.appearance=system")
|
||||||
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||||
}
|
options.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
|
||||||
|
|
||||||
if (os.isLinux) {
|
|
||||||
options.add("-Dsun.java2d.opengl=true")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage")
|
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage")
|
||||||
@@ -421,15 +456,9 @@ tasks.register<Exec>("jpackage") {
|
|||||||
|
|
||||||
tasks.register("dist") {
|
tasks.register("dist") {
|
||||||
doLast {
|
doLast {
|
||||||
val vendor = Jvm.current().vendor ?: StringUtils.EMPTY
|
|
||||||
@Suppress("UnstableApiUsage")
|
|
||||||
if (!JvmVendorSpec.JETBRAINS.matches(vendor)) {
|
|
||||||
throw GradleException("JVM: $vendor is not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
|
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
|
||||||
|
|
||||||
|
|
||||||
// 清空目录
|
// 清空目录
|
||||||
exec { commandLine(gradlew, "clean") }
|
exec { commandLine(gradlew, "clean") }
|
||||||
|
|
||||||
@@ -455,33 +484,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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -736,8 +758,6 @@ fun stapleMacOSLocalFile(file: File) {
|
|||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain {
|
jvmToolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(21)
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
@Suppress("UnstableApiUsage")
|
|
||||||
vendor = JvmVendorSpec.JETBRAINS
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.6"
|
||||||
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.1"
|
||||||
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 "1.0.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) {
|
||||||
|
|||||||
38
src/main/java/app/termora/MyKernel32.java
Normal file
38
src/main/java/app/termora/MyKernel32.java
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package app.termora;
|
||||||
|
|
||||||
|
import com.sun.jna.Native;
|
||||||
|
import com.sun.jna.Pointer;
|
||||||
|
import com.sun.jna.WString;
|
||||||
|
import com.sun.jna.win32.StdCallLibrary;
|
||||||
|
|
||||||
|
interface MyKernel32 extends StdCallLibrary {
|
||||||
|
|
||||||
|
MyKernel32 INSTANCE = Native.load("Kernel32", MyKernel32.class);
|
||||||
|
WString INVARIANT_LOCALE = new WString("");
|
||||||
|
|
||||||
|
int CompareStringEx(WString lpLocaleName,
|
||||||
|
int dwCmpFlags,
|
||||||
|
WString lpString1,
|
||||||
|
int cchCount1,
|
||||||
|
WString lpString2,
|
||||||
|
int cchCount2,
|
||||||
|
Pointer lpVersionInformation,
|
||||||
|
Pointer lpReserved,
|
||||||
|
int lParam);
|
||||||
|
|
||||||
|
default int CompareStringEx(int dwCmpFlags,
|
||||||
|
String str1,
|
||||||
|
String str2) {
|
||||||
|
return MyKernel32.INSTANCE
|
||||||
|
.CompareStringEx(
|
||||||
|
INVARIANT_LOCALE,
|
||||||
|
dwCmpFlags,
|
||||||
|
new WString(str1),
|
||||||
|
str1.length(),
|
||||||
|
new WString(str2),
|
||||||
|
str2.length(),
|
||||||
|
Pointer.NULL,
|
||||||
|
Pointer.NULL,
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,6 @@ package app.termora
|
|||||||
|
|
||||||
object Actions {
|
object Actions {
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将命令发送到多个会话
|
|
||||||
*/
|
|
||||||
const val MULTIPLE = "MultipleAction"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关键词高亮
|
* 关键词高亮
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -150,6 +148,16 @@ object Application {
|
|||||||
ProcessBuilder("xdg-open", uri.toString()).start()
|
ProcessBuilder("xdg-open", uri.toString()).start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun browseInFolder(file: File) {
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
ProcessBuilder("explorer", "/select," + file.absolutePath).start()
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
ProcessBuilder("open", "-R", file.absolutePath).start()
|
||||||
|
} else if (Desktop.getDesktop().isSupported(Desktop.Action.BROWSE_FILE_DIR)) {
|
||||||
|
Desktop.getDesktop().browseFileDirectory(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatBytes(bytes: Long): String {
|
fun formatBytes(bytes: Long): String {
|
||||||
@@ -159,7 +167,7 @@ fun formatBytes(bytes: Long): String {
|
|||||||
val exp = (ln(bytes.toDouble()) / ln(1024.0)).toInt()
|
val exp = (ln(bytes.toDouble()) / ln(1024.0)).toInt()
|
||||||
val value = bytes / 1024.0.pow(exp.toDouble())
|
val value = bytes / 1024.0.pow(exp.toDouble())
|
||||||
|
|
||||||
return String.format("%.2f %s", value, units[exp])
|
return String.format("%.2f%s", value, units[exp])
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatSeconds(seconds: Long): String {
|
fun formatSeconds(seconds: Long): String {
|
||||||
@@ -168,11 +176,33 @@ fun formatSeconds(seconds: Long): String {
|
|||||||
val minutes = (seconds % 3600) / 60
|
val minutes = (seconds % 3600) / 60
|
||||||
val remainingSeconds = seconds % 60
|
val remainingSeconds = seconds % 60
|
||||||
|
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
days > 0 -> "${days}天${hours}小时${minutes}分${remainingSeconds}秒"
|
days > 0 -> I18n.getString(
|
||||||
hours > 0 -> "${hours}小时${minutes}分${remainingSeconds}秒"
|
"termora.transport.jobs.table.estimated-time-days-format",
|
||||||
minutes > 0 -> "${minutes}分${remainingSeconds}秒"
|
days,
|
||||||
else -> "${remainingSeconds}秒"
|
hours,
|
||||||
|
minutes,
|
||||||
|
remainingSeconds
|
||||||
|
)
|
||||||
|
|
||||||
|
hours > 0 -> I18n.getString(
|
||||||
|
"termora.transport.jobs.table.estimated-time-hours-format",
|
||||||
|
hours,
|
||||||
|
minutes,
|
||||||
|
remainingSeconds
|
||||||
|
)
|
||||||
|
|
||||||
|
minutes > 0 -> I18n.getString(
|
||||||
|
"termora.transport.jobs.table.estimated-time-minutes-format",
|
||||||
|
minutes,
|
||||||
|
remainingSeconds
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> I18n.getString(
|
||||||
|
"termora.transport.jobs.table.estimated-time-seconds-format",
|
||||||
|
remainingSeconds
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
91
src/main/kotlin/app/termora/ApplicationInitializr.kt
Normal file
91
src/main/kotlin/app/termora/ApplicationInitializr.kt
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import com.pty4j.util.PtyUtil
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.SystemUtils
|
||||||
|
import org.tinylog.configuration.Configuration
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
class ApplicationInitializr {
|
||||||
|
|
||||||
|
fun run() {
|
||||||
|
|
||||||
|
// 由于 macOS 签名和公证问题,依赖二进制依赖会单独在一个文件夹
|
||||||
|
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||||
|
setupNativeLibraries()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||||
|
System.setProperty("apple.awt.application.name", Application.getName())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置 tinylog
|
||||||
|
setupTinylog()
|
||||||
|
|
||||||
|
// 检查是否单例
|
||||||
|
checkSingleton()
|
||||||
|
|
||||||
|
// 启动
|
||||||
|
ApplicationRunner().run()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun setupNativeLibraries() {
|
||||||
|
if (!SystemUtils.IS_OS_MAC_OSX) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val appPath = Application.getAppPath()
|
||||||
|
if (StringUtils.isBlank(appPath)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val contents = File(appPath).parentFile?.parentFile ?: return
|
||||||
|
val dylib = FileUtils.getFile(contents, "app", "dylib")
|
||||||
|
if (!dylib.exists()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val jna = FileUtils.getFile(dylib, "jna")
|
||||||
|
if (jna.exists()) {
|
||||||
|
System.setProperty("jna.boot.library.path", jna.absolutePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pty4j = FileUtils.getFile(dylib, "pty4j")
|
||||||
|
if (pty4j.exists()) {
|
||||||
|
System.setProperty(PtyUtil.PREFERRED_NATIVE_FOLDER_KEY, pty4j.absolutePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
val jSerialComm = FileUtils.getFile(dylib, "jSerialComm")
|
||||||
|
if (jSerialComm.exists()) {
|
||||||
|
System.setProperty("jSerialComm.library.path", jSerialComm.absolutePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
val restart4j = FileUtils.getFile(dylib, "restart4j", "restarter")
|
||||||
|
if (restart4j.exists()) {
|
||||||
|
System.setProperty("restarter.path", restart4j.absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Windows 情况覆盖
|
||||||
|
*/
|
||||||
|
private fun setupTinylog() {
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
val dir = File(Application.getBaseDataDir(), "logs")
|
||||||
|
FileUtils.forceMkdir(dir)
|
||||||
|
Configuration.set("writer_file.latest", "${dir.absolutePath}/${Application.getName().lowercase()}.log")
|
||||||
|
Configuration.set("writer_file.file", "${dir.absolutePath}/{date:yyyy}-{date:MM}-{date:dd}.log")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkSingleton() {
|
||||||
|
if (ApplicationSingleton.getInstance().isSingleton()) return
|
||||||
|
System.err.println("Program is already running")
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,55 +2,49 @@ package app.termora
|
|||||||
|
|
||||||
import app.termora.actions.ActionManager
|
import app.termora.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
|
||||||
import com.formdev.flatlaf.extras.FlatDesktop.QuitResponse
|
|
||||||
import com.formdev.flatlaf.extras.FlatInspector
|
import com.formdev.flatlaf.extras.FlatInspector
|
||||||
|
import com.formdev.flatlaf.ui.FlatTableCellBorder
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jthemedetecor.OsThemeDetector
|
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 org.tinylog.configuration.Configuration
|
import java.awt.MenuItem
|
||||||
import java.awt.KeyboardFocusManager
|
import java.awt.PopupMenu
|
||||||
import java.io.File
|
import java.awt.SystemTray
|
||||||
import java.nio.channels.FileChannel
|
import java.awt.TrayIcon
|
||||||
import java.nio.channels.FileLock
|
import java.awt.desktop.AppReopenedEvent
|
||||||
import java.nio.file.Paths
|
import java.awt.desktop.AppReopenedListener
|
||||||
import java.nio.file.StandardOpenOption
|
import java.awt.desktop.SystemEventListener
|
||||||
|
import java.awt.event.ActionEvent
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.function.Consumer
|
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
|
||||||
|
|
||||||
class ApplicationRunner {
|
class ApplicationRunner {
|
||||||
private lateinit var singletonChannel: FileChannel
|
private val log by lazy { LoggerFactory.getLogger(ApplicationRunner::class.java) }
|
||||||
private lateinit var singletonLock: FileLock
|
|
||||||
private val log by lazy {
|
|
||||||
if (!::singletonLock.isInitialized) {
|
|
||||||
throw UnsupportedOperationException("Singleton lock is not initialized")
|
|
||||||
}
|
|
||||||
LoggerFactory.getLogger(ApplicationRunner::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun run() {
|
fun run() {
|
||||||
measureTimeMillis {
|
measureTimeMillis {
|
||||||
// 覆盖 tinylog 配置
|
|
||||||
val setupTinylog = measureTimeMillis { setupTinylog() }
|
|
||||||
|
|
||||||
// 是否单例
|
|
||||||
val checkSingleton = measureTimeMillis { checkSingleton() }
|
|
||||||
|
|
||||||
// 打印系统信息
|
// 打印系统信息
|
||||||
val printSystemInfo = measureTimeMillis { printSystemInfo() }
|
val printSystemInfo = measureTimeMillis { printSystemInfo() }
|
||||||
@@ -64,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
|
||||||
@@ -84,8 +87,6 @@ class ApplicationRunner {
|
|||||||
val startMainFrame = measureTimeMillis { startMainFrame() }
|
val startMainFrame = measureTimeMillis { startMainFrame() }
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("setupTinylog: {}ms", setupTinylog)
|
|
||||||
log.debug("checkSingleton: {}ms", checkSingleton)
|
|
||||||
log.debug("printSystemInfo: {}ms", printSystemInfo)
|
log.debug("printSystemInfo: {}ms", printSystemInfo)
|
||||||
log.debug("openDatabase: {}ms", openDatabase)
|
log.debug("openDatabase: {}ms", openDatabase)
|
||||||
log.debug("loadSettings: {}ms", loadSettings)
|
log.debug("loadSettings: {}ms", loadSettings)
|
||||||
@@ -101,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())
|
||||||
}
|
}
|
||||||
@@ -120,36 +120,69 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
private fun startMainFrame() {
|
private fun startMainFrame() {
|
||||||
|
|
||||||
|
|
||||||
TermoraFrameManager.getInstance().createWindow().isVisible = true
|
TermoraFrameManager.getInstance().createWindow().isVisible = true
|
||||||
|
|
||||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
if (SystemInfo.isMacOS) {
|
||||||
SwingUtilities.invokeLater {
|
SwingUtilities.invokeLater {
|
||||||
FlatDesktop.setQuitHandler(object : Consumer<QuitResponse> {
|
|
||||||
override fun accept(response: QuitResponse) {
|
try {
|
||||||
quitHandler(response)
|
// 设置 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 quitHandler(response: QuitResponse) {
|
private fun setupSystemTray() {
|
||||||
val keyboardFocusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
if (!SystemInfo.isWindows || !SystemTray.isSupported()) return
|
||||||
|
|
||||||
if (OptionPane.showConfirmDialog(
|
val tray = SystemTray.getSystemTray()
|
||||||
keyboardFocusManager.focusedWindow,
|
val image = ImageIO.read(TermoraFrame::class.java.getResourceAsStream("/icons/termora_16x16.png"))
|
||||||
I18n.getString("termora.quit-confirm", Application.getName()),
|
val trayIcon = TrayIcon(image)
|
||||||
optionType = JOptionPane.YES_NO_OPTION,
|
val popupMenu = PopupMenu()
|
||||||
) != JOptionPane.YES_OPTION
|
trayIcon.popupMenu = popupMenu
|
||||||
) {
|
trayIcon.toolTip = Application.getName()
|
||||||
response.cancelQuit()
|
|
||||||
return
|
// PopupMenu 不支持中文
|
||||||
}
|
val exitMenu = MenuItem("Exit")
|
||||||
|
exitMenu.addActionListener { SwingUtilities.invokeLater { quitHandler() } }
|
||||||
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
popupMenu.add(exitMenu)
|
||||||
frame.dispose()
|
|
||||||
|
// 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() {
|
||||||
|
val windows = TermoraFrameManager.getInstance().getWindows()
|
||||||
|
|
||||||
|
for (frame in windows) {
|
||||||
|
frame.dispatchEvent(WindowEvent(frame, WindowEvent.WINDOW_CLOSED))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Disposer.dispose(TermoraFrameManager.getInstance())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadSettings() {
|
private fun loadSettings() {
|
||||||
@@ -164,7 +197,7 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
private fun setupLaf() {
|
private fun setupLaf() {
|
||||||
|
|
||||||
System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux}")
|
System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux || SystemInfo.isWindows}")
|
||||||
System.setProperty(FlatSystemProperties.USE_ROUNDED_POPUP_BORDER, "false")
|
System.setProperty(FlatSystemProperties.USE_ROUNDED_POPUP_BORDER, "false")
|
||||||
|
|
||||||
if (SystemInfo.isLinux) {
|
if (SystemInfo.isLinux) {
|
||||||
@@ -186,6 +219,7 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
themeManager.change(theme, true)
|
themeManager.change(theme, true)
|
||||||
|
|
||||||
|
|
||||||
if (Application.isUnknownVersion())
|
if (Application.isUnknownVersion())
|
||||||
FlatInspector.install("ctrl shift alt X")
|
FlatInspector.install("ctrl shift alt X")
|
||||||
|
|
||||||
@@ -218,9 +252,8 @@ class ApplicationRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
UIManager.put("Table.rowHeight", 24)
|
UIManager.put("Table.rowHeight", 24)
|
||||||
UIManager.put("Table.cellNoFocusBorder", BorderFactory.createEmptyBorder())
|
UIManager.put("Table.focusCellHighlightBorder", FlatTableCellBorder.Default())
|
||||||
UIManager.put("Table.focusCellHighlightBorder", BorderFactory.createEmptyBorder())
|
UIManager.put("Table.focusSelectedCellHighlightBorder", FlatTableCellBorder.Default())
|
||||||
UIManager.put("Table.focusSelectedCellHighlightBorder", BorderFactory.createEmptyBorder())
|
|
||||||
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
|
||||||
UIManager.put("Tree.rowHeight", 24)
|
UIManager.put("Tree.rowHeight", 24)
|
||||||
@@ -231,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() {
|
||||||
@@ -254,36 +315,6 @@ class ApplicationRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Windows 情况覆盖
|
|
||||||
*/
|
|
||||||
private fun setupTinylog() {
|
|
||||||
if (SystemInfo.isWindows) {
|
|
||||||
val dir = File(Application.getBaseDataDir(), "logs")
|
|
||||||
FileUtils.forceMkdir(dir)
|
|
||||||
Configuration.set("writer_file.latest", "${dir.absolutePath}/${Application.getName().lowercase()}.log")
|
|
||||||
Configuration.set("writer_file.file", "${dir.absolutePath}/{date:yyyy}-{date:MM}-{date:dd}.log")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun checkSingleton() {
|
|
||||||
singletonChannel = FileChannel.open(
|
|
||||||
Paths.get(Application.getBaseDataDir().absolutePath, "lock"),
|
|
||||||
StandardOpenOption.CREATE,
|
|
||||||
StandardOpenOption.WRITE,
|
|
||||||
)
|
|
||||||
|
|
||||||
val lock = singletonChannel.tryLock()
|
|
||||||
if (lock == null) {
|
|
||||||
System.err.println("Program is already running")
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
singletonLock = lock
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun openDatabase() {
|
private fun openDatabase() {
|
||||||
try {
|
try {
|
||||||
Database.getDatabase()
|
Database.getDatabase()
|
||||||
@@ -302,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)
|
||||||
@@ -325,7 +355,27 @@ class ApplicationRunner {
|
|||||||
.event(getAnalyticsUserID(), "launch", properties)
|
.event(getAnalyticsUserID(), "launch", properties)
|
||||||
val delivery = ClientDelivery()
|
val delivery = ClientDelivery()
|
||||||
delivery.addMessage(message)
|
delivery.addMessage(message)
|
||||||
MixpanelAPI().deliver(delivery, true)
|
val endpoints = listOf(
|
||||||
|
"https://api-eu.mixpanel.com",
|
||||||
|
"https://api-in.mixpanel.com",
|
||||||
|
"https://api.mixpanel.com",
|
||||||
|
"http://api.mixpanel.com",
|
||||||
|
)
|
||||||
|
for (endpoint in endpoints) {
|
||||||
|
try {
|
||||||
|
MixpanelAPI(
|
||||||
|
"$endpoint/track",
|
||||||
|
"$endpoint/engage",
|
||||||
|
"$endpoint/groups"
|
||||||
|
).deliver(delivery, true)
|
||||||
|
break
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (log.isErrorEnabled) {
|
if (log.isErrorEnabled) {
|
||||||
log.error(e.message, e)
|
log.error(e.message, e)
|
||||||
|
|||||||
201
src/main/kotlin/app/termora/ApplicationSingleton.kt
Normal file
201
src/main/kotlin/app/termora/ApplicationSingleton.kt
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import com.sun.jna.platform.win32.Kernel32
|
||||||
|
import com.sun.jna.platform.win32.User32
|
||||||
|
import com.sun.jna.platform.win32.WinDef.*
|
||||||
|
import com.sun.jna.platform.win32.WinError
|
||||||
|
import com.sun.jna.platform.win32.WinUser.*
|
||||||
|
import com.sun.jna.platform.win32.Wtsapi32
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.nio.channels.FileChannel
|
||||||
|
import java.nio.channels.FileLock
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
class ApplicationSingleton private constructor() : Disposable {
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var isSingleton = null as Boolean?
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getInstance(): ApplicationSingleton {
|
||||||
|
return ApplicationScope.forApplicationScope()
|
||||||
|
.getOrCreate(ApplicationSingleton::class) { ApplicationSingleton() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isSingleton(): Boolean {
|
||||||
|
var singleton = this.isSingleton
|
||||||
|
if (singleton != null) return singleton
|
||||||
|
|
||||||
|
try {
|
||||||
|
synchronized(this) {
|
||||||
|
singleton = this.isSingleton
|
||||||
|
if (singleton != null) return singleton as Boolean
|
||||||
|
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
val handle = Kernel32.INSTANCE.CreateMutex(null, false, Application.getName())
|
||||||
|
singleton = handle != null && Kernel32.INSTANCE.GetLastError() != WinError.ERROR_ALREADY_EXISTS
|
||||||
|
if (singleton == true) {
|
||||||
|
// 启动监听器,方便激活窗口
|
||||||
|
Thread.ofVirtual().start(Win32HelperWindow.getInstance())
|
||||||
|
} else {
|
||||||
|
// 尝试激活窗口
|
||||||
|
Win32HelperWindow.tick()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
singleton = FileLocker.getInstance().tryLock()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isSingleton = singleton == true
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace(System.err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return this.isSingleton == true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FileLocker private constructor() {
|
||||||
|
companion object {
|
||||||
|
fun getInstance(): FileLocker {
|
||||||
|
return ApplicationScope.forApplicationScope()
|
||||||
|
.getOrCreate(FileLocker::class) { FileLocker() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private lateinit var singletonChannel: FileChannel
|
||||||
|
private lateinit var singletonLock: FileLock
|
||||||
|
|
||||||
|
|
||||||
|
fun tryLock(): Boolean {
|
||||||
|
singletonChannel = FileChannel.open(
|
||||||
|
Paths.get(Application.getBaseDataDir().absolutePath, "lock"),
|
||||||
|
StandardOpenOption.CREATE,
|
||||||
|
StandardOpenOption.WRITE,
|
||||||
|
)
|
||||||
|
|
||||||
|
val lock = singletonChannel.tryLock() ?: return false
|
||||||
|
|
||||||
|
this.singletonLock = lock
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class Win32HelperWindow private constructor() : Runnable {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(Win32HelperWindow::class.java)
|
||||||
|
private val WindowClass = "${Application.getName()}HelperWindowClass"
|
||||||
|
private val WindowName =
|
||||||
|
"${Application.getName()} hidden helper window, used only to catch the windows events"
|
||||||
|
private const val TICK: Int = WM_USER + 1
|
||||||
|
|
||||||
|
fun getInstance(): Win32HelperWindow {
|
||||||
|
return ApplicationScope.forApplicationScope()
|
||||||
|
.getOrCreate(Win32HelperWindow::class) { Win32HelperWindow() }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun tick() {
|
||||||
|
val hWnd = User32.INSTANCE.FindWindow(WindowClass, WindowName) ?: return
|
||||||
|
User32.INSTANCE.SendMessage(hWnd, TICK, WPARAM(), LPARAM())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val isRunning = AtomicBoolean(false)
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
if (isRunning.compareAndSet(false, true)) {
|
||||||
|
Win32Window()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class Win32Window : WindowProc {
|
||||||
|
/**
|
||||||
|
* Instantiates a new win32 window test.
|
||||||
|
*/
|
||||||
|
init {
|
||||||
|
// define new window class
|
||||||
|
val hInst = Kernel32.INSTANCE.GetModuleHandle(null)
|
||||||
|
|
||||||
|
val wClass = WNDCLASSEX()
|
||||||
|
wClass.hInstance = hInst
|
||||||
|
wClass.lpfnWndProc = this
|
||||||
|
wClass.lpszClassName = WindowClass
|
||||||
|
|
||||||
|
// register window class
|
||||||
|
User32.INSTANCE.RegisterClassEx(wClass)
|
||||||
|
|
||||||
|
// create new window
|
||||||
|
val hWnd = User32.INSTANCE.CreateWindowEx(
|
||||||
|
User32.WS_EX_TOPMOST,
|
||||||
|
WindowClass,
|
||||||
|
WindowName,
|
||||||
|
0, 0, 0, 0, 0,
|
||||||
|
null, // WM_DEVICECHANGE contradicts parent=WinUser.HWND_MESSAGE
|
||||||
|
null, hInst, null
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
val msg = MSG()
|
||||||
|
while (User32.INSTANCE.GetMessage(msg, hWnd, 0, 0) > 0) {
|
||||||
|
User32.INSTANCE.TranslateMessage(msg)
|
||||||
|
User32.INSTANCE.DispatchMessage(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
Wtsapi32.INSTANCE.WTSUnRegisterSessionNotification(hWnd)
|
||||||
|
User32.INSTANCE.UnregisterClass(WindowClass, hInst)
|
||||||
|
User32.INSTANCE.DestroyWindow(hWnd)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun callback(hwnd: HWND, uMsg: Int, wParam: WPARAM, lParam: LPARAM): LRESULT {
|
||||||
|
when (uMsg) {
|
||||||
|
WM_CREATE -> {
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("win32 helper window created")
|
||||||
|
}
|
||||||
|
return LRESULT()
|
||||||
|
}
|
||||||
|
|
||||||
|
TICK -> {
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("win32 helper window tick")
|
||||||
|
}
|
||||||
|
onTick()
|
||||||
|
return LRESULT()
|
||||||
|
}
|
||||||
|
|
||||||
|
WM_DESTROY -> {
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("win32 helper window destroyed")
|
||||||
|
}
|
||||||
|
User32.INSTANCE.PostQuitMessage(0)
|
||||||
|
return LRESULT()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> return User32.INSTANCE.DefWindowProc(hwnd, uMsg, wParam, lParam)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onTick() {
|
||||||
|
TermoraFrameManager.getInstance().tick()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/main/kotlin/app/termora/BackgroundManager.kt
Normal file
88
src/main/kotlin/app/termora/BackgroundManager.kt
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import java.io.File
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
import javax.swing.JPopupMenu
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
class BackgroundManager private constructor() {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(BackgroundManager::class.java)
|
||||||
|
fun getInstance(): BackgroundManager {
|
||||||
|
return ApplicationScope.forApplicationScope().getOrCreate(BackgroundManager::class) { BackgroundManager() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val appearance get() = Database.getDatabase().appearance
|
||||||
|
private var bufferedImage: BufferedImage? = null
|
||||||
|
private var imageFilepath = StringUtils.EMPTY
|
||||||
|
|
||||||
|
fun setBackgroundImage(file: File) {
|
||||||
|
synchronized(this) {
|
||||||
|
try {
|
||||||
|
bufferedImage = file.inputStream().use { ImageIO.read(it) }
|
||||||
|
imageFilepath = file.absolutePath
|
||||||
|
appearance.backgroundImage = file.absolutePath
|
||||||
|
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
||||||
|
SwingUtilities.updateComponentTreeUI(window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBackgroundImage(): BufferedImage? {
|
||||||
|
val bg = doGetBackgroundImage()
|
||||||
|
if (bg == null) {
|
||||||
|
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
JPopupMenu.setDefaultLightWeightPopupEnabled(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||||
|
JPopupMenu.setDefaultLightWeightPopupEnabled(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bg
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doGetBackgroundImage(): BufferedImage? {
|
||||||
|
synchronized(this) {
|
||||||
|
if (bufferedImage == null || imageFilepath.isEmpty()) {
|
||||||
|
if (appearance.backgroundImage.isBlank()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val file = File(appearance.backgroundImage)
|
||||||
|
if (file.exists()) {
|
||||||
|
setBackgroundImage(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bufferedImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearBackgroundImage() {
|
||||||
|
synchronized(this) {
|
||||||
|
bufferedImage = null
|
||||||
|
imageFilepath = StringUtils.EMPTY
|
||||||
|
appearance.backgroundImage = StringUtils.EMPTY
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
||||||
|
SwingUtilities.updateComponentTreeUI(window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
|
import app.termora.actions.MultipleAction
|
||||||
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.serialization.encodeToString
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
@@ -20,6 +19,7 @@ import kotlin.math.min
|
|||||||
|
|
||||||
class CustomizeToolBarDialog(
|
class CustomizeToolBarDialog(
|
||||||
owner: Window,
|
owner: Window,
|
||||||
|
private val windowScope: WindowScope,
|
||||||
private val toolbar: TermoraToolBar
|
private val toolbar: TermoraToolBar
|
||||||
) : DialogWrapper(owner) {
|
) : DialogWrapper(owner) {
|
||||||
|
|
||||||
@@ -147,9 +147,7 @@ class CustomizeToolBarDialog(
|
|||||||
leftList.model.removeAllElements()
|
leftList.model.removeAllElements()
|
||||||
rightList.model.removeAllElements()
|
rightList.model.removeAllElements()
|
||||||
for (action in toolbar.getAllActions()) {
|
for (action in toolbar.getAllActions()) {
|
||||||
actionManager.getAction(action.id)?.let {
|
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
|
||||||
rightList.model.addElement(ActionHolder(action.id, it))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,14 +257,11 @@ class CustomizeToolBarDialog(
|
|||||||
override fun windowOpened(e: WindowEvent) {
|
override fun windowOpened(e: WindowEvent) {
|
||||||
removeWindowListener(this)
|
removeWindowListener(this)
|
||||||
|
|
||||||
|
|
||||||
for (action in toolbar.getActions()) {
|
for (action in toolbar.getActions()) {
|
||||||
if (action.visible) {
|
if (action.visible) {
|
||||||
actionManager.getAction(action.id)
|
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
|
||||||
?.let { rightList.model.addElement(ActionHolder(action.id, it)) }
|
|
||||||
} else {
|
} else {
|
||||||
actionManager.getAction(action.id)
|
getActionHolder(action.id)?.let { leftList.model.addElement(it) }
|
||||||
?.let { leftList.model.addElement(ActionHolder(action.id, it)) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +269,17 @@ class CustomizeToolBarDialog(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getActionHolder(actionId: String): ActionHolder? {
|
||||||
|
var action = actionManager.getAction(actionId)
|
||||||
|
if (action == null) {
|
||||||
|
if (actionId == MultipleAction.MULTIPLE) {
|
||||||
|
action = MultipleAction.getInstance(windowScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (action == null) return null
|
||||||
|
return ActionHolder(actionId, action)
|
||||||
|
}
|
||||||
|
|
||||||
private fun resetMoveButtons() {
|
private fun resetMoveButtons() {
|
||||||
val indices = rightList.selectedIndices
|
val indices = rightList.selectedIndices
|
||||||
if (indices.isEmpty()) {
|
if (indices.isEmpty()) {
|
||||||
|
|||||||
@@ -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,21 @@ 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 confirmTabClose by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 背景图片的地址
|
||||||
|
*/
|
||||||
|
var backgroundImage by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 语言
|
* 语言
|
||||||
*/
|
*/
|
||||||
@@ -587,6 +665,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -606,12 +689,22 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
*/
|
*/
|
||||||
var sftpCommand by StringPropertyDelegate(StringUtils.EMPTY)
|
var sftpCommand by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* defaultDirectory
|
||||||
|
*/
|
||||||
|
var defaultDirectory by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否固定在标签栏
|
* 是否固定在标签栏
|
||||||
*/
|
*/
|
||||||
var pinTab by BooleanPropertyDelegate(false)
|
var pinTab by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否保留原始文件时间
|
||||||
|
*/
|
||||||
|
var preserveModificationTime by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -652,6 +745,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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@ 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.native.osx.NativeMacLibrary
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.FlatLaf
|
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jetbrains.JBR
|
import com.jetbrains.JBR
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
@@ -12,29 +12,60 @@ import java.awt.event.WindowAdapter
|
|||||||
import java.awt.event.WindowEvent
|
import java.awt.event.WindowEvent
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
|
|
||||||
abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
||||||
private val rootPanel = JPanel(BorderLayout())
|
|
||||||
private val titleLabel = JLabel()
|
private val titleLabel = JLabel()
|
||||||
private val titleBar by lazy { LogicCustomTitleBar.createCustomTitleBar(this) }
|
|
||||||
val disposable = Disposer.newDisposable()
|
val disposable = Disposer.newDisposable()
|
||||||
|
private val customTitleBar = if (SystemInfo.isMacOS && JBR.isWindowDecorationsSupported())
|
||||||
|
JBR.getWindowDecorations().createCustomTitleBar() else null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val DEFAULT_ACTION = "DEFAULT_ACTION"
|
const val DEFAULT_ACTION = "DEFAULT_ACTION"
|
||||||
private const val PROCESS_GLOBAL_KEYMAP = "PROCESS_GLOBAL_KEYMAP"
|
private const val PROCESS_GLOBAL_KEYMAP = "PROCESS_GLOBAL_KEYMAP"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected var controlsVisible = true
|
protected var controlsVisible = true
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
titleBar.putProperty("controls.visible", value)
|
if (SystemInfo.isMacOS) {
|
||||||
|
if (customTitleBar != null) {
|
||||||
|
customTitleBar.putProperty("controls.visible", value)
|
||||||
|
} else {
|
||||||
|
NativeMacLibrary.setControlsVisible(this, value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICONIFFY, value)
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_MAXIMIZE, value)
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_CLOSE, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected var titleBarHeight = UIManager.getInt("TabbedPane.tabHeight").toFloat()
|
protected var fullWindowContent = false
|
||||||
set(value) {
|
set(value) {
|
||||||
titleBar.height = value
|
|
||||||
field = value
|
field = value
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected var titleVisible = true
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected var titleIconVisible = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICON, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected var titleBarHeight = UIManager.getInt("TabbedPane.tabHeight")
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
customTitleBar?.height = height.toFloat()
|
||||||
|
} else {
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected var lostFocusDispose = false
|
protected var lostFocusDispose = false
|
||||||
@@ -51,25 +82,43 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
|||||||
super.rootPane.putClientProperty(PROCESS_GLOBAL_KEYMAP, value)
|
super.rootPane.putClientProperty(PROCESS_GLOBAL_KEYMAP, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
super.setDefaultCloseOperation(DISPOSE_ON_CLOSE)
|
||||||
|
|
||||||
|
// 使用 FlatLaf 的 TitlePane
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
rootPane.windowDecorationStyle = JRootPane.PLAIN_DIALOG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected fun init() {
|
protected fun init() {
|
||||||
|
|
||||||
|
|
||||||
defaultCloseOperation = DISPOSE_ON_CLOSE
|
|
||||||
|
|
||||||
initTitleBar()
|
|
||||||
initEvents()
|
initEvents()
|
||||||
|
|
||||||
if (JBR.isWindowDecorationsSupported()) {
|
val rootPanel = JPanel(BorderLayout())
|
||||||
if (rootPane.getClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE) != false) {
|
rootPanel.add(createCenterPanel(), BorderLayout.CENTER)
|
||||||
val titlePanel = createTitlePanel()
|
|
||||||
if (titlePanel != null) {
|
if (SystemInfo.isMacOS) {
|
||||||
rootPanel.add(titlePanel, BorderLayout.NORTH)
|
rootPane.putClientProperty("apple.awt.windowTitleVisible", false)
|
||||||
}
|
rootPane.putClientProperty("apple.awt.fullWindowContent", true)
|
||||||
|
rootPane.putClientProperty("apple.awt.transparentTitleBar", true)
|
||||||
|
rootPane.putClientProperty(
|
||||||
|
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING,
|
||||||
|
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING_MEDIUM
|
||||||
|
)
|
||||||
|
|
||||||
|
val titlePanel = createTitlePanel()
|
||||||
|
if (titlePanel != null) {
|
||||||
|
rootPanel.add(titlePanel, BorderLayout.NORTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
val customTitleBar = this.customTitleBar
|
||||||
|
if (customTitleBar != null) {
|
||||||
|
customTitleBar.putProperty("controls.visible", controlsVisible)
|
||||||
|
customTitleBar.height = titleBarHeight.toFloat()
|
||||||
|
JBR.getWindowDecorations().setCustomTitleBar(this, customTitleBar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootPanel.add(createCenterPanel(), BorderLayout.CENTER)
|
|
||||||
|
|
||||||
val southPanel = createSouthPanel()
|
val southPanel = createSouthPanel()
|
||||||
if (southPanel != null) {
|
if (southPanel != null) {
|
||||||
rootPanel.add(southPanel, BorderLayout.SOUTH)
|
rootPanel.add(southPanel, BorderLayout.SOUTH)
|
||||||
@@ -122,7 +171,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
|||||||
|
|
||||||
val panel = JPanel(BorderLayout())
|
val panel = JPanel(BorderLayout())
|
||||||
panel.add(titleLabel, BorderLayout.CENTER)
|
panel.add(titleLabel, BorderLayout.CENTER)
|
||||||
panel.preferredSize = Dimension(-1, titleBar.height.toInt())
|
panel.preferredSize = Dimension(-1, titleBarHeight)
|
||||||
|
|
||||||
|
|
||||||
return panel
|
return panel
|
||||||
@@ -173,7 +222,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
doCancelAction()
|
SwingUtilities.invokeLater { doCancelAction() }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -191,30 +240,20 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (SystemInfo.isWindows) {
|
|
||||||
addWindowListener(object : WindowAdapter(), ThemeChangeListener {
|
|
||||||
override fun windowClosed(e: WindowEvent) {
|
|
||||||
ThemeManager.getInstance().removeThemeChangeListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun windowOpened(e: WindowEvent) {
|
|
||||||
onChanged()
|
|
||||||
ThemeManager.getInstance().addThemeChangeListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChanged() {
|
|
||||||
titleBar.putProperty("controls.dark", FlatLaf.isLafDark())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initTitleBar() {
|
override fun addNotify() {
|
||||||
titleBar.height = titleBarHeight
|
super.addNotify()
|
||||||
titleBar.putProperty("controls.visible", controlsVisible)
|
|
||||||
if (JBR.isWindowDecorationsSupported()) {
|
// 显示后触发一次重绘制
|
||||||
JBR.getWindowDecorations().setCustomTitleBar(this, titleBar)
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
this.controlsVisible = controlsVisible
|
||||||
|
this.titleBarHeight = titleBarHeight
|
||||||
|
this.titleIconVisible = titleIconVisible
|
||||||
|
this.titleVisible = titleVisible
|
||||||
|
this.fullWindowContent = fullWindowContent
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun doOKAction() {
|
protected open fun doOKAction() {
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -47,6 +54,8 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
|||||||
serialCommOption.parityComboBox.selectedItem = serialComm.parity
|
serialCommOption.parityComboBox.selectedItem = serialComm.parity
|
||||||
serialCommOption.stopBitsComboBox.selectedItem = serialComm.stopBits
|
serialCommOption.stopBitsComboBox.selectedItem = serialComm.stopBits
|
||||||
serialCommOption.flowControlComboBox.selectedItem = serialComm.flowControl
|
serialCommOption.flowControlComboBox.selectedItem = serialComm.flowControl
|
||||||
|
|
||||||
|
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getHost(): Host {
|
override fun getHost(): Host {
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +134,21 @@ data class Options(
|
|||||||
* 串口配置
|
* 串口配置
|
||||||
*/
|
*/
|
||||||
val serialComm: SerialComm = SerialComm(),
|
val serialComm: SerialComm = SerialComm(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SFTP 默认目录
|
||||||
|
*/
|
||||||
|
val sftpDefaultDirectory: String = StringUtils.EMPTY,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* X11 Forwarding
|
||||||
|
*/
|
||||||
|
val enableX11Forwarding: Boolean = false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* X11 Server,Format: host.port. default: localhost:0
|
||||||
|
*/
|
||||||
|
val x11Forwarding: String = StringUtils.EMPTY,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val Default = Options()
|
val Default = Options()
|
||||||
@@ -209,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) {
|
||||||
@@ -23,6 +25,7 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
|||||||
isModal = true
|
isModal = true
|
||||||
title = I18n.getString("termora.new-host.title")
|
title = I18n.getString("termora.new-host.title")
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(null)
|
||||||
|
pane.setSelectedIndex(0)
|
||||||
|
|
||||||
init()
|
init()
|
||||||
}
|
}
|
||||||
@@ -52,9 +55,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 +106,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,22 +6,27 @@ 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.RegExUtils
|
||||||
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.datatransfer.DataFlavor
|
||||||
import java.awt.event.*
|
import java.awt.event.*
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import javax.swing.*
|
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()
|
||||||
@@ -29,6 +34,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
protected val terminalOption = TerminalOption()
|
protected val terminalOption = TerminalOption()
|
||||||
protected val jumpHostsOption = JumpHostsOption()
|
protected val jumpHostsOption = JumpHostsOption()
|
||||||
protected val serialCommOption = SerialCommOption()
|
protected val serialCommOption = SerialCommOption()
|
||||||
|
protected val sftpOption = SFTPOption()
|
||||||
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -38,6 +44,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
addOption(jumpHostsOption)
|
addOption(jumpHostsOption)
|
||||||
addOption(terminalOption)
|
addOption(terminalOption)
|
||||||
addOption(serialCommOption)
|
addOption(serialCommOption)
|
||||||
|
addOption(sftpOption)
|
||||||
|
|
||||||
setContentBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
setContentBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
||||||
}
|
}
|
||||||
@@ -50,18 +57,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) {
|
||||||
@@ -91,7 +103,10 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
startupCommand = terminalOption.startupCommandTextField.text,
|
startupCommand = terminalOption.startupCommandTextField.text,
|
||||||
heartbeatInterval = (terminalOption.heartbeatIntervalTextField.value ?: 30) as Int,
|
heartbeatInterval = (terminalOption.heartbeatIntervalTextField.value ?: 30) as Int,
|
||||||
jumpHosts = jumpHostsOption.jumpHosts.map { it.id },
|
jumpHosts = jumpHostsOption.jumpHosts.map { it.id },
|
||||||
serialComm = serialComm
|
serialComm = serialComm,
|
||||||
|
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||||
|
enableX11Forwarding = tunnelingOption.x11ForwardingCheckBox.isSelected,
|
||||||
|
x11Forwarding = tunnelingOption.x11ServerTextField.text,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Host(
|
return Host(
|
||||||
@@ -157,6 +172,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
|
||||||
}
|
}
|
||||||
@@ -166,14 +192,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 表示有错误
|
||||||
*/
|
*/
|
||||||
@@ -193,10 +223,28 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
val nameTextField = OutlineTextField(128)
|
val nameTextField = OutlineTextField(128)
|
||||||
val protocolTypeComboBox = FlatComboBox<Protocol>()
|
val protocolTypeComboBox = FlatComboBox<Protocol>()
|
||||||
val usernameTextField = OutlineTextField(128)
|
val usernameTextField = OutlineTextField(128)
|
||||||
val hostTextField = OutlineTextField(255)
|
val hostTextField = object : OutlineTextField(255) {
|
||||||
|
override fun paste() {
|
||||||
|
if (!toolkit.systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = toolkit.systemClipboard.getData(DataFlavor.stringFlavor)?.toString() ?: return
|
||||||
|
if (text.isBlank()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除所有不可见字符
|
||||||
|
text = RegExUtils.replaceAll(text, "[\\p{C}\\s]", StringUtils.EMPTY)
|
||||||
|
|
||||||
|
// text
|
||||||
|
replaceSelection(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
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>()
|
||||||
@@ -212,6 +260,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<*>?,
|
||||||
@@ -287,10 +339,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
|
||||||
|
|
||||||
@@ -454,6 +518,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)
|
||||||
}
|
}
|
||||||
@@ -669,8 +735,58 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||||
|
val defaultDirectoryField = OutlineTextField(255)
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.folder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return "SFTP"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $formMargin, default:grow, $formMargin",
|
||||||
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||||
|
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected inner class TunnelingOption : JPanel(BorderLayout()), Option {
|
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 {
|
||||||
@@ -745,13 +861,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))
|
||||||
@@ -1003,8 +1142,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
|
||||||
@@ -13,13 +14,13 @@ import javax.swing.Icon
|
|||||||
abstract class HostTerminalTab(
|
abstract class HostTerminalTab(
|
||||||
val windowScope: WindowScope,
|
val windowScope: WindowScope,
|
||||||
val host: Host,
|
val host: Host,
|
||||||
protected val terminal: Terminal = TerminalFactory.getInstance(windowScope).createTerminal()
|
protected val terminal: Terminal = TerminalFactory.getInstance().createTerminal()
|
||||||
) : PropertyTerminalTab(), DataProvider {
|
) : PropertyTerminalTab(), DataProvider {
|
||||||
companion object {
|
companion object {
|
||||||
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,9 +10,11 @@ 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") }
|
||||||
|
val inspectionsEye by lazy { DynamicIcon("icons/inspectionsEye.svg", "icons/inspectionsEye_dark.svg") }
|
||||||
val eye by lazy { DynamicIcon("icons/eye.svg", "icons/eye_dark.svg") }
|
val eye by lazy { DynamicIcon("icons/eye.svg", "icons/eye_dark.svg") }
|
||||||
val eyeClose by lazy { DynamicIcon("icons/eyeClose.svg", "icons/eyeClose_dark.svg") }
|
val eyeClose by lazy { DynamicIcon("icons/eyeClose.svg", "icons/eyeClose_dark.svg") }
|
||||||
val matchCase by lazy { DynamicIcon("icons/matchCase.svg", "icons/matchCase_dark.svg") }
|
val matchCase by lazy { DynamicIcon("icons/matchCase.svg", "icons/matchCase_dark.svg") }
|
||||||
@@ -32,6 +34,7 @@ object Icons {
|
|||||||
val percentage by lazy { DynamicIcon("icons/percentage.svg", "icons/percentage_dark.svg") }
|
val percentage by lazy { DynamicIcon("icons/percentage.svg", "icons/percentage_dark.svg") }
|
||||||
val text by lazy { DynamicIcon("icons/text.svg", "icons/text_dark.svg") }
|
val text by lazy { DynamicIcon("icons/text.svg", "icons/text_dark.svg") }
|
||||||
val errorIntroduction by lazy { DynamicIcon("icons/errorIntroduction.svg", "icons/errorIntroduction_dark.svg") }
|
val errorIntroduction by lazy { DynamicIcon("icons/errorIntroduction.svg", "icons/errorIntroduction_dark.svg") }
|
||||||
|
val warningIntroduction by lazy { DynamicIcon("icons/warningIntroduction.svg", "icons/warningIntroduction_dark.svg") }
|
||||||
val networkPolicy by lazy { DynamicIcon("icons/networkPolicy.svg", "icons/networkPolicy_dark.svg") }
|
val networkPolicy by lazy { DynamicIcon("icons/networkPolicy.svg", "icons/networkPolicy_dark.svg") }
|
||||||
val clusterRole by lazy { DynamicIcon("icons/clusterRole.svg", "icons/clusterRole_dark.svg") }
|
val clusterRole by lazy { DynamicIcon("icons/clusterRole.svg", "icons/clusterRole_dark.svg") }
|
||||||
val daemonSets by lazy { DynamicIcon("icons/daemonSets.svg", "icons/daemonSets_dark.svg") }
|
val daemonSets by lazy { DynamicIcon("icons/daemonSets.svg", "icons/daemonSets_dark.svg") }
|
||||||
@@ -50,6 +53,7 @@ object Icons {
|
|||||||
val moreVertical by lazy { DynamicIcon("icons/moreVertical.svg", "icons/moreVertical_dark.svg") }
|
val moreVertical by lazy { DynamicIcon("icons/moreVertical.svg", "icons/moreVertical_dark.svg") }
|
||||||
val colors by lazy { DynamicIcon("icons/colors.svg", "icons/colors_dark.svg") }
|
val colors by lazy { DynamicIcon("icons/colors.svg", "icons/colors_dark.svg") }
|
||||||
val chevronDown by lazy { DynamicIcon("icons/chevronDownLarge.svg", "icons/chevronDownLarge_dark.svg") }
|
val chevronDown by lazy { DynamicIcon("icons/chevronDownLarge.svg", "icons/chevronDownLarge_dark.svg") }
|
||||||
|
val chevronRight by lazy { DynamicIcon("icons/chevronRight.svg", "icons/chevronRight_dark.svg") }
|
||||||
val homeFolder by lazy { DynamicIcon("icons/homeFolder.svg", "icons/homeFolder_dark.svg") }
|
val homeFolder by lazy { DynamicIcon("icons/homeFolder.svg", "icons/homeFolder_dark.svg") }
|
||||||
val openNewTab by lazy { DynamicIcon("icons/openNewTab.svg", "icons/openNewTab_dark.svg") }
|
val openNewTab by lazy { DynamicIcon("icons/openNewTab.svg", "icons/openNewTab_dark.svg") }
|
||||||
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") }
|
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") }
|
||||||
@@ -60,6 +64,7 @@ object Icons {
|
|||||||
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
|
val 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") }
|
||||||
@@ -90,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,74 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
import com.formdev.flatlaf.extras.components.FlatTextField
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import java.awt.Window
|
|
||||||
import java.awt.event.KeyAdapter
|
|
||||||
import java.awt.event.KeyEvent
|
|
||||||
import javax.swing.BorderFactory
|
|
||||||
import javax.swing.JComponent
|
|
||||||
import javax.swing.UIManager
|
|
||||||
|
|
||||||
class InputDialog(
|
|
||||||
owner: Window,
|
|
||||||
title: String,
|
|
||||||
text: String = StringUtils.EMPTY,
|
|
||||||
placeholderText: String = StringUtils.EMPTY
|
|
||||||
) : DialogWrapper(owner) {
|
|
||||||
private val textField = FlatTextField()
|
|
||||||
private var text: String? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
setSize(340, 60)
|
|
||||||
setLocationRelativeTo(owner)
|
|
||||||
|
|
||||||
super.setTitle(title)
|
|
||||||
|
|
||||||
isResizable = false
|
|
||||||
isModal = true
|
|
||||||
controlsVisible = false
|
|
||||||
titleBarHeight = UIManager.getInt("TabbedPane.tabHeight") * 0.8f
|
|
||||||
|
|
||||||
|
|
||||||
textField.placeholderText = placeholderText
|
|
||||||
textField.text = text
|
|
||||||
textField.addKeyListener(object : KeyAdapter() {
|
|
||||||
override fun keyPressed(e: KeyEvent) {
|
|
||||||
if (e.keyCode == KeyEvent.VK_ENTER) {
|
|
||||||
if (textField.text.isBlank()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
doOKAction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
init()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createCenterPanel(): JComponent {
|
|
||||||
textField.background = UIManager.getColor("window")
|
|
||||||
textField.border = BorderFactory.createEmptyBorder(0, 13, 0, 13)
|
|
||||||
|
|
||||||
return textField
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getText(): String? {
|
|
||||||
isVisible = true
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun doCancelAction() {
|
|
||||||
text = null
|
|
||||||
super.doCancelAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun doOKAction() {
|
|
||||||
text = textField.text
|
|
||||||
super.doOKAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createSouthPanel(): JComponent? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,25 @@
|
|||||||
package app.termora
|
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(windowScope).createPtyConnector(
|
val ptyConnector = PtyConnectorFactory.getInstance().createPtyConnector(
|
||||||
winSize.rows, winSize.cols,
|
winSize.rows, winSize.cols,
|
||||||
host.options.envs(),
|
host.options.envs(),
|
||||||
Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8),
|
Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8),
|
||||||
@@ -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,109 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
|
||||||
import com.jetbrains.JBR
|
|
||||||
import com.jetbrains.WindowDecorations.CustomTitleBar
|
|
||||||
import java.awt.Rectangle
|
|
||||||
import java.awt.Window
|
|
||||||
import javax.swing.RootPaneContainer
|
|
||||||
|
|
||||||
class LogicCustomTitleBar(private val titleBar: CustomTitleBar) : CustomTitleBar {
|
|
||||||
companion object {
|
|
||||||
fun createCustomTitleBar(rootPaneContainer: RootPaneContainer): CustomTitleBar {
|
|
||||||
if (!JBR.isWindowDecorationsSupported()) {
|
|
||||||
return LogicCustomTitleBar(object : CustomTitleBar {
|
|
||||||
override fun getHeight(): Float {
|
|
||||||
val bounds = rootPaneContainer.rootPane
|
|
||||||
.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
|
||||||
if (bounds is Rectangle) {
|
|
||||||
return bounds.height.toFloat()
|
|
||||||
}
|
|
||||||
return 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setHeight(height: Float) {
|
|
||||||
rootPaneContainer.rootPane.putClientProperty(
|
|
||||||
FlatClientProperties.TITLE_BAR_HEIGHT,
|
|
||||||
height.toInt()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getProperties(): MutableMap<String, Any> {
|
|
||||||
return mutableMapOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun putProperties(m: MutableMap<String, *>?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun putProperty(key: String?, value: Any?) {
|
|
||||||
if (key == "controls.visible" && value is Boolean) {
|
|
||||||
rootPaneContainer.rootPane.putClientProperty(
|
|
||||||
FlatClientProperties.TITLE_BAR_SHOW_CLOSE,
|
|
||||||
value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLeftInset(): Float {
|
|
||||||
return 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getRightInset(): Float {
|
|
||||||
val bounds = rootPaneContainer.rootPane
|
|
||||||
.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
|
||||||
if (bounds is Rectangle) {
|
|
||||||
return bounds.width.toFloat()
|
|
||||||
}
|
|
||||||
return 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun forceHitTest(client: Boolean) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getContainingWindow(): Window {
|
|
||||||
return rootPaneContainer as Window
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return JBR.getWindowDecorations().createCustomTitleBar()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getHeight(): Float {
|
|
||||||
return titleBar.height
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setHeight(height: Float) {
|
|
||||||
titleBar.height = height
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getProperties(): MutableMap<String, Any> {
|
|
||||||
return titleBar.properties
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun putProperties(m: MutableMap<String, *>?) {
|
|
||||||
titleBar.putProperties(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun putProperty(key: String?, value: Any?) {
|
|
||||||
titleBar.putProperty(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLeftInset(): Float {
|
|
||||||
return titleBar.leftInset
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getRightInset(): Float {
|
|
||||||
return titleBar.rightInset
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun forceHitTest(client: Boolean) {
|
|
||||||
titleBar.forceHitTest(client)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getContainingWindow(): Window {
|
|
||||||
return titleBar.containingWindow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import com.pty4j.util.PtyUtil
|
|
||||||
import org.apache.commons.io.FileUtils
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.apache.commons.lang3.SystemUtils
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
// 由于 macOS 签名和公证问题,依赖二进制依赖会单独在一个文件夹
|
ApplicationInitializr().run()
|
||||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
|
||||||
setupNativeLibraries()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
|
||||||
System.setProperty("apple.awt.application.name", Application.getName())
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplicationRunner().run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun setupNativeLibraries() {
|
|
||||||
if (!SystemUtils.IS_OS_MAC_OSX) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val appPath = Application.getAppPath()
|
|
||||||
if (StringUtils.isBlank(appPath)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val contents = File(appPath).parentFile?.parentFile ?: return
|
|
||||||
val dylib = FileUtils.getFile(contents, "app", "dylib")
|
|
||||||
if (!dylib.exists()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val jna = FileUtils.getFile(dylib, "jna")
|
|
||||||
if (jna.exists()) {
|
|
||||||
System.setProperty("jna.boot.library.path", jna.absolutePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
val pty4j = FileUtils.getFile(dylib, "pty4j")
|
|
||||||
if (pty4j.exists()) {
|
|
||||||
System.setProperty(PtyUtil.PREFERRED_NATIVE_FOLDER_KEY, pty4j.absolutePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
val jSerialComm = FileUtils.getFile(dylib, "jSerialComm")
|
|
||||||
if (jSerialComm.exists()) {
|
|
||||||
System.setProperty("jSerialComm.library.path", jSerialComm.absolutePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
val restart4j = FileUtils.getFile(dylib, "restart4j", "restarter")
|
|
||||||
if (restart4j.exists()) {
|
|
||||||
System.setProperty("restarter.path", restart4j.absolutePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
|
|
||||||
import app.termora.terminal.PtyConnector
|
|
||||||
import app.termora.terminal.PtyConnectorDelegate
|
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当开启转发时,会获取到所有的 [PtyConnector] 然后跳过中间层,直接找到最近的一个 [MultiplePtyConnector],如果找不到那就以最后一个匹配不到的为准 [getMultiplePtyConnector]。
|
|
||||||
*/
|
|
||||||
class MultiplePtyConnector(
|
|
||||||
private val myConnector: PtyConnector
|
|
||||||
) : PtyConnectorDelegate(myConnector) {
|
|
||||||
|
|
||||||
private val isMultiple get() = ActionManager.getInstance().isSelected(Actions.MULTIPLE)
|
|
||||||
private val ptyConnectors
|
|
||||||
get() = ApplicationScope.forApplicationScope()
|
|
||||||
.windowScopes().map { PtyConnectorFactory.getInstance(it).getPtyConnectors() }
|
|
||||||
.flatten()
|
|
||||||
|
|
||||||
override fun write(buffer: ByteArray, offset: Int, len: Int) {
|
|
||||||
if (isMultiple) {
|
|
||||||
for (connector in ptyConnectors) {
|
|
||||||
getMultiplePtyConnector(connector).write(buffer, offset, len)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
myConnector.write(buffer, offset, len)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun getMultiplePtyConnector(connector: PtyConnector): PtyConnector {
|
|
||||||
if (connector is MultiplePtyConnector) {
|
|
||||||
val c = connector.myConnector
|
|
||||||
if (c is MultiplePtyConnector) {
|
|
||||||
return getMultiplePtyConnector(c)
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connector is PtyConnectorDelegate) {
|
|
||||||
val c = connector.ptyConnector
|
|
||||||
if (c != null) {
|
|
||||||
return getMultiplePtyConnector(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return connector
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
|
||||||
import app.termora.actions.ActionManager
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
|
import app.termora.actions.MultipleAction
|
||||||
import app.termora.terminal.Terminal
|
import app.termora.terminal.Terminal
|
||||||
import app.termora.terminal.TerminalColor
|
import app.termora.terminal.TerminalColor
|
||||||
import app.termora.terminal.TextStyle
|
import app.termora.terminal.TextStyle
|
||||||
@@ -9,8 +11,10 @@ import app.termora.terminal.panel.FloatingToolbarPanel
|
|||||||
import app.termora.terminal.panel.TerminalDisplay
|
import app.termora.terminal.panel.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.apache.commons.lang3.StringUtils
|
||||||
import java.awt.Color
|
import java.awt.Color
|
||||||
import java.awt.Graphics
|
import java.awt.Graphics
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class MultipleTerminalListener : TerminalPaintListener {
|
class MultipleTerminalListener : TerminalPaintListener {
|
||||||
override fun after(
|
override fun after(
|
||||||
@@ -21,9 +25,9 @@ class MultipleTerminalListener : TerminalPaintListener {
|
|||||||
terminalDisplay: TerminalDisplay,
|
terminalDisplay: TerminalDisplay,
|
||||||
terminal: Terminal
|
terminal: Terminal
|
||||||
) {
|
) {
|
||||||
if (!ActionManager.getInstance().isSelected(Actions.MULTIPLE)) {
|
val windowScope = AnActionEvent(terminalPanel, StringUtils.EMPTY, EventObject(terminalPanel))
|
||||||
return
|
.getData(DataProviders.WindowScope) ?: return
|
||||||
}
|
if (!MultipleAction.getInstance(windowScope).isSelected) return
|
||||||
|
|
||||||
val oldFont = g.font
|
val oldFont = g.font
|
||||||
val colorPalette = terminal.getTerminalModel().getColorPalette()
|
val colorPalette = terminal.getTerminalModel().getColorPalette()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.actions.DataProvider
|
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
@@ -10,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() {
|
||||||
@@ -22,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"),
|
||||||
@@ -237,11 +229,8 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
|
|
||||||
private fun dragToAnotherWindow(oldFrame: TermoraFrame, frame: TermoraFrame) {
|
private fun dragToAnotherWindow(oldFrame: TermoraFrame, frame: TermoraFrame) {
|
||||||
val tab = this.terminalTab ?: return
|
val tab = this.terminalTab ?: return
|
||||||
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
|
||||||
val tabbedManager = frame.getData(DataProviders.TerminalTabbed) ?: return
|
val tabbedManager = frame.getData(DataProviders.TerminalTabbed) ?: return
|
||||||
val tabbedPane = frame.getData(DataProviders.TabbedPane) ?: return
|
val tabbedPane = frame.getData(DataProviders.TabbedPane) ?: return
|
||||||
val windowScope = frame.getData(DataProviders.WindowScope) ?: return
|
|
||||||
val oldWindowScope = oldFrame.getData(DataProviders.WindowScope) ?: return
|
|
||||||
val location = Point(MouseInfo.getPointerInfo().location)
|
val location = Point(MouseInfo.getPointerInfo().location)
|
||||||
SwingUtilities.convertPointFromScreen(location, tabbedPane)
|
SwingUtilities.convertPointFromScreen(location, tabbedPane)
|
||||||
val index = tabbedPane.indexAtLocation(location.x, location.y)
|
val index = tabbedPane.indexAtLocation(location.x, location.y)
|
||||||
@@ -253,11 +242,6 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
index
|
index
|
||||||
)
|
)
|
||||||
|
|
||||||
TerminalPanelFactory.getInstance(oldWindowScope).removeTerminalPanel(terminalPanel)
|
|
||||||
TerminalPanelFactory.getInstance(windowScope).addTerminalPanel(terminalPanel)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (frame.hasFocus()) {
|
if (frame.hasFocus()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
41
src/main/kotlin/app/termora/NativeStringComparator.kt
Normal file
41
src/main/kotlin/app/termora/NativeStringComparator.kt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import de.jangassen.jfa.foundation.Foundation
|
||||||
|
import de.jangassen.jfa.foundation.Foundation.NSAutoreleasePool
|
||||||
|
import java.text.Collator
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class NativeStringComparator private constructor() : Comparator<String> {
|
||||||
|
private val collator by lazy { Collator.getInstance(Locale.getDefault()).apply { strength = Collator.PRIMARY } }
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getInstance(): NativeStringComparator {
|
||||||
|
return ApplicationScope.forApplicationScope()
|
||||||
|
.getOrCreate(NativeStringComparator::class) { NativeStringComparator() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val SORT_DIGITSASNUMBERS: Int = 0x00000008
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun compare(o1: String, o2: String): Int {
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
// CompareStringEx returns 1, 2, 3 respectively instead of -1, 0, 1
|
||||||
|
return MyKernel32.INSTANCE.CompareStringEx(SORT_DIGITSASNUMBERS, o1, o2) - 2
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
val pool = NSAutoreleasePool()
|
||||||
|
try {
|
||||||
|
val a = Foundation.nsString(o1)
|
||||||
|
val b = Foundation.nsString(o2)
|
||||||
|
return Foundation.invoke(a, "localizedStandardCompare:", b).toInt()
|
||||||
|
} finally {
|
||||||
|
pool.drain()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return collator.compare(o1, o2)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.actions.AnActionEvent
|
|
||||||
import app.termora.actions.OpenHostAction
|
import app.termora.actions.OpenHostAction
|
||||||
import app.termora.transport.SFTPAction
|
import app.termora.sftp.SFTPActionEvent
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
@@ -15,6 +14,7 @@ import org.apache.commons.io.FilenameUtils
|
|||||||
import org.apache.commons.io.filefilter.FileFilterUtils
|
import org.apache.commons.io.filefilter.FileFilterUtils
|
||||||
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.client.config.hosts.HostConfigEntry
|
||||||
import org.ini4j.Ini
|
import org.ini4j.Ini
|
||||||
import org.ini4j.Reg
|
import org.ini4j.Reg
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
@@ -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 {
|
||||||
@@ -46,10 +49,11 @@ class NewHostTree : SimpleTree() {
|
|||||||
private val properties get() = Database.getDatabase().properties
|
private val properties get() = Database.getDatabase().properties
|
||||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||||
private val openHostAction get() = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
|
private val openHostAction get() = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
|
||||||
|
private val sftpAction get() = ActionManager.getInstance().getAction(app.termora.Actions.SFTP)
|
||||||
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()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,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
|
||||||
@@ -107,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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -134,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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,8 +192,10 @@ class NewHostTree : SimpleTree() {
|
|||||||
val finalShellMenu = importMenu.add("FinalShell")
|
val finalShellMenu = importMenu.add("FinalShell")
|
||||||
val windTermMenu = importMenu.add("WindTerm")
|
val windTermMenu = importMenu.add("WindTerm")
|
||||||
val secureCRTMenu = importMenu.add("SecureCRT")
|
val secureCRTMenu = importMenu.add("SecureCRT")
|
||||||
|
val sshMenu = importMenu.add(".ssh/config")
|
||||||
val mobaXtermMenu = importMenu.add("MobaXterm")
|
val mobaXtermMenu = importMenu.add("MobaXterm")
|
||||||
|
|
||||||
|
|
||||||
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect"))
|
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect"))
|
||||||
val openWith = popupMenu.add(JMenu(I18n.getString("termora.welcome.contextmenu.connect-with"))) as JMenu
|
val openWith = popupMenu.add(JMenu(I18n.getString("termora.welcome.contextmenu.connect-with"))) as JMenu
|
||||||
val openWithSFTP = openWith.add("SFTP")
|
val openWithSFTP = openWith.add("SFTP")
|
||||||
@@ -218,6 +227,7 @@ class NewHostTree : SimpleTree() {
|
|||||||
secureCRTMenu.addActionListener { importHosts(lastNode, ImportType.SecureCRT) }
|
secureCRTMenu.addActionListener { importHosts(lastNode, ImportType.SecureCRT) }
|
||||||
electermMenu.addActionListener { importHosts(lastNode, ImportType.electerm) }
|
electermMenu.addActionListener { importHosts(lastNode, ImportType.electerm) }
|
||||||
mobaXtermMenu.addActionListener { importHosts(lastNode, ImportType.MobaXterm) }
|
mobaXtermMenu.addActionListener { importHosts(lastNode, ImportType.MobaXterm) }
|
||||||
|
sshMenu.addActionListener { importHosts(lastNode, ImportType.SSH) }
|
||||||
finalShellMenu.addActionListener { importHosts(lastNode, ImportType.FinalShell) }
|
finalShellMenu.addActionListener { importHosts(lastNode, ImportType.FinalShell) }
|
||||||
csvMenu.addActionListener { importHosts(lastNode, ImportType.CSV) }
|
csvMenu.addActionListener { importHosts(lastNode, ImportType.CSV) }
|
||||||
windTermMenu.addActionListener { importHosts(lastNode, ImportType.WindTerm) }
|
windTermMenu.addActionListener { importHosts(lastNode, ImportType.WindTerm) }
|
||||||
@@ -323,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,10 +421,8 @@ class NewHostTree : SimpleTree() {
|
|||||||
val nodes = getSelectionSimpleTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
|
val nodes = getSelectionSimpleTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
|
||||||
if (nodes.isEmpty()) return
|
if (nodes.isEmpty()) return
|
||||||
|
|
||||||
val sftpAction = ActionManager.getInstance().getAction(app.termora.Actions.SFTP) as SFTPAction? ?: return
|
|
||||||
val tab = sftpAction.openOrCreateSFTPTerminalTab(AnActionEvent(this, StringUtils.EMPTY, evt)) ?: return
|
|
||||||
for (node in nodes) {
|
for (node in nodes) {
|
||||||
sftpAction.connectHost(node, tab)
|
sftpAction.actionPerformed(SFTPActionEvent(this, node.id, evt))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,6 +453,7 @@ class NewHostTree : SimpleTree() {
|
|||||||
|
|
||||||
when (type) {
|
when (type) {
|
||||||
ImportType.WindTerm -> chooser.fileFilter = FileNameExtensionFilter("WindTerm (*.sessions)", "sessions")
|
ImportType.WindTerm -> chooser.fileFilter = FileNameExtensionFilter("WindTerm (*.sessions)", "sessions")
|
||||||
|
ImportType.SSH -> chooser.fileFilter = FileNameExtensionFilter("SSH (config)", "config")
|
||||||
ImportType.CSV -> chooser.fileFilter = FileNameExtensionFilter("CSV (*.csv)", "csv")
|
ImportType.CSV -> chooser.fileFilter = FileNameExtensionFilter("CSV (*.csv)", "csv")
|
||||||
ImportType.SecureCRT -> chooser.fileFilter = FileNameExtensionFilter("SecureCRT (*.xml)", "xml")
|
ImportType.SecureCRT -> chooser.fileFilter = FileNameExtensionFilter("SecureCRT (*.xml)", "xml")
|
||||||
ImportType.electerm -> chooser.fileFilter = FileNameExtensionFilter("electerm (*.json)", "json")
|
ImportType.electerm -> chooser.fileFilter = FileNameExtensionFilter("electerm (*.json)", "json")
|
||||||
@@ -495,19 +519,23 @@ class NewHostTree : SimpleTree() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 选择文件
|
// 选择文件
|
||||||
val code = chooser.showOpenDialog(owner)
|
if (type != ImportType.SSH) {
|
||||||
|
val code = chooser.showOpenDialog(owner)
|
||||||
if (code != JFileChooser.APPROVE_OPTION) {
|
if (code != JFileChooser.APPROVE_OPTION) {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val file = chooser.selectedFile
|
val file = chooser.selectedFile
|
||||||
properties.putString(
|
if (file != null && file.parentFile != null) {
|
||||||
"NewHostTree.ImportHosts.defaultDir",
|
properties.putString(
|
||||||
(if (FileUtils.isDirectory(file)) file else file.parentFile).absolutePath
|
"NewHostTree.ImportHosts.defaultDir",
|
||||||
)
|
(if (FileUtils.isDirectory(file)) file else file.parentFile).absolutePath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val nodes = when (type) {
|
val nodes = when (type) {
|
||||||
|
ImportType.SSH -> parseFromSSH(folder)
|
||||||
ImportType.WindTerm -> parseFromWindTerm(folder, file)
|
ImportType.WindTerm -> parseFromWindTerm(folder, file)
|
||||||
ImportType.SecureCRT -> parseFromSecureCRT(folder, file)
|
ImportType.SecureCRT -> parseFromSecureCRT(folder, file)
|
||||||
ImportType.MobaXterm -> parseFromMobaXterm(folder, file)
|
ImportType.MobaXterm -> parseFromMobaXterm(folder, file)
|
||||||
@@ -539,6 +567,9 @@ class NewHostTree : SimpleTree() {
|
|||||||
|
|
||||||
// 重新加载
|
// 重新加载
|
||||||
model.reload(folder)
|
model.reload(folder)
|
||||||
|
|
||||||
|
// expand root
|
||||||
|
expandPath(TreePath(model.getPathToRoot(folder)))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseFromWindTerm(folder: HostTreeNode, file: File): List<HostTreeNode> {
|
private fun parseFromWindTerm(folder: HostTreeNode, file: File): List<HostTreeNode> {
|
||||||
@@ -564,6 +595,26 @@ class NewHostTree : SimpleTree() {
|
|||||||
return parseFromCSV(folder, StringReader(sw.toString()))
|
return parseFromCSV(folder, StringReader(sw.toString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseFromSSH(folder: HostTreeNode): List<HostTreeNode> {
|
||||||
|
val entries = HostConfigEntry.readHostConfigEntries(HostConfigEntry.getDefaultHostConfigFile())
|
||||||
|
|
||||||
|
val sw = StringWriter()
|
||||||
|
CSVPrinter(sw, CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).get()).use { printer ->
|
||||||
|
for (entry in entries) {
|
||||||
|
printer.printRecord(
|
||||||
|
StringUtils.EMPTY,
|
||||||
|
StringUtils.defaultString(entry.host),
|
||||||
|
StringUtils.defaultString(entry.hostName),
|
||||||
|
if (entry.port == 0) 22 else entry.port,
|
||||||
|
StringUtils.defaultString(entry.username),
|
||||||
|
"SSH"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseFromCSV(folder, StringReader(sw.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseFromSecureCRT(folder: HostTreeNode, file: File): List<HostTreeNode> {
|
private fun parseFromSecureCRT(folder: HostTreeNode, file: File): List<HostTreeNode> {
|
||||||
val xPath = XPathFactory.newInstance().newXPath()
|
val xPath = XPathFactory.newInstance().newXPath()
|
||||||
val db = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
val db = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||||
@@ -805,7 +856,8 @@ class NewHostTree : SimpleTree() {
|
|||||||
val port = map["Port"]?.toIntOrNull() ?: 22
|
val port = map["Port"]?.toIntOrNull() ?: 22
|
||||||
val username = map["Username"] ?: StringUtils.EMPTY
|
val username = map["Username"] ?: StringUtils.EMPTY
|
||||||
val protocol = map["Protocol"] ?: "SSH"
|
val protocol = map["Protocol"] ?: "SSH"
|
||||||
if (!StringUtils.equalsIgnoreCase(protocol, "SSH")) continue
|
// 仅支持 SSH、RDP 协议
|
||||||
|
if (StringUtils.equalsAnyIgnoreCase(protocol, "SSH", "RDP").not()) continue
|
||||||
if (StringUtils.isAllBlank(hostname, label)) continue
|
if (StringUtils.isAllBlank(hostname, label)) continue
|
||||||
|
|
||||||
var p: HostTreeNode? = null
|
var p: HostTreeNode? = null
|
||||||
@@ -840,7 +892,7 @@ class NewHostTree : SimpleTree() {
|
|||||||
host = hostname,
|
host = hostname,
|
||||||
port = port,
|
port = port,
|
||||||
username = username,
|
username = username,
|
||||||
protocol = Protocol.SSH,
|
protocol = runCatching { Protocol.valueOf(protocol) }.getOrNull() ?: Protocol.SSH,
|
||||||
parentId = p?.host?.id ?: StringUtils.EMPTY,
|
parentId = p?.host?.id ?: StringUtils.EMPTY,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -863,6 +915,7 @@ class NewHostTree : SimpleTree() {
|
|||||||
PuTTY,
|
PuTTY,
|
||||||
SecureCRT,
|
SecureCRT,
|
||||||
MobaXterm,
|
MobaXterm,
|
||||||
|
SSH,
|
||||||
FinalShell,
|
FinalShell,
|
||||||
electerm,
|
electerm,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ package app.termora
|
|||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
|
import java.awt.event.MouseAdapter
|
||||||
|
import java.awt.event.MouseEvent
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
import javax.swing.BorderFactory
|
import javax.swing.*
|
||||||
import javax.swing.JComponent
|
|
||||||
import javax.swing.JScrollPane
|
|
||||||
import javax.swing.UIManager
|
|
||||||
|
|
||||||
class NewHostTreeDialog(
|
class NewHostTreeDialog(
|
||||||
owner: Window,
|
owner: Window,
|
||||||
@@ -19,7 +18,7 @@ class NewHostTreeDialog(
|
|||||||
private val tree = NewHostTree()
|
private val tree = NewHostTree()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
size = Dimension(UIManager.getInt("Dialog.width") - 250, UIManager.getInt("Dialog.height") - 150)
|
||||||
isModal = true
|
isModal = true
|
||||||
isResizable = false
|
isResizable = false
|
||||||
controlsVisible = false
|
controlsVisible = false
|
||||||
@@ -29,10 +28,19 @@ class NewHostTreeDialog(
|
|||||||
tree.doubleClickConnection = false
|
tree.doubleClickConnection = false
|
||||||
tree.dragEnabled = false
|
tree.dragEnabled = false
|
||||||
|
|
||||||
|
tree.addMouseListener(object : MouseAdapter() {
|
||||||
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
|
if (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||||
|
val node = tree.getLastSelectedPathNode() ?: return
|
||||||
|
if (node.isFolder) return
|
||||||
|
doOKAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
init()
|
init()
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(owner)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
src/main/kotlin/app/termora/NotifyListener.kt
Normal file
7
src/main/kotlin/app/termora/NotifyListener.kt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
interface NotifyListener : EventListener {
|
||||||
|
fun addNotify()
|
||||||
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.native.osx.NativeMacLibrary
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
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 java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
@@ -18,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,
|
||||||
@@ -27,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) {
|
||||||
@@ -45,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,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
|
||||||
@@ -97,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
|
||||||
@@ -113,6 +123,36 @@ object OptionPane {
|
|||||||
dialog.dispose()
|
dialog.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showInputDialog(
|
||||||
|
parentComponent: Component?,
|
||||||
|
title: String = UIManager.getString("OptionPane.messageDialogTitle"),
|
||||||
|
value: String = StringUtils.EMPTY,
|
||||||
|
placeholder: String = StringUtils.EMPTY,
|
||||||
|
): String? {
|
||||||
|
val pane = JOptionPane(StringUtils.EMPTY, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION)
|
||||||
|
val dialog = initDialog(pane.createDialog(parentComponent, title))
|
||||||
|
pane.wantsInput = true
|
||||||
|
pane.initialSelectionValue = value
|
||||||
|
|
||||||
|
val textField = SwingUtils.getDescendantsOfType(JTextField::class.java, pane, true).firstOrNull()
|
||||||
|
if (textField?.name == "OptionPane.textField") {
|
||||||
|
textField.border = BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor),
|
||||||
|
BorderFactory.createEmptyBorder(0, 0, 2, 0)
|
||||||
|
)
|
||||||
|
textField.background = UIManager.getColor("window")
|
||||||
|
textField.putClientProperty(FlatClientProperties.PLACEHOLDER_TEXT, placeholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.isVisible = true
|
||||||
|
dialog.dispose()
|
||||||
|
|
||||||
|
val inputValue = pane.inputValue
|
||||||
|
if (inputValue == JOptionPane.UNINITIALIZED_VALUE) return null
|
||||||
|
|
||||||
|
return inputValue as? String
|
||||||
|
}
|
||||||
|
|
||||||
fun openFileInFolder(
|
fun openFileInFolder(
|
||||||
parentComponent: Component,
|
parentComponent: Component,
|
||||||
file: File,
|
file: File,
|
||||||
@@ -140,14 +180,31 @@ object OptionPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initDialog(dialog: JDialog): JDialog {
|
private fun initDialog(dialog: JDialog): JDialog {
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
dialog.rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_CLOSE, false)
|
||||||
|
dialog.rootPane.putClientProperty(
|
||||||
|
FlatClientProperties.TITLE_BAR_HEIGHT,
|
||||||
|
UIManager.getInt("TabbedPane.tabHeight")
|
||||||
|
)
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
dialog.rootPane.putClientProperty("apple.awt.windowTitleVisible", false)
|
||||||
|
dialog.rootPane.putClientProperty("apple.awt.fullWindowContent", true)
|
||||||
|
dialog.rootPane.putClientProperty("apple.awt.transparentTitleBar", true)
|
||||||
|
dialog.rootPane.putClientProperty(
|
||||||
|
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING,
|
||||||
|
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING_MEDIUM
|
||||||
|
)
|
||||||
|
|
||||||
if (JBR.isWindowDecorationsSupported()) {
|
|
||||||
|
|
||||||
val windowDecorations = JBR.getWindowDecorations()
|
val height = UIManager.getInt("TabbedPane.tabHeight") - 10
|
||||||
val titleBar = windowDecorations.createCustomTitleBar()
|
if (JBR.isWindowDecorationsSupported()) {
|
||||||
titleBar.putProperty("controls.visible", false)
|
val customTitleBar = JBR.getWindowDecorations().createCustomTitleBar()
|
||||||
titleBar.height = UIManager.getInt("TabbedPane.tabHeight") - if (SystemInfo.isMacOS) 10f else 6f
|
customTitleBar.putProperty("controls.visible", false)
|
||||||
windowDecorations.setCustomTitleBar(dialog, titleBar)
|
customTitleBar.height = height.toFloat()
|
||||||
|
JBR.getWindowDecorations().setCustomTitleBar(dialog, customTitleBar)
|
||||||
|
} else {
|
||||||
|
NativeMacLibrary.setControlsVisible(dialog, false)
|
||||||
|
}
|
||||||
|
|
||||||
val label = JLabel(dialog.title)
|
val label = JLabel(dialog.title)
|
||||||
label.putClientProperty(FlatClientProperties.STYLE, "font: bold")
|
label.putClientProperty(FlatClientProperties.STYLE, "font: bold")
|
||||||
@@ -155,11 +212,9 @@ object OptionPane {
|
|||||||
box.add(Box.createHorizontalGlue())
|
box.add(Box.createHorizontalGlue())
|
||||||
box.add(label)
|
box.add(label)
|
||||||
box.add(Box.createHorizontalGlue())
|
box.add(Box.createHorizontalGlue())
|
||||||
box.preferredSize = Dimension(-1, titleBar.height.toInt())
|
box.preferredSize = Dimension(-1, height)
|
||||||
|
|
||||||
dialog.contentPane.add(box, BorderLayout.NORTH)
|
dialog.contentPane.add(box, BorderLayout.NORTH)
|
||||||
}
|
}
|
||||||
|
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@ open class OptionsPane : JPanel(BorderLayout()) {
|
|||||||
}
|
}
|
||||||
private val cardLayout = CardLayout()
|
private val cardLayout = CardLayout()
|
||||||
private val contentPanel = JPanel(cardLayout)
|
private val contentPanel = JPanel(cardLayout)
|
||||||
|
private val loadedComponents = mutableMapOf<String, JComponent>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -103,16 +104,15 @@ open class OptionsPane : JPanel(BorderLayout()) {
|
|||||||
throw UnsupportedOperationException("Title already exists")
|
throw UnsupportedOperationException("Title already exists")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
contentPanel.add(option.getJComponent(), option.getTitle())
|
|
||||||
tabListModel.addElement(option)
|
tabListModel.addElement(option)
|
||||||
|
|
||||||
if (tabList.selectedIndex < 0) {
|
|
||||||
tabList.selectedIndex = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeOption(option: Option) {
|
fun removeOption(option: Option) {
|
||||||
contentPanel.remove(option.getJComponent())
|
val title = option.getTitle()
|
||||||
|
loadedComponents[title]?.let {
|
||||||
|
contentPanel.remove(it)
|
||||||
|
loadedComponents.remove(title)
|
||||||
|
}
|
||||||
tabListModel.removeElement(option)
|
tabListModel.removeElement(option)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +123,17 @@ open class OptionsPane : JPanel(BorderLayout()) {
|
|||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
tabList.addListSelectionListener {
|
tabList.addListSelectionListener {
|
||||||
if (tabList.selectedIndex >= 0) {
|
if (tabList.selectedIndex >= 0) {
|
||||||
cardLayout.show(contentPanel, tabListModel.get(tabList.selectedIndex).getTitle())
|
val option = tabListModel.get(tabList.selectedIndex)
|
||||||
|
val title = option.getTitle()
|
||||||
|
|
||||||
|
if (!loadedComponents.containsKey(title)) {
|
||||||
|
val component = option.getJComponent()
|
||||||
|
loadedComponents[title] = component
|
||||||
|
contentPanel.add(component, title)
|
||||||
|
SwingUtilities.updateComponentTreeUI(component)
|
||||||
|
}
|
||||||
|
|
||||||
|
cardLayout.show(contentPanel, title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ class PtyConnectorFactory : Disposable {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(PtyConnectorFactory::class.java)
|
private val log = LoggerFactory.getLogger(PtyConnectorFactory::class.java)
|
||||||
fun getInstance(scope: Scope): PtyConnectorFactory {
|
fun getInstance(): PtyConnectorFactory {
|
||||||
return scope.getOrCreate(PtyConnectorFactory::class) { PtyConnectorFactory() }
|
return ApplicationScope.forApplicationScope()
|
||||||
|
.getOrCreate(PtyConnectorFactory::class) { PtyConnectorFactory() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,14 +34,21 @@ class PtyConnectorFactory : Disposable {
|
|||||||
if (SystemUtils.IS_OS_UNIX) {
|
if (SystemUtils.IS_OS_UNIX) {
|
||||||
commands.add("-l")
|
commands.add("-l")
|
||||||
}
|
}
|
||||||
return createPtyConnector(commands.toTypedArray(), rows, cols, env, charset)
|
return createPtyConnector(
|
||||||
|
commands = commands.toTypedArray(),
|
||||||
|
rows = rows,
|
||||||
|
cols = cols,
|
||||||
|
env = env,
|
||||||
|
charset = charset
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createPtyConnector(
|
fun createPtyConnector(
|
||||||
commands: Array<String>,
|
commands: Array<String>,
|
||||||
rows: Int = 24, cols: Int = 80,
|
rows: Int = 24, cols: Int = 80,
|
||||||
env: Map<String, String> = emptyMap(),
|
env: Map<String, String> = emptyMap(),
|
||||||
charset: Charset = StandardCharsets.UTF_8
|
directory: String = SystemUtils.USER_HOME,
|
||||||
|
charset: Charset = StandardCharsets.UTF_8,
|
||||||
): PtyConnector {
|
): PtyConnector {
|
||||||
val envs = mutableMapOf<String, String>()
|
val envs = mutableMapOf<String, String>()
|
||||||
envs.putAll(System.getenv())
|
envs.putAll(System.getenv())
|
||||||
@@ -67,7 +75,7 @@ class PtyConnectorFactory : Disposable {
|
|||||||
.setInitialRows(rows)
|
.setInitialRows(rows)
|
||||||
.setInitialColumns(cols)
|
.setInitialColumns(cols)
|
||||||
.setConsole(false)
|
.setConsole(false)
|
||||||
.setDirectory(SystemUtils.USER_HOME)
|
.setDirectory(StringUtils.defaultIfBlank(directory, SystemUtils.USER_HOME))
|
||||||
.setCygwin(false)
|
.setCygwin(false)
|
||||||
.setUseWinConPty(SystemUtils.IS_OS_WINDOWS)
|
.setUseWinConPty(SystemUtils.IS_OS_WINDOWS)
|
||||||
.setRedirectErrorStream(false)
|
.setRedirectErrorStream(false)
|
||||||
@@ -79,20 +87,14 @@ class PtyConnectorFactory : Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun decorate(ptyConnector: PtyConnector): PtyConnector {
|
fun decorate(ptyConnector: PtyConnector): PtyConnector {
|
||||||
// 集成转发,如果PtyConnector支持转发那么应该在当前注释行前面代理
|
// 宏
|
||||||
val multiplePtyConnector = MultiplePtyConnector(ptyConnector)
|
val macroPtyConnector = MacroPtyConnector(ptyConnector)
|
||||||
// 宏应该在转发前面执行,不然会导致重复录制
|
|
||||||
val macroPtyConnector = MacroPtyConnector(multiplePtyConnector)
|
|
||||||
// 集成自动删除
|
// 集成自动删除
|
||||||
val autoRemovePtyConnector = AutoRemovePtyConnector(macroPtyConnector)
|
val autoRemovePtyConnector = AutoRemovePtyConnector(macroPtyConnector)
|
||||||
ptyConnectors.add(autoRemovePtyConnector)
|
ptyConnectors.add(autoRemovePtyConnector)
|
||||||
return autoRemovePtyConnector
|
return autoRemovePtyConnector
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPtyConnectors(): List<PtyConnector> {
|
|
||||||
return ptyConnectors
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class AutoRemovePtyConnector(connector: PtyConnector) : PtyConnectorDelegate(connector) {
|
private inner class AutoRemovePtyConnector(connector: PtyConnector) : PtyConnectorDelegate(connector) {
|
||||||
override fun close() {
|
override fun close() {
|
||||||
ptyConnectors.remove(this)
|
ptyConnectors.remove(this)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||||||
abstract class PtyHostTerminalTab(
|
abstract class PtyHostTerminalTab(
|
||||||
windowScope: WindowScope,
|
windowScope: WindowScope,
|
||||||
host: Host,
|
host: Host,
|
||||||
terminal: Terminal = TerminalFactory.getInstance(windowScope).createTerminal()
|
terminal: Terminal = TerminalFactory.getInstance().createTerminal()
|
||||||
) : HostTerminalTab(windowScope, host, terminal) {
|
) : HostTerminalTab(windowScope, host, terminal) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -23,15 +23,8 @@ abstract class PtyHostTerminalTab(
|
|||||||
|
|
||||||
private var readerJob: Job? = null
|
private var readerJob: Job? = null
|
||||||
private val ptyConnectorDelegate = PtyConnectorDelegate()
|
private val ptyConnectorDelegate = PtyConnectorDelegate()
|
||||||
|
protected val terminalPanel = TerminalPanelFactory.getInstance().createTerminalPanel(terminal, ptyConnectorDelegate)
|
||||||
private val terminalPanelFactory = TerminalPanelFactory.getInstance(windowScope)
|
protected val ptyConnectorFactory get() = PtyConnectorFactory.getInstance()
|
||||||
protected val terminalPanel = terminalPanelFactory.createTerminalPanel(terminal, ptyConnectorDelegate)
|
|
||||||
.apply { Disposer.register(this@PtyHostTerminalTab, this) }
|
|
||||||
protected val ptyConnectorFactory get() = PtyConnectorFactory.getInstance(windowScope)
|
|
||||||
|
|
||||||
init {
|
|
||||||
terminal.getTerminalModel().setData(DataKey.PtyConnector, ptyConnectorDelegate)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
@@ -122,10 +115,9 @@ abstract class PtyHostTerminalTab(
|
|||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
stop()
|
stop()
|
||||||
terminalPanel
|
Disposer.dispose(terminalPanel)
|
||||||
super.dispose()
|
super.dispose()
|
||||||
|
|
||||||
|
|
||||||
if (log.isInfoEnabled) {
|
if (log.isInfoEnabled) {
|
||||||
log.info("Host: {} disposed", host.name)
|
log.info("Host: {} disposed", host.name)
|
||||||
}
|
}
|
||||||
@@ -141,6 +133,8 @@ abstract class PtyHostTerminalTab(
|
|||||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
if (dataKey == DataProviders.TerminalPanel) {
|
if (dataKey == DataProviders.TerminalPanel) {
|
||||||
return terminalPanel as T?
|
return terminalPanel as T?
|
||||||
|
} else if (dataKey == DataProviders.TerminalWriter) {
|
||||||
|
return terminalPanel.getData(DataKey.TerminalWriter) as T?
|
||||||
}
|
}
|
||||||
return super.getData(dataKey)
|
return super.getData(dataKey)
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/main/kotlin/app/termora/RememberFocusTerminalTab.kt
Normal file
16
src/main/kotlin/app/termora/RememberFocusTerminalTab.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.KeyboardFocusManager
|
||||||
|
|
||||||
|
abstract class RememberFocusTerminalTab : TerminalTab {
|
||||||
|
private var lastFocusedComponent: Component? = null
|
||||||
|
|
||||||
|
override fun onLostFocus() {
|
||||||
|
lastFocusedComponent = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGrabFocus() {
|
||||||
|
lastFocusedComponent?.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ import kotlin.math.max
|
|||||||
class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(owner) {
|
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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import org.apache.commons.io.Charsets
|
|||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
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.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.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
@@ -28,6 +29,12 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
|||||||
private var sshSession: ClientSession? = null
|
private var sshSession: ClientSession? = null
|
||||||
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 owner get() = SwingUtilities.getWindowAncestor(terminalPanel)
|
||||||
|
|
||||||
|
init {
|
||||||
|
terminalPanel.dropFiles = true
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val canSupports by lazy {
|
val canSupports by lazy {
|
||||||
@@ -61,7 +68,7 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val sshClient = SshClients.openClient(host).apply { sshClient = this }
|
val sshClient = SshClients.openClient(host, owner).apply { sshClient = this }
|
||||||
val sshSession = SshClients.openSession(host, sshClient).apply { sshSession = this }
|
val sshSession = SshClients.openSession(host, sshClient).apply { sshSession = this }
|
||||||
|
|
||||||
// 打开通道
|
// 打开通道
|
||||||
@@ -115,16 +122,21 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
|||||||
if (envs.containsKey("CurrentDir")) {
|
if (envs.containsKey("CurrentDir")) {
|
||||||
val currentDir = envs.getValue("CurrentDir")
|
val currentDir = envs.getValue("CurrentDir")
|
||||||
commands.add("${host.username}@${host.host}:${currentDir}")
|
commands.add("${host.username}@${host.host}:${currentDir}")
|
||||||
|
} else if (host.options.sftpDefaultDirectory.isNotBlank()) {
|
||||||
|
commands.add("${host.username}@${host.host}:${host.options.sftpDefaultDirectory.trim()}")
|
||||||
} else {
|
} else {
|
||||||
commands.add("${host.username}@${host.host}")
|
commands.add("${host.username}@${host.host}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val directory = FileUtils.getFile(StringUtils.defaultIfBlank(defaultDirectory, SystemUtils.USER_HOME))
|
||||||
|
|
||||||
val winSize = terminalPanel.winSize()
|
val winSize = terminalPanel.winSize()
|
||||||
val ptyConnector = ptyConnectorFactory.createPtyConnector(
|
val ptyConnector = ptyConnectorFactory.createPtyConnector(
|
||||||
commands.toTypedArray(),
|
commands = commands.toTypedArray(),
|
||||||
winSize.rows, winSize.cols,
|
rows = winSize.rows, cols = winSize.cols,
|
||||||
host.options.envs(),
|
env = host.options.envs(),
|
||||||
Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8),
|
charset = Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8),
|
||||||
|
directory = if (directory.exists()) directory.absolutePath else SystemUtils.USER_HOME
|
||||||
)
|
)
|
||||||
|
|
||||||
return ptyConnector
|
return ptyConnector
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
import app.termora.actions.DataProvider
|
|
||||||
import app.termora.terminal.DataKey
|
|
||||||
import app.termora.transport.TransportDataProviders
|
|
||||||
import app.termora.transport.TransportPanel
|
|
||||||
import java.beans.PropertyChangeListener
|
|
||||||
import javax.swing.Icon
|
|
||||||
import javax.swing.JComponent
|
|
||||||
import javax.swing.JOptionPane
|
|
||||||
import javax.swing.SwingUtilities
|
|
||||||
|
|
||||||
class SFTPTerminalTab : Disposable, TerminalTab, DataProvider {
|
|
||||||
|
|
||||||
private val sftp get() = Database.getDatabase().sftp
|
|
||||||
private val transportPanel = TransportPanel()
|
|
||||||
|
|
||||||
init {
|
|
||||||
Disposer.register(this, transportPanel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getTitle(): String {
|
|
||||||
return "SFTP"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getIcon(): Icon {
|
|
||||||
return Icons.folder
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addPropertyChangeListener(listener: PropertyChangeListener) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removePropertyChangeListener(listener: PropertyChangeListener) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getJComponent(): JComponent {
|
|
||||||
return transportPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canClone(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canClose(): Boolean {
|
|
||||||
assertEventDispatchThread()
|
|
||||||
|
|
||||||
if (sftp.pinTab) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val transportManager = transportPanel.getData(TransportDataProviders.TransportManager) ?: return true
|
|
||||||
if (transportManager.getTransports().isEmpty()) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return OptionPane.showConfirmDialog(
|
|
||||||
SwingUtilities.getWindowAncestor(getJComponent()),
|
|
||||||
I18n.getString("termora.transport.sftp.close-tab"),
|
|
||||||
messageType = JOptionPane.QUESTION_MESSAGE,
|
|
||||||
optionType = JOptionPane.OK_CANCEL_OPTION
|
|
||||||
) == JOptionPane.OK_OPTION
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
|
||||||
if (dataKey == TransportDataProviders.TransportPanel) {
|
|
||||||
return transportPanel as T
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ import app.termora.actions.AnActionEvent
|
|||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.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,10 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun beforeClose() {
|
||||||
|
// 保存窗口状态
|
||||||
|
terminalPanel.storeVisualWindows(host.id)
|
||||||
|
}
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package app.termora
|
|||||||
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.awt.event.WindowAdapter
|
|
||||||
import java.awt.event.WindowEvent
|
|
||||||
import javax.swing.BorderFactory
|
import javax.swing.BorderFactory
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.JPanel
|
import javax.swing.JPanel
|
||||||
@@ -20,8 +18,10 @@ class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
title = I18n.getString("termora.setting")
|
title = I18n.getString("termora.setting")
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
init()
|
val index = properties.getString("Settings-SelectedOption")?.toIntOrNull() ?: 0
|
||||||
|
optionsPane.setSelectedIndex(index)
|
||||||
|
|
||||||
|
init()
|
||||||
initEvents()
|
initEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,14 +31,6 @@ class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
properties.putString("Settings-SelectedOption", optionsPane.getSelectedIndex().toString())
|
properties.putString("Settings-SelectedOption", optionsPane.getSelectedIndex().toString())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
addWindowListener(object : WindowAdapter() {
|
|
||||||
override fun windowActivated(e: WindowEvent) {
|
|
||||||
removeWindowListener(this)
|
|
||||||
val index = properties.getString("Settings-SelectedOption")?.toIntOrNull() ?: return
|
|
||||||
optionsPane.setSelectedIndex(index)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createCenterPanel(): JComponent {
|
override fun createCenterPanel(): JComponent {
|
||||||
|
|||||||
@@ -15,17 +15,14 @@ import app.termora.keymgr.OhKeyPair
|
|||||||
import app.termora.macro.Macro
|
import app.termora.macro.Macro
|
||||||
import app.termora.macro.MacroManager
|
import app.termora.macro.MacroManager
|
||||||
import app.termora.native.FileChooser
|
import app.termora.native.FileChooser
|
||||||
|
import app.termora.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
|
||||||
import app.termora.terminal.panel.TerminalPanel
|
import app.termora.terminal.panel.TerminalPanel
|
||||||
import app.termora.transport.SFTPAction
|
|
||||||
import cash.z.ecc.android.bip39.Mnemonics
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
@@ -36,33 +33,44 @@ 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 com.sun.jna.Native
|
||||||
|
import com.sun.jna.platform.win32.Shell32
|
||||||
|
import com.sun.jna.platform.win32.ShlObj
|
||||||
|
import com.sun.jna.platform.win32.WinDef
|
||||||
|
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
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import org.apache.commons.lang3.time.DateFormatUtils
|
import org.apache.commons.lang3.time.DateFormatUtils
|
||||||
import org.jdesktop.swingx.JXEditorPane
|
import org.jdesktop.swingx.JXEditorPane
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Toolkit
|
import java.awt.Toolkit
|
||||||
import java.awt.datatransfer.StringSelection
|
import java.awt.datatransfer.StringSelection
|
||||||
|
import java.awt.event.ActionEvent
|
||||||
import java.awt.event.ItemEvent
|
import java.awt.event.ItemEvent
|
||||||
|
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() {
|
||||||
@@ -72,14 +80,12 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private val snippetManager get() = SnippetManager.getInstance()
|
private val snippetManager get() = SnippetManager.getInstance()
|
||||||
private val keymapManager get() = KeymapManager.getInstance()
|
private val keymapManager get() = KeymapManager.getInstance()
|
||||||
private val macroManager get() = MacroManager.getInstance()
|
private val macroManager get() = MacroManager.getInstance()
|
||||||
private val actionManager get() = ActionManager.getInstance()
|
|
||||||
private val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
private val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
||||||
private val keyManager get() = KeyManager.getInstance()
|
private val keyManager get() = KeyManager.getInstance()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java)
|
private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java)
|
||||||
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 +135,16 @@ 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 confirmTabCloseComBoBox = 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 +153,39 @@ 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
|
||||||
|
confirmTabCloseComBoBox.selectedItem = appearance.confirmTabClose
|
||||||
|
|
||||||
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
||||||
themeManager.themes.keys.forEach { themeComboBox.addItem(it) }
|
themeManager.themes.keys.forEach { themeComboBox.addItem(it) }
|
||||||
@@ -178,6 +222,27 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
confirmTabCloseComBoBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
appearance.confirmTabClose = confirmTabCloseComBoBox.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 +272,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 +381,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, $formMargin, pref"
|
||||||
)
|
)
|
||||||
val box = FlatToolBar()
|
val box = FlatToolBar()
|
||||||
box.add(followSystemCheckBox)
|
box.add(followSystemCheckBox)
|
||||||
@@ -285,7 +390,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 +401,28 @@ 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).apply { rows += step }
|
||||||
|
|
||||||
|
val confirmTabCloseBox = Box.createHorizontalBox()
|
||||||
|
confirmTabCloseBox.add(JLabel("${I18n.getString("termora.settings.appearance.confirm-tab-close")}:"))
|
||||||
|
confirmTabCloseBox.add(Box.createHorizontalStrut(8))
|
||||||
|
confirmTabCloseBox.add(confirmTabCloseComBoBox)
|
||||||
|
builder.add(confirmTabCloseBox).xyw(1, rows, 3).apply { rows += step }
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -315,6 +441,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()
|
||||||
@@ -340,7 +467,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
floatingToolbarComboBox.addItemListener { e ->
|
floatingToolbarComboBox.addItemListener { e ->
|
||||||
if (e.stateChange == ItemEvent.SELECTED) {
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
terminalSetting.floatingToolbar = floatingToolbarComboBox.selectedItem as Boolean
|
terminalSetting.floatingToolbar = floatingToolbarComboBox.selectedItem as Boolean
|
||||||
TerminalPanelFactory.getAllTerminalPanel().forEach { tp ->
|
TerminalPanelFactory.getInstance().getTerminalPanels().forEach { tp ->
|
||||||
if (terminalSetting.floatingToolbar && FloatingToolbarPanel.isPined) {
|
if (terminalSetting.floatingToolbar && FloatingToolbarPanel.isPined) {
|
||||||
tp.getData(FloatingToolbarPanel.FloatingToolbar)?.triggerShow()
|
tp.getData(FloatingToolbarPanel.FloatingToolbar)?.triggerShow()
|
||||||
} else {
|
} else {
|
||||||
@@ -369,7 +496,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
if (it.stateChange == ItemEvent.SELECTED) {
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
val style = cursorStyleComboBox.selectedItem as CursorStyle
|
val style = cursorStyleComboBox.selectedItem as CursorStyle
|
||||||
terminalSetting.cursor = style
|
terminalSetting.cursor = style
|
||||||
TerminalFactory.getInstance(ApplicationScope.forWindowScope(owner)).getTerminals().forEach { e ->
|
TerminalFactory.getInstance().getTerminals().forEach { e ->
|
||||||
e.getTerminalModel().setData(DataKey.CursorStyle, style)
|
e.getTerminalModel().setData(DataKey.CursorStyle, style)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -379,7 +506,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
debugComboBox.addItemListener { e ->
|
debugComboBox.addItemListener { e ->
|
||||||
if (e.stateChange == ItemEvent.SELECTED) {
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
terminalSetting.debug = debugComboBox.selectedItem as Boolean
|
terminalSetting.debug = debugComboBox.selectedItem as Boolean
|
||||||
TerminalFactory.getInstance(ApplicationScope.forWindowScope(owner)).getTerminals().forEach {
|
TerminalFactory.getInstance().getTerminals().forEach {
|
||||||
it.getTerminalModel().setData(TerminalPanel.Debug, terminalSetting.debug)
|
it.getTerminalModel().setData(TerminalPanel.Debug, terminalSetting.debug)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,6 +519,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,10 +542,8 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fireFontChanged() {
|
private fun fireFontChanged() {
|
||||||
ApplicationScope.windowScopes().forEach {
|
TerminalPanelFactory.getInstance()
|
||||||
TerminalPanelFactory.getInstance(it)
|
.fireResize()
|
||||||
.fireResize()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
@@ -472,20 +604,33 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
shellComboBox.selectedItem = terminalSetting.localShell
|
shellComboBox.selectedItem = terminalSetting.localShell
|
||||||
|
|
||||||
val fonts = linkedSetOf<String>("JetBrains Mono", "Source Code Pro", "Monospaced")
|
fontComboBox.addItem(terminalSetting.font)
|
||||||
FontUtils.getAllFonts().forEach {
|
var fontsLoaded = false
|
||||||
if (!fonts.contains(it.family)) {
|
|
||||||
fonts.addLast(it.family)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (font in fonts) {
|
fontComboBox.addPopupMenuListener(object : PopupMenuListener {
|
||||||
fontComboBox.addItem(font)
|
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
||||||
}
|
if (!fontsLoaded) {
|
||||||
|
val selectedItem = fontComboBox.selectedItem
|
||||||
|
fontComboBox.removeAllItems();
|
||||||
|
fontComboBox.addItem("JetBrains Mono")
|
||||||
|
fontComboBox.addItem("Source Code Pro")
|
||||||
|
fontComboBox.addItem("Monospaced")
|
||||||
|
FontUtils.getAvailableFontFamilyNames().forEach {
|
||||||
|
fontComboBox.addItem(it)
|
||||||
|
}
|
||||||
|
fontComboBox.selectedItem = selectedItem
|
||||||
|
fontsLoaded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {}
|
||||||
|
override fun popupMenuCanceled(e: PopupMenuEvent) {}
|
||||||
|
})
|
||||||
|
|
||||||
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
|
||||||
@@ -508,7 +653,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)
|
||||||
@@ -531,6 +676,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)
|
||||||
@@ -555,11 +702,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"))
|
||||||
@@ -577,19 +724,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) {
|
||||||
@@ -608,6 +759,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)
|
||||||
@@ -628,6 +785,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()) {
|
||||||
@@ -674,17 +832,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() {
|
||||||
@@ -1010,8 +1198,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()) {
|
||||||
@@ -1019,7 +1210,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
domainTextField.outline = "error"
|
domainTextField.outline = "error"
|
||||||
domainTextField.requestFocusInWindow()
|
domainTextField.requestFocusInWindow()
|
||||||
}
|
}
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1028,7 +1219,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) {
|
||||||
@@ -1036,39 +1227,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
|
||||||
@@ -1079,19 +1244,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 {
|
||||||
@@ -1101,10 +1261,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
|
||||||
@@ -1115,11 +1274,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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果失败,提示错误
|
// 如果失败,提示错误
|
||||||
@@ -1139,10 +1294,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
|
||||||
@@ -1151,14 +1304,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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1168,6 +1317,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
|
||||||
@@ -1182,6 +1334,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
|
||||||
@@ -1229,6 +1387,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")}: ${
|
||||||
@@ -1239,6 +1414,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
refreshButtons()
|
refreshButtons()
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(isSelected: Boolean): Icon {
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
@@ -1256,7 +1432,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()
|
||||||
@@ -1306,20 +1482,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 }
|
||||||
@@ -1334,9 +1516,12 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
private val editCommandField = OutlineTextField(255)
|
private val editCommandField = OutlineTextField(255)
|
||||||
private val sftpCommandField = OutlineTextField(255)
|
private val sftpCommandField = OutlineTextField(255)
|
||||||
|
private val defaultDirectoryField = OutlineTextField(255)
|
||||||
|
private val browseDirectoryBtn = JButton(Icons.folder)
|
||||||
|
private val browseEditCommandBtn = JButton(Icons.folder)
|
||||||
private val pinTabComboBox = YesOrNoComboBox()
|
private val pinTabComboBox = YesOrNoComboBox()
|
||||||
|
private val preserveModificationTimeComboBox = YesOrNoComboBox()
|
||||||
private val sftp get() = database.sftp
|
private val sftp get() = database.sftp
|
||||||
private val sftpAction get() = actionManager.getAction(Actions.SFTP) as SFTPAction
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -1358,25 +1543,88 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
pinTabComboBox.addItemListener {
|
defaultDirectoryField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
if (it.stateChange == ItemEvent.SELECTED) {
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
|
sftp.defaultDirectory = defaultDirectoryField.text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
pinTabComboBox.addItemListener(object : ItemListener {
|
||||||
|
override fun itemStateChanged(e: ItemEvent) {
|
||||||
|
if (e.stateChange != ItemEvent.SELECTED) return
|
||||||
sftp.pinTab = pinTabComboBox.selectedItem as Boolean
|
sftp.pinTab = pinTabComboBox.selectedItem as Boolean
|
||||||
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
||||||
val evt = AnActionEvent(window, StringUtils.EMPTY, EventObject(window))
|
val evt = AnActionEvent(window, StringUtils.EMPTY, EventObject(window))
|
||||||
if (pinTabComboBox.selectedItem == true) {
|
|
||||||
sftpAction.openOrCreateSFTPTerminalTab(evt)
|
|
||||||
}
|
|
||||||
val tabbed = evt.getData(DataProviders.TabbedPane) ?: continue
|
|
||||||
val manager = evt.getData(DataProviders.TerminalTabbedManager) ?: continue
|
val manager = evt.getData(DataProviders.TerminalTabbedManager) ?: continue
|
||||||
for ((index, tab) in manager.getTerminalTabs().withIndex()) {
|
|
||||||
if (tab is SFTPTerminalTab) {
|
if (sftp.pinTab) {
|
||||||
tabbed.setTabClosable(index, pinTabComboBox.selectedItem != true)
|
if (manager.getTerminalTabs().none { it is SFTPTab }) {
|
||||||
break
|
manager.addTerminalTab(1, SFTPTab(), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新状态
|
||||||
|
manager.refreshTerminalTabs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
preserveModificationTimeComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
sftp.preserveModificationTime = preserveModificationTimeComboBox.selectedItem as Boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
browseDirectoryBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val chooser = FileChooser()
|
||||||
|
chooser.allowsMultiSelection = false
|
||||||
|
chooser.defaultDirectory = StringUtils.defaultIfBlank(
|
||||||
|
defaultDirectoryField.text,
|
||||||
|
SystemUtils.USER_HOME
|
||||||
|
)
|
||||||
|
chooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||||
|
chooser.showOpenDialog(owner).thenAccept { files ->
|
||||||
|
if (files.isNotEmpty()) defaultDirectoryField.text = files.first().absolutePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
browseEditCommandBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val chooser = FileChooser()
|
||||||
|
chooser.allowsMultiSelection = false
|
||||||
|
chooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
||||||
|
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
chooser.defaultDirectory = "/Applications"
|
||||||
|
} else {
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
val pszPath = CharArray(WinDef.MAX_PATH)
|
||||||
|
Shell32.INSTANCE.SHGetFolderPath(
|
||||||
|
null,
|
||||||
|
ShlObj.CSIDL_DESKTOPDIRECTORY, null, ShlObj.SHGFP_TYPE_CURRENT,
|
||||||
|
pszPath
|
||||||
|
)
|
||||||
|
chooser.defaultDirectory = Native.toString(pszPath)
|
||||||
|
} else {
|
||||||
|
chooser.defaultDirectory = SystemUtils.USER_HOME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chooser.showOpenDialog(owner).thenAccept { files ->
|
||||||
|
if (files.isNotEmpty()) {
|
||||||
|
val file = files.first()
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
editCommandField.text = "open -a ${file.absolutePath} {0}"
|
||||||
|
} else {
|
||||||
|
editCommandField.text = "${file.absolutePath} {0}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1393,9 +1641,16 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
sftpCommandField.placeholderText = "sftp"
|
sftpCommandField.placeholderText = "sftp"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editCommandField.trailingComponent = browseEditCommandBtn
|
||||||
|
|
||||||
|
defaultDirectoryField.placeholderText = SystemUtils.USER_HOME
|
||||||
|
defaultDirectoryField.trailingComponent = browseDirectoryBtn
|
||||||
|
|
||||||
|
defaultDirectoryField.text = sftp.defaultDirectory
|
||||||
editCommandField.text = sftp.editCommand
|
editCommandField.text = sftp.editCommand
|
||||||
sftpCommandField.text = sftp.sftpCommand
|
sftpCommandField.text = sftp.sftpCommand
|
||||||
pinTabComboBox.selectedItem = sftp.pinTab
|
pinTabComboBox.selectedItem = sftp.pinTab
|
||||||
|
preserveModificationTimeComboBox.selectedItem = sftp.preserveModificationTime
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(isSelected: Boolean): Icon {
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
@@ -1416,13 +1671,23 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val box = Box.createHorizontalBox()
|
||||||
|
box.add(JLabel("${I18n.getString("termora.settings.sftp.preserve-time")}:"))
|
||||||
|
box.add(Box.createHorizontalStrut(8))
|
||||||
|
box.add(preserveModificationTimeComboBox)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
val builder = FormBuilder.create().layout(layout).debug(false)
|
val builder = FormBuilder.create().layout(layout).debug(false)
|
||||||
builder.add("${I18n.getString("termora.settings.sftp.fixed-tab")}:").xy(1, 1)
|
builder.add("${I18n.getString("termora.settings.sftp.fixed-tab")}:").xy(1, rows)
|
||||||
builder.add(pinTabComboBox).xy(3, 1)
|
builder.add(pinTabComboBox).xy(3, rows).apply { rows += 2 }
|
||||||
builder.add("${I18n.getString("termora.settings.sftp.edit-command")}:").xy(1, 3)
|
builder.add("${I18n.getString("termora.settings.sftp.edit-command")}:").xy(1, rows)
|
||||||
builder.add(editCommandField).xy(3, 3)
|
builder.add(editCommandField).xy(3, rows).apply { rows += 2 }
|
||||||
builder.add("${I18n.getString("termora.tabbed.contextmenu.sftp-command")}:").xy(1, 5)
|
builder.add("${I18n.getString("termora.tabbed.contextmenu.sftp-command")}:").xy(1, rows)
|
||||||
builder.add(sftpCommandField).xy(3, 5)
|
builder.add(sftpCommandField).xy(3, rows).apply { rows += 2 }
|
||||||
|
builder.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||||
|
builder.add(defaultDirectoryField).xy(3, rows).apply { rows += 2 }
|
||||||
|
builder.add(box).xyw(1, rows, 3).apply { rows += 2 }
|
||||||
|
|
||||||
|
|
||||||
return builder.build()
|
return builder.build()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
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 app.termora.x11.X11ChannelFactory
|
||||||
|
import com.formdev.flatlaf.FlatLaf
|
||||||
|
import com.formdev.flatlaf.util.FontUtils
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.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
|
||||||
@@ -15,40 +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.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>()
|
||||||
|
|
||||||
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) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,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")
|
||||||
}
|
}
|
||||||
@@ -111,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) {
|
||||||
@@ -132,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) {
|
||||||
@@ -151,7 +197,8 @@ object SshClients {
|
|||||||
log.info("jump host: ${currentHost.host}:${currentHost.port} , next host: ${nextHost.host}:${nextHost.port} , local address: ${address.hostName}:${address.port}")
|
log.info("jump host: ${currentHost.host}:${currentHost.port} , next host: ${nextHost.host}:${nextHost.port} , local address: ${address.hostName}:${address.port}")
|
||||||
}
|
}
|
||||||
// 映射完毕之后修改Host和端口
|
// 映射完毕之后修改Host和端口
|
||||||
jumpHosts[i + 1] = nextHost.copy(host = address.hostName, port = address.port, updateDate = System.currentTimeMillis())
|
jumpHosts[i + 1] =
|
||||||
|
nextHost.copy(host = address.hostName, port = address.port, updateDate = System.currentTimeMillis())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,20 +224,59 @@ 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 askUserInfo = ask(host, entry, owner) ?: throw e
|
||||||
|
if (askUserInfo.authentication.type == AuthenticationType.No) throw e
|
||||||
|
return doOpenSession(
|
||||||
|
host.copy(
|
||||||
|
authentication = askUserInfo.authentication,
|
||||||
|
username = askUserInfo.username
|
||||||
|
), client
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.setAttribute(HOST_KEY, host)
|
||||||
|
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,17 +316,27 @@ object SshClients {
|
|||||||
return sshdSocketAddress
|
return sshdSocketAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openClient(host: Host, owner: Window): SshClient {
|
||||||
|
val h = hostManager.getHost(host.id) ?: host
|
||||||
|
val client = openClient(h)
|
||||||
|
client.userInteraction = TerminalUserInteraction(owner)
|
||||||
|
client.serverKeyVerifier = DialogServerKeyVerifier(owner)
|
||||||
|
client.properties["owner"] = owner
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开一个客户端
|
* 打开一个客户端
|
||||||
*/
|
*/
|
||||||
fun openClient(host: Host): SshClient {
|
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),
|
||||||
@@ -250,6 +346,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 {
|
||||||
@@ -258,129 +372,350 @@ 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 {
|
private data class AskUserInfo(val username: String, val authentication: Authentication)
|
||||||
override fun verifyServerKey(
|
|
||||||
clientSession: ClientSession,
|
|
||||||
remoteAddress: SocketAddress,
|
|
||||||
serverKey: PublicKey
|
|
||||||
): Boolean {
|
|
||||||
|
|
||||||
if (SshClients.isMiddleware(clientSession)) {
|
private fun ask(host: Host, entry: HostConfigEntry, owner: Window): AskUserInfo? {
|
||||||
|
val ref = AtomicReference<AskUserInfo>(null)
|
||||||
|
|
||||||
|
SwingUtilities.invokeAndWait {
|
||||||
|
val dialog = RequestAuthenticationDialog(owner, host)
|
||||||
|
dialog.setLocationRelativeTo(owner)
|
||||||
|
val authentication = dialog.getAuthentication()
|
||||||
|
ref.set(AskUserInfo(dialog.getUsername(), authentication))
|
||||||
|
|
||||||
|
// save
|
||||||
|
if (dialog.isRemembered()) {
|
||||||
|
// fix https://github.com/TermoraDev/termora/issues/609
|
||||||
|
val hostId = entry.getProperty("Host", host.id)
|
||||||
|
val h = hostManager.getHost(hostId)
|
||||||
|
if (h != null) {
|
||||||
|
hostManager.addHost(
|
||||||
|
h.copy(
|
||||||
|
authentication = authentication,
|
||||||
|
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ref.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MyDialogServerKeyVerifier(private val owner: Window) : ServerKeyVerifier, ModifiedServerKeyAcceptor {
|
||||||
|
override fun verifyServerKey(
|
||||||
|
clientSession: ClientSession,
|
||||||
|
remoteAddress: SocketAddress,
|
||||||
|
serverKey: PublicKey
|
||||||
|
): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
val result = AtomicBoolean(false)
|
override fun acceptModifiedServerKey(
|
||||||
|
clientSession: ClientSession?,
|
||||||
SwingUtilities.invokeAndWait {
|
remoteAddress: SocketAddress?,
|
||||||
result.set(
|
entry: KnownHostEntry?,
|
||||||
OptionPane.showConfirmDialog(
|
expected: PublicKey?,
|
||||||
parentComponent = owner,
|
actual: PublicKey?
|
||||||
message = I18n.getString(
|
): Boolean {
|
||||||
"termora.host.verify-server-key",
|
val result = AtomicBoolean(false)
|
||||||
remoteAddress.toString().replace("/", StringUtils.EMPTY),
|
SwingUtilities.invokeAndWait { result.set(ask(remoteAddress, expected, actual) == JOptionPane.OK_OPTION) }
|
||||||
KeyUtils.getKeyType(serverKey),
|
return result.get()
|
||||||
KeyUtils.getFingerPrint(serverKey)
|
|
||||||
),
|
|
||||||
optionType = JOptionPane.OK_CANCEL_OPTION,
|
|
||||||
messageType = JOptionPane.WARNING_MESSAGE,
|
|
||||||
) == 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
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun acceptModifiedServerKey(
|
private class DialogServerKeyVerifier(
|
||||||
clientSession: ClientSession?,
|
owner: Window,
|
||||||
remoteAddress: SocketAddress?,
|
) : KnownHostsServerKeyVerifier(
|
||||||
entry: KnownHostEntry?,
|
MyDialogServerKeyVerifier(owner),
|
||||||
expected: PublicKey?,
|
Paths.get(Application.getBaseDataDir().absolutePath, "known_hosts")
|
||||||
actual: PublicKey?
|
) {
|
||||||
): Boolean {
|
init {
|
||||||
val result = AtomicBoolean(false)
|
modifiedServerKeyAcceptor = delegateVerifier as ModifiedServerKeyAcceptor
|
||||||
|
|
||||||
SwingUtilities.invokeAndWait {
|
|
||||||
result.set(
|
|
||||||
OptionPane.showConfirmDialog(
|
|
||||||
parentComponent = owner,
|
|
||||||
message = I18n.getString(
|
|
||||||
"termora.host.modified-server-key",
|
|
||||||
remoteAddress.toString().replace("/", StringUtils.EMPTY),
|
|
||||||
KeyUtils.getKeyType(expected),
|
|
||||||
KeyUtils.getFingerPrint(expected),
|
|
||||||
KeyUtils.getKeyType(actual),
|
|
||||||
KeyUtils.getFingerPrint(actual),
|
|
||||||
),
|
|
||||||
optionType = JOptionPane.OK_CANCEL_OPTION,
|
|
||||||
messageType = JOptionPane.WARNING_MESSAGE,
|
|
||||||
) == JOptionPane.OK_OPTION
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.get()
|
override fun updateKnownHostsFile(
|
||||||
}
|
clientSession: ClientSession?,
|
||||||
}
|
remoteAddress: SocketAddress?,
|
||||||
|
serverKey: PublicKey?,
|
||||||
class DialogServerKeyVerifier(
|
file: Path?,
|
||||||
owner: Window,
|
knownHosts: Collection<HostEntryPair?>?
|
||||||
) : KnownHostsServerKeyVerifier(
|
): KnownHostEntry? {
|
||||||
MyDialogServerKeyVerifier(owner),
|
if (clientSession is JGitClientSession) {
|
||||||
Paths.get(Application.getBaseDataDir().absolutePath, "known_hosts")
|
if (isMiddleware(clientSession)) {
|
||||||
) {
|
return null
|
||||||
init {
|
}
|
||||||
modifiedServerKeyAcceptor = delegateVerifier as ModifiedServerKeyAcceptor
|
}
|
||||||
|
return super.updateKnownHostsFile(clientSession, remoteAddress, serverKey, file, knownHosts)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateKnownHostsFile(
|
|
||||||
clientSession: ClientSession?,
|
@Suppress("UNCHECKED_CAST")
|
||||||
remoteAddress: SocketAddress?,
|
private class MyJGitSshClient : JGitSshClient() {
|
||||||
serverKey: PublicKey?,
|
companion object {
|
||||||
file: Path?,
|
private val HOST_CONFIG_ENTRY: AttributeRepository.AttributeKey<HostConfigEntry> by lazy {
|
||||||
knownHosts: Collection<HostEntryPair?>?
|
JGitSshClient::class.java.getDeclaredField("HOST_CONFIG_ENTRY").apply { isAccessible = true }
|
||||||
): KnownHostEntry? {
|
.get(null) as AttributeRepository.AttributeKey<HostConfigEntry>
|
||||||
if (clientSession is JGitClientSession) {
|
}
|
||||||
if (SshClients.isMiddleware(clientSession)) {
|
private const val CLIENT_PROXY_CONNECTOR = "ClientProxyConnectorId"
|
||||||
return null
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return super.updateKnownHostsFile(clientSession, remoteAddress, serverKey, file, knownHosts)
|
|
||||||
|
override fun setClientProxyConnector(proxyConnector: ClientProxyConnector?) {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MyIoConnector(private val sshClient: MyJGitSshClient, private val ioConnector: IoConnector) :
|
||||||
|
IoConnector {
|
||||||
|
override fun close(immediately: Boolean): CloseFuture {
|
||||||
|
return ioConnector.close(immediately)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addCloseFutureListener(listener: SshFutureListener<CloseFuture>?) {
|
||||||
|
return ioConnector.addCloseFutureListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeCloseFutureListener(listener: SshFutureListener<CloseFuture>?) {
|
||||||
|
return ioConnector.removeCloseFutureListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isClosed(): Boolean {
|
||||||
|
return ioConnector.isClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isClosing(): Boolean {
|
||||||
|
return ioConnector.isClosing
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIoServiceEventListener(): IoServiceEventListener {
|
||||||
|
return ioConnector.ioServiceEventListener
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setIoServiceEventListener(listener: IoServiceEventListener?) {
|
||||||
|
return ioConnector.setIoServiceEventListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getManagedSessions(): MutableMap<Long, IoSession> {
|
||||||
|
return ioConnector.managedSessions
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun connect(
|
||||||
|
targetAddress: SocketAddress,
|
||||||
|
context: AttributeRepository?,
|
||||||
|
localAddress: SocketAddress?
|
||||||
|
): IoConnectFuture {
|
||||||
|
var tAddress = targetAddress
|
||||||
|
val entry = context?.getAttribute(HOST_CONFIG_ENTRY)
|
||||||
|
if (entry != null) {
|
||||||
|
val host = hostManager.getHost(entry.getProperty("Host") ?: StringUtils.EMPTY)
|
||||||
|
if (host != null) {
|
||||||
|
tAddress = configureProxy(entry, host, tAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ioConnector.connect(tAddress, context, localAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureProxy(
|
||||||
|
entry: HostConfigEntry,
|
||||||
|
host: Host,
|
||||||
|
targetAddress: SocketAddress
|
||||||
|
): SocketAddress {
|
||||||
|
if (host.proxy.type == ProxyType.No) return targetAddress
|
||||||
|
val address = targetAddress as? InetSocketAddress ?: return targetAddress
|
||||||
|
if (address.hostString == (SshdSocketAddress.LOCALHOST_IPV4)) return targetAddress
|
||||||
|
|
||||||
|
// 获取代理连接器
|
||||||
|
val clientProxyConnector = getClientProxyConnector(host, address) ?: return targetAddress
|
||||||
|
|
||||||
|
val id = UUID.randomUUID().toSimpleString()
|
||||||
|
entry.setProperty(CLIENT_PROXY_CONNECTOR, id)
|
||||||
|
sshClient.clientProxyConnectors[id] = clientProxyConnector
|
||||||
|
|
||||||
|
return InetSocketAddress(host.proxy.host, host.proxy.port)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getClientProxyConnector(
|
||||||
|
host: Host,
|
||||||
|
remoteAddress: InetSocketAddress
|
||||||
|
): AbstractClientProxyConnector? {
|
||||||
|
if (host.proxy.type == ProxyType.HTTP) {
|
||||||
|
return HttpClientConnector(
|
||||||
|
InetSocketAddress(host.proxy.host, host.proxy.port),
|
||||||
|
remoteAddress,
|
||||||
|
host.proxy.username.ifBlank { null },
|
||||||
|
if (host.proxy.password.isBlank()) null else host.proxy.password.toCharArray()
|
||||||
|
)
|
||||||
|
} else if (host.proxy.type == ProxyType.SOCKS5) {
|
||||||
|
return Socks5ClientConnector(
|
||||||
|
InetSocketAddress(host.proxy.host, host.proxy.port),
|
||||||
|
remoteAddress,
|
||||||
|
host.proxy.username.ifBlank { null },
|
||||||
|
if (host.proxy.password.isBlank()) null else host.proxy.password.toCharArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ class TerminalFactory private constructor() : Disposable {
|
|||||||
private val terminals = mutableListOf<Terminal>()
|
private val terminals = mutableListOf<Terminal>()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getInstance(scope: WindowScope): TerminalFactory {
|
fun getInstance(): TerminalFactory {
|
||||||
return scope.getOrCreate(TerminalFactory::class) { TerminalFactory() }
|
return ApplicationScope.forApplicationScope().getOrCreate(TerminalFactory::class) { TerminalFactory() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
|
import app.termora.actions.MultipleAction
|
||||||
import app.termora.highlight.KeywordHighlightPaintListener
|
import app.termora.highlight.KeywordHighlightPaintListener
|
||||||
import app.termora.terminal.DataKey
|
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
|
||||||
import app.termora.terminal.panel.TerminalPanel
|
import app.termora.terminal.panel.TerminalPanel
|
||||||
|
import app.termora.terminal.panel.TerminalWriter
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
import java.awt.event.ComponentListener
|
import java.awt.event.ComponentListener
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.JComponent
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@@ -17,16 +26,9 @@ class TerminalPanelFactory : Disposable {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val Factory = DataKey(TerminalPanelFactory::class)
|
fun getInstance(): TerminalPanelFactory {
|
||||||
|
return ApplicationScope.forApplicationScope()
|
||||||
fun getInstance(scope: Scope): TerminalPanelFactory {
|
.getOrCreate(TerminalPanelFactory::class) { TerminalPanelFactory() }
|
||||||
return scope.getOrCreate(TerminalPanelFactory::class) { TerminalPanelFactory() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAllTerminalPanel(): Array<TerminalPanel> {
|
|
||||||
return ApplicationScope.forApplicationScope().windowScopes()
|
|
||||||
.map { getInstance(it) }
|
|
||||||
.flatMap { it.terminalPanels }.toTypedArray()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,17 +39,19 @@ class TerminalPanelFactory : Disposable {
|
|||||||
|
|
||||||
|
|
||||||
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
||||||
val terminalPanel = TerminalPanel(terminal, ptyConnector)
|
val writer = MyTerminalWriter(ptyConnector)
|
||||||
|
val terminalPanel = TerminalPanel(terminal, writer)
|
||||||
|
|
||||||
|
// processDeviceStatusReport
|
||||||
|
terminal.getTerminalModel().setData(DataKey.TerminalWriter, writer)
|
||||||
|
|
||||||
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
||||||
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
||||||
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
||||||
terminal.getTerminalModel().setData(Factory, this)
|
|
||||||
|
|
||||||
Disposer.register(terminalPanel, object : Disposable {
|
Disposer.register(terminalPanel, object : Disposable {
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
if (terminal.getTerminalModel().hasData(Factory)) {
|
removeTerminalPanel(terminalPanel)
|
||||||
terminal.getTerminalModel().getData(Factory).removeTerminalPanel(terminalPanel)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -75,13 +79,12 @@ class TerminalPanelFactory : Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeTerminalPanel(terminalPanel: TerminalPanel) {
|
private fun removeTerminalPanel(terminalPanel: TerminalPanel) {
|
||||||
terminalPanels.remove(terminalPanel)
|
terminalPanels.remove(terminalPanel)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addTerminalPanel(terminalPanel: TerminalPanel) {
|
private fun addTerminalPanel(terminalPanel: TerminalPanel) {
|
||||||
terminalPanels.add(terminalPanel)
|
terminalPanels.add(terminalPanel)
|
||||||
terminalPanel.terminal.getTerminalModel().setData(Factory, this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Painter : Disposable {
|
private class Painter : Disposable {
|
||||||
@@ -91,16 +94,13 @@ class TerminalPanelFactory : Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
while (coroutineScope.isActive) {
|
while (coroutineScope.isActive) {
|
||||||
delay(500.milliseconds)
|
delay(500.milliseconds)
|
||||||
SwingUtilities.invokeLater {
|
SwingUtilities.invokeLater { TerminalPanelFactory.getInstance().repaintAll() }
|
||||||
ApplicationScope.forApplicationScope().windowScopes()
|
|
||||||
.map { getInstance(it) }.forEach { it.repaintAll() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,4 +110,58 @@ class TerminalPanelFactory : Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class MyTerminalWriter(private val ptyConnector: PtyConnector) : TerminalWriter {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(MyTerminalWriter::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var evt: AnActionEvent
|
||||||
|
|
||||||
|
override fun onMounted(c: JComponent) {
|
||||||
|
evt = AnActionEvent(c, StringUtils.EMPTY, EventObject(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(request: TerminalWriter.WriteRequest) {
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("write: ${String(request.buffer, getCharset())}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val windowScope = evt.getData(DataProviders.WindowScope)
|
||||||
|
if (windowScope == null) {
|
||||||
|
ptyConnector.write(request.buffer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val multipleAction = MultipleAction.getInstance(windowScope)
|
||||||
|
if (!multipleAction.isSelected) {
|
||||||
|
ptyConnector.write(request.buffer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager)
|
||||||
|
if (terminalTabbedManager == null) {
|
||||||
|
ptyConnector.write(request.buffer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (tab in terminalTabbedManager.getTerminalTabs()) {
|
||||||
|
val writer = tab.getData(DataProviders.TerminalWriter) ?: continue
|
||||||
|
if (writer is MyTerminalWriter) {
|
||||||
|
writer.ptyConnector.write(request.buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resize(rows: Int, cols: Int) {
|
||||||
|
ptyConnector.resize(rows, cols)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCharset(): Charset {
|
||||||
|
return ptyConnector.getCharset()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.Database.Appearance
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
@@ -43,6 +44,16 @@ interface TerminalTab : Disposable, DataProvider {
|
|||||||
*/
|
*/
|
||||||
fun canClose(): Boolean = true
|
fun canClose(): Boolean = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示可以关闭,只有当 [Appearance.confirmTabClose] 为 false 时才会调用
|
||||||
|
*/
|
||||||
|
fun willBeClose(): Boolean = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 即将关闭,已经无法挽回
|
||||||
|
*/
|
||||||
|
fun beforeClose() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否可以克隆
|
* 是否可以克隆
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
|
|||||||
import app.termora.findeverywhere.FindEverywhereProvider
|
import app.termora.findeverywhere.FindEverywhereProvider
|
||||||
import app.termora.findeverywhere.FindEverywhereResult
|
import app.termora.findeverywhere.FindEverywhereResult
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.transport.TransportPanel
|
|
||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
@@ -33,6 +32,7 @@ class TerminalTabbed(
|
|||||||
private val actionManager = ActionManager.getInstance()
|
private val actionManager = ActionManager.getInstance()
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
private val titleProperty = UUID.randomUUID().toSimpleString()
|
private val titleProperty = UUID.randomUUID().toSimpleString()
|
||||||
|
private val appearance get() = Database.getDatabase().appearance
|
||||||
private val iconListener = PropertyChangeListener { e ->
|
private val iconListener = PropertyChangeListener { e ->
|
||||||
val source = e.source
|
val source = e.source
|
||||||
if (e.propertyName == "icon" && source is TerminalTab) {
|
if (e.propertyName == "icon" && source is TerminalTab) {
|
||||||
@@ -121,7 +121,7 @@ class TerminalTabbed(
|
|||||||
val results = mutableListOf<FindEverywhereResult>()
|
val results = mutableListOf<FindEverywhereResult>()
|
||||||
for (i in 0 until tabbedPane.tabCount) {
|
for (i in 0 until tabbedPane.tabCount) {
|
||||||
val c = tabbedPane.getComponentAt(i)
|
val c = tabbedPane.getComponentAt(i)
|
||||||
if (c is WelcomePanel || c is TransportPanel) {
|
if (c is JComponent && c.getClientProperty(FindEverywhereProvider.SKIP_FIND_EVERYWHERE) != null) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
results.add(
|
results.add(
|
||||||
@@ -154,8 +154,29 @@ class TerminalTabbed(
|
|||||||
if (tabbedPane.isTabClosable(index)) {
|
if (tabbedPane.isTabClosable(index)) {
|
||||||
val tab = tabs[index]
|
val tab = tabs[index]
|
||||||
|
|
||||||
|
// 询问是否可以关闭
|
||||||
if (disposable) {
|
if (disposable) {
|
||||||
if (!tab.canClose()) {
|
// 如果开启了关闭确认,那么直接询问用户
|
||||||
|
if (appearance.confirmTabClose) {
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
windowScope.window,
|
||||||
|
I18n.getString("termora.tabbed.tab.close-prompt"),
|
||||||
|
messageType = JOptionPane.QUESTION_MESSAGE,
|
||||||
|
optionType = JOptionPane.OK_CANCEL_OPTION
|
||||||
|
) != JOptionPane.OK_OPTION
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (!tab.willBeClose()) { // 如果没有开启则询问用户
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知即将关闭
|
||||||
|
if (disposable) {
|
||||||
|
try {
|
||||||
|
tab.beforeClose()
|
||||||
|
} catch (_: Exception) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,12 +212,11 @@ class TerminalTabbed(
|
|||||||
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
|
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
|
||||||
rename.addActionListener {
|
rename.addActionListener {
|
||||||
if (tabIndex > 0) {
|
if (tabIndex > 0) {
|
||||||
val dialog = InputDialog(
|
val text = OptionPane.showInputDialog(
|
||||||
SwingUtilities.getWindowAncestor(this),
|
SwingUtilities.getWindowAncestor(this),
|
||||||
title = rename.text,
|
title = rename.text,
|
||||||
text = tabbedPane.getTitleAt(tabIndex),
|
value = tabbedPane.getTitleAt(tabIndex)
|
||||||
)
|
)
|
||||||
val text = dialog.getText()
|
|
||||||
if (!text.isNullOrBlank()) {
|
if (!text.isNullOrBlank()) {
|
||||||
tabbedPane.setTitleAt(tabIndex, text)
|
tabbedPane.setTitleAt(tabIndex, text)
|
||||||
c.putClientProperty(titleProperty, text)
|
c.putClientProperty(titleProperty, text)
|
||||||
@@ -215,6 +235,21 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
val edit = popupMenu.add(I18n.getString("termora.keymgr.edit"))
|
||||||
|
edit.addActionListener(object : AnAction() {
|
||||||
|
private val hostManager get() = HostManager.getInstance()
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
if (tab is HostTerminalTab) {
|
||||||
|
val host = hostManager.getHost(tab.host.id) ?: return
|
||||||
|
val dialog = HostDialog(evt.window, host)
|
||||||
|
dialog.setLocationRelativeTo(evt.window)
|
||||||
|
dialog.isVisible = true
|
||||||
|
hostManager.addHost(dialog.host ?: return)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 在新窗口中打开
|
// 在新窗口中打开
|
||||||
val openInNewWindow = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.open-in-new-window"))
|
val openInNewWindow = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.open-in-new-window"))
|
||||||
openInNewWindow.addActionListener(object : AnAction() {
|
openInNewWindow.addActionListener(object : AnAction() {
|
||||||
@@ -276,6 +311,7 @@ class TerminalTabbed(
|
|||||||
close.isEnabled = tab.canClose()
|
close.isEnabled = tab.canClose()
|
||||||
rename.isEnabled = close.isEnabled
|
rename.isEnabled = close.isEnabled
|
||||||
clone.isEnabled = close.isEnabled
|
clone.isEnabled = close.isEnabled
|
||||||
|
edit.isEnabled = tab is HostTerminalTab && tab.host.id != "local"
|
||||||
openInNewWindow.isEnabled = close.isEnabled
|
openInNewWindow.isEnabled = close.isEnabled
|
||||||
|
|
||||||
// 如果不允许克隆
|
// 如果不允许克隆
|
||||||
@@ -327,6 +363,13 @@ class TerminalTabbed(
|
|||||||
Disposer.register(this, tab)
|
Disposer.register(this, tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun refreshTerminalTabs() {
|
||||||
|
for (i in 0 until tabbedPane.tabCount) {
|
||||||
|
tabbedPane.setTabClosable(i, tabs[i].canClose())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun openSFTPPtyTab(tab: HostTerminalTab, openHostAction: Action, evt: EventObject) {
|
private fun openSFTPPtyTab(tab: HostTerminalTab, openHostAction: Action, evt: EventObject) {
|
||||||
if (!SFTPPtyTerminalTab.canSupports) {
|
if (!SFTPPtyTerminalTab.canSupports) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
@@ -379,12 +422,11 @@ class TerminalTabbed(
|
|||||||
private fun showContextMenu(event: MouseEvent) {
|
private fun showContextMenu(event: MouseEvent) {
|
||||||
val popupMenu = FlatPopupMenu()
|
val popupMenu = FlatPopupMenu()
|
||||||
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
|
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
|
||||||
val dialog = CustomizeToolBarDialog(
|
val owner = SwingUtilities.getWindowAncestor(this@TerminalTabbed)
|
||||||
SwingUtilities.getWindowAncestor(this@TerminalTabbed),
|
val dialog = CustomizeToolBarDialog(owner, windowScope, termoraToolBar)
|
||||||
termoraToolBar
|
dialog.setLocationRelativeTo(owner)
|
||||||
)
|
|
||||||
if (dialog.open()) {
|
if (dialog.open()) {
|
||||||
termoraToolBar.rebuild()
|
TermoraToolBar.rebuild()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
popupMenu.show(event.component, event.x, event.y)
|
popupMenu.show(event.component, event.x, event.y)
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ interface TerminalTabbedManager {
|
|||||||
fun getTerminalTabs(): List<TerminalTab>
|
fun getTerminalTabs(): List<TerminalTab>
|
||||||
fun setSelectedTerminalTab(tab: TerminalTab)
|
fun setSelectedTerminalTab(tab: TerminalTab)
|
||||||
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true)
|
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true)
|
||||||
|
fun refreshTerminalTabs()
|
||||||
}
|
}
|
||||||
@@ -4,23 +4,28 @@ package app.termora
|
|||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.actions.DataProviderSupport
|
import app.termora.actions.DataProviderSupport
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
|
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.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.Dimension
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
import java.awt.Insets
|
import java.awt.*
|
||||||
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.MouseMotionListener
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.imageio.ImageIO
|
import javax.imageio.ImageIO
|
||||||
import javax.swing.Box
|
import javax.swing.JComponent
|
||||||
import javax.swing.JFrame
|
import javax.swing.JFrame
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
import javax.swing.SwingUtilities.isEventDispatchThread
|
import javax.swing.SwingUtilities.isEventDispatchThread
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
fun assertEventDispatchThread() {
|
fun assertEventDispatchThread() {
|
||||||
if (!isEventDispatchThread()) throw WrongThreadException("AWT EventQueue")
|
if (!isEventDispatchThread()) throw WrongThreadException("AWT EventQueue")
|
||||||
@@ -32,14 +37,13 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
|
|
||||||
private val id = UUID.randomUUID().toString()
|
private val id = UUID.randomUUID().toString()
|
||||||
private val windowScope = ApplicationScope.forWindowScope(this)
|
private val windowScope = ApplicationScope.forWindowScope(this)
|
||||||
private val titleBar = LogicCustomTitleBar.createCustomTitleBar(this)
|
|
||||||
private val tabbedPane = MyTabbedPane()
|
private val tabbedPane = MyTabbedPane()
|
||||||
private val toolbar = TermoraToolBar(titleBar, tabbedPane)
|
private val toolbar = TermoraToolBar(windowScope, this)
|
||||||
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane)
|
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane)
|
||||||
private val isWindowDecorationsSupported by lazy { JBR.isWindowDecorationsSupported() }
|
|
||||||
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 var notifyListeners = emptyArray<NotifyListener>()
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -48,44 +52,144 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
|
if (SystemInfo.isLinux) {
|
||||||
|
val mouseAdapter = object : MouseAdapter() {
|
||||||
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
|
getMouseHandler()?.mouseClicked(e)
|
||||||
|
}
|
||||||
|
|
||||||
forceHitTest()
|
override fun mousePressed(e: MouseEvent) {
|
||||||
|
getMouseHandler()?.mousePressed(e)
|
||||||
|
}
|
||||||
|
|
||||||
// macos 需要判断是否全部删除
|
override fun mouseDragged(e: MouseEvent) {
|
||||||
// 当 Tab 为 0 的时候,需要加一个边距,避开控制栏
|
getMouseMotionListener()?.mouseDragged(
|
||||||
if (SystemInfo.isMacOS && isWindowDecorationsSupported) {
|
MouseEvent(
|
||||||
tabbedPane.addChangeListener {
|
e.component,
|
||||||
tabbedPane.leadingComponent = if (tabbedPane.tabCount == 0) {
|
e.id,
|
||||||
Box.createHorizontalStrut(titleBar.leftInset.toInt())
|
e.`when`,
|
||||||
} else {
|
e.modifiersEx,
|
||||||
null
|
e.x,
|
||||||
|
e.y,
|
||||||
|
e.clickCount,
|
||||||
|
e.isPopupTrigger,
|
||||||
|
e.button
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMouseHandler(): MouseListener? {
|
||||||
|
return getHandler() as? MouseListener
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMouseMotionListener(): MouseMotionListener? {
|
||||||
|
return getHandler() as? MouseMotionListener
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getHandler(): Any? {
|
||||||
|
val titlePane = getTitlePane() ?: return null
|
||||||
|
val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null
|
||||||
|
handlerField.isAccessible = true
|
||||||
|
return handlerField.get(titlePane)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTitlePane(): FlatTitlePane? {
|
||||||
|
val ui = rootPane.ui as? FlatRootPaneUI ?: return null
|
||||||
|
val titlePaneField = ui.javaClass.getDeclaredField("titlePane")
|
||||||
|
titlePaneField.isAccessible = true
|
||||||
|
return titlePaneField.get(ui) as? FlatTitlePane
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
toolbar.getJToolBar().addMouseListener(mouseAdapter)
|
||||||
|
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// force hit
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
if (JBR.isWindowDecorationsSupported()) {
|
||||||
|
val height = UIManager.getInt("TabbedPane.tabHeight") + tabbedPane.tabAreaInsets.top
|
||||||
|
val customTitleBar = JBR.getWindowDecorations().createCustomTitleBar()
|
||||||
|
customTitleBar.height = height.toFloat()
|
||||||
|
|
||||||
// 监听主题变化 需要动态修改控制栏颜色
|
val mouseAdapter = object : MouseAdapter() {
|
||||||
if (SystemInfo.isWindows && isWindowDecorationsSupported) {
|
|
||||||
ThemeManager.getInstance().addThemeChangeListener(object : ThemeChangeListener {
|
private fun hit(e: MouseEvent) {
|
||||||
override fun onChanged() {
|
if (e.source == tabbedPane) {
|
||||||
titleBar.putProperty("controls.dark", FlatLaf.isLafDark())
|
val index = tabbedPane.indexAtLocation(e.x, e.y)
|
||||||
|
if (index >= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customTitleBar.forceHitTest(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
|
hit(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mousePressed(e: MouseEvent) {
|
||||||
|
hit(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseReleased(e: MouseEvent) {
|
||||||
|
hit(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseEntered(e: MouseEvent) {
|
||||||
|
hit(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseDragged(e: MouseEvent) {
|
||||||
|
hit(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseMoved(e: MouseEvent) {
|
||||||
|
hit(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
terminalTabbed.addMouseListener(mouseAdapter)
|
||||||
|
terminalTabbed.addMouseMotionListener(mouseAdapter)
|
||||||
|
|
||||||
|
tabbedPane.addMouseListener(mouseAdapter)
|
||||||
|
tabbedPane.addMouseMotionListener(mouseAdapter)
|
||||||
|
|
||||||
|
toolbar.getJToolBar().addMouseListener(mouseAdapter)
|
||||||
|
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
||||||
|
|
||||||
|
JBR.getWindowDecorations().setCustomTitleBar(this, customTitleBar)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
if (isWindowDecorationsSupported) {
|
|
||||||
titleBar.height = UIManager.getInt("TabbedPane.tabHeight").toFloat()
|
// macOS 要避开左边的控制栏
|
||||||
titleBar.putProperty("controls.dark", FlatLaf.isLafDark())
|
if (SystemInfo.isMacOS) {
|
||||||
JBR.getWindowDecorations().setCustomTitleBar(this, titleBar)
|
tabbedPane.tabAreaInsets = Insets(0, 76, 0, 0)
|
||||||
|
} else if (SystemInfo.isWindows) {
|
||||||
|
// Windows 10 会有1像素误差
|
||||||
|
tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0)
|
||||||
|
} else if (SystemInfo.isLinux) {
|
||||||
|
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SystemInfo.isLinux) {
|
val height = UIManager.getInt("TabbedPane.tabHeight") + tabbedPane.tabAreaInsets.top
|
||||||
|
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
||||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, UIManager.getInt("TabbedPane.tabHeight"))
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICON, false)
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, height)
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
rootPane.putClientProperty("apple.awt.windowTitleVisible", false)
|
||||||
|
rootPane.putClientProperty("apple.awt.fullWindowContent", true)
|
||||||
|
rootPane.putClientProperty("apple.awt.transparentTitleBar", true)
|
||||||
|
rootPane.putClientProperty(
|
||||||
|
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING,
|
||||||
|
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING_MEDIUM
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
@@ -101,88 +205,26 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
terminalTabbed.addTerminalTab(welcomePanel)
|
terminalTabbed.addTerminalTab(welcomePanel)
|
||||||
|
|
||||||
// 下一次事件循环检测是否固定 SFTP
|
// 下一次事件循环检测是否固定 SFTP
|
||||||
SwingUtilities.invokeLater {
|
if (sftp.pinTab) {
|
||||||
if (sftp.pinTab) {
|
SwingUtilities.invokeLater {
|
||||||
terminalTabbed.addTerminalTab(SFTPTerminalTab(), false)
|
terminalTabbed.addTerminalTab(SFTPTab(), false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// macOS 要避开左边的控制栏
|
val glassPane = GlassPane()
|
||||||
if (SystemInfo.isMacOS) {
|
rootPane.glassPane = glassPane
|
||||||
val left = max(titleBar.leftInset.toInt(), 76)
|
glassPane.isOpaque = false
|
||||||
if (tabbedPane.tabCount == 0) {
|
glassPane.isVisible = true
|
||||||
tabbedPane.leadingComponent = Box.createHorizontalStrut(left)
|
|
||||||
} else {
|
|
||||||
tabbedPane.tabAreaInsets = Insets(0, left, 0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Disposer.register(windowScope, terminalTabbed)
|
Disposer.register(windowScope, terminalTabbed)
|
||||||
add(terminalTabbed)
|
add(terminalTabbed, BorderLayout.CENTER)
|
||||||
|
|
||||||
dataProviderSupport.addData(DataProviders.TabbedPane, tabbedPane)
|
dataProviderSupport.addData(DataProviders.TabbedPane, tabbedPane)
|
||||||
dataProviderSupport.addData(DataProviders.TermoraFrame, this)
|
dataProviderSupport.addData(DataProviders.TermoraFrame, this)
|
||||||
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
|
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun forceHitTest() {
|
|
||||||
val mouseAdapter = object : MouseAdapter() {
|
|
||||||
|
|
||||||
private fun hit(e: MouseEvent) {
|
|
||||||
if (e.source == tabbedPane) {
|
|
||||||
val index = tabbedPane.indexAtLocation(e.x, e.y)
|
|
||||||
if (index >= 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
titleBar.forceHitTest(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
|
||||||
hit(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mousePressed(e: MouseEvent) {
|
|
||||||
if (e.source == toolbar.getJToolBar()) {
|
|
||||||
if (!isWindowDecorationsSupported && SwingUtilities.isLeftMouseButton(e)) {
|
|
||||||
if (JBR.isWindowMoveSupported()) {
|
|
||||||
JBR.getWindowMove().startMovingTogetherWithMouse(this@TermoraFrame, e.button)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hit(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mouseReleased(e: MouseEvent) {
|
|
||||||
hit(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mouseEntered(e: MouseEvent) {
|
|
||||||
hit(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mouseDragged(e: MouseEvent) {
|
|
||||||
|
|
||||||
hit(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mouseMoved(e: MouseEvent) {
|
|
||||||
hit(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
terminalTabbed.addMouseListener(mouseAdapter)
|
|
||||||
terminalTabbed.addMouseMotionListener(mouseAdapter)
|
|
||||||
|
|
||||||
tabbedPane.addMouseListener(mouseAdapter)
|
|
||||||
tabbedPane.addMouseMotionListener(mouseAdapter)
|
|
||||||
|
|
||||||
toolbar.getJToolBar().addMouseListener(mouseAdapter)
|
|
||||||
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
return dataProviderSupport.getData(dataKey)
|
return dataProviderSupport.getData(dataKey)
|
||||||
?: terminalTabbed.getData(dataKey)
|
?: terminalTabbed.getData(dataKey)
|
||||||
@@ -203,5 +245,39 @@ 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() {
|
||||||
|
init {
|
||||||
|
isFocusable = false
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun contains(x: Int, y: Int): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,31 @@
|
|||||||
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.JOptionPane
|
import javax.swing.JOptionPane
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
import javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE
|
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)
|
||||||
@@ -24,10 +38,12 @@ 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) }
|
||||||
frame.title = if (SystemInfo.isLinux) null else Application.getName()
|
frame.title = Application.getName()
|
||||||
frame.defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
frame.defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
||||||
|
|
||||||
val rectangle = getFrameRectangle() ?: FrameRectangle(-1, -1, 1280, 800, 0)
|
val rectangle = getFrameRectangle() ?: FrameRectangle(-1, -1, 1280, 800, 0)
|
||||||
@@ -48,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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,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) {
|
||||||
|
|
||||||
@@ -72,38 +98,81 @@ 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dispose() {
|
fun tick() {
|
||||||
Disposer.dispose(ApplicationScope.forApplicationScope())
|
if (SwingUtilities.isEventDispatchThread()) {
|
||||||
|
val windows = getWindows()
|
||||||
|
if (windows.isEmpty()) return
|
||||||
|
for (window in windows) {
|
||||||
|
if (window.extendedState and JFrame.ICONIFIED == JFrame.ICONIFIED) {
|
||||||
|
window.extendedState = window.extendedState and JFrame.ICONIFIED.inv()
|
||||||
|
}
|
||||||
|
window.isVisible = true
|
||||||
|
}
|
||||||
|
windows.last().toFront()
|
||||||
|
} else {
|
||||||
|
SwingUtilities.invokeLater { tick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
override fun dispose() {
|
||||||
Disposer.getTree().assertIsEmpty(true)
|
if (isDisposed.compareAndSet(false, true)) {
|
||||||
} catch (e: Exception) {
|
Disposer.dispose(ApplicationScope.forApplicationScope())
|
||||||
if (log.isErrorEnabled) {
|
|
||||||
log.error(e.message, e)
|
try {
|
||||||
|
Disposer.getTree().assertIsEmpty(true)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,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
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.actions.ActionManager
|
import app.termora.actions.*
|
||||||
import app.termora.actions.AnAction
|
|
||||||
import app.termora.actions.AnActionEvent
|
|
||||||
import app.termora.actions.SettingsAction
|
|
||||||
import app.termora.findeverywhere.FindEverywhereAction
|
import app.termora.findeverywhere.FindEverywhereAction
|
||||||
import app.termora.snippet.SnippetAction
|
import app.termora.snippet.SnippetAction
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jetbrains.WindowDecorations
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.jdesktop.swingx.action.ActionContainerFactory
|
import org.jdesktop.swingx.action.ActionContainerFactory
|
||||||
import java.awt.Insets
|
import java.awt.Rectangle
|
||||||
import java.awt.event.ComponentAdapter
|
import java.awt.event.ComponentAdapter
|
||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
import javax.swing.Box
|
import javax.swing.Box
|
||||||
@@ -27,11 +23,23 @@ data class ToolBarAction(
|
|||||||
)
|
)
|
||||||
|
|
||||||
class TermoraToolBar(
|
class TermoraToolBar(
|
||||||
private val titleBar: WindowDecorations.CustomTitleBar,
|
private val windowScope: WindowScope,
|
||||||
private val tabbedPane: FlatTabbedPane
|
private val frame: TermoraFrame,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun rebuild() {
|
||||||
|
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
||||||
|
val toolbars = SwingUtils.getDescendantsOfClass(MyToolBar::class.java, frame)
|
||||||
|
for (toolbar in toolbars) {
|
||||||
|
toolbar.rebuild()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val properties by lazy { Database.getDatabase().properties }
|
private val properties by lazy { Database.getDatabase().properties }
|
||||||
private val toolbar by lazy { MyToolBar().apply { rebuild(this) } }
|
private val toolbar by lazy { MyToolBar().apply { rebuild() } }
|
||||||
|
|
||||||
|
|
||||||
fun getJToolBar(): JToolBar {
|
fun getJToolBar(): JToolBar {
|
||||||
@@ -49,7 +57,7 @@ class TermoraToolBar(
|
|||||||
ToolBarAction(Actions.MACRO, true),
|
ToolBarAction(Actions.MACRO, true),
|
||||||
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
|
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
|
||||||
ToolBarAction(Actions.KEY_MANAGER, true),
|
ToolBarAction(Actions.KEY_MANAGER, true),
|
||||||
ToolBarAction(Actions.MULTIPLE, true),
|
ToolBarAction(MultipleAction.MULTIPLE, true),
|
||||||
ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
|
ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
|
||||||
ToolBarAction(SettingsAction.SETTING, true),
|
ToolBarAction(SettingsAction.SETTING, true),
|
||||||
)
|
)
|
||||||
@@ -89,58 +97,6 @@ class TermoraToolBar(
|
|||||||
return storageActions
|
return storageActions
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rebuild() {
|
|
||||||
rebuild(this.toolbar)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun rebuild(toolbar: JToolBar) {
|
|
||||||
val actionManager = ActionManager.getInstance()
|
|
||||||
val actionContainerFactory = ActionContainerFactory(actionManager)
|
|
||||||
|
|
||||||
toolbar.removeAll()
|
|
||||||
|
|
||||||
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
|
||||||
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isEnabled(): Boolean {
|
|
||||||
return actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.isEnabled ?: false
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
toolbar.add(Box.createHorizontalGlue())
|
|
||||||
|
|
||||||
if (SystemInfo.isLinux || SystemInfo.isWindows) {
|
|
||||||
toolbar.add(Box.createHorizontalStrut(16))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// update btn
|
|
||||||
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
|
|
||||||
updateBtn.isVisible = updateBtn.isEnabled
|
|
||||||
updateBtn.addChangeListener { updateBtn.isVisible = updateBtn.isEnabled }
|
|
||||||
toolbar.add(updateBtn)
|
|
||||||
|
|
||||||
|
|
||||||
// 获取显示的Action,如果不是 false 那么就是显示出来
|
|
||||||
for (action in getActions()) {
|
|
||||||
if (action.visible) {
|
|
||||||
actionManager.getAction(action.id)?.let {
|
|
||||||
toolbar.add(actionContainerFactory.createButton(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (toolbar is MyToolBar) {
|
|
||||||
toolbar.adjust()
|
|
||||||
}
|
|
||||||
|
|
||||||
toolbar.revalidate()
|
|
||||||
toolbar.repaint()
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class MyToolBar : JToolBar() {
|
private inner class MyToolBar : JToolBar() {
|
||||||
init {
|
init {
|
||||||
// 监听窗口大小变动,然后修改边距避开控制按钮
|
// 监听窗口大小变动,然后修改边距避开控制按钮
|
||||||
@@ -152,14 +108,11 @@ class TermoraToolBar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun adjust() {
|
fun adjust() {
|
||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
val left = titleBar.leftInset.toInt()
|
val rectangle =
|
||||||
if (tabbedPane.tabAreaInsets.left != left) {
|
frame.rootPane.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
||||||
tabbedPane.tabAreaInsets = Insets(0, left, 0, 0)
|
as? Rectangle ?: return
|
||||||
}
|
val right = rectangle.width
|
||||||
} else if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
|
||||||
|
|
||||||
val right = titleBar.rightInset.toInt()
|
|
||||||
val toolbar = this@MyToolBar
|
val toolbar = this@MyToolBar
|
||||||
for (i in 0 until toolbar.componentCount) {
|
for (i in 0 until toolbar.componentCount) {
|
||||||
val c = toolbar.getComponent(i)
|
val c = toolbar.getComponent(i)
|
||||||
@@ -179,5 +132,60 @@ class TermoraToolBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun rebuild() {
|
||||||
|
val toolbar: JToolBar = this
|
||||||
|
val actionManager = ActionManager.getInstance()
|
||||||
|
val actionContainerFactory = ActionContainerFactory(actionManager)
|
||||||
|
|
||||||
|
toolbar.removeAll()
|
||||||
|
|
||||||
|
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isEnabled(): Boolean {
|
||||||
|
return actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.isEnabled ?: false
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
toolbar.add(Box.createHorizontalGlue())
|
||||||
|
|
||||||
|
if (SystemInfo.isLinux || SystemInfo.isWindows) {
|
||||||
|
toolbar.add(Box.createHorizontalStrut(16))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// update btn
|
||||||
|
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
|
||||||
|
updateBtn.isVisible = updateBtn.isEnabled
|
||||||
|
updateBtn.addChangeListener { updateBtn.isVisible = updateBtn.isEnabled }
|
||||||
|
toolbar.add(updateBtn)
|
||||||
|
|
||||||
|
|
||||||
|
// 获取显示的Action,如果不是 false 那么就是显示出来
|
||||||
|
for (action in getActions()) {
|
||||||
|
if (action.visible) {
|
||||||
|
val ac = actionManager.getAction(action.id)
|
||||||
|
if (ac == null) {
|
||||||
|
if (action.id == MultipleAction.MULTIPLE) {
|
||||||
|
toolbar.add(actionContainerFactory.createButton(MultipleAction.getInstance(windowScope)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toolbar.add(actionContainerFactory.createButton(ac))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (toolbar is MyToolBar) {
|
||||||
|
toolbar.adjust()
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbar.revalidate()
|
||||||
|
toolbar.repaint()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
@@ -47,6 +45,7 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
putClientProperty(FlatClientProperties.TABBED_PANE_TAB_CLOSABLE, false)
|
putClientProperty(FlatClientProperties.TABBED_PANE_TAB_CLOSABLE, false)
|
||||||
|
putClientProperty(FindEverywhereProvider.SKIP_FIND_EVERYWHERE, true)
|
||||||
|
|
||||||
val panel = JPanel(BorderLayout())
|
val panel = JPanel(BorderLayout())
|
||||||
panel.add(createSearchPanel(), BorderLayout.NORTH)
|
panel.add(createSearchPanel(), BorderLayout.NORTH)
|
||||||
@@ -218,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() {
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import app.termora.findeverywhere.FindEverywhereAction
|
|||||||
import app.termora.highlight.KeywordHighlightAction
|
import app.termora.highlight.KeywordHighlightAction
|
||||||
import app.termora.keymgr.KeyManagerAction
|
import app.termora.keymgr.KeyManagerAction
|
||||||
import app.termora.macro.MacroAction
|
import app.termora.macro.MacroAction
|
||||||
|
import app.termora.sftp.SFTPAction
|
||||||
import app.termora.snippet.SnippetAction
|
import app.termora.snippet.SnippetAction
|
||||||
import app.termora.tlog.TerminalLoggerAction
|
import app.termora.tlog.TerminalLoggerAction
|
||||||
import app.termora.transport.SFTPAction
|
|
||||||
import javax.swing.Action
|
import javax.swing.Action
|
||||||
|
|
||||||
class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
||||||
@@ -29,7 +29,6 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
|||||||
addAction(NewWindowAction.NEW_WINDOW, NewWindowAction())
|
addAction(NewWindowAction.NEW_WINDOW, NewWindowAction())
|
||||||
addAction(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
|
addAction(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
|
||||||
|
|
||||||
addAction(Actions.MULTIPLE, MultipleAction())
|
|
||||||
addAction(Actions.APP_UPDATE, AppUpdateAction.getInstance())
|
addAction(Actions.APP_UPDATE, AppUpdateAction.getInstance())
|
||||||
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())
|
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())
|
||||||
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
|
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
|
||||||
|
|||||||
@@ -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,12 +67,15 @@ 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() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkUpdate() {
|
private suspend fun checkUpdate() {
|
||||||
|
if (Application.isUnknownVersion()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val latestVersion = updaterManager.fetchLatestVersion()
|
val latestVersion = updaterManager.fetchLatestVersion()
|
||||||
if (latestVersion.isSelf) {
|
if (latestVersion.isSelf) {
|
||||||
@@ -220,7 +223,10 @@ class AppUpdateAction private constructor() : AnAction(
|
|||||||
// 没有安装过 则打开安装向导
|
// 没有安装过 则打开安装向导
|
||||||
else listOf(file.absolutePath)
|
else listOf(file.absolutePath)
|
||||||
|
|
||||||
println(commands)
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("restart {}", commands.joinToString(StringUtils.SPACE))
|
||||||
|
}
|
||||||
|
|
||||||
TermoraRestarter.getInstance().scheduleRestart(owner, commands)
|
TermoraRestarter.getInstance().scheduleRestart(owner, commands)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import app.termora.terminal.DataKey
|
|||||||
object DataProviders {
|
object DataProviders {
|
||||||
val TerminalPanel = DataKey(app.termora.terminal.panel.TerminalPanel::class)
|
val TerminalPanel = DataKey(app.termora.terminal.panel.TerminalPanel::class)
|
||||||
val Terminal = DataKey(app.termora.terminal.Terminal::class)
|
val Terminal = DataKey(app.termora.terminal.Terminal::class)
|
||||||
val PtyConnector get() = DataKey.PtyConnector
|
val TerminalWriter get() = DataKey.TerminalWriter
|
||||||
|
|
||||||
val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
|
val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
|
||||||
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
|
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
|
||||||
|
|||||||
@@ -1,17 +1,32 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.I18n
|
||||||
|
import app.termora.Icons
|
||||||
|
import app.termora.TerminalPanelFactory
|
||||||
|
import app.termora.WindowScope
|
||||||
|
|
||||||
class MultipleAction : AnAction(
|
class MultipleAction private constructor() : AnAction(
|
||||||
I18n.getString("termora.tools.multiple"),
|
I18n.getString("termora.tools.multiple"),
|
||||||
Icons.vcs
|
Icons.vcs
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将命令发送到多个会话
|
||||||
|
*/
|
||||||
|
const val MULTIPLE = "MultipleAction"
|
||||||
|
|
||||||
|
fun getInstance(windowScope: WindowScope): MultipleAction {
|
||||||
|
return windowScope.getOrCreate(MultipleAction::class) { MultipleAction() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setStateAction()
|
setStateAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
ApplicationScope.windowScopes().map { TerminalPanelFactory.getInstance(it) }
|
TerminalPanelFactory.getInstance().repaintAll()
|
||||||
.forEach { it.repaintAll() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,7 @@ class OpenLocalTerminalAction : AnAction(
|
|||||||
OpenHostActionEvent(
|
OpenHostActionEvent(
|
||||||
evt.source,
|
evt.source,
|
||||||
Host(
|
Host(
|
||||||
|
id = "local",
|
||||||
name = name,
|
name = name,
|
||||||
protocol = Protocol.Local
|
protocol = Protocol.Local
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ class TerminalCopyAction : AnAction() {
|
|||||||
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val terminalPanel = evt.getData(DataProviders.TerminalPanel) ?: return
|
val terminalPanel = evt.getData(DataProviders.TerminalPanel) ?: return
|
||||||
|
val selectionModel = terminalPanel.terminal.getSelectionModel()
|
||||||
|
if (!selectionModel.hasSelection()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val text = terminalPanel.copy()
|
val text = terminalPanel.copy()
|
||||||
val systemClipboard = terminalPanel.toolkit.systemClipboard
|
val systemClipboard = terminalPanel.toolkit.systemClipboard
|
||||||
|
|
||||||
@@ -32,7 +36,6 @@ class TerminalCopyAction : AnAction() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
systemClipboard.setContents(StringSelection(text), null)
|
systemClipboard.setContents(StringSelection(text), null)
|
||||||
terminalPanel.toast(I18n.getString("termora.terminal.copied"))
|
|
||||||
if (log.isTraceEnabled) {
|
if (log.isTraceEnabled) {
|
||||||
log.trace("Copy to clipboard. {}", text)
|
log.trace("Copy to clipboard. {}", text)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
import app.termora.ApplicationScope
|
|
||||||
import app.termora.Database
|
import app.termora.Database
|
||||||
import app.termora.TerminalPanelFactory
|
import app.termora.TerminalPanelFactory
|
||||||
|
|
||||||
@@ -13,10 +12,8 @@ abstract class TerminalZoomAction : AnAction() {
|
|||||||
evt.getData(DataProviders.TerminalPanel) ?: return
|
evt.getData(DataProviders.TerminalPanel) ?: return
|
||||||
|
|
||||||
if (zoom()) {
|
if (zoom()) {
|
||||||
ApplicationScope.windowScopes().forEach {
|
TerminalPanelFactory.getInstance()
|
||||||
TerminalPanelFactory.getInstance(it)
|
|
||||||
.fireResize()
|
.fireResize()
|
||||||
}
|
|
||||||
evt.consume()
|
evt.consume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ package app.termora.findeverywhere
|
|||||||
import app.termora.DialogWrapper
|
import app.termora.DialogWrapper
|
||||||
import app.termora.DynamicColor
|
import app.termora.DynamicColor
|
||||||
import app.termora.I18n
|
import app.termora.I18n
|
||||||
|
import app.termora.WindowScope
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.macro.MacroFindEverywhereProvider
|
import app.termora.macro.MacroFindEverywhereProvider
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
|
||||||
import com.formdev.flatlaf.extras.components.FlatTextField
|
import com.formdev.flatlaf.extras.components.FlatTextField
|
||||||
import com.jetbrains.JBR
|
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Insets
|
import java.awt.Insets
|
||||||
@@ -18,7 +17,7 @@ import javax.swing.*
|
|||||||
import javax.swing.event.DocumentEvent
|
import javax.swing.event.DocumentEvent
|
||||||
import javax.swing.event.DocumentListener
|
import javax.swing.event.DocumentListener
|
||||||
|
|
||||||
class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
class FindEverywhere(owner: Window, windowScope: WindowScope) : DialogWrapper(owner) {
|
||||||
private val searchTextField = FlatTextField()
|
private val searchTextField = FlatTextField()
|
||||||
private val model = DefaultListModel<FindEverywhereResult>()
|
private val model = DefaultListModel<FindEverywhereResult>()
|
||||||
private val resultList = FindEverywhereXList(model)
|
private val resultList = FindEverywhereXList(model)
|
||||||
@@ -26,7 +25,7 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
|||||||
private val providers = mutableListOf<FindEverywhereProvider>(
|
private val providers = mutableListOf<FindEverywhereProvider>(
|
||||||
BasicFilterFindEverywhereProvider(QuickCommandFindEverywhereProvider()),
|
BasicFilterFindEverywhereProvider(QuickCommandFindEverywhereProvider()),
|
||||||
BasicFilterFindEverywhereProvider(SettingsFindEverywhereProvider()),
|
BasicFilterFindEverywhereProvider(SettingsFindEverywhereProvider()),
|
||||||
BasicFilterFindEverywhereProvider(QuickActionsFindEverywhereProvider()),
|
BasicFilterFindEverywhereProvider(QuickActionsFindEverywhereProvider(windowScope)),
|
||||||
BasicFilterFindEverywhereProvider(MacroFindEverywhereProvider()),
|
BasicFilterFindEverywhereProvider(MacroFindEverywhereProvider()),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,17 +43,10 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
|||||||
minimumSize = Dimension(size.width / 2, size.height / 2)
|
minimumSize = Dimension(size.width / 2, size.height / 2)
|
||||||
isModal = false
|
isModal = false
|
||||||
lostFocusDispose = true
|
lostFocusDispose = true
|
||||||
controlsVisible = false
|
|
||||||
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
|
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
// 不支持装饰,铺满
|
|
||||||
if (!JBR.isWindowDecorationsSupported()) {
|
|
||||||
rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
|
||||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_CLOSE, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
rootPane.background = DynamicColor("desktop")
|
val desktopBackground = DynamicColor("desktop")
|
||||||
centerPanel.background = DynamicColor("desktop")
|
centerPanel.background = DynamicColor("desktop")
|
||||||
centerPanel.border = BorderFactory.createEmptyBorder(12, 12, 12, 12)
|
centerPanel.border = BorderFactory.createEmptyBorder(12, 12, 12, 12)
|
||||||
|
|
||||||
@@ -69,7 +61,7 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
|||||||
resultList.isRolloverEnabled = false
|
resultList.isRolloverEnabled = false
|
||||||
resultList.selectionMode = ListSelectionModel.SINGLE_SELECTION
|
resultList.selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||||
resultList.border = BorderFactory.createEmptyBorder(5, 0, 0, 0)
|
resultList.border = BorderFactory.createEmptyBorder(5, 0, 0, 0)
|
||||||
resultList.background = rootPane.background
|
resultList.background = desktopBackground
|
||||||
|
|
||||||
|
|
||||||
val scrollPane = JScrollPane(resultList)
|
val scrollPane = JScrollPane(resultList)
|
||||||
@@ -225,5 +217,11 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
|||||||
super.setVisible(visible)
|
super.setVisible(visible)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun addNotify() {
|
||||||
|
super.addNotify()
|
||||||
|
|
||||||
|
controlsVisible = false
|
||||||
|
fullWindowContent = true
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class FindEverywhereAction : AnAction(StringUtils.EMPTY, Icons.find) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val dialog = FindEverywhere(owner)
|
val dialog = FindEverywhere(owner, scope)
|
||||||
for (provider in FindEverywhereProvider.getFindEverywhereProviders(scope)) {
|
for (provider in FindEverywhereProvider.getFindEverywhereProviders(scope)) {
|
||||||
dialog.registerProvider(provider)
|
dialog.registerProvider(provider)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import app.termora.Scope
|
|||||||
interface FindEverywhereProvider {
|
interface FindEverywhereProvider {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
const val SKIP_FIND_EVERYWHERE = "SKIP_FIND_EVERYWHERE"
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun getFindEverywhereProviders(scope: Scope): MutableList<FindEverywhereProvider> {
|
fun getFindEverywhereProviders(scope: Scope): MutableList<FindEverywhereProvider> {
|
||||||
var list = scope.getAnyOrNull("FindEverywhereProviders")
|
var list = scope.getAnyOrNull("FindEverywhereProviders")
|
||||||
|
|||||||
@@ -2,21 +2,32 @@ package app.termora.findeverywhere
|
|||||||
|
|
||||||
import app.termora.Actions
|
import app.termora.Actions
|
||||||
import app.termora.I18n
|
import app.termora.I18n
|
||||||
|
import app.termora.WindowScope
|
||||||
|
import app.termora.actions.MultipleAction
|
||||||
|
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
|
|
||||||
class QuickActionsFindEverywhereProvider : FindEverywhereProvider {
|
class QuickActionsFindEverywhereProvider(private val windowScope: WindowScope) : FindEverywhereProvider {
|
||||||
private val actions = listOf(
|
private val actions = listOf(
|
||||||
Actions.KEY_MANAGER,
|
Actions.KEY_MANAGER,
|
||||||
Actions.KEYWORD_HIGHLIGHT,
|
Actions.KEYWORD_HIGHLIGHT,
|
||||||
Actions.MULTIPLE,
|
MultipleAction.MULTIPLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun find(pattern: String): List<FindEverywhereResult> {
|
override fun find(pattern: String): List<FindEverywhereResult> {
|
||||||
val actionManager = ActionManager.getInstance()
|
val actionManager = ActionManager.getInstance()
|
||||||
return actions
|
val results = ArrayList<FindEverywhereResult>()
|
||||||
.mapNotNull { actionManager.getAction(it) }
|
for (action in actions) {
|
||||||
.map { ActionFindEverywhereResult(it) }
|
val ac = actionManager.getAction(action)
|
||||||
|
if (ac == null) {
|
||||||
|
if (action == MultipleAction.MULTIPLE) {
|
||||||
|
results.add(ActionFindEverywhereResult(MultipleAction.getInstance(windowScope)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.add(ActionFindEverywhereResult(ac))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package app.termora.highlight
|
package app.termora.highlight
|
||||||
|
|
||||||
import app.termora.ApplicationScope
|
|
||||||
import app.termora.DialogWrapper
|
import app.termora.DialogWrapper
|
||||||
import app.termora.TerminalFactory
|
import app.termora.TerminalFactory
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
@@ -31,7 +30,7 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
|
|||||||
|
|
||||||
override fun createCenterPanel(): JComponent {
|
override fun createCenterPanel(): JComponent {
|
||||||
val panel = JPanel(GridLayout(2, 8, 4, 4))
|
val panel = JPanel(GridLayout(2, 8, 4, 4))
|
||||||
val colorPalette = TerminalFactory.getInstance(ApplicationScope.forWindowScope(this))
|
val colorPalette = TerminalFactory.getInstance()
|
||||||
.createTerminal().getTerminalModel().getColorPalette()
|
.createTerminal().getTerminalModel().getColorPalette()
|
||||||
for (i in 1..16) {
|
for (i in 1..16) {
|
||||||
val c = JPanel()
|
val c = JPanel()
|
||||||
@@ -52,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(ApplicationScope.forWindowScope(this)).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 {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package app.termora.highlight
|
package app.termora.highlight
|
||||||
|
|
||||||
import app.termora.ApplicationScope
|
import app.termora.ApplicationScope
|
||||||
import app.termora.TerminalPanelFactory
|
|
||||||
import app.termora.Database
|
import app.termora.Database
|
||||||
|
import app.termora.DeleteDataManager
|
||||||
|
import app.termora.TerminalPanelFactory
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
class KeywordHighlightManager private constructor() {
|
class KeywordHighlightManager private constructor() {
|
||||||
@@ -27,7 +28,7 @@ class KeywordHighlightManager private constructor() {
|
|||||||
fun addKeywordHighlight(keywordHighlight: KeywordHighlight) {
|
fun addKeywordHighlight(keywordHighlight: KeywordHighlight) {
|
||||||
database.addKeywordHighlight(keywordHighlight)
|
database.addKeywordHighlight(keywordHighlight)
|
||||||
keywordHighlights[keywordHighlight.id] = keywordHighlight
|
keywordHighlights[keywordHighlight.id] = keywordHighlight
|
||||||
ApplicationScope.windowScopes().forEach { TerminalPanelFactory.getInstance(it).repaintAll() }
|
TerminalPanelFactory.getInstance().repaintAll()
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Keyword highlighter added. {}", keywordHighlight)
|
log.debug("Keyword highlighter added. {}", keywordHighlight)
|
||||||
@@ -37,7 +38,8 @@ class KeywordHighlightManager private constructor() {
|
|||||||
fun removeKeywordHighlight(id: String) {
|
fun removeKeywordHighlight(id: String) {
|
||||||
database.removeKeywordHighlight(id)
|
database.removeKeywordHighlight(id)
|
||||||
keywordHighlights.remove(id)
|
keywordHighlights.remove(id)
|
||||||
ApplicationScope.windowScopes().forEach { TerminalPanelFactory.getInstance(it).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 {
|
||||||
|
|||||||
@@ -119,10 +119,11 @@ class KeymapPanel : JPanel(BorderLayout()) {
|
|||||||
val keymap = getCurrentKeymap()
|
val keymap = getCurrentKeymap()
|
||||||
val index = keymapComboBox.selectedIndex
|
val index = keymapComboBox.selectedIndex
|
||||||
if (keymap != null && !keymap.isReadonly && index >= 0) {
|
if (keymap != null && !keymap.isReadonly && index >= 0) {
|
||||||
val text = InputDialog(
|
val text = OptionPane.showInputDialog(
|
||||||
SwingUtilities.getWindowAncestor(this@KeymapPanel),
|
SwingUtilities.getWindowAncestor(this@KeymapPanel),
|
||||||
title = renameBtn.toolTipText, text = keymap.name
|
title = renameBtn.toolTipText,
|
||||||
).getText()
|
value = keymap.name
|
||||||
|
)
|
||||||
if (!text.isNullOrBlank()) {
|
if (!text.isNullOrBlank()) {
|
||||||
if (text != keymap.name) {
|
if (text != keymap.name) {
|
||||||
keymapManager.removeKeymap(keymap.name)
|
keymapManager.removeKeymap(keymap.name)
|
||||||
|
|||||||
@@ -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,10 +1,8 @@
|
|||||||
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.actions.DataProviders
|
|
||||||
import app.termora.native.FileChooser
|
import app.termora.native.FileChooser
|
||||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
import com.formdev.flatlaf.extras.components.FlatTable
|
import com.formdev.flatlaf.extras.components.FlatTable
|
||||||
@@ -14,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
|
||||||
@@ -34,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
|
||||||
@@ -104,7 +100,9 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
generateBtn.addActionListener {
|
generateBtn.addActionListener {
|
||||||
val dialog = GenerateKeyDialog(SwingUtilities.getWindowAncestor(this))
|
val owner = SwingUtilities.getWindowAncestor(this)
|
||||||
|
val dialog = GenerateKeyDialog(owner)
|
||||||
|
dialog.setLocationRelativeTo(owner)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
if (dialog.ohKeyPair != OhKeyPair.empty) {
|
if (dialog.ohKeyPair != OhKeyPair.empty) {
|
||||||
val keyPair = dialog.ohKeyPair
|
val keyPair = dialog.ohKeyPair
|
||||||
@@ -143,12 +141,14 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
editBtn.addActionListener {
|
editBtn.addActionListener {
|
||||||
val row = keyPairTable.selectedRow
|
val row = keyPairTable.selectedRow
|
||||||
if (row >= 0) {
|
if (row >= 0) {
|
||||||
|
val owner = SwingUtilities.getWindowAncestor(this)
|
||||||
var ohKeyPair = keyPairTableModel.getOhKeyPair(row)
|
var ohKeyPair = keyPairTableModel.getOhKeyPair(row)
|
||||||
val dialog = GenerateKeyDialog(
|
val dialog = GenerateKeyDialog(
|
||||||
SwingUtilities.getWindowAncestor(this),
|
owner,
|
||||||
ohKeyPair,
|
ohKeyPair,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
dialog.setLocationRelativeTo(owner)
|
||||||
dialog.title = ohKeyPair.name
|
dialog.title = ohKeyPair.name
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
ohKeyPair = dialog.ohKeyPair
|
ohKeyPair = dialog.ohKeyPair
|
||||||
@@ -196,7 +196,6 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun sshCopyId(evt: AnActionEvent) {
|
private fun sshCopyId(evt: AnActionEvent) {
|
||||||
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
|
|
||||||
val keyPairs = keyPairTable.selectedRows.map { keyPairTableModel.getOhKeyPair(it) }
|
val keyPairs = keyPairTable.selectedRows.map { keyPairTableModel.getOhKeyPair(it) }
|
||||||
val publicKeys = mutableListOf<Pair<String, String>>()
|
val publicKeys = mutableListOf<Pair<String, String>>()
|
||||||
for (keyPair in keyPairs) {
|
for (keyPair in keyPairs) {
|
||||||
@@ -220,7 +219,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
SSHCopyIdDialog(owner, windowScope, hosts, publicKeys).start()
|
SSHCopyIdDialog(owner, hosts, publicKeys).start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportKeyPairs(file: File, keyPairs: List<OhKeyPair>) {
|
private fun exportKeyPairs(file: File, keyPairs: List<OhKeyPair>) {
|
||||||
@@ -311,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 {
|
||||||
@@ -344,7 +337,6 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
|
|
||||||
pack()
|
pack()
|
||||||
size = Dimension(UIManager.getInt("Dialog.width") - 300, size.height)
|
size = Dimension(UIManager.getInt("Dialog.width") - 300, size.height)
|
||||||
setLocationRelativeTo(null)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +348,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
|
|
||||||
var rows = 1
|
var rows = 1
|
||||||
val step = 2
|
val step = 2
|
||||||
return FormBuilder.create().layout(layout).padding("0dlu, $formMargin, $formMargin, $formMargin")
|
return FormBuilder.create().layout(layout).padding("2dlu, $formMargin, $formMargin, $formMargin")
|
||||||
.add("${I18n.getString("termora.keymgr.table.type")}:").xy(1, rows)
|
.add("${I18n.getString("termora.keymgr.table.type")}:").xy(1, rows)
|
||||||
.add(typeComboBox).xy(3, rows).apply { rows += step }
|
.add(typeComboBox).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.keymgr.table.length")}:").xy(1, rows)
|
.add("${I18n.getString("termora.keymgr.table.length")}:").xy(1, rows)
|
||||||
@@ -514,7 +506,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
|
|
||||||
var rows = 1
|
var rows = 1
|
||||||
val step = 2
|
val step = 2
|
||||||
return FormBuilder.create().layout(layout).padding("0dlu, $formMargin, $formMargin, $formMargin")
|
return FormBuilder.create().layout(layout).padding("2dlu, $formMargin, $formMargin, $formMargin")
|
||||||
.add("File:").xy(1, rows)
|
.add("File:").xy(1, rows)
|
||||||
.add(fileTextField).xy(3, rows).apply { rows += step }
|
.add(fileTextField).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.keymgr.table.type")}:").xy(1, rows)
|
.add("${I18n.getString("termora.keymgr.table.type")}:").xy(1, rows)
|
||||||
@@ -589,8 +581,10 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
try {
|
try {
|
||||||
val provider = FileKeyPairProvider(file.toPath())
|
val provider = FileKeyPairProvider(file.toPath())
|
||||||
provider.passwordFinder = FilePasswordProvider { _, _, _ ->
|
provider.passwordFinder = FilePasswordProvider { _, _, _ ->
|
||||||
val dialog = InputDialog(owner = this@ImportKeyDialog, title = "Password")
|
OptionPane.showInputDialog(
|
||||||
dialog.getText() ?: String()
|
SwingUtilities.getWindowAncestor(this),
|
||||||
|
title = I18n.getString("termora.new-host.general.password"),
|
||||||
|
) ?: String()
|
||||||
}
|
}
|
||||||
val keyPair = provider.loadKeys(null).firstOrNull()
|
val keyPair = provider.loadKeys(null).firstOrNull()
|
||||||
?: throw IllegalStateException("Failed to load the key file")
|
?: throw IllegalStateException("Failed to load the key file")
|
||||||
@@ -651,9 +645,13 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ohKeyPair = ohKeyPair.copy(
|
||||||
|
name = nameTextField.text,
|
||||||
|
remark = remarkTextField.text,
|
||||||
|
)
|
||||||
|
|
||||||
if (ohKeyPair.remark.isEmpty()) {
|
if (ohKeyPair.remark.isEmpty()) {
|
||||||
ohKeyPair = ohKeyPair.copy(
|
ohKeyPair = ohKeyPair.copy(
|
||||||
name = nameTextField.text,
|
|
||||||
remark = "Import on " + DateFormatUtils.format(Date(), I18n.getString("termora.date-format"))
|
remark = "Import on " + DateFormatUtils.format(Date(), I18n.getString("termora.date-format"))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import javax.swing.UIManager
|
|||||||
|
|
||||||
class SSHCopyIdDialog(
|
class SSHCopyIdDialog(
|
||||||
owner: Window,
|
owner: Window,
|
||||||
private val windowScope: WindowScope,
|
|
||||||
private val hosts: List<Host>,
|
private val hosts: List<Host>,
|
||||||
// key: name , value: public key
|
// key: name , value: public key
|
||||||
private val publicKeys: List<Pair<String, String>>,
|
private val publicKeys: List<Pair<String, String>>,
|
||||||
@@ -33,9 +32,9 @@ class SSHCopyIdDialog(
|
|||||||
private val log = LoggerFactory.getLogger(SSHCopyIdDialog::class.java)
|
private val log = LoggerFactory.getLogger(SSHCopyIdDialog::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val terminalPanelFactory = TerminalPanelFactory.getInstance(windowScope)
|
private val terminalPanelFactory = TerminalPanelFactory.getInstance()
|
||||||
private val terminal by lazy {
|
private val terminal by lazy {
|
||||||
TerminalFactory.getInstance(windowScope).createTerminal().apply {
|
TerminalFactory.getInstance().createTerminal().apply {
|
||||||
getTerminalModel().setData(DataKey.ShowCursor, false)
|
getTerminalModel().setData(DataKey.ShowCursor, false)
|
||||||
getTerminalModel().setData(DataKey.AutoNewline, true)
|
getTerminalModel().setData(DataKey.AutoNewline, true)
|
||||||
}
|
}
|
||||||
@@ -44,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)
|
||||||
@@ -145,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() }
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ class MacroDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
initEvents()
|
initEvents()
|
||||||
|
|
||||||
init()
|
init()
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(owner)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
@@ -83,8 +83,11 @@ class MacroDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
val index = list.selectedIndex
|
val index = list.selectedIndex
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
val macro = model.getElementAt(index)
|
val macro = model.getElementAt(index)
|
||||||
val dialog = InputDialog(owner = this, title = macro.name, text = macro.name)
|
val text = OptionPane.showInputDialog(
|
||||||
val text = dialog.getText() ?: String()
|
this,
|
||||||
|
title = macro.name,
|
||||||
|
value = macro.name
|
||||||
|
) ?: String()
|
||||||
if (text.isNotBlank()) {
|
if (text.isNotBlank()) {
|
||||||
val newMacro = macro.copy(name = text)
|
val newMacro = macro.copy(name = text)
|
||||||
macroManager.addMacro(newMacro)
|
macroManager.addMacro(newMacro)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user