mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
86 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 |
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/windows-x86-64.yml
vendored
4
.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:
|
||||||
|
|||||||
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 }}
|
||||||
|
|||||||
134
build.gradle.kts
134
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.12"
|
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,67 +57,67 @@ dependencies {
|
|||||||
|
|
||||||
// implementation(platform(libs.koin.bom))
|
// implementation(platform(libs.koin.bom))
|
||||||
// implementation(libs.koin.core)
|
// implementation(libs.koin.core)
|
||||||
implementation(libs.slf4j.api)
|
api(libs.slf4j.api)
|
||||||
implementation(libs.pty4j)
|
api(libs.pty4j)
|
||||||
implementation(libs.slf4j.tinylog)
|
api(libs.slf4j.tinylog)
|
||||||
implementation(libs.tinylog.impl)
|
api(libs.tinylog.impl)
|
||||||
implementation(libs.commons.codec)
|
api(libs.commons.codec)
|
||||||
implementation(libs.commons.io)
|
api(libs.commons.io)
|
||||||
implementation(libs.commons.lang3)
|
api(libs.commons.lang3)
|
||||||
implementation(libs.commons.csv)
|
api(libs.commons.csv)
|
||||||
implementation(libs.commons.net)
|
api(libs.commons.net)
|
||||||
implementation(libs.commons.text)
|
api(libs.commons.text)
|
||||||
implementation(libs.commons.compress)
|
api(libs.commons.compress)
|
||||||
implementation(libs.commons.vfs2) { exclude(group = "*", module = "*") }
|
api(libs.commons.vfs2) { exclude(group = "*", module = "*") }
|
||||||
implementation(libs.kotlinx.coroutines.swing)
|
api(libs.kotlinx.coroutines.swing)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
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.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
||||||
implementation(libs.eddsa)
|
api(libs.eddsa)
|
||||||
implementation(libs.jnafilechooser)
|
api(libs.jnafilechooser)
|
||||||
implementation(libs.xodus.vfs)
|
api(libs.xodus.vfs)
|
||||||
implementation(libs.xodus.openAPI)
|
api(libs.xodus.openAPI)
|
||||||
implementation(libs.xodus.environment)
|
api(libs.xodus.environment)
|
||||||
implementation(libs.bip39)
|
api(libs.bip39)
|
||||||
implementation(libs.colorpicker)
|
api(libs.colorpicker)
|
||||||
implementation(libs.mixpanel)
|
api(libs.mixpanel)
|
||||||
implementation(libs.jSerialComm)
|
api(libs.jSerialComm)
|
||||||
implementation(libs.ini4j)
|
api(libs.ini4j)
|
||||||
implementation(libs.restart4j)
|
api(libs.restart4j)
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
@@ -133,20 +134,48 @@ application {
|
|||||||
args.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
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=ALL-UNNAMED")
|
||||||
args.add("--add-opens java.desktop/sun.lwawt.macosx=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()
|
||||||
}
|
}
|
||||||
@@ -356,10 +385,7 @@ tasks.register<Exec>("jpackage") {
|
|||||||
options.add("--add-opens java.desktop/sun.lwawt.macosx=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")
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "2.1.20"
|
kotlin = "2.1.21"
|
||||||
slf4j = "2.0.17"
|
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"
|
||||||
kotlinx-serialization-json = "1.8.1"
|
kotlinx-serialization-json = "1.8.1"
|
||||||
commons-codec = "1.18.0"
|
commons-codec = "1.18.0"
|
||||||
commons-lang3 = "3.17.0"
|
commons-lang3 = "3.17.0"
|
||||||
commons-csv = "1.14.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"
|
||||||
commons-vfs2="2.10.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.17.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"
|
||||||
hutool = "5.8.37"
|
hutool = "5.8.37"
|
||||||
jsch = "0.2.21"
|
jsch = "0.2.26"
|
||||||
okhttp = "4.12.0"
|
okhttp = "4.12.0"
|
||||||
sshj = "0.39.0"
|
sshj = "0.39.0"
|
||||||
sshd-core = "2.15.0"
|
sshd-core = "2.15.0"
|
||||||
@@ -35,7 +35,7 @@ bip39 = "1.0.9"
|
|||||||
colorpicker = "2.0.1"
|
colorpicker = "2.0.1"
|
||||||
rhino = "1.8.0"
|
rhino = "1.8.0"
|
||||||
delight-rhino-sandbox = "0.0.17"
|
delight-rhino-sandbox = "0.0.17"
|
||||||
testcontainers = "1.20.6"
|
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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import java.security.GeneralSecurityException;
|
|||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public class CombinedKeyIdentityProvider implements KeyIdentityProvider {
|
public class CombinedKeyIdentityProvider implements KeyIdentityProvider {
|
||||||
|
|
||||||
private final List<KeyIdentityProvider> providers = new ArrayList<>();
|
private final List<KeyIdentityProvider> providers = new ArrayList<>();
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -28,8 +28,13 @@ import java.awt.MenuItem
|
|||||||
import java.awt.PopupMenu
|
import java.awt.PopupMenu
|
||||||
import java.awt.SystemTray
|
import java.awt.SystemTray
|
||||||
import java.awt.TrayIcon
|
import java.awt.TrayIcon
|
||||||
|
import java.awt.desktop.AppReopenedEvent
|
||||||
|
import java.awt.desktop.AppReopenedListener
|
||||||
|
import java.awt.desktop.SystemEventListener
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
import javax.imageio.ImageIO
|
import javax.imageio.ImageIO
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
@@ -64,6 +69,9 @@ class ApplicationRunner {
|
|||||||
fileSystemManager.filesCache = WeakRefFilesCache()
|
fileSystemManager.filesCache = WeakRefFilesCache()
|
||||||
fileSystemManager.init()
|
fileSystemManager.init()
|
||||||
VFS.setManager(fileSystemManager)
|
VFS.setManager(fileSystemManager)
|
||||||
|
|
||||||
|
// async init
|
||||||
|
BackgroundManager.getInstance().getBackgroundImage()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置 LAF
|
// 设置 LAF
|
||||||
@@ -78,9 +86,6 @@ class ApplicationRunner {
|
|||||||
// 启动主窗口
|
// 启动主窗口
|
||||||
val startMainFrame = measureTimeMillis { startMainFrame() }
|
val startMainFrame = measureTimeMillis { startMainFrame() }
|
||||||
|
|
||||||
// 设置托盘
|
|
||||||
val setupSystemTray = measureTimeMillis { SwingUtilities.invokeLater { setupSystemTray() } }
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("printSystemInfo: {}ms", printSystemInfo)
|
log.debug("printSystemInfo: {}ms", printSystemInfo)
|
||||||
log.debug("openDatabase: {}ms", openDatabase)
|
log.debug("openDatabase: {}ms", openDatabase)
|
||||||
@@ -89,7 +94,6 @@ class ApplicationRunner {
|
|||||||
log.debug("setupLaf: {}ms", setupLaf)
|
log.debug("setupLaf: {}ms", setupLaf)
|
||||||
log.debug("openDoor: {}ms", openDoor)
|
log.debug("openDoor: {}ms", openDoor)
|
||||||
log.debug("startMainFrame: {}ms", startMainFrame)
|
log.debug("startMainFrame: {}ms", startMainFrame)
|
||||||
log.debug("setupSystemTray: {}ms", setupSystemTray)
|
|
||||||
}
|
}
|
||||||
}.let {
|
}.let {
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
@@ -119,8 +123,24 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
TermoraFrameManager.getInstance().createWindow().isVisible = true
|
TermoraFrameManager.getInstance().createWindow().isVisible = true
|
||||||
|
|
||||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
if (SystemInfo.isMacOS) {
|
||||||
SwingUtilities.invokeLater { FlatDesktop.setQuitHandler { quitHandler() } }
|
SwingUtilities.invokeLater {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 设置 Dock
|
||||||
|
setupMacOSDock()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command + Q
|
||||||
|
FlatDesktop.setQuitHandler { quitHandler() }
|
||||||
|
}
|
||||||
|
} else if (SystemInfo.isWindows) {
|
||||||
|
// 设置托盘
|
||||||
|
SwingUtilities.invokeLater { setupSystemTray() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,9 +176,13 @@ class ApplicationRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun quitHandler() {
|
private fun quitHandler() {
|
||||||
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
val windows = TermoraFrameManager.getInstance().getWindows()
|
||||||
frame.dispose()
|
|
||||||
|
for (frame in windows) {
|
||||||
|
frame.dispatchEvent(WindowEvent(frame, WindowEvent.WINDOW_CLOSED))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Disposer.dispose(TermoraFrameManager.getInstance())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadSettings() {
|
private fun loadSettings() {
|
||||||
@@ -240,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() {
|
||||||
@@ -303,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)
|
||||||
|
|||||||
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()) {
|
||||||
|
|||||||
@@ -523,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)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 光标闪烁
|
* 光标闪烁
|
||||||
*/
|
*/
|
||||||
@@ -643,6 +648,16 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
*/
|
*/
|
||||||
var backgroundRunning by BooleanPropertyDelegate(false)
|
var backgroundRunning by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签关闭前确认
|
||||||
|
*/
|
||||||
|
var confirmTabClose by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 背景图片的地址
|
||||||
|
*/
|
||||||
|
var backgroundImage by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 语言
|
* 语言
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ enum class Protocol {
|
|||||||
SSH,
|
SSH,
|
||||||
Local,
|
Local,
|
||||||
Serial,
|
Serial,
|
||||||
|
RDP,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 交互式的 SFTP,此协议只在系统内部交互不应该暴露给用户也不应该持久化
|
* 交互式的 SFTP,此协议只在系统内部交互不应该暴露给用户也不应该持久化
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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) {
|
||||||
@@ -24,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()
|
||||||
}
|
}
|
||||||
@@ -54,7 +56,8 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
|||||||
isEnabled = false
|
isEnabled = false
|
||||||
|
|
||||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
testConnection(pane.getHost())
|
// 因为测试连接的时候从数据库读取会导致失效,所以这里生成随机ID
|
||||||
|
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
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
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.PageantConnector
|
||||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocketConnector
|
import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocketConnector
|
||||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.WinPipeConnector
|
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.*
|
||||||
@@ -221,7 +223,24 @@ 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)
|
||||||
@@ -320,6 +339,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
protocolTypeComboBox.addItem(Protocol.SSH)
|
protocolTypeComboBox.addItem(Protocol.SSH)
|
||||||
protocolTypeComboBox.addItem(Protocol.Local)
|
protocolTypeComboBox.addItem(Protocol.Local)
|
||||||
protocolTypeComboBox.addItem(Protocol.Serial)
|
protocolTypeComboBox.addItem(Protocol.Serial)
|
||||||
|
protocolTypeComboBox.addItem(Protocol.RDP)
|
||||||
|
|
||||||
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||||
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class HostTreeNode(host: Host) : SimpleTreeNode<Host>(host) {
|
|||||||
return when (host.protocol) {
|
return when (host.protocol) {
|
||||||
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||||
Protocol.Serial -> if (selected && hasFocus) Icons.plugin.dark else Icons.plugin
|
Protocol.Serial -> if (selected && hasFocus) Icons.plugin.dark else Icons.plugin
|
||||||
|
Protocol.RDP -> if (selected && hasFocus) Icons.microsoftWindows.dark else Icons.microsoftWindows
|
||||||
else -> if (selected && hasFocus) Icons.terminal.dark else Icons.terminal
|
else -> if (selected && hasFocus) Icons.terminal.dark else Icons.terminal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ object Icons {
|
|||||||
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
|
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
|
||||||
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
|
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
|
||||||
val close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") }
|
val close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") }
|
||||||
|
val settingSync by lazy { DynamicIcon("icons/settingSync.svg", "icons/settingSync_dark.svg") }
|
||||||
val openInNewWindow by lazy { DynamicIcon("icons/openInNewWindow.svg", "icons/openInNewWindow_dark.svg") }
|
val openInNewWindow by lazy { DynamicIcon("icons/openInNewWindow.svg", "icons/openInNewWindow_dark.svg") }
|
||||||
val openInToolWindow by lazy { DynamicIcon("icons/openInToolWindow.svg", "icons/openInToolWindow_dark.svg") }
|
val openInToolWindow by lazy { DynamicIcon("icons/openInToolWindow.svg", "icons/openInToolWindow_dark.svg") }
|
||||||
val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
|
val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
|
||||||
@@ -63,6 +64,7 @@ object Icons {
|
|||||||
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
|
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
|
||||||
val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") }
|
val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") }
|
||||||
val microsoft by lazy { DynamicIcon("icons/microsoft.svg", "icons/microsoft_dark.svg") }
|
val microsoft by lazy { DynamicIcon("icons/microsoft.svg", "icons/microsoft_dark.svg") }
|
||||||
|
val microsoftWindows by lazy { DynamicIcon("icons/microsoftWindows.svg", "icons/microsoftWindows_dark.svg") }
|
||||||
val tencent by lazy { DynamicIcon("icons/tencent.svg") }
|
val tencent by lazy { DynamicIcon("icons/tencent.svg") }
|
||||||
val google by lazy { DynamicIcon("icons/google-small.svg") }
|
val google by lazy { DynamicIcon("icons/google-small.svg") }
|
||||||
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }
|
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
|
import app.termora.terminal.PtyConnectorDelegate
|
||||||
|
import app.termora.terminal.PtyProcessConnector
|
||||||
import org.apache.commons.io.Charsets
|
import org.apache.commons.io.Charsets
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
||||||
class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
||||||
PtyHostTerminalTab(windowScope, host) {
|
PtyHostTerminalTab(windowScope, host) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(LocalTerminalTab::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun openPtyConnector(): PtyConnector {
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
val winSize = terminalPanel.winSize()
|
val winSize = terminalPanel.winSize()
|
||||||
val ptyConnector = PtyConnectorFactory.getInstance().createPtyConnector(
|
val ptyConnector = PtyConnectorFactory.getInstance().createPtyConnector(
|
||||||
@@ -18,4 +28,42 @@ class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
return ptyConnector
|
return ptyConnector
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun willBeClose(): Boolean {
|
||||||
|
val ptyProcessConnector = getPtyProcessConnector() ?: return true
|
||||||
|
val process = ptyProcessConnector.process
|
||||||
|
var consoleProcessCount = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
val processHandle = ProcessHandle.of(process.pid()).getOrNull()
|
||||||
|
if (processHandle != null) {
|
||||||
|
consoleProcessCount = processHandle.children().count().toInt()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有正在运行的进程
|
||||||
|
if (consoleProcessCount < 1) return true
|
||||||
|
|
||||||
|
val owner = SwingUtilities.getWindowAncestor(terminalPanel) ?: return true
|
||||||
|
return OptionPane.showConfirmDialog(
|
||||||
|
owner,
|
||||||
|
I18n.getString("termora.tabbed.local-tab.close-prompt"),
|
||||||
|
messageType = JOptionPane.INFORMATION_MESSAGE,
|
||||||
|
optionType = JOptionPane.OK_CANCEL_OPTION
|
||||||
|
) == JOptionPane.OK_OPTION
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun getPtyProcessConnector(): PtyProcessConnector? {
|
||||||
|
var p = getPtyConnector() as PtyConnector?
|
||||||
|
while (p != null) {
|
||||||
|
if (p is PtyProcessConnector) return p
|
||||||
|
if (p is PtyConnectorDelegate) p = p.ptyConnector
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
import com.formdev.flatlaf.ui.FlatRootPaneUI
|
|
||||||
import com.formdev.flatlaf.ui.FlatTitlePane
|
|
||||||
|
|
||||||
class MyFlatRootPaneUI : FlatRootPaneUI() {
|
|
||||||
|
|
||||||
fun getTitlePane(): FlatTitlePane? {
|
|
||||||
return super.titlePane
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@ import java.awt.event.*
|
|||||||
import java.awt.image.BufferedImage
|
import java.awt.image.BufferedImage
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.plaf.TabbedPaneUI
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
class MyTabbedPane : FlatTabbedPane() {
|
class MyTabbedPane : FlatTabbedPane() {
|
||||||
@@ -21,18 +20,12 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
private val owner
|
private val owner
|
||||||
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||||
.getData(DataProviders.TermoraFrame) as TermoraFrame
|
.getData(DataProviders.TermoraFrame) as TermoraFrame
|
||||||
private val myUI = MyFlatTabbedPaneUI()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
isFocusable = false
|
isFocusable = false
|
||||||
super.setUI(myUI)
|
|
||||||
initEvents()
|
initEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setUI(ui: TabbedPaneUI?) {
|
|
||||||
super.setUI(myUI)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateUI() {
|
override fun updateUI() {
|
||||||
styleMap = mapOf(
|
styleMap = mapOf(
|
||||||
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground"),
|
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground"),
|
||||||
|
|||||||
@@ -37,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 {
|
||||||
@@ -97,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
|
||||||
@@ -110,15 +111,15 @@ 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)
|
||||||
@@ -137,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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -852,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
|
||||||
@@ -887,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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class NewHostTreeDialog(
|
|||||||
|
|
||||||
|
|
||||||
init()
|
init()
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(owner)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,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 {
|
||||||
@@ -222,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) {
|
||||||
|
|||||||
@@ -151,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -33,12 +33,18 @@ 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 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.Dispatchers
|
||||||
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 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
|
||||||
@@ -57,6 +63,7 @@ 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 java.util.function.Consumer
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
@@ -129,11 +136,15 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
val themeComboBox = FlatComboBox<String>()
|
val themeComboBox = FlatComboBox<String>()
|
||||||
val languageComboBox = FlatComboBox<String>()
|
val languageComboBox = FlatComboBox<String>()
|
||||||
val backgroundComBoBox = YesOrNoComboBox()
|
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 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()
|
||||||
@@ -142,7 +153,21 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
|
|
||||||
backgroundComBoBox.isEnabled = SystemInfo.isWindows
|
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.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows
|
||||||
opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) {
|
opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) {
|
||||||
@@ -160,6 +185,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
followSystemCheckBox.isSelected = appearance.followSystem
|
followSystemCheckBox.isSelected = appearance.followSystem
|
||||||
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
|
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
|
||||||
backgroundComBoBox.selectedItem = appearance.backgroundRunning
|
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) }
|
||||||
@@ -210,6 +236,13 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
@@ -239,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 {
|
||||||
@@ -308,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"
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
)
|
)
|
||||||
val box = FlatToolBar()
|
val box = FlatToolBar()
|
||||||
box.add(followSystemCheckBox)
|
box.add(followSystemCheckBox)
|
||||||
@@ -330,11 +403,24 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
})).xy(5, rows).apply { rows += step }
|
})).xy(5, rows).apply { rows += step }
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
builder.add("${I18n.getString("termora.settings.appearance.opacity")}:").xy(1, rows)
|
||||||
.add(opacitySpinner).xy(3, rows).apply { rows += step }
|
.add(opacitySpinner).xy(3, rows).apply { rows += step }
|
||||||
|
|
||||||
builder.add("${I18n.getString("termora.settings.appearance.background-running")}:").xy(1, rows)
|
builder.add("${I18n.getString("termora.settings.appearance.background-running")}:").xy(1, rows)
|
||||||
.add(backgroundComBoBox).xy(3, 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()
|
return builder.build()
|
||||||
}
|
}
|
||||||
@@ -355,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()
|
||||||
@@ -432,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
|
||||||
@@ -510,20 +604,33 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
shellComboBox.selectedItem = terminalSetting.localShell
|
shellComboBox.selectedItem = terminalSetting.localShell
|
||||||
|
|
||||||
val fonts = linkedSetOf("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)
|
fontComboBox.addPopupMenuListener(object : PopupMenuListener {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (font in fonts) {
|
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {}
|
||||||
fontComboBox.addItem(font)
|
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
|
||||||
@@ -546,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)
|
||||||
@@ -569,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)
|
||||||
@@ -595,7 +704,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
val gistTextField = OutlineTextField(255)
|
val gistTextField = OutlineTextField(255)
|
||||||
val policyComboBox = JComboBox<SyncPolicy>()
|
val policyComboBox = JComboBox<SyncPolicy>()
|
||||||
val domainTextField = OutlineTextField(255)
|
val domainTextField = OutlineTextField(255)
|
||||||
val syncConfigButton = JButton(I18n.getString("termora.settings.sync"), Icons.download)
|
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 lastSyncTimeLabel = JLabel()
|
val lastSyncTimeLabel = JLabel()
|
||||||
@@ -1409,6 +1518,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private val sftpCommandField = OutlineTextField(255)
|
private val sftpCommandField = OutlineTextField(255)
|
||||||
private val defaultDirectoryField = OutlineTextField(255)
|
private val defaultDirectoryField = OutlineTextField(255)
|
||||||
private val browseDirectoryBtn = JButton(Icons.folder)
|
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 preserveModificationTimeComboBox = YesOrNoComboBox()
|
||||||
private val sftp get() = database.sftp
|
private val sftp get() = database.sftp
|
||||||
@@ -1480,6 +1590,41 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1496,6 +1641,8 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
sftpCommandField.placeholderText = "sftp"
|
sftpCommandField.placeholderText = "sftp"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editCommandField.trailingComponent = browseEditCommandBtn
|
||||||
|
|
||||||
defaultDirectoryField.placeholderText = SystemUtils.USER_HOME
|
defaultDirectoryField.placeholderText = SystemUtils.USER_HOME
|
||||||
defaultDirectoryField.trailingComponent = browseDirectoryBtn
|
defaultDirectoryField.trailingComponent = browseDirectoryBtn
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ 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.channel.PtyChannelConfigurationHolder
|
||||||
import org.apache.sshd.common.cipher.CipherNone
|
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.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.CloseFuture
|
||||||
@@ -47,6 +48,7 @@ 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.Session
|
||||||
import org.apache.sshd.common.session.SessionListener
|
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
|
||||||
@@ -263,9 +265,14 @@ object SshClients {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e !is SshException || e.disconnectCode != SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) throw e
|
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 owner = client.properties["owner"] as Window? ?: throw e
|
||||||
val authentication = ask(host, owner) ?: throw e
|
val askUserInfo = ask(host, entry, owner) ?: throw e
|
||||||
if (authentication.type == AuthenticationType.No) throw e
|
if (askUserInfo.authentication.type == AuthenticationType.No) throw e
|
||||||
return doOpenSession(host.copy(authentication = authentication), client)
|
return doOpenSession(
|
||||||
|
host.copy(
|
||||||
|
authentication = askUserInfo.authentication,
|
||||||
|
username = askUserInfo.username
|
||||||
|
), client
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
session.setAttribute(HOST_KEY, host)
|
session.setAttribute(HOST_KEY, host)
|
||||||
@@ -339,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 {
|
||||||
@@ -394,21 +419,33 @@ object SshClients {
|
|||||||
return sshClient
|
return sshClient
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ask(host: Host, owner: Window): Authentication? {
|
|
||||||
val ref = AtomicReference<Authentication>(null)
|
private data class AskUserInfo(val username: String, val authentication: Authentication)
|
||||||
|
|
||||||
|
private fun ask(host: Host, entry: HostConfigEntry, owner: Window): AskUserInfo? {
|
||||||
|
val ref = AtomicReference<AskUserInfo>(null)
|
||||||
|
|
||||||
SwingUtilities.invokeAndWait {
|
SwingUtilities.invokeAndWait {
|
||||||
val dialog = RequestAuthenticationDialog(owner, host)
|
val dialog = RequestAuthenticationDialog(owner, host)
|
||||||
dialog.setLocationRelativeTo(owner)
|
dialog.setLocationRelativeTo(owner)
|
||||||
val authentication = dialog.getAuthentication().apply { ref.set(this) }
|
val authentication = dialog.getAuthentication()
|
||||||
|
ref.set(AskUserInfo(dialog.getUsername(), authentication))
|
||||||
|
|
||||||
// save
|
// save
|
||||||
if (dialog.isRemembered()) {
|
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(
|
hostManager.addHost(
|
||||||
host.copy(
|
h.copy(
|
||||||
authentication = authentication,
|
authentication = authentication,
|
||||||
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ref.get()
|
return ref.get()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import app.termora.actions.AnActionEvent
|
|||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.actions.MultipleAction
|
import app.termora.actions.MultipleAction
|
||||||
import app.termora.highlight.KeywordHighlightPaintListener
|
import app.termora.highlight.KeywordHighlightPaintListener
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
import app.termora.terminal.Terminal
|
import app.termora.terminal.Terminal
|
||||||
import app.termora.terminal.panel.TerminalHyperlinkPaintListener
|
import app.termora.terminal.panel.TerminalHyperlinkPaintListener
|
||||||
@@ -40,6 +41,10 @@ class TerminalPanelFactory : Disposable {
|
|||||||
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
||||||
val writer = MyTerminalWriter(ptyConnector)
|
val writer = MyTerminalWriter(ptyConnector)
|
||||||
val terminalPanel = TerminalPanel(terminal, writer)
|
val terminalPanel = TerminalPanel(terminal, writer)
|
||||||
|
|
||||||
|
// processDeviceStatusReport
|
||||||
|
terminal.getTerminalModel().setData(DataKey.TerminalWriter, writer)
|
||||||
|
|
||||||
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
||||||
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
||||||
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
||||||
|
|||||||
@@ -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,8 +44,16 @@ interface TerminalTab : Disposable, DataProvider {
|
|||||||
*/
|
*/
|
||||||
fun canClose(): Boolean = true
|
fun canClose(): Boolean = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示可以关闭,只有当 [Appearance.confirmTabClose] 为 false 时才会调用
|
||||||
|
*/
|
||||||
fun willBeClose(): Boolean = true
|
fun willBeClose(): Boolean = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 即将关闭,已经无法挽回
|
||||||
|
*/
|
||||||
|
fun beforeClose() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否可以克隆
|
* 是否可以克隆
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -32,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) {
|
||||||
@@ -153,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.willBeClose()) {
|
// 如果开启了关闭确认,那么直接询问用户
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -400,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,12 +7,13 @@ import app.termora.actions.DataProviders
|
|||||||
import app.termora.sftp.SFTPTab
|
import app.termora.sftp.SFTPTab
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.FlatLaf
|
||||||
|
import com.formdev.flatlaf.ui.FlatRootPaneUI
|
||||||
|
import com.formdev.flatlaf.ui.FlatTitlePane
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jetbrains.JBR
|
import com.jetbrains.JBR
|
||||||
import org.apache.commons.lang3.ArrayUtils
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
import java.awt.BorderLayout
|
import java.awt.*
|
||||||
import java.awt.Dimension
|
|
||||||
import java.awt.Insets
|
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.awt.event.MouseListener
|
import java.awt.event.MouseListener
|
||||||
@@ -37,12 +38,11 @@ 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 tabbedPane = MyTabbedPane()
|
private val tabbedPane = MyTabbedPane()
|
||||||
private val toolbar = TermoraToolBar(windowScope, this, tabbedPane)
|
private val toolbar = TermoraToolBar(windowScope, this)
|
||||||
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane)
|
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane)
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
private val welcomePanel = WelcomePanel(windowScope)
|
private val welcomePanel = WelcomePanel(windowScope)
|
||||||
private val sftp get() = Database.getDatabase().sftp
|
private val sftp get() = Database.getDatabase().sftp
|
||||||
private val myUI = MyFlatRootPaneUI()
|
|
||||||
private var notifyListeners = emptyArray<NotifyListener>()
|
private var notifyListeners = emptyArray<NotifyListener>()
|
||||||
|
|
||||||
|
|
||||||
@@ -63,10 +63,9 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun mouseDragged(e: MouseEvent) {
|
override fun mouseDragged(e: MouseEvent) {
|
||||||
val mouseLayer = getMouseLayer() ?: return
|
|
||||||
getMouseMotionListener()?.mouseDragged(
|
getMouseMotionListener()?.mouseDragged(
|
||||||
MouseEvent(
|
MouseEvent(
|
||||||
mouseLayer,
|
e.component,
|
||||||
e.id,
|
e.id,
|
||||||
e.`when`,
|
e.`when`,
|
||||||
e.modifiersEx,
|
e.modifiersEx,
|
||||||
@@ -87,19 +86,19 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
return getHandler() as? MouseMotionListener
|
return getHandler() as? MouseMotionListener
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMouseLayer(): JComponent? {
|
|
||||||
val titlePane = myUI.getTitlePane() ?: return null
|
|
||||||
val handlerField = titlePane.javaClass.getDeclaredField("mouseLayer") ?: return null
|
|
||||||
handlerField.isAccessible = true
|
|
||||||
return handlerField.get(titlePane) as? JComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getHandler(): Any? {
|
private fun getHandler(): Any? {
|
||||||
val titlePane = myUI.getTitlePane() ?: return null
|
val titlePane = getTitlePane() ?: return null
|
||||||
val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null
|
val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null
|
||||||
handlerField.isAccessible = true
|
handlerField.isAccessible = true
|
||||||
return handlerField.get(titlePane)
|
return handlerField.get(titlePane)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getTitlePane(): FlatTitlePane? {
|
||||||
|
val ui = rootPane.ui as? FlatRootPaneUI ?: return null
|
||||||
|
val titlePaneField = ui.javaClass.getDeclaredField("titlePane")
|
||||||
|
titlePaneField.isAccessible = true
|
||||||
|
return titlePaneField.get(ui) as? FlatTitlePane
|
||||||
|
}
|
||||||
}
|
}
|
||||||
toolbar.getJToolBar().addMouseListener(mouseAdapter)
|
toolbar.getJToolBar().addMouseListener(mouseAdapter)
|
||||||
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
||||||
@@ -173,7 +172,6 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
// Windows 10 会有1像素误差
|
// Windows 10 会有1像素误差
|
||||||
tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0)
|
tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0)
|
||||||
} else if (SystemInfo.isLinux) {
|
} else if (SystemInfo.isLinux) {
|
||||||
rootPane.setUI(myUI)
|
|
||||||
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
|
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +211,11 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val glassPane = GlassPane()
|
||||||
|
rootPane.glassPane = glassPane
|
||||||
|
glassPane.isOpaque = false
|
||||||
|
glassPane.isVisible = true
|
||||||
|
|
||||||
|
|
||||||
Disposer.register(windowScope, terminalTabbed)
|
Disposer.register(windowScope, terminalTabbed)
|
||||||
add(terminalTabbed, BorderLayout.CENTER)
|
add(terminalTabbed, BorderLayout.CENTER)
|
||||||
@@ -254,4 +257,27 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
super.addNotify()
|
super.addNotify()
|
||||||
notifyListeners.forEach { it.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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@ import java.awt.Frame
|
|||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
import java.awt.event.WindowAdapter
|
import java.awt.event.WindowAdapter
|
||||||
import java.awt.event.WindowEvent
|
import java.awt.event.WindowEvent
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.swing.JFrame
|
import javax.swing.JFrame
|
||||||
import javax.swing.JOptionPane
|
import javax.swing.JOptionPane
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
@@ -24,7 +25,7 @@ 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)
|
||||||
@@ -37,11 +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
|
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)
|
||||||
@@ -80,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) {
|
||||||
|
|
||||||
@@ -95,31 +98,49 @@ class TermoraFrameManager {
|
|||||||
Disposer.dispose(windowScope)
|
Disposer.dispose(windowScope)
|
||||||
|
|
||||||
val windowScopes = ApplicationScope.windowScopes()
|
val windowScopes = ApplicationScope.windowScopes()
|
||||||
|
if (windowScopes.isNotEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 如果已经没有 Window 域了,那么就可以退出程序了
|
// 如果已经没有 Window 域了,那么就可以退出程序了
|
||||||
if (windowScopes.isEmpty()) {
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
this@TermoraFrameManager.dispose()
|
Disposer.dispose(manager)
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
// 如果 macOS 开启了后台运行,那么尽管所有窗口都没了,也不会退出
|
||||||
|
if (isBackgroundRunning) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Disposer.dispose(manager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun windowClosing(e: WindowEvent) {
|
override fun windowClosing(e: WindowEvent) {
|
||||||
if (ApplicationScope.windowScopes().size == 1) {
|
if (ApplicationScope.windowScopes().size != 1) {
|
||||||
|
window.dispose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 Windows 开启了后台运行,那么最小化
|
||||||
if (SystemInfo.isWindows && isBackgroundRunning) {
|
if (SystemInfo.isWindows && isBackgroundRunning) {
|
||||||
// 最小化
|
// 最小化
|
||||||
window.extendedState = window.extendedState or JFrame.ICONIFIED
|
window.extendedState = window.extendedState or JFrame.ICONIFIED
|
||||||
// 隐藏
|
// 隐藏
|
||||||
window.isVisible = false
|
window.isVisible = false
|
||||||
} else {
|
return
|
||||||
if (OptionPane.showConfirmDialog(
|
}
|
||||||
|
|
||||||
|
// 如果 macOS 已经开启了后台运行,那么直接销毁,因为会有一个进程驻守
|
||||||
|
if (SystemInfo.isMacOS && isBackgroundRunning) {
|
||||||
|
window.dispose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val option = OptionPane.showConfirmDialog(
|
||||||
window,
|
window,
|
||||||
I18n.getString("termora.quit-confirm", Application.getName()),
|
I18n.getString("termora.quit-confirm", Application.getName()),
|
||||||
optionType = JOptionPane.YES_NO_OPTION,
|
optionType = JOptionPane.YES_NO_OPTION,
|
||||||
) == JOptionPane.YES_OPTION
|
)
|
||||||
) {
|
if (option == JOptionPane.YES_OPTION) {
|
||||||
window.dispose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
window.dispose()
|
window.dispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,7 +163,8 @@ class TermoraFrameManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dispose() {
|
override fun dispose() {
|
||||||
|
if (isDisposed.compareAndSet(false, true)) {
|
||||||
Disposer.dispose(ApplicationScope.forApplicationScope())
|
Disposer.dispose(ApplicationScope.forApplicationScope())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -152,6 +174,7 @@ class TermoraFrameManager {
|
|||||||
log.error(e.message, e)
|
log.error(e.message, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exitProcess(0)
|
exitProcess(0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import app.termora.actions.*
|
|||||||
import app.termora.findeverywhere.FindEverywhereAction
|
import app.termora.findeverywhere.FindEverywhereAction
|
||||||
import app.termora.snippet.SnippetAction
|
import app.termora.snippet.SnippetAction
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
@@ -26,10 +25,21 @@ data class ToolBarAction(
|
|||||||
class TermoraToolBar(
|
class TermoraToolBar(
|
||||||
private val windowScope: WindowScope,
|
private val windowScope: WindowScope,
|
||||||
private val frame: TermoraFrame,
|
private val frame: TermoraFrame,
|
||||||
private val tabbedPane: FlatTabbedPane
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -87,11 +97,45 @@ class TermoraToolBar(
|
|||||||
return storageActions
|
return storageActions
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rebuild() {
|
private inner class MyToolBar : JToolBar() {
|
||||||
rebuild(this.toolbar)
|
init {
|
||||||
|
// 监听窗口大小变动,然后修改边距避开控制按钮
|
||||||
|
addComponentListener(object : ComponentAdapter() {
|
||||||
|
override fun componentResized(e: ComponentEvent) {
|
||||||
|
adjust()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun rebuild(toolbar: JToolBar) {
|
fun adjust() {
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
val rectangle =
|
||||||
|
frame.rootPane.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
||||||
|
as? Rectangle ?: return
|
||||||
|
val right = rectangle.width
|
||||||
|
val toolbar = this@MyToolBar
|
||||||
|
for (i in 0 until toolbar.componentCount) {
|
||||||
|
val c = toolbar.getComponent(i)
|
||||||
|
if (c.name == "spacing") {
|
||||||
|
if (c.width == right) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toolbar.remove(i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (right > 0) {
|
||||||
|
val spacing = Box.createHorizontalStrut(right)
|
||||||
|
spacing.name = "spacing"
|
||||||
|
toolbar.add(spacing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun rebuild() {
|
||||||
|
val toolbar: JToolBar = this
|
||||||
val actionManager = ActionManager.getInstance()
|
val actionManager = ActionManager.getInstance()
|
||||||
val actionContainerFactory = ActionContainerFactory(actionManager)
|
val actionContainerFactory = ActionContainerFactory(actionManager)
|
||||||
|
|
||||||
@@ -143,41 +187,5 @@ class TermoraToolBar(
|
|||||||
toolbar.revalidate()
|
toolbar.revalidate()
|
||||||
toolbar.repaint()
|
toolbar.repaint()
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class MyToolBar : JToolBar() {
|
|
||||||
init {
|
|
||||||
// 监听窗口大小变动,然后修改边距避开控制按钮
|
|
||||||
addComponentListener(object : ComponentAdapter() {
|
|
||||||
override fun componentResized(e: ComponentEvent) {
|
|
||||||
adjust()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun adjust() {
|
|
||||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
|
||||||
val rectangle =
|
|
||||||
frame.rootPane.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
|
||||||
as? Rectangle ?: return
|
|
||||||
val right = rectangle.width
|
|
||||||
val toolbar = this@MyToolBar
|
|
||||||
for (i in 0 until toolbar.componentCount) {
|
|
||||||
val c = toolbar.getComponent(i)
|
|
||||||
if (c.name == "spacing") {
|
|
||||||
if (c.width == right) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
toolbar.remove(i)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (right > 0) {
|
|
||||||
val spacing = Box.createHorizontalStrut(right)
|
|
||||||
spacing.name = "spacing"
|
|
||||||
toolbar.add(spacing)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tab is TerminalTab) {
|
||||||
terminalTabbedManager.addTerminalTab(tab)
|
terminalTabbedManager.addTerminalTab(tab)
|
||||||
|
if (tab is PtyHostTerminalTab) {
|
||||||
tab.start()
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
|
|||||||
val customBtn = JButton("Custom")
|
val customBtn = JButton("Custom")
|
||||||
customBtn.addActionListener {
|
customBtn.addActionListener {
|
||||||
val dialog = MyColorPickerDialog(this)
|
val dialog = MyColorPickerDialog(this)
|
||||||
|
dialog.setLocationRelativeTo(this)
|
||||||
dialog.colorPicker.color = defaultColor
|
dialog.colorPicker.color = defaultColor
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
val color = dialog.color
|
val color = dialog.color
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ data class KeywordHighlight(
|
|||||||
*/
|
*/
|
||||||
val matchCase: Boolean = false,
|
val matchCase: Boolean = false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否是正则表达式
|
||||||
|
*/
|
||||||
|
val regex: Boolean = false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 0 是取前景色
|
* 0 是取前景色
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -20,10 +20,8 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
private val model = KeywordHighlightTableModel()
|
private val model = KeywordHighlightTableModel()
|
||||||
private val table = FlatTable()
|
private val table = FlatTable()
|
||||||
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
|
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
|
||||||
private val colorPalette by lazy {
|
private val terminal by lazy { TerminalFactory.getInstance().createTerminal() }
|
||||||
TerminalFactory.getInstance().createTerminal().getTerminalModel()
|
private val colorPalette by lazy { terminal.getTerminalModel().getColorPalette() }
|
||||||
.getColorPalette()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
|
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
|
||||||
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
|
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
|
||||||
@@ -130,6 +128,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
|
|
||||||
addBtn.addActionListener {
|
addBtn.addActionListener {
|
||||||
val dialog = NewKeywordHighlightDialog(this, colorPalette)
|
val dialog = NewKeywordHighlightDialog(this, colorPalette)
|
||||||
|
dialog.setLocationRelativeTo(this)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
val keywordHighlight = dialog.keywordHighlight
|
val keywordHighlight = dialog.keywordHighlight
|
||||||
if (keywordHighlight != null) {
|
if (keywordHighlight != null) {
|
||||||
@@ -143,6 +142,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
if (row > -1) {
|
if (row > -1) {
|
||||||
var keywordHighlight = model.getKeywordHighlight(row)
|
var keywordHighlight = model.getKeywordHighlight(row)
|
||||||
val dialog = NewKeywordHighlightDialog(this, colorPalette)
|
val dialog = NewKeywordHighlightDialog(this, colorPalette)
|
||||||
|
dialog.setLocationRelativeTo(this)
|
||||||
dialog.keywordTextField.text = keywordHighlight.keyword
|
dialog.keywordTextField.text = keywordHighlight.keyword
|
||||||
dialog.descriptionTextField.text = keywordHighlight.description
|
dialog.descriptionTextField.text = keywordHighlight.description
|
||||||
|
|
||||||
@@ -176,6 +176,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
dialog.underlineCheckBox.isSelected = keywordHighlight.underline
|
dialog.underlineCheckBox.isSelected = keywordHighlight.underline
|
||||||
dialog.lineThroughCheckBox.isSelected = keywordHighlight.lineThrough
|
dialog.lineThroughCheckBox.isSelected = keywordHighlight.lineThrough
|
||||||
dialog.matchCaseBtn.isSelected = keywordHighlight.matchCase
|
dialog.matchCaseBtn.isSelected = keywordHighlight.matchCase
|
||||||
|
dialog.regexBtn.isSelected = keywordHighlight.regex
|
||||||
|
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -645,9 +645,13 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ohKeyPair.remark.isEmpty()) {
|
|
||||||
ohKeyPair = ohKeyPair.copy(
|
ohKeyPair = ohKeyPair.copy(
|
||||||
name = nameTextField.text,
|
name = nameTextField.text,
|
||||||
|
remark = remarkTextField.text,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (ohKeyPair.remark.isEmpty()) {
|
||||||
|
ohKeyPair = ohKeyPair.copy(
|
||||||
remark = "Import on " + DateFormatUtils.format(Date(), I18n.getString("termora.date-format"))
|
remark = "Import on " + DateFormatUtils.format(Date(), I18n.getString("termora.date-format"))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class MacroDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
initEvents()
|
initEvents()
|
||||||
|
|
||||||
init()
|
init()
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(owner)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
|
|||||||
9
src/main/kotlin/app/termora/sftp/FileSystemProvider.kt
Normal file
9
src/main/kotlin/app/termora/sftp/FileSystemProvider.kt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package app.termora.sftp
|
||||||
|
|
||||||
|
import org.apache.commons.vfs2.FileSystem
|
||||||
|
|
||||||
|
|
||||||
|
interface FileSystemProvider {
|
||||||
|
fun getFileSystem(): FileSystem
|
||||||
|
fun setFileSystem(fileSystem: FileSystem)
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ import javax.swing.filechooser.FileSystemView
|
|||||||
import kotlin.io.path.absolutePathString
|
import kotlin.io.path.absolutePathString
|
||||||
|
|
||||||
class FileSystemViewNav(
|
class FileSystemViewNav(
|
||||||
private val fileSystem: org.apache.commons.vfs2.FileSystem,
|
private val fileSystemProvider: FileSystemProvider,
|
||||||
private val homeDirectory: FileObject
|
private val homeDirectory: FileObject
|
||||||
) : JPanel(BorderLayout()) {
|
) : JPanel(BorderLayout()) {
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ class FileSystemViewNav(
|
|||||||
add(layeredPane, BorderLayout.CENTER)
|
add(layeredPane, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
|
||||||
if (SystemInfo.isWindows && fileSystem is LocalFileSystem) {
|
if (SystemInfo.isWindows && fileSystemProvider.getFileSystem() is LocalFileSystem) {
|
||||||
try {
|
try {
|
||||||
for (root in fileSystemView.roots) {
|
for (root in fileSystemView.roots) {
|
||||||
history.add(root.absolutePath)
|
history.add(root.absolutePath)
|
||||||
@@ -174,9 +174,14 @@ class FileSystemViewNav(
|
|||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
val name = textField.text.trim()
|
val name = textField.text.trim()
|
||||||
if (name.isBlank()) return
|
if (name.isBlank()) return
|
||||||
|
val fileSystem = fileSystemProvider.getFileSystem()
|
||||||
try {
|
try {
|
||||||
if (fileSystem is LocalFileSystem && SystemUtils.IS_OS_WINDOWS) {
|
if (fileSystem is LocalFileSystem && SystemUtils.IS_OS_WINDOWS) {
|
||||||
changeSelectedPath(fileSystem.resolveFile("file://${name}"))
|
val file = VFS.getManager().resolveFile("file://${name}")
|
||||||
|
if (!StringUtils.equals(file.fileSystem.rootURI, fileSystemProvider.getFileSystem().rootURI)) {
|
||||||
|
fileSystemProvider.setFileSystem(file.fileSystem)
|
||||||
|
}
|
||||||
|
changeSelectedPath(file)
|
||||||
} else {
|
} else {
|
||||||
changeSelectedPath(fileSystem.resolveFile(name))
|
changeSelectedPath(fileSystem.resolveFile(name))
|
||||||
}
|
}
|
||||||
@@ -192,6 +197,7 @@ class FileSystemViewNav(
|
|||||||
private fun showComboBoxPopup() {
|
private fun showComboBoxPopup() {
|
||||||
|
|
||||||
comboBox.removeAllItems()
|
comboBox.removeAllItems()
|
||||||
|
val fileSystem = fileSystemProvider.getFileSystem()
|
||||||
|
|
||||||
for (text in history) {
|
for (text in history) {
|
||||||
val path = if (SystemInfo.isWindows && fileSystem is LocalFileSystem) {
|
val path = if (SystemInfo.isWindows && fileSystem is LocalFileSystem) {
|
||||||
@@ -244,6 +250,13 @@ class FileSystemViewNav(
|
|||||||
textField.text = formatDisplayPath(file)
|
textField.text = formatDisplayPath(file)
|
||||||
textField.putClientProperty(PATH, file)
|
textField.putClientProperty(PATH, file)
|
||||||
|
|
||||||
|
val fileSystem = fileSystemProvider.getFileSystem()
|
||||||
|
if (SystemInfo.isWindows && fileSystem is LocalFileSystem) {
|
||||||
|
if (!StringUtils.equals(fileSystem.rootURI, file.fileSystem.rootURI)) {
|
||||||
|
fileSystemProvider.setFileSystem(file.fileSystem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (listener in listenerList.getListeners(ActionListener::class.java)) {
|
for (listener in listenerList.getListeners(ActionListener::class.java)) {
|
||||||
listener.actionPerformed(ActionEvent(this, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
listener.actionPerformed(ActionEvent(this, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,19 @@ import app.termora.actions.DataProvider
|
|||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.vfs2.sftp.MySftpFileSystem
|
import app.termora.vfs2.sftp.MySftpFileSystem
|
||||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.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.vfs2.FileObject
|
import org.apache.commons.vfs2.FileObject
|
||||||
|
import org.apache.commons.vfs2.FileSystem
|
||||||
|
import org.apache.commons.vfs2.VFS
|
||||||
|
import org.apache.commons.vfs2.provider.local.LocalFileSystem
|
||||||
import org.jdesktop.swingx.JXBusyLabel
|
import org.jdesktop.swingx.JXBusyLabel
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.event.*
|
import java.awt.event.*
|
||||||
@@ -22,14 +27,14 @@ import javax.swing.*
|
|||||||
|
|
||||||
class FileSystemViewPanel(
|
class FileSystemViewPanel(
|
||||||
val host: Host,
|
val host: Host,
|
||||||
val fileSystem: org.apache.commons.vfs2.FileSystem,
|
private var fileSystem: FileSystem,
|
||||||
private val transportManager: TransportManager,
|
private val transportManager: TransportManager,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
) : JPanel(BorderLayout()), Disposable, DataProvider {
|
) : JPanel(BorderLayout()), Disposable, DataProvider, FileSystemProvider {
|
||||||
|
|
||||||
private val properties get() = Database.getDatabase().properties
|
private val properties get() = Database.getDatabase().properties
|
||||||
private val sftp get() = Database.getDatabase().sftp
|
private val sftp get() = Database.getDatabase().sftp
|
||||||
private val table = FileSystemViewTable(fileSystem, transportManager, coroutineScope)
|
private val table = FileSystemViewTable(this, transportManager, coroutineScope)
|
||||||
private val disposed = AtomicBoolean(false)
|
private val disposed = AtomicBoolean(false)
|
||||||
private var nextReloadTicks = emptyArray<Consumer<Unit>>()
|
private var nextReloadTicks = emptyArray<Consumer<Unit>>()
|
||||||
private val isLoading = AtomicBoolean(false)
|
private val isLoading = AtomicBoolean(false)
|
||||||
@@ -37,7 +42,7 @@ class FileSystemViewPanel(
|
|||||||
private val loadingPanel = LoadingPanel()
|
private val loadingPanel = LoadingPanel()
|
||||||
private val layeredPane = LayeredPane()
|
private val layeredPane = LayeredPane()
|
||||||
private val homeDirectory = getHomeDirectory()
|
private val homeDirectory = getHomeDirectory()
|
||||||
private val nav = FileSystemViewNav(fileSystem, homeDirectory)
|
private val nav = FileSystemViewNav(this, homeDirectory)
|
||||||
private var workdir = homeDirectory
|
private var workdir = homeDirectory
|
||||||
private val model get() = table.model as FileSystemViewTableModel
|
private val model get() = table.model as FileSystemViewTableModel
|
||||||
private val showHiddenFilesKey = "termora.transport.host.${host.id}.show-hidden-files"
|
private val showHiddenFilesKey = "termora.transport.host.${host.id}.show-hidden-files"
|
||||||
@@ -172,10 +177,18 @@ class FileSystemViewPanel(
|
|||||||
bookmarkBtn.addBookmark(workdir.absolutePathString())
|
bookmarkBtn.addBookmark(workdir.absolutePathString())
|
||||||
}
|
}
|
||||||
bookmarkBtn.isBookmark = !bookmarkBtn.isBookmark
|
bookmarkBtn.isBookmark = !bookmarkBtn.isBookmark
|
||||||
|
} else {
|
||||||
|
if (fileSystem is LocalFileSystem && SystemUtils.IS_OS_WINDOWS) {
|
||||||
|
val file = VFS.getManager().resolveFile("file://${e.actionCommand}")
|
||||||
|
if (!StringUtils.equals(file.fileSystem.rootURI, fileSystem.rootURI)) {
|
||||||
|
fileSystem = file.fileSystem
|
||||||
|
}
|
||||||
|
changeWorkdir(file)
|
||||||
} else {
|
} else {
|
||||||
changeWorkdir(fileSystem.resolveFile(e.actionCommand))
|
changeWorkdir(fileSystem.resolveFile(e.actionCommand))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nav.addActionListener(object : AbstractAction() {
|
nav.addActionListener(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
@@ -192,8 +205,7 @@ class FileSystemViewPanel(
|
|||||||
button.addActionListener(object : AbstractAction() {
|
button.addActionListener(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
if (model.rowCount < 1) return
|
if (model.rowCount < 1) return
|
||||||
if (model.hasParent) return
|
if (model.hasParent) enterTableSelectionFolder(0)
|
||||||
enterTableSelectionFolder(0)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -373,6 +385,7 @@ class FileSystemViewPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getHomeDirectory(): FileObject {
|
private fun getHomeDirectory(): FileObject {
|
||||||
|
val fileSystem = this.fileSystem
|
||||||
if (fileSystem is MySftpFileSystem) {
|
if (fileSystem is MySftpFileSystem) {
|
||||||
val host = fileSystem.getClientSession().getAttribute(SshClients.HOST_KEY)
|
val host = fileSystem.getClientSession().getAttribute(SshClients.HOST_KEY)
|
||||||
?: return fileSystem.resolveFile(fileSystem.getDefaultDir())
|
?: return fileSystem.resolveFile(fileSystem.getDefaultDir())
|
||||||
@@ -384,8 +397,13 @@ class FileSystemViewPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sftp.defaultDirectory.isNotBlank()) {
|
if (sftp.defaultDirectory.isNotBlank()) {
|
||||||
val resolveFile = fileSystem.resolveFile("file://${sftp.defaultDirectory}")
|
val resolveFile = if (fileSystem is LocalFileSystem && SystemInfo.isWindows) {
|
||||||
|
VFS.getManager().resolveFile("file://${sftp.defaultDirectory}")
|
||||||
|
} else {
|
||||||
|
fileSystem.resolveFile("file://${sftp.defaultDirectory}")
|
||||||
|
}
|
||||||
if (resolveFile.exists()) {
|
if (resolveFile.exists()) {
|
||||||
|
setFileSystem(resolveFile.fileSystem)
|
||||||
return resolveFile
|
return resolveFile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,6 +448,14 @@ class FileSystemViewPanel(
|
|||||||
return if (dataKey == SFTPDataProviders.FileSystemViewTable) table as T else null
|
return if (dataKey == SFTPDataProviders.FileSystemViewTable) table as T else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getFileSystem(): FileSystem {
|
||||||
|
return fileSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setFileSystem(fileSystem: FileSystem) {
|
||||||
|
this.fileSystem = fileSystem
|
||||||
|
}
|
||||||
|
|
||||||
private class LoadingPanel : JPanel() {
|
private class LoadingPanel : JPanel() {
|
||||||
private val busyLabel = JXBusyLabel()
|
private val busyLabel = JXBusyLabel()
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import app.termora.*
|
|||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.actions.SettingsAction
|
import app.termora.actions.SettingsAction
|
||||||
import app.termora.sftp.FileSystemViewTable.AskTransfer.Action
|
import app.termora.sftp.FileSystemViewTable.AskTransfer.Action
|
||||||
|
import app.termora.vfs2.VFSWalker
|
||||||
import app.termora.vfs2.sftp.MySftpFileObject
|
import app.termora.vfs2.sftp.MySftpFileObject
|
||||||
import app.termora.vfs2.sftp.MySftpFileSystem
|
import app.termora.vfs2.sftp.MySftpFileSystem
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
@@ -37,7 +38,6 @@ import java.nio.file.FileVisitor
|
|||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.nio.file.StandardOpenOption
|
import java.nio.file.StandardOpenOption
|
||||||
import java.nio.file.attribute.BasicFileAttributes
|
import java.nio.file.attribute.BasicFileAttributes
|
||||||
import java.nio.file.attribute.FileTime
|
|
||||||
import java.text.MessageFormat
|
import java.text.MessageFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
@@ -45,6 +45,21 @@ import java.util.regex.Pattern
|
|||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.table.DefaultTableCellRenderer
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
import kotlin.collections.ArrayDeque
|
import kotlin.collections.ArrayDeque
|
||||||
|
import kotlin.collections.List
|
||||||
|
import kotlin.collections.all
|
||||||
|
import kotlin.collections.contains
|
||||||
|
import kotlin.collections.filter
|
||||||
|
import kotlin.collections.filterIsInstance
|
||||||
|
import kotlin.collections.find
|
||||||
|
import kotlin.collections.forEach
|
||||||
|
import kotlin.collections.isEmpty
|
||||||
|
import kotlin.collections.isNotEmpty
|
||||||
|
import kotlin.collections.last
|
||||||
|
import kotlin.collections.listOf
|
||||||
|
import kotlin.collections.map
|
||||||
|
import kotlin.collections.mapOf
|
||||||
|
import kotlin.collections.mutableListOf
|
||||||
|
import kotlin.collections.sortedArray
|
||||||
import kotlin.io.path.absolutePathString
|
import kotlin.io.path.absolutePathString
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
@@ -52,7 +67,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||||||
|
|
||||||
@Suppress("DuplicatedCode", "CascadeIf")
|
@Suppress("DuplicatedCode", "CascadeIf")
|
||||||
class FileSystemViewTable(
|
class FileSystemViewTable(
|
||||||
private val fileSystem: org.apache.commons.vfs2.FileSystem,
|
private val fileSystemProvider: FileSystemProvider,
|
||||||
private val transportManager: TransportManager,
|
private val transportManager: TransportManager,
|
||||||
private val coroutineScope: CoroutineScope
|
private val coroutineScope: CoroutineScope
|
||||||
) : JTable(), Disposable {
|
) : JTable(), Disposable {
|
||||||
@@ -184,7 +199,7 @@ class FileSystemViewTable(
|
|||||||
val data = support.transferable.getTransferData(FileSystemTableRowTransferable.dataFlavor)
|
val data = support.transferable.getTransferData(FileSystemTableRowTransferable.dataFlavor)
|
||||||
return data is FileSystemTableRowTransferable && data.source != table
|
return data is FileSystemTableRowTransferable && data.source != table
|
||||||
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||||
return fileSystem !is LocalFileSystem
|
return fileSystemProvider.getFileSystem() !is LocalFileSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -218,7 +233,7 @@ class FileSystemViewTable(
|
|||||||
val localTarget = sftpPanel.getLocalTarget()
|
val localTarget = sftpPanel.getLocalTarget()
|
||||||
val table = localTarget.getData(SFTPDataProviders.FileSystemViewTable) ?: return false
|
val table = localTarget.getData(SFTPDataProviders.FileSystemViewTable) ?: return false
|
||||||
// 委托最左侧的本地文件系统传输
|
// 委托最左侧的本地文件系统传输
|
||||||
table.transfer(paths, true, targetWorkdir)
|
table.transfer(paths, true, targetWorkdir, fileSystemViewPanel)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -261,6 +276,7 @@ class FileSystemViewTable(
|
|||||||
private fun showContextMenu(rows: IntArray, e: MouseEvent) {
|
private fun showContextMenu(rows: IntArray, e: MouseEvent) {
|
||||||
val files = rows.map { model.getFileObject(it) }
|
val files = rows.map { model.getFileObject(it) }
|
||||||
val hasParent = rows.contains(0)
|
val hasParent = rows.contains(0)
|
||||||
|
val fileSystem = fileSystemProvider.getFileSystem()
|
||||||
|
|
||||||
val popupMenu = FlatPopupMenu()
|
val popupMenu = FlatPopupMenu()
|
||||||
val newMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.new"))
|
val newMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.new"))
|
||||||
@@ -359,34 +375,7 @@ class FileSystemViewTable(
|
|||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
val last = files.last()
|
val last = files.last()
|
||||||
if (last !is MySftpFileObject) return
|
if (last !is MySftpFileObject) return
|
||||||
|
changePermission(last)
|
||||||
val dialog = PosixFilePermissionDialog(
|
|
||||||
SwingUtilities.getWindowAncestor(table),
|
|
||||||
model.getFilePermissions(last)
|
|
||||||
)
|
|
||||||
val permissions = dialog.open() ?: return
|
|
||||||
|
|
||||||
if (fileSystemViewPanel.requestLoading()) {
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
|
||||||
val c = runCatching { last.setPosixFilePermissions(permissions) }.onFailure {
|
|
||||||
withContext(Dispatchers.Swing) {
|
|
||||||
OptionPane.showMessageDialog(
|
|
||||||
owner,
|
|
||||||
ExceptionUtils.getMessage(it),
|
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop loading
|
|
||||||
fileSystemViewPanel.stopLoading()
|
|
||||||
|
|
||||||
// reload
|
|
||||||
if (c.isSuccess) {
|
|
||||||
fileSystemViewPanel.reload(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
refresh.addActionListener { fileSystemViewPanel.reload() }
|
refresh.addActionListener { fileSystemViewPanel.reload() }
|
||||||
@@ -408,6 +397,80 @@ class FileSystemViewTable(
|
|||||||
popupMenu.show(table, e.x, e.y)
|
popupMenu.show(table, e.x, e.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun changePermission(file: MySftpFileObject) {
|
||||||
|
|
||||||
|
val dialog = PosixFilePermissionDialog(
|
||||||
|
SwingUtilities.getWindowAncestor(table),
|
||||||
|
model.getFilePermissions(file)
|
||||||
|
)
|
||||||
|
val permissions = dialog.open() ?: return
|
||||||
|
val isIncludeSubdirectories = dialog.isIncludeSubdirectories()
|
||||||
|
|
||||||
|
if (fileSystemViewPanel.requestLoading()) {
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
val c = runCatching {
|
||||||
|
file.setPosixFilePermissions(permissions)
|
||||||
|
if (isIncludeSubdirectories && file.isFolder) {
|
||||||
|
file.refresh()
|
||||||
|
VFSWalker.walk(file, object : FileVisitor<FileObject> {
|
||||||
|
override fun preVisitDirectory(
|
||||||
|
dir: FileObject,
|
||||||
|
attrs: BasicFileAttributes
|
||||||
|
): FileVisitResult {
|
||||||
|
dir.refresh()
|
||||||
|
if (dir is MySftpFileObject) {
|
||||||
|
dir.setPosixFilePermissions(permissions)
|
||||||
|
}
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitFile(
|
||||||
|
file: FileObject,
|
||||||
|
attrs: BasicFileAttributes
|
||||||
|
): FileVisitResult {
|
||||||
|
if (file is MySftpFileObject) {
|
||||||
|
file.setPosixFilePermissions(permissions)
|
||||||
|
}
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitFileFailed(
|
||||||
|
file: FileObject,
|
||||||
|
exc: IOException
|
||||||
|
): FileVisitResult {
|
||||||
|
return FileVisitResult.TERMINATE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun postVisitDirectory(
|
||||||
|
dir: FileObject,
|
||||||
|
exc: IOException?
|
||||||
|
): FileVisitResult {
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
ExceptionUtils.getMessage(it),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop loading
|
||||||
|
fileSystemViewPanel.stopLoading()
|
||||||
|
|
||||||
|
// reload
|
||||||
|
if (c.isSuccess) {
|
||||||
|
fileSystemViewPanel.reload(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun renameSelection() {
|
private fun renameSelection() {
|
||||||
val index = selectedRow
|
val index = selectedRow
|
||||||
if (index < 0) return
|
if (index < 0) return
|
||||||
@@ -571,7 +634,7 @@ class FileSystemViewTable(
|
|||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
if (fileSystem is MySftpFileSystem) {
|
if (fileSystemProvider.getFileSystem() is MySftpFileSystem) {
|
||||||
deleteSftpPaths(paths, rm)
|
deleteSftpPaths(paths, rm)
|
||||||
} else {
|
} else {
|
||||||
deleteRecursively(paths)
|
deleteRecursively(paths)
|
||||||
@@ -594,7 +657,7 @@ class FileSystemViewTable(
|
|||||||
|
|
||||||
private fun deleteSftpPaths(files: List<FileObject>, rm: Boolean = false) {
|
private fun deleteSftpPaths(files: List<FileObject>, rm: Boolean = false) {
|
||||||
if (rm) {
|
if (rm) {
|
||||||
val session = (this.fileSystem as MySftpFileSystem).getClientSession()
|
val session = (this.fileSystemProvider.getFileSystem() as MySftpFileSystem).getClientSession()
|
||||||
for (path in files) {
|
for (path in files) {
|
||||||
session.executeRemoteCommand(
|
session.executeRemoteCommand(
|
||||||
"rm -rf '${path.absolutePathString()}'",
|
"rm -rf '${path.absolutePathString()}'",
|
||||||
@@ -618,12 +681,13 @@ class FileSystemViewTable(
|
|||||||
private fun transfer(
|
private fun transfer(
|
||||||
files: List<FileObject>,
|
files: List<FileObject>,
|
||||||
fromLocalSystem: Boolean = false,
|
fromLocalSystem: Boolean = false,
|
||||||
targetWorkdir: FileObject? = null
|
targetWorkdir: FileObject? = null,
|
||||||
|
target: FileSystemViewPanel? = null,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
assertEventDispatchThread()
|
assertEventDispatchThread()
|
||||||
|
|
||||||
val target = sftpPanel.getTarget(table) ?: return
|
val target = (target ?: sftpPanel.getTarget(table)) ?: return
|
||||||
val table = target.getData(SFTPDataProviders.FileSystemViewTable) ?: return
|
val table = target.getData(SFTPDataProviders.FileSystemViewTable) ?: return
|
||||||
var isApplyAll = false
|
var isApplyAll = false
|
||||||
var lastAction = Action.Overwrite
|
var lastAction = Action.Overwrite
|
||||||
@@ -649,7 +713,7 @@ class FileSystemViewTable(
|
|||||||
|
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
try {
|
try {
|
||||||
doTransfer(file, lastAction, fromLocalSystem, targetWorkdir)
|
doTransfer(file, lastAction, fromLocalSystem, targetWorkdir, target)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (log.isErrorEnabled) {
|
if (log.isErrorEnabled) {
|
||||||
log.error(e.message, e)
|
log.error(e.message, e)
|
||||||
@@ -800,10 +864,11 @@ class FileSystemViewTable(
|
|||||||
file: FileObject,
|
file: FileObject,
|
||||||
action: Action,
|
action: Action,
|
||||||
fromLocalSystem: Boolean,
|
fromLocalSystem: Boolean,
|
||||||
targetWorkdir: FileObject?
|
targetWorkdir: FileObject?,
|
||||||
|
target: FileSystemViewPanel? = null
|
||||||
) {
|
) {
|
||||||
val sftpPanel = this.sftpPanel
|
val sftpPanel = this.sftpPanel
|
||||||
val target = sftpPanel.getTarget(table) ?: return
|
val target = (target ?: sftpPanel.getTarget(table)) ?: return
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定义一个添加器,它可以自动的判断导入/拖拽行为
|
* 定义一个添加器,它可以自动的判断导入/拖拽行为
|
||||||
@@ -885,36 +950,7 @@ class FileSystemViewTable(
|
|||||||
dir: FileObject,
|
dir: FileObject,
|
||||||
visitor: FileVisitor<FileObject>,
|
visitor: FileVisitor<FileObject>,
|
||||||
): FileVisitResult {
|
): FileVisitResult {
|
||||||
|
return VFSWalker.walk(dir, visitor)
|
||||||
// clear cache
|
|
||||||
if (visitor.preVisitDirectory(dir, EmptyBasicFileAttributes.INSTANCE) == FileVisitResult.TERMINATE) {
|
|
||||||
return FileVisitResult.TERMINATE
|
|
||||||
}
|
|
||||||
|
|
||||||
for (e in dir.children) {
|
|
||||||
if (e.name.baseName == ".." || e.name.baseName == ".") continue
|
|
||||||
if (e.isFolder) {
|
|
||||||
if (walk(dir.resolveFile(e.name.baseName), visitor) == FileVisitResult.TERMINATE) {
|
|
||||||
return FileVisitResult.TERMINATE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val result = visitor.visitFile(
|
|
||||||
dir.resolveFile(e.name.baseName),
|
|
||||||
EmptyBasicFileAttributes.INSTANCE
|
|
||||||
)
|
|
||||||
if (result == FileVisitResult.TERMINATE) {
|
|
||||||
return FileVisitResult.TERMINATE
|
|
||||||
} else if (result == FileVisitResult.SKIP_SUBTREE) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visitor.postVisitDirectory(dir, null) == FileVisitResult.TERMINATE) {
|
|
||||||
return FileVisitResult.TERMINATE
|
|
||||||
}
|
|
||||||
|
|
||||||
return FileVisitResult.CONTINUE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addTransport(
|
private fun addTransport(
|
||||||
@@ -973,47 +1009,5 @@ class FileSystemViewTable(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class EmptyBasicFileAttributes : BasicFileAttributes {
|
|
||||||
companion object {
|
|
||||||
val INSTANCE = EmptyBasicFileAttributes()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun lastModifiedTime(): FileTime {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun lastAccessTime(): FileTime {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun creationTime(): FileTime {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isRegularFile(): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isDirectory(): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isSymbolicLink(): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isOther(): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun size(): Long {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fileKey(): Any {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -157,8 +157,12 @@ class FileSystemViewTableModel : DefaultTableModel() {
|
|||||||
fun getPathNames(): Set<String> {
|
fun getPathNames(): Set<String> {
|
||||||
val names = linkedSetOf<String>()
|
val names = linkedSetOf<String>()
|
||||||
for (i in 0 until rowCount) {
|
for (i in 0 until rowCount) {
|
||||||
|
if (hasParent && i == 0) {
|
||||||
|
names.add("..")
|
||||||
|
} else {
|
||||||
names.add(getFileObject(i).name.baseName)
|
names.add(getFileObject(i).name.baseName)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class PosixFilePermissionDialog(
|
|||||||
private val otherRead = JCheckBox(I18n.getString("termora.transport.permissions.read"))
|
private val otherRead = JCheckBox(I18n.getString("termora.transport.permissions.read"))
|
||||||
private val otherWrite = JCheckBox(I18n.getString("termora.transport.permissions.write"))
|
private val otherWrite = JCheckBox(I18n.getString("termora.transport.permissions.write"))
|
||||||
private val otherExecute = JCheckBox(I18n.getString("termora.transport.permissions.execute"))
|
private val otherExecute = JCheckBox(I18n.getString("termora.transport.permissions.execute"))
|
||||||
|
private val includeSubFolder = JCheckBox(I18n.getString("termora.transport.permissions.include-subfolder"))
|
||||||
|
|
||||||
private var isCancelled = false
|
private var isCancelled = false
|
||||||
|
|
||||||
@@ -60,13 +61,14 @@ class PosixFilePermissionDialog(
|
|||||||
otherRead.isFocusable = false
|
otherRead.isFocusable = false
|
||||||
otherWrite.isFocusable = false
|
otherWrite.isFocusable = false
|
||||||
otherExecute.isFocusable = false
|
otherExecute.isFocusable = false
|
||||||
|
includeSubFolder.isFocusable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createCenterPanel(): JComponent {
|
override fun createCenterPanel(): JComponent {
|
||||||
val formMargin = "7dlu"
|
val formMargin = "7dlu"
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"default:grow, $formMargin, default:grow, $formMargin, default:grow",
|
"default:grow, $formMargin, default:grow, $formMargin, default:grow",
|
||||||
"pref, $formMargin, pref, $formMargin, pref"
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
)
|
)
|
||||||
|
|
||||||
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
|
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
|
||||||
@@ -95,6 +97,8 @@ class PosixFilePermissionDialog(
|
|||||||
otherBox.border = BorderFactory.createTitledBorder(I18n.getString("termora.transport.permissions.others"))
|
otherBox.border = BorderFactory.createTitledBorder(I18n.getString("termora.transport.permissions.others"))
|
||||||
builder.add(otherBox).xy(5, 3)
|
builder.add(otherBox).xy(5, 3)
|
||||||
|
|
||||||
|
builder.add(includeSubFolder).xyw(1, 5, 5)
|
||||||
|
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +107,10 @@ class PosixFilePermissionDialog(
|
|||||||
super.doCancelAction()
|
super.doCancelAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isIncludeSubdirectories(): Boolean {
|
||||||
|
return includeSubFolder.isSelected
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return 返回空表示取消了
|
* @return 返回空表示取消了
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -56,6 +56,16 @@ class SFTPAction : AnAction("SFTP", Icons.folder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val host = hostManager.getHost(hostId) ?: return
|
val host = hostManager.getHost(hostId) ?: return
|
||||||
|
for (i in 0 until tabbed.tabCount) {
|
||||||
|
val c = tabbed.getComponentAt(i)
|
||||||
|
if (c is SFTPFileSystemViewPanel) {
|
||||||
|
if (c.state == SFTPFileSystemViewPanel.State.Initialized) {
|
||||||
|
c.selectHost(host)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tabbed.addSFTPFileSystemViewPanelTab(host)
|
tabbed.addSFTPFileSystemViewPanelTab(host)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import java.awt.event.MouseAdapter
|
|||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
import javax.swing.event.TreeExpansionEvent
|
||||||
|
import javax.swing.event.TreeExpansionListener
|
||||||
|
|
||||||
class SFTPFileSystemViewPanel(
|
class SFTPFileSystemViewPanel(
|
||||||
var host: Host? = null,
|
var host: Host? = null,
|
||||||
@@ -35,17 +37,18 @@ class SFTPFileSystemViewPanel(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(SFTPFileSystemViewPanel::class.java)
|
private val log = LoggerFactory.getLogger(SFTPFileSystemViewPanel::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
private enum class State {
|
enum class State {
|
||||||
Initialized,
|
Initialized,
|
||||||
Connecting,
|
Connecting,
|
||||||
Connected,
|
Connected,
|
||||||
ConnectFailed,
|
ConnectFailed,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var state = State.Initialized
|
var state = State.Initialized
|
||||||
|
private set
|
||||||
private val cardLayout = CardLayout()
|
private val cardLayout = CardLayout()
|
||||||
private val cardPanel = JPanel(cardLayout)
|
private val cardPanel = JPanel(cardLayout)
|
||||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
@@ -283,12 +286,20 @@ class SFTPFileSystemViewPanel(
|
|||||||
val node = tree.getLastSelectedPathNode() ?: return
|
val node = tree.getLastSelectedPathNode() ?: return
|
||||||
if (node.isFolder) return
|
if (node.isFolder) return
|
||||||
val host = node.data as Host
|
val host = node.data as Host
|
||||||
that.setTabTitle(host.name)
|
selectHost(host)
|
||||||
that.host = host
|
|
||||||
that.connect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
tree.addTreeExpansionListener(object : TreeExpansionListener {
|
||||||
|
override fun treeExpanded(event: TreeExpansionEvent) {
|
||||||
|
properties.putString("SFTPTabbed.Tree.state", TreeUtils.saveExpansionState(tree))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun treeCollapsed(event: TreeExpansionEvent) {
|
||||||
|
properties.putString("SFTPTabbed.Tree.state", TreeUtils.saveExpansionState(tree))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
@@ -305,6 +316,12 @@ class SFTPFileSystemViewPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun selectHost(host: Host) {
|
||||||
|
that.setTabTitle(host.name)
|
||||||
|
that.host = host
|
||||||
|
that.connect()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setTabTitle(title: String) {
|
private fun setTabTitle(title: String) {
|
||||||
val tabbed = SwingUtilities.getAncestorOfClass(JTabbedPane::class.java, that)
|
val tabbed = SwingUtilities.getAncestorOfClass(JTabbedPane::class.java, that)
|
||||||
if (tabbed is JTabbedPane) {
|
if (tabbed is JTabbedPane) {
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class SFTPPanel : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val fs = c.fileSystem
|
val fs = c.getFileSystem()
|
||||||
val root = transportManager.root
|
val root = transportManager.root
|
||||||
|
|
||||||
transportManager.lock.withLock {
|
transportManager.lock.withLock {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import app.termora.actions.AnAction
|
|||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
import java.awt.Point
|
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
@@ -13,7 +12,6 @@ import javax.swing.JButton
|
|||||||
import javax.swing.JToolBar
|
import javax.swing.JToolBar
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
@Suppress("DuplicatedCode")
|
@Suppress("DuplicatedCode")
|
||||||
class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPane(), Disposable {
|
class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPane(), Disposable {
|
||||||
@@ -43,23 +41,20 @@ class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPan
|
|||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
addBtn.addActionListener(object : AnAction() {
|
addBtn.addActionListener(object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val dialog = NewHostTreeDialog(SwingUtilities.getWindowAncestor(tabbed))
|
for (i in 0 until tabCount) {
|
||||||
dialog.location = Point(
|
val c = getComponentAt(i)
|
||||||
max(0, addBtn.locationOnScreen.x - dialog.width / 2 + addBtn.width / 2),
|
if (c !is SFTPFileSystemViewPanel) continue
|
||||||
addBtn.locationOnScreen.y + max(tabHeight, addBtn.height)
|
if (c.state != SFTPFileSystemViewPanel.State.Initialized) continue
|
||||||
)
|
selectedIndex = i
|
||||||
dialog.setFilter { it.host.protocol == Protocol.SSH }
|
return
|
||||||
dialog.setTreeName("SFTPTabbed.Tree")
|
|
||||||
dialog.allowMulti = true
|
|
||||||
dialog.isVisible = true
|
|
||||||
|
|
||||||
val hosts = dialog.hosts
|
|
||||||
if (hosts.isEmpty()) return
|
|
||||||
|
|
||||||
for (host in hosts) {
|
|
||||||
addSFTPFileSystemViewPanelTab(host)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加一个新的
|
||||||
|
addTab(
|
||||||
|
I18n.getString("termora.transport.sftp.select-host"),
|
||||||
|
SFTPFileSystemViewPanel(transportManager = transportManager)
|
||||||
|
)
|
||||||
|
selectedIndex = tabCount - 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import app.termora.Icons
|
|||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.terminal.ControlCharacters
|
import app.termora.terminal.ControlCharacters
|
||||||
|
import app.termora.terminal.Null
|
||||||
import app.termora.terminal.panel.TerminalWriter
|
import app.termora.terminal.panel.TerminalWriter
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.text.StringEscapeUtils
|
||||||
|
|
||||||
class SnippetAction private constructor() : AnAction(I18n.getString("termora.snippet.title"), Icons.codeSpan) {
|
class SnippetAction private constructor() : AnAction(I18n.getString("termora.snippet.title"), Icons.codeSpan) {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -15,6 +18,16 @@ class SnippetAction private constructor() : AnAction(I18n.getString("termora.sni
|
|||||||
}
|
}
|
||||||
|
|
||||||
const val SNIPPET = "SnippetAction"
|
const val SNIPPET = "SnippetAction"
|
||||||
|
|
||||||
|
// \r \n \t \a \e \b
|
||||||
|
private val SpecialChars = mutableMapOf(
|
||||||
|
'r' to '\r',
|
||||||
|
'n' to '\n',
|
||||||
|
't' to '\t',
|
||||||
|
'a' to ControlCharacters.BEL,
|
||||||
|
'e' to ControlCharacters.ESC,
|
||||||
|
'b' to ControlCharacters.BS
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
@@ -24,19 +37,39 @@ class SnippetAction private constructor() : AnAction(I18n.getString("termora.sni
|
|||||||
|
|
||||||
fun runSnippet(snippet: Snippet, writer: TerminalWriter) {
|
fun runSnippet(snippet: Snippet, writer: TerminalWriter) {
|
||||||
if (snippet.type != SnippetType.Snippet) return
|
if (snippet.type != SnippetType.Snippet) return
|
||||||
val map = mapOf(
|
writer.write(TerminalWriter.WriteRequest.fromBytes(unescape(snippet.snippet).toByteArray(writer.getCharset())))
|
||||||
"\\r" to ControlCharacters.CR,
|
|
||||||
"\\n" to ControlCharacters.LF,
|
|
||||||
"\\t" to ControlCharacters.TAB,
|
|
||||||
"\\a" to ControlCharacters.BEL,
|
|
||||||
"\\e" to ControlCharacters.ESC,
|
|
||||||
"\\b" to ControlCharacters.BS,
|
|
||||||
)
|
|
||||||
|
|
||||||
var text = snippet.snippet
|
|
||||||
for (e in map.entries) {
|
|
||||||
text = text.replace(e.key, e.value.toString())
|
|
||||||
}
|
}
|
||||||
writer.write(TerminalWriter.WriteRequest.fromBytes(text.toByteArray(writer.getCharset())))
|
|
||||||
|
private fun unescape(text: String): String {
|
||||||
|
val chars = text.toCharArray()
|
||||||
|
val sb = StringBuilder()
|
||||||
|
for (i in chars.indices) {
|
||||||
|
val c = chars[i]
|
||||||
|
|
||||||
|
// 不是特殊字符不处理
|
||||||
|
if (SpecialChars.containsKey(c).not()) {
|
||||||
|
sb.append(c)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 特殊字符前面不是 `\` 不处理
|
||||||
|
if (chars.getOrNull(i - 1) != '\\') {
|
||||||
|
sb.append(c)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果构成的字符串是:\\r 就会生成 \r 字符串,并非转译成:CR
|
||||||
|
if (chars.getOrNull(i - 2) == '\\') {
|
||||||
|
sb.deleteCharAt(sb.length - 1)
|
||||||
|
sb.append(c)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 命中条件之后,那么 sb 最后一个字符肯定是 \
|
||||||
|
sb.deleteCharAt(sb.length - 1)
|
||||||
|
sb.append(SpecialChars.getValue(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,8 +41,10 @@ class SnippetPanel : JPanel(BorderLayout()), Disposable {
|
|||||||
private fun initViews() {
|
private fun initViews() {
|
||||||
val splitPane = JSplitPane()
|
val splitPane = JSplitPane()
|
||||||
splitPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
splitPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||||
|
val scrollPane = JScrollPane(snippetTree)
|
||||||
|
scrollPane.border = BorderFactory.createEmptyBorder()
|
||||||
|
|
||||||
leftPanel.add(snippetTree, BorderLayout.CENTER)
|
leftPanel.add(scrollPane, BorderLayout.CENTER)
|
||||||
leftPanel.border = BorderFactory.createCompoundBorder(
|
leftPanel.border = BorderFactory.createCompoundBorder(
|
||||||
BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor),
|
BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor),
|
||||||
BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
@@ -51,6 +53,7 @@ class SnippetPanel : JPanel(BorderLayout()), Disposable {
|
|||||||
properties.getString("SnippetPanel.LeftPanel.width", "180").toIntOrNull() ?: 180,
|
properties.getString("SnippetPanel.LeftPanel.width", "180").toIntOrNull() ?: 180,
|
||||||
-1
|
-1
|
||||||
)
|
)
|
||||||
|
leftPanel.minimumSize = Dimension(leftPanel.preferredSize.width, leftPanel.preferredSize.height)
|
||||||
|
|
||||||
rightPanel.border = BorderFactory.createCompoundBorder(
|
rightPanel.border = BorderFactory.createCompoundBorder(
|
||||||
BorderFactory.createMatteBorder(0, 1, 0, 0, DynamicColor.BorderColor),
|
BorderFactory.createMatteBorder(0, 1, 0, 0, DynamicColor.BorderColor),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.snippet
|
package app.termora.snippet
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
import app.termora.SimpleTreeModel
|
import app.termora.SimpleTreeModel
|
||||||
import javax.swing.tree.MutableTreeNode
|
import javax.swing.tree.MutableTreeNode
|
||||||
import javax.swing.tree.TreeNode
|
import javax.swing.tree.TreeNode
|
||||||
@@ -8,7 +9,7 @@ class SnippetTreeModel : SimpleTreeModel<Snippet>(
|
|||||||
SnippetTreeNode(
|
SnippetTreeNode(
|
||||||
Snippet(
|
Snippet(
|
||||||
id = "0",
|
id = "0",
|
||||||
name = "全部片段",
|
name = I18n.getString("termora.snippet.title"),
|
||||||
type = SnippetType.Folder
|
type = SnippetType.Folder
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -390,7 +390,15 @@ abstract class SafetySyncer : Syncer {
|
|||||||
|
|
||||||
protected fun decodeKeymaps(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
protected fun decodeKeymaps(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
||||||
|
|
||||||
for (keymap in ohMyJson.decodeFromString<List<JsonObject>>(text).mapNotNull { Keymap.fromJSON(it) }) {
|
val localKeymaps = keymapManager.getKeymaps().associateBy { it.name }
|
||||||
|
val remoteKeymaps = ohMyJson.decodeFromString<List<JsonObject>>(text).mapNotNull { Keymap.fromJSON(it) }
|
||||||
|
for (keymap in remoteKeymaps) {
|
||||||
|
val localKeymap = localKeymaps[keymap.name]
|
||||||
|
if (localKeymap != null) {
|
||||||
|
if (localKeymap.updateDate > keymap.updateDate) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
keymapManager.addKeymap(keymap)
|
keymapManager.addKeymap(keymap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ class SyncManager private constructor() : Disposable {
|
|||||||
|
|
||||||
sync(config)
|
sync(config)
|
||||||
|
|
||||||
|
sync.lastSyncTime = System.currentTimeMillis()
|
||||||
|
|
||||||
if (log.isInfoEnabled) {
|
if (log.isInfoEnabled) {
|
||||||
log.info("Automatic synchronisation end")
|
log.info("Automatic synchronisation end")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -360,8 +360,9 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Send Device Attributes (Primary DA).
|
// Send Device Attributes (Primary DA).
|
||||||
'c' -> {
|
'c' -> {
|
||||||
|
sendDeviceAttributes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSI Ps M Delete Ps Line(s) (default = 1) (DL).
|
// CSI Ps M Delete Ps Line(s) (default = 1) (DL).
|
||||||
@@ -399,6 +400,16 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).
|
||||||
|
'Z' -> {
|
||||||
|
val count = args.toInt(1)
|
||||||
|
val cursorModel = terminal.getCursorModel()
|
||||||
|
for (i in 0 until count) {
|
||||||
|
val x = terminal.getTabulator().previousTab(cursorModel.getPosition().x - 1) + 1
|
||||||
|
terminal.getCursorModel().move(cursorModel.getPosition().y, x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// split
|
// split
|
||||||
';' -> {
|
';' -> {
|
||||||
args.append(ch)
|
args.append(ch)
|
||||||
@@ -495,6 +506,22 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sendDeviceAttributes() {
|
||||||
|
|
||||||
|
assertEventDispatchThread()
|
||||||
|
|
||||||
|
if (!terminalModel.hasData(DataKey.TerminalWriter)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val writer = terminalModel.getData(DataKey.TerminalWriter)
|
||||||
|
|
||||||
|
// VT102_RESPONSE
|
||||||
|
val bytes = "${ControlCharacters.ESC}[?6c".toByteArray(writer.getCharset())
|
||||||
|
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-?-Pm-h.1D0E
|
* https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-?-Pm-h.1D0E
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import java.io.InputStreamReader
|
|||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
class PtyProcessConnector(private val process: PtyProcess, private val charset: Charset = StandardCharsets.UTF_8) :
|
class PtyProcessConnector(val process: PtyProcess, private val charset: Charset = StandardCharsets.UTF_8) :
|
||||||
StreamPtyConnector(process.inputStream, process.outputStream) {
|
StreamPtyConnector(process.inputStream, process.outputStream) {
|
||||||
|
|
||||||
private val reader = InputStreamReader(input)
|
private val reader = InputStreamReader(input)
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ interface SelectionModel {
|
|||||||
*/
|
*/
|
||||||
fun setSelection(startPosition: Position, endPosition: Position)
|
fun setSelection(startPosition: Position, endPosition: Position)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置块选中模式
|
||||||
|
*/
|
||||||
|
fun setBlockSelection(block: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否是块选中模式
|
||||||
|
*/
|
||||||
|
fun isBlockSelection(): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取开始选中的位置
|
* 获取开始选中的位置
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import kotlin.math.min
|
|||||||
open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
||||||
private var startPosition = Position.unknown
|
private var startPosition = Position.unknown
|
||||||
private var endPosition = Position.unknown
|
private var endPosition = Position.unknown
|
||||||
|
private var block = false
|
||||||
private val document = terminal.getDocument()
|
private val document = terminal.getDocument()
|
||||||
|
|
||||||
internal companion object {
|
internal companion object {
|
||||||
@@ -67,7 +68,37 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
|||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
val iterator = getChars(getSelectionStartPosition(), getSelectionEndPosition())
|
val start = getSelectionStartPosition()
|
||||||
|
val end = getSelectionEndPosition()
|
||||||
|
|
||||||
|
if (isBlockSelection()) {
|
||||||
|
val left = min(start.x, end.x)
|
||||||
|
val right = max(start.x, end.x)
|
||||||
|
val top = min(start.y, end.y)
|
||||||
|
val bottom = max(start.y, end.y)
|
||||||
|
|
||||||
|
for (lineNum in top..bottom) {
|
||||||
|
val line = document.getLine(lineNum)
|
||||||
|
val chars = line.chars()
|
||||||
|
|
||||||
|
// 块选中要处理超出边界
|
||||||
|
val from = (left - 1).coerceAtLeast(0)
|
||||||
|
val to = right.coerceAtMost(chars.size)
|
||||||
|
|
||||||
|
if (from < to) {
|
||||||
|
val selected = chars.subList(from, to)
|
||||||
|
.filter { !it.first.isNull && !it.first.isSoftHyphen }
|
||||||
|
.joinToString("") { it.first.toString() }
|
||||||
|
sb.append(selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineNum != bottom) {
|
||||||
|
sb.appendLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
val iterator = getChars(start, end)
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
val line = iterator.next()
|
val line = iterator.next()
|
||||||
val chars = line.chars()
|
val chars = line.chars()
|
||||||
@@ -92,6 +123,7 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
|||||||
sb.appendLine()
|
sb.appendLine()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (sb.isNotEmpty() && sb.last() == ControlCharacters.LF) {
|
if (sb.isNotEmpty() && sb.last() == ControlCharacters.LF) {
|
||||||
sb.deleteCharAt(sb.length - 1)
|
sb.deleteCharAt(sb.length - 1)
|
||||||
@@ -171,6 +203,12 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
|||||||
fireSelectionChanged()
|
fireSelectionChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setBlockSelection(block: Boolean) {
|
||||||
|
this.block = block
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isBlockSelection() = block
|
||||||
|
|
||||||
override fun getSelectionStartPosition(): Position {
|
override fun getSelectionStartPosition(): Position {
|
||||||
return startPosition
|
return startPosition
|
||||||
}
|
}
|
||||||
@@ -202,13 +240,20 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun hasSelection(x: Int, y: Int): Boolean {
|
override fun hasSelection(x: Int, y: Int): Boolean {
|
||||||
return hasSelection() && isPointInsideArea(
|
|
||||||
startPosition,
|
if (hasSelection().not()) return false
|
||||||
endPosition,
|
|
||||||
x,
|
// 如果是块选中
|
||||||
y,
|
if (isBlockSelection()) {
|
||||||
terminal.getTerminalModel().getCols()
|
val left = min(startPosition.x, endPosition.x)
|
||||||
)
|
val right = max(startPosition.x, endPosition.x)
|
||||||
|
val top = min(startPosition.y, endPosition.y)
|
||||||
|
val bottom = max(startPosition.y, endPosition.y)
|
||||||
|
|
||||||
|
return x in left..right && y in top..bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
return isPointInsideArea(startPosition, endPosition, x, y, terminal.getTerminalModel().getCols())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,16 @@ import com.formdev.flatlaf.extras.components.FlatToolBar
|
|||||||
import com.formdev.flatlaf.ui.FlatRoundBorder
|
import com.formdev.flatlaf.ui.FlatRoundBorder
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.event.ActionListener
|
import java.awt.event.ActionListener
|
||||||
|
import java.beans.PropertyChangeEvent
|
||||||
|
import java.beans.PropertyChangeListener
|
||||||
|
import java.util.*
|
||||||
import javax.swing.JButton
|
import javax.swing.JButton
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||||
private val floatingToolbarEnable get() = Database.getDatabase().terminal.floatingToolbar
|
private val floatingToolbarEnable get() = Database.getDatabase().terminal.floatingToolbar
|
||||||
private var closed = false
|
private var closed = false
|
||||||
|
private val anEvent get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@@ -72,6 +77,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initActions()
|
initActions()
|
||||||
|
initEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateUI() {
|
override fun updateUI() {
|
||||||
@@ -123,12 +129,38 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
add(initCloseActionButton())
|
add(initCloseActionButton())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
// 被添加到组件后
|
||||||
|
addPropertyChangeListener("ancestor", object : PropertyChangeListener {
|
||||||
|
override fun propertyChange(evt: PropertyChangeEvent) {
|
||||||
|
removePropertyChangeListener("ancestor", this)
|
||||||
|
SwingUtilities.invokeLater { resumeVisualWindows() }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun resumeVisualWindows() {
|
||||||
|
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
|
||||||
|
if (tab !is SSHTerminalTab) return
|
||||||
|
val terminalPanel = tab.getData(DataProviders.TerminalPanel) ?: return
|
||||||
|
terminalPanel.resumeVisualWindows(tab.host.id, object : DataProvider {
|
||||||
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
|
if (dataKey == DataProviders.TerminalTab) {
|
||||||
|
return tab as T
|
||||||
|
}
|
||||||
|
return super.getData(dataKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun initServerInfoActionButton(): JButton {
|
private fun initServerInfoActionButton(): JButton {
|
||||||
val btn = JButton(Icons.infoOutline)
|
val btn = JButton(Icons.infoOutline)
|
||||||
btn.toolTipText = I18n.getString("termora.visual-window.system-information")
|
btn.toolTipText = I18n.getString("termora.visual-window.system-information")
|
||||||
btn.addActionListener(object : AnAction() {
|
btn.addActionListener(object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
|
||||||
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
||||||
|
|
||||||
if (tab !is SSHTerminalTab) {
|
if (tab !is SSHTerminalTab) {
|
||||||
@@ -156,7 +188,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
btn.toolTipText = I18n.getString("termora.snippet.title")
|
btn.toolTipText = I18n.getString("termora.snippet.title")
|
||||||
btn.addActionListener(object : AnAction() {
|
btn.addActionListener(object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
|
||||||
val writer = tab.getData(DataProviders.TerminalWriter) ?: return
|
val writer = tab.getData(DataProviders.TerminalWriter) ?: return
|
||||||
val dialog = SnippetTreeDialog(evt.window)
|
val dialog = SnippetTreeDialog(evt.window)
|
||||||
dialog.setLocationRelativeTo(btn)
|
dialog.setLocationRelativeTo(btn)
|
||||||
@@ -174,7 +206,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi")
|
btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi")
|
||||||
btn.addActionListener(object : AnAction() {
|
btn.addActionListener(object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
|
||||||
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
||||||
|
|
||||||
if (tab !is SSHTerminalTab) {
|
if (tab !is SSHTerminalTab) {
|
||||||
@@ -233,7 +265,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
|
|
||||||
btn.addActionListener(object : AnAction() {
|
btn.addActionListener(object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
|
||||||
if (tab.canReconnect()) {
|
if (tab.canReconnect()) {
|
||||||
tab.reconnect()
|
tab.reconnect()
|
||||||
}
|
}
|
||||||
@@ -242,8 +274,4 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
return btn
|
return btn
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package app.termora.terminal.panel
|
|||||||
|
|
||||||
import app.termora.Application
|
import app.termora.Application
|
||||||
import app.termora.ApplicationScope
|
import app.termora.ApplicationScope
|
||||||
|
import app.termora.Database
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import java.awt.Graphics
|
import java.awt.Graphics
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@@ -16,6 +17,7 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val regex = Regex("https?://\\S*[^.\\s'\",()<>\\[\\]]")
|
private val regex = Regex("https?://\\S*[^.\\s'\",()<>\\[\\]]")
|
||||||
|
private val isEnableHyperlink get() = Database.getDatabase().terminal.hyperlink
|
||||||
|
|
||||||
override fun before(
|
override fun before(
|
||||||
offset: Int,
|
offset: Int,
|
||||||
@@ -25,6 +27,9 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
|
|||||||
terminalDisplay: TerminalDisplay,
|
terminalDisplay: TerminalDisplay,
|
||||||
terminal: Terminal
|
terminal: Terminal
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
if (isEnableHyperlink.not()) return
|
||||||
|
|
||||||
val document = terminal.getDocument()
|
val document = terminal.getDocument()
|
||||||
var startOffset = offset
|
var startOffset = offset
|
||||||
var endOffset = startOffset + count
|
var endOffset = startOffset + count
|
||||||
@@ -91,4 +96,18 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun after(
|
||||||
|
offset: Int,
|
||||||
|
count: Int,
|
||||||
|
g: Graphics,
|
||||||
|
terminalPanel: TerminalPanel,
|
||||||
|
terminalDisplay: TerminalDisplay,
|
||||||
|
terminal: Terminal
|
||||||
|
) {
|
||||||
|
if (isEnableHyperlink.not()) {
|
||||||
|
// 删除之前的
|
||||||
|
terminal.getMarkupModel().removeAllHighlighters(Highlighter.HYPERLINK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
package app.termora.terminal.panel
|
package app.termora.terminal.panel
|
||||||
|
|
||||||
|
import app.termora.Database
|
||||||
import app.termora.Disposable
|
import app.termora.Disposable
|
||||||
import app.termora.Disposer
|
import app.termora.Disposer
|
||||||
|
import app.termora.SSHTerminalTab
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.actions.DataProviderSupport
|
import app.termora.actions.DataProviderSupport
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import app.termora.terminal.panel.vw.VisualWindow
|
import app.termora.terminal.panel.vw.*
|
||||||
import app.termora.terminal.panel.vw.VisualWindowManager
|
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.apache.commons.lang3.ArrayUtils
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
@@ -44,15 +45,15 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
|
|||||||
val SelectCopy = DataKey(Boolean::class)
|
val SelectCopy = DataKey(Boolean::class)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val properties get() = Database.getDatabase().properties
|
||||||
private val terminalBlink = TerminalBlink(terminal)
|
private val terminalBlink = TerminalBlink(terminal)
|
||||||
private val terminalFindPanel = TerminalFindPanel(this, terminal)
|
private val terminalFindPanel = TerminalFindPanel(this, terminal)
|
||||||
private val floatingToolbar = FloatingToolbarPanel()
|
private val floatingToolbar = FloatingToolbarPanel()
|
||||||
private val terminalDisplay = TerminalDisplay(this, terminal, terminalBlink)
|
private val terminalDisplay = TerminalDisplay(this, terminal, terminalBlink)
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
|
||||||
private val layeredPane = TerminalLayeredPane()
|
private val layeredPane = TerminalLayeredPane()
|
||||||
private var visualWindows = emptyArray<VisualWindow>()
|
private var visualWindows = emptyArray<VisualWindow>()
|
||||||
|
|
||||||
val scrollBar = TerminalScrollBar(this@TerminalPanel, terminalFindPanel, terminal)
|
val scrollBar = TerminalScrollBar(this, terminalFindPanel, terminal)
|
||||||
var enableFloatingToolbar = true
|
var enableFloatingToolbar = true
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
@@ -63,6 +64,8 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val dataProviderSupport = DataProviderSupport()
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 键盘事件
|
* 键盘事件
|
||||||
@@ -585,6 +588,37 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
|
|||||||
requestFocusInWindow()
|
requestFocusInWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun resumeVisualWindows(id: String, dataProvider: DataProvider) {
|
||||||
|
val windows = properties.getString("VisualWindow.${id}.store") ?: return
|
||||||
|
for (name in windows.split(",")) {
|
||||||
|
if (name == "NVIDIA-SMI") {
|
||||||
|
addVisualWindow(
|
||||||
|
NvidiaSMIVisualWindow(
|
||||||
|
dataProvider.getData(DataProviders.TerminalTab) as SSHTerminalTab,
|
||||||
|
this
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else if (name == "SystemInformation") {
|
||||||
|
addVisualWindow(
|
||||||
|
SystemInformationVisualWindow(
|
||||||
|
dataProvider.getData(DataProviders.TerminalTab) as SSHTerminalTab,
|
||||||
|
this
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun storeVisualWindows(id: String) {
|
||||||
|
val windows = mutableListOf<String>()
|
||||||
|
for (window in getVisualWindows()) {
|
||||||
|
if (window is Resumeable) {
|
||||||
|
windows.add(window.getWindowName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
properties.putString("VisualWindow.${id}.store", windows.joinToString(","))
|
||||||
|
}
|
||||||
|
|
||||||
override fun getDimension(): Dimension {
|
override fun getDimension(): Dimension {
|
||||||
return Dimension(
|
return Dimension(
|
||||||
terminalDisplay.size.width + padding.left + padding.right,
|
terminalDisplay.size.width + padding.left + padding.right,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package app.termora.terminal.panel
|
package app.termora.terminal.panel
|
||||||
|
|
||||||
|
import app.termora.actions.TerminalCopyAction
|
||||||
import app.termora.keymap.KeyShortcut
|
import app.termora.keymap.KeyShortcut
|
||||||
import app.termora.keymap.KeymapManager
|
import app.termora.keymap.KeymapManager
|
||||||
|
import app.termora.terminal.ControlCharacters
|
||||||
import app.termora.terminal.Terminal
|
import app.termora.terminal.Terminal
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -68,6 +70,7 @@ class TerminalPanelKeyAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val keyStroke = KeyStroke.getKeyStrokeForEvent(e)
|
val keyStroke = KeyStroke.getKeyStrokeForEvent(e)
|
||||||
|
val keymapActions = activeKeymap.getActionIds(KeyShortcut(keyStroke))
|
||||||
for (action in terminalPanel.getTerminalActions()) {
|
for (action in terminalPanel.getTerminalActions()) {
|
||||||
if (action.test(keyStroke, e)) {
|
if (action.test(keyStroke, e)) {
|
||||||
action.actionPerformed(e)
|
action.actionPerformed(e)
|
||||||
@@ -78,6 +81,8 @@ class TerminalPanelKeyAdapter(
|
|||||||
val encode = terminal.getKeyEncoder().encode(AWTTerminalKeyEvent(e))
|
val encode = terminal.getKeyEncoder().encode(AWTTerminalKeyEvent(e))
|
||||||
if (encode.isNotEmpty()) {
|
if (encode.isNotEmpty()) {
|
||||||
writer.write(TerminalWriter.WriteRequest.fromBytes(encode.toByteArray(writer.getCharset())))
|
writer.write(TerminalWriter.WriteRequest.fromBytes(encode.toByteArray(writer.getCharset())))
|
||||||
|
// scroll to bottom
|
||||||
|
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
||||||
e.consume()
|
e.consume()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,20 +95,25 @@ class TerminalPanelKeyAdapter(
|
|||||||
if (isAltPressedOnly(e) && Character.isDefined(e.keyChar)) {
|
if (isAltPressedOnly(e) && Character.isDefined(e.keyChar)) {
|
||||||
val c = String(charArrayOf(ASCII_ESC, simpleMapKeyCodeToChar(e)))
|
val c = String(charArrayOf(ASCII_ESC, simpleMapKeyCodeToChar(e)))
|
||||||
writer.write(TerminalWriter.WriteRequest.fromBytes(c.toByteArray(writer.getCharset())))
|
writer.write(TerminalWriter.WriteRequest.fromBytes(c.toByteArray(writer.getCharset())))
|
||||||
|
// scroll to bottom
|
||||||
|
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
||||||
e.consume()
|
e.consume()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果命中了全局快捷键,那么不处理
|
// 如果命中了全局快捷键,那么不处理
|
||||||
if (keyStroke.modifiers != 0 && activeKeymap.getActionIds(KeyShortcut(keyStroke)).isNotEmpty()) {
|
val copyShortcutWithoutSelection =
|
||||||
|
keymapActions.contains(TerminalCopyAction.COPY) && terminal.getSelectionModel().hasSelection().not()
|
||||||
|
if (keyStroke.modifiers != 0 && keymapActions.isNotEmpty() && !copyShortcutWithoutSelection) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Character.isISOControl(e.keyChar)) {
|
val keyChar = mapKeyChar(e)
|
||||||
|
if (Character.isISOControl(keyChar)) {
|
||||||
terminal.getSelectionModel().clearSelection()
|
terminal.getSelectionModel().clearSelection()
|
||||||
// 如果不为空表示已经发送过了,所以这里为空的时候再发送
|
// 如果不为空表示已经发送过了,所以这里为空的时候再发送
|
||||||
if (encode.isEmpty()) {
|
if (encode.isEmpty()) {
|
||||||
writer.write(TerminalWriter.WriteRequest.fromBytes("${e.keyChar}".toByteArray(writer.getCharset())))
|
writer.write(TerminalWriter.WriteRequest.fromBytes("$keyChar".toByteArray(writer.getCharset())))
|
||||||
e.consume()
|
e.consume()
|
||||||
}
|
}
|
||||||
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
||||||
@@ -111,6 +121,21 @@ class TerminalPanelKeyAdapter(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun mapKeyChar(e: KeyEvent): Char {
|
||||||
|
if (Character.isISOControl(e.keyChar)) {
|
||||||
|
return e.keyChar
|
||||||
|
}
|
||||||
|
|
||||||
|
val isCtrlPressedOnly = isCtrlPressedOnly(e)
|
||||||
|
|
||||||
|
// https://github.com/TermoraDev/termora/issues/478
|
||||||
|
if (isCtrlPressedOnly && e.keyCode == KeyEvent.VK_OPEN_BRACKET) {
|
||||||
|
return ControlCharacters.ESC
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.keyChar
|
||||||
|
}
|
||||||
|
|
||||||
private fun isCtrlPressedOnly(e: KeyEvent): Boolean {
|
private fun isCtrlPressedOnly(e: KeyEvent): Boolean {
|
||||||
val modifiersEx = e.modifiersEx
|
val modifiersEx = e.modifiersEx
|
||||||
return (modifiersEx and InputEvent.ALT_DOWN_MASK) == 0
|
return (modifiersEx and InputEvent.ALT_DOWN_MASK) == 0
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
|
|||||||
// 如果不判断的话可能会导致移动了一点点就就进入选择状态了
|
// 如果不判断的话可能会导致移动了一点点就就进入选择状态了
|
||||||
val diff = terminalPanel.getAverageCharWidth() / 5.0
|
val diff = terminalPanel.getAverageCharWidth() / 5.0
|
||||||
if (abs(mousePressedPoint.y - e.y) >= diff || abs(mousePressedPoint.x - e.x) >= diff) {
|
if (abs(mousePressedPoint.y - e.y) >= diff || abs(mousePressedPoint.x - e.x) >= diff) {
|
||||||
|
// 设置选中模式
|
||||||
|
terminal.getSelectionModel().setBlockSelection(isOnlyAltDown(e))
|
||||||
beginSelect(
|
beginSelect(
|
||||||
Position(x = mousePressedPoint.x, y = mousePressedPoint.y),
|
Position(x = mousePressedPoint.x, y = mousePressedPoint.y),
|
||||||
)
|
)
|
||||||
@@ -141,6 +143,13 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isOnlyAltDown(e: MouseEvent): Boolean {
|
||||||
|
return e.isAltDown &&
|
||||||
|
e.isMetaDown.not() &&
|
||||||
|
e.isControlDown.not() &&
|
||||||
|
e.isShiftDown.not() &&
|
||||||
|
e.isAltGraphDown.not()
|
||||||
|
}
|
||||||
|
|
||||||
private fun beginSelect(position: Position) {
|
private fun beginSelect(position: Position) {
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class NvidiaSMIVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWind
|
|||||||
private val percentageBtn by lazy { JButton(if (isPercentage) Icons.text else Icons.percentage) }
|
private val percentageBtn by lazy { JButton(if (isPercentage) Icons.text else Icons.percentage) }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
Disposer.register(tab, this)
|
||||||
initViews()
|
initViews()
|
||||||
initEvents()
|
initEvents()
|
||||||
initVisualWindowPanel()
|
initVisualWindowPanel()
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package app.termora.terminal.panel.vw
|
||||||
|
|
||||||
|
interface Resumeable
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package app.termora.terminal.panel.vw
|
package app.termora.terminal.panel.vw
|
||||||
|
|
||||||
import app.termora.Disposer
|
|
||||||
import app.termora.SSHTerminalTab
|
import app.termora.SSHTerminalTab
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
@@ -11,11 +10,7 @@ abstract class SSHVisualWindow(
|
|||||||
protected val tab: SSHTerminalTab,
|
protected val tab: SSHTerminalTab,
|
||||||
id: String,
|
id: String,
|
||||||
visualWindowManager: VisualWindowManager
|
visualWindowManager: VisualWindowManager
|
||||||
) : VisualWindowPanel(id, visualWindowManager) {
|
) : VisualWindowPanel(id, visualWindowManager), Resumeable {
|
||||||
|
|
||||||
init {
|
|
||||||
Disposer.register(tab, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toggleWindow() {
|
override fun toggleWindow() {
|
||||||
val evt = AnActionEvent(tab.getJComponent(), StringUtils.EMPTY, EventObject(this))
|
val evt = AnActionEvent(tab.getJComponent(), StringUtils.EMPTY, EventObject(this))
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
|||||||
private val systemInformationPanel by lazy { SystemInformationPanel() }
|
private val systemInformationPanel by lazy { SystemInformationPanel() }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
Disposer.register(tab, this)
|
||||||
initViews()
|
initViews()
|
||||||
initEvents()
|
initEvents()
|
||||||
initVisualWindowPanel()
|
initVisualWindowPanel()
|
||||||
@@ -137,7 +138,7 @@ class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
|||||||
private suspend fun refreshCPUAndMem(session: ClientSession) {
|
private suspend fun refreshCPUAndMem(session: ClientSession) {
|
||||||
|
|
||||||
// top
|
// top
|
||||||
var pair = SshClients.execChannel(session, "top -bn1")
|
val pair = SshClients.execChannel(session, "top -bn1")
|
||||||
if (pair.first != 0) {
|
if (pair.first != 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -236,7 +237,7 @@ class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
|||||||
private suspend fun refreshDisk(session: ClientSession) {
|
private suspend fun refreshDisk(session: ClientSession) {
|
||||||
|
|
||||||
// df -h
|
// df -h
|
||||||
var pair = SshClients.execChannel(session, "df -B1")
|
val pair = SshClients.execChannel(session, "df -B1")
|
||||||
if (pair.first != 0) {
|
if (pair.first != 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,4 +28,9 @@ interface VisualWindow : Disposable {
|
|||||||
* 切换独立模式
|
* 切换独立模式
|
||||||
*/
|
*/
|
||||||
fun toggleWindow()
|
fun toggleWindow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同一个类,返回的相同
|
||||||
|
*/
|
||||||
|
fun getWindowName(): String
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.terminal.panel.vw
|
package app.termora.terminal.panel.vw
|
||||||
|
|
||||||
|
import app.termora.actions.DataProvider
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
|
|
||||||
interface VisualWindowManager {
|
interface VisualWindowManager {
|
||||||
@@ -33,4 +34,14 @@ interface VisualWindowManager {
|
|||||||
* 获取管理器的宽高
|
* 获取管理器的宽高
|
||||||
*/
|
*/
|
||||||
fun getDimension(): Dimension
|
fun getDimension(): Dimension
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复所有窗口
|
||||||
|
*/
|
||||||
|
fun resumeVisualWindows(id: String, dataProvider: DataProvider)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储所有窗口
|
||||||
|
*/
|
||||||
|
fun storeVisualWindows(id: String)
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,12 @@ package app.termora.terminal.panel.vw
|
|||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
import java.awt.event.*
|
import java.awt.event.*
|
||||||
import java.beans.PropertyChangeEvent
|
import java.beans.PropertyChangeEvent
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
|
import javax.imageio.ImageIO
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -333,6 +335,15 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
|||||||
title = getWindowTitle()
|
title = getWindowTitle()
|
||||||
isAlwaysOnTop = isAlwaysTop
|
isAlwaysOnTop = isAlwaysTop
|
||||||
|
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
val sizes = listOf(16, 20, 24, 28, 32, 48, 64)
|
||||||
|
val loader = TermoraFrame::class.java.classLoader
|
||||||
|
val images = sizes.mapNotNull { e ->
|
||||||
|
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
|
||||||
|
}
|
||||||
|
iconImages = images
|
||||||
|
}
|
||||||
|
|
||||||
initEvents()
|
initEvents()
|
||||||
|
|
||||||
init()
|
init()
|
||||||
@@ -374,4 +385,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getWindowName(): String {
|
||||||
|
return id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
88
src/main/kotlin/app/termora/vfs2/VFSWalker.kt
Normal file
88
src/main/kotlin/app/termora/vfs2/VFSWalker.kt
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package app.termora.vfs2
|
||||||
|
|
||||||
|
import org.apache.commons.vfs2.FileObject
|
||||||
|
import java.nio.file.FileVisitResult
|
||||||
|
import java.nio.file.FileVisitor
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes
|
||||||
|
import java.nio.file.attribute.FileTime
|
||||||
|
|
||||||
|
object VFSWalker {
|
||||||
|
fun walk(
|
||||||
|
dir: FileObject,
|
||||||
|
visitor: FileVisitor<FileObject>,
|
||||||
|
): FileVisitResult {
|
||||||
|
|
||||||
|
// clear cache
|
||||||
|
if (visitor.preVisitDirectory(dir, EmptyBasicFileAttributes.INSTANCE) == FileVisitResult.TERMINATE) {
|
||||||
|
return FileVisitResult.TERMINATE
|
||||||
|
}
|
||||||
|
|
||||||
|
for (e in dir.children) {
|
||||||
|
if (e.name.baseName == ".." || e.name.baseName == ".") continue
|
||||||
|
if (e.isFolder) {
|
||||||
|
if (walk(dir.resolveFile(e.name.baseName), visitor) == FileVisitResult.TERMINATE) {
|
||||||
|
return FileVisitResult.TERMINATE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val result = visitor.visitFile(
|
||||||
|
dir.resolveFile(e.name.baseName),
|
||||||
|
EmptyBasicFileAttributes.INSTANCE
|
||||||
|
)
|
||||||
|
if (result == FileVisitResult.TERMINATE) {
|
||||||
|
return FileVisitResult.TERMINATE
|
||||||
|
} else if (result == FileVisitResult.SKIP_SUBTREE) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visitor.postVisitDirectory(dir, null) == FileVisitResult.TERMINATE) {
|
||||||
|
return FileVisitResult.TERMINATE
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
|
||||||
|
private class EmptyBasicFileAttributes : BasicFileAttributes {
|
||||||
|
companion object {
|
||||||
|
val INSTANCE = EmptyBasicFileAttributes()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun lastModifiedTime(): FileTime {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun lastAccessTime(): FileTime {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun creationTime(): FileTime {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isRegularFile(): Boolean {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isDirectory(): Boolean {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isSymbolicLink(): Boolean {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isOther(): Boolean {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun size(): Long {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fileKey(): Any {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,7 +55,9 @@ termora.settings.appearance.language=Language
|
|||||||
termora.settings.appearance.i-want-to-translate=I want to translate
|
termora.settings.appearance.i-want-to-translate=I want to translate
|
||||||
termora.settings.appearance.follow-system=Sync with OS
|
termora.settings.appearance.follow-system=Sync with OS
|
||||||
termora.settings.appearance.opacity=Opacity
|
termora.settings.appearance.opacity=Opacity
|
||||||
|
termora.settings.appearance.background-image=BG Image
|
||||||
termora.settings.appearance.background-running=Backgrounding
|
termora.settings.appearance.background-running=Backgrounding
|
||||||
|
termora.settings.appearance.confirm-tab-close=Confirm tab close
|
||||||
|
|
||||||
termora.setting.security=Security
|
termora.setting.security=Security
|
||||||
termora.setting.security.enter-password=Enter password
|
termora.setting.security.enter-password=Enter password
|
||||||
@@ -72,6 +74,7 @@ termora.settings.terminal.size=Size
|
|||||||
termora.settings.terminal.max-rows=Max rows
|
termora.settings.terminal.max-rows=Max rows
|
||||||
termora.settings.terminal.debug=Debug mode
|
termora.settings.terminal.debug=Debug mode
|
||||||
termora.settings.terminal.beep=Beep
|
termora.settings.terminal.beep=Beep
|
||||||
|
termora.settings.terminal.hyperlink=Hyperlink
|
||||||
termora.settings.terminal.select-copy=Select copy
|
termora.settings.terminal.select-copy=Select copy
|
||||||
termora.settings.terminal.cursor-style=Cursor type
|
termora.settings.terminal.cursor-style=Cursor type
|
||||||
termora.settings.terminal.cursor-blink=Cursor blink
|
termora.settings.terminal.cursor-blink=Cursor blink
|
||||||
@@ -230,6 +233,8 @@ termora.tabbed.contextmenu.close=Close
|
|||||||
termora.tabbed.contextmenu.close-other-tabs=Close Other Tabs
|
termora.tabbed.contextmenu.close-other-tabs=Close Other Tabs
|
||||||
termora.tabbed.contextmenu.close-all-tabs=Close All Tabs
|
termora.tabbed.contextmenu.close-all-tabs=Close All Tabs
|
||||||
termora.tabbed.contextmenu.reconnect=Reconnect
|
termora.tabbed.contextmenu.reconnect=Reconnect
|
||||||
|
termora.tabbed.local-tab.close-prompt=Do you want to terminal a running process in this terminal?
|
||||||
|
termora.tabbed.tab.close-prompt=Are you sure you want to close this tab?
|
||||||
|
|
||||||
# Terminal logger
|
# Terminal logger
|
||||||
termora.terminal-logger=Terminal Logger
|
termora.terminal-logger=Terminal Logger
|
||||||
@@ -307,6 +312,7 @@ termora.transport.permissions.execute=Execute
|
|||||||
termora.transport.permissions.owner=Owner
|
termora.transport.permissions.owner=Owner
|
||||||
termora.transport.permissions.group=Group
|
termora.transport.permissions.group=Group
|
||||||
termora.transport.permissions.others=Others
|
termora.transport.permissions.others=Others
|
||||||
|
termora.transport.permissions.include-subfolder=Include subdirectories
|
||||||
|
|
||||||
termora.transport.sftp.retry=Retry
|
termora.transport.sftp.retry=Retry
|
||||||
termora.transport.sftp.select-another-host=Select another host
|
termora.transport.sftp.select-another-host=Select another host
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ termora.settings.appearance.language=语言
|
|||||||
termora.settings.appearance.i-want-to-translate=我想要翻译
|
termora.settings.appearance.i-want-to-translate=我想要翻译
|
||||||
termora.settings.appearance.follow-system=跟随系统
|
termora.settings.appearance.follow-system=跟随系统
|
||||||
termora.settings.appearance.opacity=透明度
|
termora.settings.appearance.opacity=透明度
|
||||||
|
termora.settings.appearance.background-image=背景图
|
||||||
termora.settings.appearance.background-running=后台运行
|
termora.settings.appearance.background-running=后台运行
|
||||||
|
termora.settings.appearance.confirm-tab-close=标签关闭前确认
|
||||||
|
|
||||||
termora.setting.security=安全
|
termora.setting.security=安全
|
||||||
termora.setting.security.enter-password=请输入密码
|
termora.setting.security.enter-password=请输入密码
|
||||||
@@ -76,6 +78,7 @@ termora.settings.terminal.size=大小
|
|||||||
termora.settings.terminal.max-rows=最大行数
|
termora.settings.terminal.max-rows=最大行数
|
||||||
termora.settings.terminal.debug=调试模式
|
termora.settings.terminal.debug=调试模式
|
||||||
termora.settings.terminal.beep=蜂鸣声
|
termora.settings.terminal.beep=蜂鸣声
|
||||||
|
termora.settings.terminal.hyperlink=超链接
|
||||||
termora.settings.terminal.select-copy=选中复制
|
termora.settings.terminal.select-copy=选中复制
|
||||||
termora.settings.terminal.cursor-style=光标样式
|
termora.settings.terminal.cursor-style=光标样式
|
||||||
termora.settings.terminal.cursor-blink=光标闪烁
|
termora.settings.terminal.cursor-blink=光标闪烁
|
||||||
@@ -219,7 +222,8 @@ termora.tabbed.contextmenu.close=关闭
|
|||||||
termora.tabbed.contextmenu.close-other-tabs=关闭其他标签页
|
termora.tabbed.contextmenu.close-other-tabs=关闭其他标签页
|
||||||
termora.tabbed.contextmenu.close-all-tabs=关闭所有标签页
|
termora.tabbed.contextmenu.close-all-tabs=关闭所有标签页
|
||||||
termora.tabbed.contextmenu.reconnect=重新连接
|
termora.tabbed.contextmenu.reconnect=重新连接
|
||||||
|
termora.tabbed.local-tab.close-prompt=你想要终止这个终端中正在运行的进程吗?
|
||||||
|
termora.tabbed.tab.close-prompt=你确定要关闭这个标签页吗?
|
||||||
|
|
||||||
|
|
||||||
# Terminal logger
|
# Terminal logger
|
||||||
@@ -298,7 +302,7 @@ termora.transport.sftp.status.done=已完成
|
|||||||
termora.transport.sftp.status.failed=已失败
|
termora.transport.sftp.status.failed=已失败
|
||||||
|
|
||||||
|
|
||||||
termora.transport.sftp.already-exists.message1=此文件夹已包含一下名称的对象
|
termora.transport.sftp.already-exists.message1=此文件夹已包含以下名称的对象
|
||||||
termora.transport.sftp.already-exists.message2=请选择要执行的操作
|
termora.transport.sftp.already-exists.message2=请选择要执行的操作
|
||||||
termora.transport.sftp.already-exists.overwrite=覆盖
|
termora.transport.sftp.already-exists.overwrite=覆盖
|
||||||
termora.transport.sftp.already-exists.append=追加
|
termora.transport.sftp.already-exists.append=追加
|
||||||
@@ -321,6 +325,7 @@ termora.transport.permissions.execute=执行
|
|||||||
termora.transport.permissions.owner=所有者
|
termora.transport.permissions.owner=所有者
|
||||||
termora.transport.permissions.group=组
|
termora.transport.permissions.group=组
|
||||||
termora.transport.permissions.others=其他
|
termora.transport.permissions.others=其他
|
||||||
|
termora.transport.permissions.include-subfolder=包含子目录
|
||||||
|
|
||||||
# transport job
|
# transport job
|
||||||
termora.transport.jobs.table.name=名称
|
termora.transport.jobs.table.name=名称
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ termora.settings.appearance.language=語言
|
|||||||
termora.settings.appearance.i-want-to-translate=我想要翻譯
|
termora.settings.appearance.i-want-to-translate=我想要翻譯
|
||||||
termora.settings.appearance.follow-system=跟隨系統
|
termora.settings.appearance.follow-system=跟隨系統
|
||||||
termora.settings.appearance.opacity=透明度
|
termora.settings.appearance.opacity=透明度
|
||||||
|
termora.settings.appearance.background-image=背景圖
|
||||||
termora.settings.appearance.background-running=後台運行
|
termora.settings.appearance.background-running=後台運行
|
||||||
|
termora.settings.appearance.confirm-tab-close=關閉分頁確認
|
||||||
|
|
||||||
termora.setting.security=安全
|
termora.setting.security=安全
|
||||||
termora.setting.security.enter-password=請輸入密碼
|
termora.setting.security.enter-password=請輸入密碼
|
||||||
@@ -87,7 +89,8 @@ termora.settings.terminal.font=字體
|
|||||||
termora.settings.terminal.size=大小
|
termora.settings.terminal.size=大小
|
||||||
termora.settings.terminal.max-rows=最大行數
|
termora.settings.terminal.max-rows=最大行數
|
||||||
termora.settings.terminal.debug=偵錯模式
|
termora.settings.terminal.debug=偵錯模式
|
||||||
termora.settings.terminal.beep=蜂鳴聲
|
termora.settings.terminal.beep=超連結
|
||||||
|
termora.settings.terminal.hyperlink=Hyperlink
|
||||||
termora.settings.terminal.select-copy=選取複製
|
termora.settings.terminal.select-copy=選取複製
|
||||||
termora.settings.terminal.cursor-style=遊標風格
|
termora.settings.terminal.cursor-style=遊標風格
|
||||||
termora.settings.terminal.cursor-blink=遊標閃爍
|
termora.settings.terminal.cursor-blink=遊標閃爍
|
||||||
@@ -215,7 +218,8 @@ termora.tabbed.contextmenu.close=關閉
|
|||||||
termora.tabbed.contextmenu.close-other-tabs=關閉其他標籤頁
|
termora.tabbed.contextmenu.close-other-tabs=關閉其他標籤頁
|
||||||
termora.tabbed.contextmenu.close-all-tabs=關閉所有標籤
|
termora.tabbed.contextmenu.close-all-tabs=關閉所有標籤
|
||||||
termora.tabbed.contextmenu.reconnect=重新連接
|
termora.tabbed.contextmenu.reconnect=重新連接
|
||||||
|
termora.tabbed.local-tab.close-prompt=你想要終止這個終端機中正在運作的進程嗎?
|
||||||
|
termora.tabbed.tab.close-prompt=你確定要關閉這個分頁嗎?
|
||||||
|
|
||||||
|
|
||||||
# Terminal logger
|
# Terminal logger
|
||||||
@@ -292,7 +296,7 @@ termora.transport.sftp.status.waiting=等待中
|
|||||||
termora.transport.sftp.status.done=已完成
|
termora.transport.sftp.status.done=已完成
|
||||||
termora.transport.sftp.status.failed=已失敗
|
termora.transport.sftp.status.failed=已失敗
|
||||||
|
|
||||||
termora.transport.sftp.already-exists.message1=此資料夾已包含一下名稱的對象
|
termora.transport.sftp.already-exists.message1=此資料夾已包含以下名稱的對象
|
||||||
termora.transport.sftp.already-exists.message2=請選擇要執行的操作
|
termora.transport.sftp.already-exists.message2=請選擇要執行的操作
|
||||||
termora.transport.sftp.already-exists.overwrite=覆蓋
|
termora.transport.sftp.already-exists.overwrite=覆蓋
|
||||||
termora.transport.sftp.already-exists.append=追加
|
termora.transport.sftp.already-exists.append=追加
|
||||||
@@ -303,6 +307,17 @@ termora.transport.sftp.already-exists.destination=目標文件
|
|||||||
termora.transport.sftp.already-exists.source=原始檔
|
termora.transport.sftp.already-exists.source=原始檔
|
||||||
termora.transport.sftp.already-exists.actions=操作
|
termora.transport.sftp.already-exists.actions=操作
|
||||||
|
|
||||||
|
# permissions
|
||||||
|
termora.transport.permissions=更改權限
|
||||||
|
termora.transport.permissions.file-folder-permissions=檔案/資料夾權限
|
||||||
|
termora.transport.permissions.read=讀取
|
||||||
|
termora.transport.permissions.write=寫入
|
||||||
|
termora.transport.permissions.execute=執行
|
||||||
|
termora.transport.permissions.owner=所有者
|
||||||
|
termora.transport.permissions.group=群組
|
||||||
|
termora.transport.permissions.others=其他
|
||||||
|
termora.transport.permissions.include-subfolder=包含子目錄
|
||||||
|
|
||||||
# transport job
|
# transport job
|
||||||
termora.transport.jobs.table.name=名稱
|
termora.transport.jobs.table.name=名稱
|
||||||
termora.transport.jobs.table.status=狀態
|
termora.transport.jobs.table.status=狀態
|
||||||
|
|||||||
4
src/main/resources/icons/microsoftWindows.svg
Normal file
4
src/main/resources/icons/microsoftWindows.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2.00098 2H7.00195L7.00098 7H2L2.00098 2ZM8.00293 2H13V7H8.00293V2ZM2 7.99902L7 8V13.001L2 13V7.99902ZM8.00195 8H12.999L12.998 13.001H8.00195" fill="#6C707E"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 399 B |
4
src/main/resources/icons/microsoftWindows_dark.svg
Normal file
4
src/main/resources/icons/microsoftWindows_dark.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2.00098 2H7.00195L7.00098 7H2L2.00098 2ZM8.00293 2H13V7H8.00293V2ZM2 7.99902L7 8V13.001L2 13V7.99902ZM8.00195 8H12.999L12.998 13.001H8.00195" fill="#CED0D6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 399 B |
7
src/main/resources/icons/settingSync.svg
Normal file
7
src/main/resources/icons/settingSync.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3.5 9V8C3.5 4.96243 5.96243 2.5 9 2.5C10.1068 2.5 11.1372 2.82692 12 3.38947" stroke="#6C707E" stroke-linecap="round"/>
|
||||||
|
<path d="M6 12.6105C6.86278 13.1731 7.89321 13.5 9 13.5C12.0376 13.5 14.5 11.0376 14.5 8V7" stroke="#6C707E" stroke-linecap="round"/>
|
||||||
|
<path d="M1.37868 7.32133L3.5 9.44265L5.62132 7.32133" stroke="#6C707E" stroke-linecap="round"/>
|
||||||
|
<path d="M12.3787 8.67867L14.5 6.55735L16.6213 8.67867" stroke="#6C707E" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 689 B |
7
src/main/resources/icons/settingSync_dark.svg
Normal file
7
src/main/resources/icons/settingSync_dark.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3.5 9V8C3.5 4.96243 5.96243 2.5 9 2.5C10.1068 2.5 11.1372 2.82692 12 3.38947" stroke="#CED0D6" stroke-linecap="round"/>
|
||||||
|
<path d="M6 12.6105C6.86278 13.1731 7.89321 13.5 9 13.5C12.0376 13.5 14.5 11.0376 14.5 8V7" stroke="#CED0D6" stroke-linecap="round"/>
|
||||||
|
<path d="M1.37868 7.32133L3.5 9.44265L5.62132 7.32133" stroke="#CED0D6" stroke-linecap="round"/>
|
||||||
|
<path d="M12.3787 8.67867L14.5 6.55735L16.6213 8.67867" stroke="#CED0D6" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 689 B |
@@ -55,6 +55,10 @@ Source: "{#MySourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdir
|
|||||||
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||||
|
|
||||||
|
[InstallDelete]
|
||||||
|
Type: files; Name: "{app}\app\*.jar"
|
||||||
|
Type: filesandordirs; Name: "{app}\runtime\*"
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall; Check: ShouldPromptStart
|
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall; Check: ShouldPromptStart
|
||||||
Filename: "{app}\{#MyAppExeName}"; Flags: nowait runhidden; Check: ShouldAutoStart
|
Filename: "{app}\{#MyAppExeName}"; Flags: nowait runhidden; Check: ShouldAutoStart
|
||||||
|
|||||||
29
src/test/resources/issue-564/Dockerfile
Normal file
29
src/test/resources/issue-564/Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
FROM debian:bookworm-slim
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
# 安装基础包 + sshd + nvim 依赖
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
openssh-server curl ca-certificates tzdata git unzip \
|
||||||
|
libfuse2 locales && \
|
||||||
|
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
# 安装 nvim 最新版(AppImage 提取)
|
||||||
|
RUN curl -LO https://github.com/neovim/neovim/releases/download/v0.11.1/nvim-linux-arm64.appimage && \
|
||||||
|
mv nvim-linux-arm64.appimage nvim.appimage && chmod u+x nvim.appimage && ./nvim.appimage --appimage-extract && \
|
||||||
|
mv squashfs-root/usr/bin/nvim /usr/local/bin/nvim && \
|
||||||
|
rm -rf squashfs-root nvim.appimage
|
||||||
|
# 配置 SSH
|
||||||
|
RUN mkdir /var/run/sshd && \
|
||||||
|
echo 'root:root' | chpasswd && \
|
||||||
|
echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config && \
|
||||||
|
echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config
|
||||||
|
# 设置语言环境(可选)
|
||||||
|
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \
|
||||||
|
apt-get update && apt-get install -y locales && \
|
||||||
|
locale-gen en_US.UTF-8 && \
|
||||||
|
update-locale LANG=en_US.UTF-8
|
||||||
|
ENV LANG=en_US.UTF-8 \
|
||||||
|
LANGUAGE=en_US:en \
|
||||||
|
LC_ALL=en_US.UTF-8
|
||||||
|
# 启动 SSHD
|
||||||
|
EXPOSE 22
|
||||||
|
CMD ["/usr/sbin/sshd", "-D"]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
FROM linuxserver/openssh-server
|
FROM linuxserver/openssh-server
|
||||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||||
&& apk update && apk add wget gcc g++ git make zsh htop stress-ng inetutils-telnet xclock xcalc xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
|
&& apk update && apk add wget tmux gcc g++ git make zsh htop stress-ng inetutils-telnet xclock xcalc xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
|
||||||
&& tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \
|
&& tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \
|
||||||
&& ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz
|
&& ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz
|
||||||
RUN sed -i 's/#AllowAgentForwarding yes/AllowAgentForwarding yes/g' /etc/ssh/sshd_config
|
RUN sed -i 's/#AllowAgentForwarding yes/AllowAgentForwarding yes/g' /etc/ssh/sshd_config
|
||||||
|
|||||||
Reference in New Issue
Block a user