Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef9caf2578 | ||
|
|
b85bdf840e | ||
|
|
a2d7f3b5bb | ||
|
|
02a96d73c8 | ||
|
|
9fb12c7a71 | ||
|
|
145d8fc802 | ||
|
|
72c9dba806 | ||
|
|
de20bd654c | ||
|
|
35b3a10746 | ||
|
|
05fe6a0eb1 | ||
|
|
0552917c26 | ||
|
|
51c355c113 | ||
|
|
034ee3791d | ||
|
|
adabaf8f2d | ||
|
|
1f392c52a1 | ||
|
|
28fe4c725f | ||
|
|
18fe92cb11 | ||
|
|
c49acf7b51 | ||
|
|
7df317a1b9 | ||
|
|
219e5420f5 | ||
|
|
aefb7c3014 | ||
|
|
f0c7f06ff5 | ||
|
|
604e07b43a | ||
|
|
0000e4610a | ||
|
|
510324d7c4 | ||
|
|
33a359fcbf | ||
|
|
0b84d3271c | ||
|
|
57547c95cb | ||
|
|
503cfa9a4e | ||
|
|
af1f979e31 | ||
|
|
3cd9f92ea9 |
19
.github/workflows/osx-aarch64.yml
vendored
@@ -33,6 +33,16 @@ jobs:
|
||||
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
- name: Setup the Notary Information
|
||||
if: github.ref_type == 'tag' && github.repository == 'TermoraDev/termora'
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
STORE_CREDENTIALS: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||
run: |
|
||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b825.69.tar.gz
|
||||
|
||||
@@ -57,8 +67,11 @@ jobs:
|
||||
# dist
|
||||
- name: Dist
|
||||
env:
|
||||
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' }}
|
||||
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
|
||||
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||
# 只有发布版本时才需要公证
|
||||
TERMORA_MAC_NOTARY: ${{ github.ref_type == 'tag' && github.repository == 'TermoraDev/termora' }}
|
||||
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||
run: |
|
||||
./gradlew dist --no-daemon
|
||||
|
||||
@@ -66,4 +79,6 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: termora-osx-aarch64
|
||||
path: build/distributions/*.dmg
|
||||
path: |
|
||||
build/distributions/*.zip
|
||||
build/distributions/*.dmg
|
||||
|
||||
19
.github/workflows/osx-x86-64.yml
vendored
@@ -33,6 +33,16 @@ jobs:
|
||||
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
- name: Setup the Notary Information
|
||||
if: github.ref_type == 'tag' && github.repository == 'TermoraDev/termora'
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
STORE_CREDENTIALS: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||
run: |
|
||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b825.69.tar.gz
|
||||
|
||||
@@ -59,8 +69,11 @@ jobs:
|
||||
# dist
|
||||
- name: Dist
|
||||
env:
|
||||
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' }}
|
||||
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
|
||||
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||
# 只有发布版本时才需要公证
|
||||
TERMORA_MAC_NOTARY: ${{ github.ref_type == 'tag' && github.repository == 'TermoraDev/termora' }}
|
||||
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||
run: |
|
||||
./gradlew dist --no-daemon
|
||||
|
||||
@@ -68,4 +81,6 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: termora-osx-x86-64
|
||||
path: build/distributions/*.dmg
|
||||
path: |
|
||||
build/distributions/*.zip
|
||||
build/distributions/*.dmg
|
||||
|
||||
20
.github/workflows/windows-x86-64.yml
vendored
@@ -10,11 +10,20 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install zip
|
||||
run: |
|
||||
$system32 = [System.Environment]::GetEnvironmentVariable("WINDIR") + "\System32"
|
||||
Invoke-WebRequest -Uri "http://stahlworks.com/dev/zip.exe" -OutFile "$system32\zip.exe"
|
||||
Invoke-WebRequest -Uri "http://stahlworks.com/dev/unzip.exe" -OutFile "$system32\unzip.exe"
|
||||
|
||||
- name: Install 7z
|
||||
uses: milliewalky/setup-7-zip@v2
|
||||
|
||||
- name: Installing Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'jetbrains'
|
||||
java-version: '21'
|
||||
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
|
||||
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
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
@@ -36,4 +45,5 @@ jobs:
|
||||
name: termora-windows-x86-64
|
||||
path: |
|
||||
build/distributions/*.zip
|
||||
build/distributions/*.msi
|
||||
build/distributions/*.msi
|
||||
build/distributions/*.exe
|
||||
|
||||
3
.github/workflows/winget.yml
vendored
@@ -7,7 +7,8 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: vedantmgoyal9/winget-releaser@main
|
||||
if: github.repository == 'TermoraDev/termora'
|
||||
with:
|
||||
identifier: TermoraDev.Termora
|
||||
installers-regex: 'x86-64\.msi$' # Only x86-64.msi files
|
||||
installers-regex: 'x86-64\.exe$' # Only x86-64.exe files
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
22
THIRDPARTY
@@ -14,7 +14,7 @@ commonmark 0.24.0
|
||||
BSD 2-Clause "Simplified" License
|
||||
https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt
|
||||
|
||||
commons-codec 1.17.1
|
||||
commons-codec 1.18.0
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-codec/blob/master/LICENSE.txt
|
||||
|
||||
@@ -34,10 +34,18 @@ commons-net 3.11.1
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-net/blob/master/LICENSE.txt
|
||||
|
||||
commons-text 1.12.0
|
||||
commons-text 1.13.0
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-text/blob/master/LICENSE.txt
|
||||
|
||||
commons-csv 1.13.0
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-csv/blob/master/LICENSE.txt
|
||||
|
||||
ini4j 0.5.5-2
|
||||
Apache License 2.0
|
||||
http://www.apache.org/licenses/LICENSE-2.0.txt
|
||||
|
||||
eddsa 0.3.0
|
||||
Creative Commons Zero v1.0 Universal
|
||||
https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt
|
||||
@@ -110,7 +118,7 @@ kotlin-logging 1.7.9
|
||||
Apache License 2.0
|
||||
https://github.com/oshai/kotlin-logging/blob/master/LICENSE
|
||||
|
||||
kotlin-stdlib 2.1.0
|
||||
kotlin-stdlib 2.1.10
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||
|
||||
@@ -126,6 +134,10 @@ kotlin-stdlib-jdk8 1.9.10
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||
|
||||
restart4j 0.0.1
|
||||
Apache License 2.0
|
||||
https://github.com/hstyi/restart4j/blob/main/LICENSE
|
||||
|
||||
kotlinx-coroutines-core-jvm 1.10.1
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
@@ -134,11 +146,11 @@ kotlinx-coroutines-swing 1.10.1
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
kotlinx-serialization-core-jvm 1.7.3
|
||||
kotlinx-serialization-core-jvm 1.8.0
|
||||
Apache License 2.0
|
||||
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
||||
|
||||
kotlinx-serialization-json-jvm 1.7.3
|
||||
kotlinx-serialization-json-jvm 1.8.0
|
||||
Apache License 2.0
|
||||
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
||||
|
||||
|
||||
458
build.gradle.kts
@@ -3,8 +3,12 @@ import org.gradle.kotlin.dsl.support.uppercaseFirstChar
|
||||
import org.gradle.nativeplatform.platform.internal.ArchitectureInternal
|
||||
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
|
||||
import org.jetbrains.kotlin.org.apache.commons.io.filefilter.FileFilterUtils
|
||||
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
||||
import java.io.FileNotFoundException
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.Future
|
||||
|
||||
plugins {
|
||||
java
|
||||
@@ -16,7 +20,7 @@ plugins {
|
||||
|
||||
|
||||
group = "app.termora"
|
||||
version = "1.0.8"
|
||||
version = "1.0.9"
|
||||
|
||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||
@@ -59,6 +63,7 @@ dependencies {
|
||||
implementation(libs.commons.codec)
|
||||
implementation(libs.commons.io)
|
||||
implementation(libs.commons.lang3)
|
||||
implementation(libs.commons.csv)
|
||||
implementation(libs.commons.net)
|
||||
implementation(libs.commons.text)
|
||||
implementation(libs.commons.compress)
|
||||
@@ -98,7 +103,7 @@ dependencies {
|
||||
implementation(libs.sshd.core)
|
||||
implementation(libs.commonmark)
|
||||
implementation(libs.jgit)
|
||||
implementation(libs.jgit.sshd)
|
||||
implementation(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
||||
implementation(libs.jnafilechooser)
|
||||
implementation(libs.xodus.vfs)
|
||||
implementation(libs.xodus.openAPI)
|
||||
@@ -107,6 +112,8 @@ dependencies {
|
||||
implementation(libs.colorpicker)
|
||||
implementation(libs.mixpanel)
|
||||
implementation(libs.jSerialComm)
|
||||
implementation(libs.ini4j)
|
||||
implementation(libs.restart4j)
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -142,16 +149,17 @@ tasks.test {
|
||||
tasks.register<Copy>("copy-dependencies") {
|
||||
val dir = layout.buildDirectory.dir("libs")
|
||||
from(configurations.runtimeClasspath).into(dir)
|
||||
val jna = libs.jna.asProvider().get()
|
||||
val pty4j = libs.pty4j.get()
|
||||
val jSerialComm = libs.jSerialComm.get()
|
||||
val restart4j = libs.restart4j.get()
|
||||
|
||||
// 对 JNA 和 PTY4J 的本地库提取
|
||||
// 提取出来是为了单独签名,不然无法通过公证
|
||||
if (os.isMacOsX && macOSSign) {
|
||||
doLast {
|
||||
val jna = libs.jna.asProvider().get()
|
||||
val archName = if (arch.isArm) "aarch64" else "x86_64"
|
||||
val dylib = dir.get().dir("dylib").asFile
|
||||
val pty4j = libs.pty4j.get()
|
||||
val jSerialComm = libs.jSerialComm.get()
|
||||
|
||||
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
|
||||
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
||||
val targetDir = File(dylib, jna.name)
|
||||
@@ -177,7 +185,6 @@ tasks.register<Copy>("copy-dependencies") {
|
||||
// 删除所有二进制类库
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
|
||||
} else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) {
|
||||
val archName = if (arch.isArm) "aarch64" else "x86_64"
|
||||
val targetDir = FileUtils.getFile(dylib, jSerialComm.name, "OSX", archName)
|
||||
FileUtils.forceMkdir(targetDir)
|
||||
// @formatter:off
|
||||
@@ -191,6 +198,24 @@ tasks.register<Copy>("copy-dependencies") {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") }
|
||||
} else if ("${restart4j.name}-${restart4j.version}" == file.nameWithoutExtension) {
|
||||
val targetDir = FileUtils.getFile(dylib, restart4j.name)
|
||||
FileUtils.forceMkdir(targetDir)
|
||||
// @formatter:off
|
||||
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "darwin/${archName}/*", "-d", targetDir.absolutePath) }
|
||||
// @formatter:on
|
||||
// 删除所有二进制类库
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "win32/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "darwin/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "linux/*") }
|
||||
// 设置可执行权限
|
||||
for (e in FileUtils.listFiles(
|
||||
targetDir,
|
||||
FileFilterUtils.trueFileFilter(),
|
||||
FileFilterUtils.falseFileFilter()
|
||||
)) {
|
||||
e.setExecutable(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +228,73 @@ tasks.register<Copy>("copy-dependencies") {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (os.isLinux || os.isWindows) { // 缩减安装包
|
||||
doLast {
|
||||
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
|
||||
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/darwin-*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/sunos-*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/openbsd-*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/freebsd-*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/dragonflybsd-*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
|
||||
if (os.isWindows) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/linux-*") }
|
||||
if (arch.isArm) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-x86*") }
|
||||
} else {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-aarch64/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-x86/*") }
|
||||
}
|
||||
} else if (os.isLinux) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-*") }
|
||||
}
|
||||
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*darwin*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*freebsd*") }
|
||||
if (os.isWindows) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*linux*") }
|
||||
if (arch.isArm) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86-64*") }
|
||||
} else {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/aarch64/*") }
|
||||
}
|
||||
} else if (os.isLinux) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win*") }
|
||||
}
|
||||
} else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "Android/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "FreeBSD/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "OpenBSD/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") }
|
||||
if (os.isWindows) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "Linux/*") }
|
||||
} else if (os.isLinux) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") }
|
||||
}
|
||||
} else if ("${restart4j.name}-${restart4j.version}" == file.nameWithoutExtension) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "darwin/*") }
|
||||
if (os.isWindows) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "linux/*") }
|
||||
if (arch.isArm) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "win32/x86_64/*") }
|
||||
} else {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "win32/aarch64/*") }
|
||||
}
|
||||
} else if (os.isLinux) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "win32/*") }
|
||||
if (arch.isArm) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "linux/x86_64/*") }
|
||||
} else {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "linux/aarch64/*") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,11 +427,8 @@ tasks.register("dist") {
|
||||
throw GradleException("JVM: $vendor is not supported")
|
||||
}
|
||||
|
||||
val distributionDir = layout.buildDirectory.dir("distributions").get()
|
||||
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
|
||||
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
|
||||
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
|
||||
val macOSFinalFilePath = distributionDir.file("${finalFilenameWithoutExtension}.dmg").asFile.absolutePath
|
||||
|
||||
|
||||
// 清空目录
|
||||
exec { commandLine(gradlew, "clean") }
|
||||
@@ -359,125 +448,8 @@ tasks.register("dist") {
|
||||
// 打包
|
||||
exec { commandLine(gradlew, "jpackage") }
|
||||
|
||||
// pack
|
||||
if (os.isWindows) { // zip and msi
|
||||
// zip
|
||||
exec {
|
||||
commandLine(
|
||||
"tar", "-vacf",
|
||||
distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile.absolutePath,
|
||||
project.name.uppercaseFirstChar()
|
||||
)
|
||||
workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
|
||||
}
|
||||
|
||||
// msi
|
||||
exec {
|
||||
commandLine(
|
||||
"cmd", "/c", "move",
|
||||
"${project.name.uppercaseFirstChar()}-${project.version}.msi",
|
||||
"${finalFilenameWithoutExtension}.msi"
|
||||
)
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
} else if (os.isLinux) { // tar.gz
|
||||
exec {
|
||||
commandLine(
|
||||
"tar", "-czvf",
|
||||
distributionDir.file("${finalFilenameWithoutExtension}.tar.gz").asFile.absolutePath,
|
||||
project.name.uppercaseFirstChar()
|
||||
)
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
} else if (os.isMacOsX) { // rename
|
||||
exec {
|
||||
commandLine(
|
||||
"mv",
|
||||
distributionDir.file("${project.name.uppercaseFirstChar()}-${project.version}.dmg").asFile.absolutePath,
|
||||
macOSFinalFilePath,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw GradleException("${os.name} is not supported")
|
||||
}
|
||||
|
||||
// AppImage
|
||||
if (os.isLinux) {
|
||||
|
||||
// Download AppImageKit
|
||||
val appimagetool = FileUtils.getFile(projectDir, ".gradle", "appimagetool")
|
||||
if (!appimagetool.exists()) {
|
||||
exec {
|
||||
commandLine(
|
||||
"wget",
|
||||
"-O", appimagetool.absolutePath,
|
||||
"https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-${if (arch.isArm) "aarch64" else "x86_64"}.AppImage"
|
||||
)
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
|
||||
// AppImageKit chmod
|
||||
exec { commandLine("chmod", "+x", appimagetool.absolutePath) }
|
||||
}
|
||||
|
||||
|
||||
// Desktop file
|
||||
val termoraName = project.name.uppercaseFirstChar()
|
||||
val desktopFile = distributionDir.file(termoraName + File.separator + termoraName + ".desktop").asFile
|
||||
desktopFile.writeText(
|
||||
"""[Desktop Entry]
|
||||
Type=Application
|
||||
Name=${termoraName}
|
||||
Comment=Terminal emulator and SSH client
|
||||
Icon=/lib/${termoraName}
|
||||
Categories=Development;
|
||||
Terminal=false
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
// AppRun file
|
||||
val appRun = File(desktopFile.parentFile, "AppRun")
|
||||
val sb = StringBuilder()
|
||||
sb.append("#!/bin/sh").appendLine()
|
||||
sb.append("SELF=$(readlink -f \"$0\")").appendLine()
|
||||
sb.append("HERE=\${SELF%/*}").appendLine()
|
||||
sb.append("exec \"\${HERE}/bin/${termoraName}\" \"$@\"")
|
||||
appRun.writeText(sb.toString())
|
||||
appRun.setExecutable(true)
|
||||
|
||||
exec {
|
||||
commandLine(appimagetool.absolutePath, termoraName, "${finalFilenameWithoutExtension}.AppImage")
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// sign dmg
|
||||
if (os.isMacOsX && macOSSign) {
|
||||
|
||||
// sign
|
||||
signMacOSLocalFile(File(macOSFinalFilePath))
|
||||
|
||||
// notary
|
||||
if (macOSNotary) {
|
||||
exec {
|
||||
commandLine(
|
||||
"/usr/bin/xcrun", "notarytool",
|
||||
"submit", macOSFinalFilePath,
|
||||
"--keychain-profile", macOSNotaryKeychainProfile,
|
||||
"--wait",
|
||||
)
|
||||
}
|
||||
|
||||
// 绑定公证信息
|
||||
exec {
|
||||
commandLine(
|
||||
"/usr/bin/xcrun",
|
||||
"stapler", "staple", macOSFinalFilePath,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 根据不同的系统构建不同的二进制包
|
||||
pack()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,6 +487,198 @@ tasks.register("check-license") {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建包
|
||||
*/
|
||||
fun pack() {
|
||||
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
|
||||
val distributionDir = layout.buildDirectory.dir("distributions").get()
|
||||
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
|
||||
val projectName = project.name.uppercaseFirstChar()
|
||||
|
||||
if (os.isWindows) {
|
||||
packOnWindows(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||
} else if (os.isLinux) {
|
||||
packOnLinux(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||
} else if (os.isMacOsX) {
|
||||
packOnMac(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||
} else {
|
||||
throw GradleException("${os.name} is not supported")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 zip、7z、msi
|
||||
*/
|
||||
fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
||||
// zip
|
||||
exec {
|
||||
commandLine(
|
||||
"tar", "-vacf",
|
||||
distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile.absolutePath,
|
||||
projectName
|
||||
)
|
||||
workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
|
||||
}
|
||||
|
||||
// exe
|
||||
exec {
|
||||
commandLine(
|
||||
"iscc",
|
||||
"/DMyAppId=${projectName}",
|
||||
"/DMyAppName=${projectName}",
|
||||
"/DMyAppVersion=${project.version}",
|
||||
"/DMyOutputDir=${distributionDir.asFile.absolutePath}",
|
||||
"/DMySetupIconFile=${FileUtils.getFile(projectDir, "src", "main", "resources", "icons", "termora.ico")}",
|
||||
"/DMySourceDir=${layout.buildDirectory.dir("jpackage/images/win-msi.image/${projectName}").get().asFile}",
|
||||
"/F${finalFilenameWithoutExtension}",
|
||||
FileUtils.getFile(projectDir, "src", "main", "resources", "termora.iss")
|
||||
)
|
||||
}
|
||||
|
||||
// msi
|
||||
exec {
|
||||
commandLine(
|
||||
"cmd", "/c", "move",
|
||||
"${projectName}-${project.version}.msi",
|
||||
"${finalFilenameWithoutExtension}.msi"
|
||||
)
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对于 macOS 先对 jpackage 构建的 dmg 重命名 -> 签名 -> 公证,另外还会创建一个 zip 包
|
||||
*/
|
||||
fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
||||
val dmgFile = distributionDir.file("${finalFilenameWithoutExtension}.dmg").asFile
|
||||
val zipFile = distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile
|
||||
|
||||
// rename
|
||||
// @formatter:off
|
||||
exec { commandLine("mv", distributionDir.file("${projectName}-${project.version}.dmg").asFile.absolutePath, dmgFile.absolutePath,) }
|
||||
// @formatter:on
|
||||
|
||||
// sign dmg
|
||||
if (macOSSign) signMacOSLocalFile(dmgFile)
|
||||
|
||||
// 找到 .app
|
||||
val imageFile = layout.buildDirectory.dir("jpackage/images/").get().asFile
|
||||
val appFile = imageFile.listFiles()?.firstOrNull()?.listFiles()?.firstOrNull()
|
||||
?: throw FileNotFoundException("${projectName}.app")
|
||||
|
||||
// zip
|
||||
// @formatter:off
|
||||
exec { commandLine("ditto", "-c", "-k", "--sequesterRsrc", "--keepParent", appFile.absolutePath, zipFile.absolutePath) }
|
||||
// @formatter:on
|
||||
|
||||
// sign zip
|
||||
if (macOSSign) signMacOSLocalFile(zipFile)
|
||||
|
||||
// 公证
|
||||
if (macOSNotary) {
|
||||
val pool = Executors.newCachedThreadPool()
|
||||
val jobs = mutableListOf<Future<*>>()
|
||||
|
||||
// zip
|
||||
pool.submit {
|
||||
// 对 zip 公证
|
||||
notaryMacOSLocalFile(zipFile)
|
||||
// 对 .app 盖章
|
||||
stapleMacOSLocalFile(appFile)
|
||||
// 删除旧的 zip ,旧的 zip 仅仅是为了公证
|
||||
FileUtils.deleteQuietly(zipFile)
|
||||
// 再对盖完章的 app 打成 zip 包
|
||||
// @formatter:off
|
||||
exec { commandLine("ditto", "-c", "-k", "--sequesterRsrc", "--keepParent", appFile.absolutePath, zipFile.absolutePath) }
|
||||
// @formatter:on
|
||||
// 再对 zip 签名
|
||||
signMacOSLocalFile(zipFile)
|
||||
}.apply { jobs.add(this) }
|
||||
|
||||
// dmg
|
||||
pool.submit {
|
||||
// 公证
|
||||
notaryMacOSLocalFile(dmgFile)
|
||||
// 盖章
|
||||
stapleMacOSLocalFile(dmgFile)
|
||||
}.apply { jobs.add(this) }
|
||||
|
||||
// join ...
|
||||
jobs.forEach { it.get() }
|
||||
|
||||
// shutdown
|
||||
pool.shutdown()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 tar.gz 和 AppImage
|
||||
*/
|
||||
fun packOnLinux(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
||||
// tar.gz
|
||||
exec {
|
||||
commandLine(
|
||||
"tar", "-czvf",
|
||||
distributionDir.file("${finalFilenameWithoutExtension}.tar.gz").asFile.absolutePath,
|
||||
projectName
|
||||
)
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
|
||||
|
||||
// AppImage
|
||||
// Download AppImageKit
|
||||
val appimagetool = FileUtils.getFile(projectDir, ".gradle", "appimagetool")
|
||||
if (!appimagetool.exists()) {
|
||||
exec {
|
||||
commandLine(
|
||||
"wget",
|
||||
"-O", appimagetool.absolutePath,
|
||||
"https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-${if (arch.isArm) "aarch64" else "x86_64"}.AppImage"
|
||||
)
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
|
||||
// AppImageKit chmod
|
||||
exec { commandLine("chmod", "+x", appimagetool.absolutePath) }
|
||||
}
|
||||
|
||||
|
||||
// Desktop file
|
||||
val termoraName = project.name.uppercaseFirstChar()
|
||||
val desktopFile = distributionDir.file(termoraName + File.separator + termoraName + ".desktop").asFile
|
||||
desktopFile.writeText(
|
||||
"""[Desktop Entry]
|
||||
Type=Application
|
||||
Name=${termoraName}
|
||||
Comment=Terminal emulator and SSH client
|
||||
Icon=/lib/${termoraName}
|
||||
Categories=Development;
|
||||
Terminal=false
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
// AppRun file
|
||||
val appRun = File(desktopFile.parentFile, "AppRun")
|
||||
val sb = StringBuilder()
|
||||
sb.append("#!/bin/sh").appendLine()
|
||||
sb.append("SELF=$(readlink -f \"$0\")").appendLine()
|
||||
sb.append("HERE=\${SELF%/*}").appendLine()
|
||||
sb.append("export LinuxAppImage=true").appendLine()
|
||||
sb.append("exec \"\${HERE}/bin/${termoraName}\" \"$@\"")
|
||||
appRun.writeText(sb.toString())
|
||||
appRun.setExecutable(true)
|
||||
|
||||
// AppImage
|
||||
exec {
|
||||
commandLine(appimagetool.absolutePath, termoraName, "${finalFilenameWithoutExtension}.AppImage")
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* macOS 对本地文件进行签名
|
||||
*/
|
||||
@@ -534,6 +698,40 @@ fun signMacOSLocalFile(file: File) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* macOS 对本地文件进行公证
|
||||
*/
|
||||
fun notaryMacOSLocalFile(file: File) {
|
||||
if (os.isMacOsX && macOSNotary) {
|
||||
if (file.exists()) {
|
||||
exec {
|
||||
commandLine(
|
||||
"/usr/bin/xcrun", "notarytool",
|
||||
"submit", file,
|
||||
"--keychain-profile", macOSNotaryKeychainProfile,
|
||||
"--wait",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 盖章
|
||||
*/
|
||||
fun stapleMacOSLocalFile(file: File) {
|
||||
if (os.isMacOsX && macOSNotary) {
|
||||
if (file.exists()) {
|
||||
exec {
|
||||
commandLine(
|
||||
"/usr/bin/xcrun",
|
||||
"stapler", "staple", file,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
kotlin {
|
||||
jvmToolchain {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
[versions]
|
||||
kotlin = "2.1.0"
|
||||
kotlin = "2.1.10"
|
||||
slf4j = "2.0.16"
|
||||
pty4j = "0.13.2"
|
||||
tinylog = "2.7.0"
|
||||
kotlinx-coroutines = "1.10.1"
|
||||
flatlaf = "3.5.4"
|
||||
trove4j = "1.0.20200330"
|
||||
kotlinx-serialization-json = "1.7.3"
|
||||
commons-codec = "1.17.1"
|
||||
kotlinx-serialization-json = "1.8.0"
|
||||
commons-codec = "1.18.0"
|
||||
commons-lang3 = "3.17.0"
|
||||
commons-csv = "1.13.0"
|
||||
commons-net = "3.11.1"
|
||||
commons-text = "1.12.0"
|
||||
commons-text = "1.13.0"
|
||||
commons-compress = "1.27.1"
|
||||
koin-bom = "4.0.0"
|
||||
swingx = "1.6.5-1"
|
||||
@@ -41,7 +42,9 @@ rhino = "1.7.15"
|
||||
delight-rhino-sandbox = "0.0.17"
|
||||
testcontainers = "1.20.4"
|
||||
mixpanel = "1.5.3"
|
||||
jSerialComm="2.11.0"
|
||||
jSerialComm = "2.11.0"
|
||||
ini4j = "0.5.5-2"
|
||||
restart4j = "0.0.1"
|
||||
|
||||
[libraries]
|
||||
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||
@@ -53,9 +56,11 @@ tinylog-impl = { group = "org.tinylog", name = "tinylog-impl", version.ref = "ti
|
||||
commons-codec = { group = "commons-codec", name = "commons-codec", version.ref = "commons-codec" }
|
||||
commons-net = { group = "commons-net", name = "commons-net", version.ref = "commons-net" }
|
||||
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commons-lang3" }
|
||||
commons-csv = { group = "org.apache.commons", name = "commons-csv", version.ref = "commons-csv" }
|
||||
commons-text = { group = "org.apache.commons", name = "commons-text", version.ref = "commons-text" }
|
||||
commons-compress = { group = "org.apache.commons", name = "commons-compress", version.ref = "commons-compress" }
|
||||
pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" }
|
||||
ini4j = { module = "org.jetbrains.intellij.deps:ini4j", version.ref = "ini4j" }
|
||||
flatlaf = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
|
||||
flatlaf-extras = { group = "com.formdev", name = "flatlaf-extras", version.ref = "flatlaf" }
|
||||
trove4j = { group = "org.jetbrains.intellij.deps", name = "trove4j", version.ref = "trove4j" }
|
||||
@@ -72,6 +77,7 @@ versioncompare = { module = "io.github.g00fy2:versioncompare", version.ref = "ve
|
||||
jfa = { module = "de.jangassen:jfa", version.ref = "jfa" }
|
||||
oshi-core = { module = "com.github.oshi:oshi-core", version.ref = "oshi" }
|
||||
commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
|
||||
restart4j = { module = "com.github.hstyi:restart4j", version.ref = "restart4j" }
|
||||
jbr-api = { module = "com.jetbrains:jbr-api", version.ref = "jbr-api" }
|
||||
flatlaf-swingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
|
||||
leveldb = { module = "org.iq80.leveldb:leveldb", version.ref = "leveldb" }
|
||||
|
||||
@@ -4,6 +4,8 @@ import app.termora.actions.ActionManager
|
||||
import app.termora.keymap.KeymapManager
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.FlatSystemProperties
|
||||
import com.formdev.flatlaf.extras.FlatDesktop
|
||||
import com.formdev.flatlaf.extras.FlatDesktop.QuitResponse
|
||||
import com.formdev.flatlaf.extras.FlatInspector
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jthemedetecor.OsThemeDetector
|
||||
@@ -20,12 +22,14 @@ import org.apache.commons.lang3.SystemUtils
|
||||
import org.json.JSONObject
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.tinylog.configuration.Configuration
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.io.File
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.channels.FileLock
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.util.*
|
||||
import java.util.function.Consumer
|
||||
import javax.swing.*
|
||||
import kotlin.system.exitProcess
|
||||
import kotlin.system.measureTimeMillis
|
||||
@@ -100,16 +104,8 @@ class ApplicationRunner {
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
private fun clearTemporary() {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
|
||||
// 启动时清除
|
||||
FileUtils.cleanDirectory(Application.getTemporaryDir())
|
||||
|
||||
// 关闭时清除
|
||||
Disposer.register(ApplicationScope.forApplicationScope(), object : Disposable {
|
||||
override fun dispose() {
|
||||
FileUtils.cleanDirectory(Application.getTemporaryDir())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -123,7 +119,37 @@ class ApplicationRunner {
|
||||
}
|
||||
|
||||
private fun startMainFrame() {
|
||||
|
||||
TermoraFrameManager.getInstance().createWindow().isVisible = true
|
||||
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
SwingUtilities.invokeLater {
|
||||
FlatDesktop.setQuitHandler(object : Consumer<QuitResponse> {
|
||||
override fun accept(response: QuitResponse) {
|
||||
quitHandler(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun quitHandler(response: QuitResponse) {
|
||||
val keyboardFocusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
|
||||
if (OptionPane.showConfirmDialog(
|
||||
keyboardFocusManager.focusedWindow,
|
||||
I18n.getString("termora.quit-confirm", Application.getName()),
|
||||
optionType = JOptionPane.YES_NO_OPTION,
|
||||
) != JOptionPane.YES_OPTION
|
||||
) {
|
||||
response.cancelQuit()
|
||||
return
|
||||
}
|
||||
|
||||
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
||||
frame.dispose()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun loadSettings() {
|
||||
@@ -161,7 +187,7 @@ class ApplicationRunner {
|
||||
themeManager.change(theme, true)
|
||||
|
||||
if (Application.isUnknownVersion())
|
||||
FlatInspector.install("ctrl shift alt X");
|
||||
FlatInspector.install("ctrl shift alt X")
|
||||
|
||||
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
||||
UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false)
|
||||
|
||||
@@ -400,10 +400,10 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
protected inner class CursorStylePropertyDelegate(defaultValue: CursorStyle) :
|
||||
PropertyDelegate<CursorStyle>(defaultValue) {
|
||||
override fun convertValue(value: String): CursorStyle {
|
||||
try {
|
||||
return CursorStyle.valueOf(value)
|
||||
} catch (e: Exception) {
|
||||
return initializer.invoke()
|
||||
return try {
|
||||
CursorStyle.valueOf(value)
|
||||
} catch (_: Exception) {
|
||||
initializer.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -458,6 +458,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
*/
|
||||
var beep by BooleanPropertyDelegate(true)
|
||||
|
||||
/**
|
||||
* 光标闪烁
|
||||
*/
|
||||
var cursorBlink by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 选中复制
|
||||
*/
|
||||
@@ -588,6 +593,18 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
*/
|
||||
var editCommand by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
|
||||
|
||||
/**
|
||||
* sftp command
|
||||
*/
|
||||
var sftpCommand by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
|
||||
|
||||
/**
|
||||
* 是否固定在标签栏
|
||||
*/
|
||||
var pinTab by BooleanPropertyDelegate(false)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,7 +54,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
||||
protected fun init() {
|
||||
|
||||
|
||||
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
|
||||
defaultCloseOperation = DISPOSE_ON_CLOSE
|
||||
|
||||
initTitleBar()
|
||||
initEvents()
|
||||
@@ -158,12 +158,14 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
||||
openPopup = true
|
||||
}
|
||||
|
||||
val window = SwingUtilities.windowForComponent(c)
|
||||
val windows = window.ownedWindows
|
||||
for (w in windows) {
|
||||
if (w.isVisible && w.javaClass.getName().endsWith("HeavyWeightWindow")) {
|
||||
openPopup = true
|
||||
w.dispose()
|
||||
val window = c as? Window ?: SwingUtilities.windowForComponent(c)
|
||||
if (window != null) {
|
||||
val windows = window.ownedWindows
|
||||
for (w in windows) {
|
||||
if (w.isVisible && w.javaClass.getName().endsWith("HeavyWeightWindow")) {
|
||||
openPopup = true
|
||||
w.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
156
src/main/kotlin/app/termora/FilterableHostTreeModel.kt
Normal file
@@ -0,0 +1,156 @@
|
||||
package app.termora
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import java.util.function.Function
|
||||
import javax.swing.JTree
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.event.TreeModelEvent
|
||||
import javax.swing.event.TreeModelListener
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
import javax.swing.tree.TreeModel
|
||||
import javax.swing.tree.TreeNode
|
||||
import javax.swing.tree.TreePath
|
||||
|
||||
class FilterableHostTreeModel(
|
||||
private val tree: JTree,
|
||||
/**
|
||||
* 如果返回 true 则空文件夹也展示
|
||||
*/
|
||||
private val showEmptyFolder: () -> Boolean = { true }
|
||||
) : TreeModel {
|
||||
private val model = tree.model
|
||||
private val root = ReferenceTreeNode(model.root)
|
||||
private var listeners = emptyArray<TreeModelListener>()
|
||||
private var filters = emptyArray<Function<HostTreeNode, Boolean>>()
|
||||
private val mapping = mutableMapOf<TreeNode, ReferenceTreeNode>()
|
||||
|
||||
init {
|
||||
refresh()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param a 旧的
|
||||
* @param b 新的
|
||||
*/
|
||||
private fun cloneTree(a: HostTreeNode, b: DefaultMutableTreeNode) {
|
||||
b.removeAllChildren()
|
||||
for (c in a.children()) {
|
||||
if (c !is HostTreeNode) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (c.host.protocol != Protocol.Folder) {
|
||||
if (filters.isNotEmpty() && filters.none { it.apply(c) }) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
val n = ReferenceTreeNode(c).apply { mapping[c] = this }.apply { b.add(this) }
|
||||
|
||||
// 文件夹递归复制
|
||||
if (c.host.protocol == Protocol.Folder) {
|
||||
cloneTree(c, n)
|
||||
}
|
||||
|
||||
// 如果是文件夹
|
||||
if (c.host.protocol == Protocol.Folder) {
|
||||
if (n.childCount == 0) {
|
||||
if (showEmptyFolder.invoke()) {
|
||||
continue
|
||||
}
|
||||
n.removeFromParent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
model.addTreeModelListener(object : TreeModelListener {
|
||||
override fun treeNodesChanged(e: TreeModelEvent) {
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun treeNodesInserted(e: TreeModelEvent) {
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun treeNodesRemoved(e: TreeModelEvent) {
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun treeStructureChanged(e: TreeModelEvent) {
|
||||
refresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getRoot(): Any {
|
||||
return root.userObject
|
||||
}
|
||||
|
||||
override fun getChild(parent: Any, index: Int): Any {
|
||||
val c = map(parent)?.getChildAt(index)
|
||||
if (c is ReferenceTreeNode) {
|
||||
return c.userObject
|
||||
}
|
||||
throw IndexOutOfBoundsException("Index out of bounds")
|
||||
}
|
||||
|
||||
override fun getChildCount(parent: Any): Int {
|
||||
return map(parent)?.childCount ?: 0
|
||||
}
|
||||
|
||||
private fun map(parent: Any): ReferenceTreeNode? {
|
||||
if (parent is TreeNode) {
|
||||
return mapping[parent]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun isLeaf(node: Any?): Boolean {
|
||||
return (node as TreeNode).isLeaf
|
||||
}
|
||||
|
||||
override fun valueForPathChanged(path: TreePath, newValue: Any) {
|
||||
|
||||
}
|
||||
|
||||
override fun getIndexOfChild(parent: Any, child: Any): Int {
|
||||
val c = map(parent) ?: return -1
|
||||
for (i in 0 until c.childCount) {
|
||||
val e = c.getChildAt(i)
|
||||
if (e is ReferenceTreeNode && e.userObject == child) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun addTreeModelListener(l: TreeModelListener) {
|
||||
listeners = ArrayUtils.addAll(listeners, l)
|
||||
}
|
||||
|
||||
override fun removeTreeModelListener(l: TreeModelListener) {
|
||||
listeners = ArrayUtils.removeElement(listeners, l)
|
||||
}
|
||||
|
||||
fun addFilter(f: Function<HostTreeNode, Boolean>) {
|
||||
filters = ArrayUtils.add(filters, f)
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
mapping.clear()
|
||||
mapping[model.root as TreeNode] = root
|
||||
cloneTree(model.root as HostTreeNode, root)
|
||||
SwingUtilities.updateComponentTreeUI(tree)
|
||||
}
|
||||
|
||||
fun getModel(): TreeModel {
|
||||
return model
|
||||
}
|
||||
|
||||
private class ReferenceTreeNode(any: Any) : DefaultMutableTreeNode(any)
|
||||
|
||||
}
|
||||
@@ -260,7 +260,7 @@ data class Host(
|
||||
val tunnelings: List<Tunneling> = emptyList(),
|
||||
|
||||
/**
|
||||
* 排序
|
||||
* 排序,越小越靠前
|
||||
*/
|
||||
val sort: Long = 0,
|
||||
/**
|
||||
@@ -307,4 +307,8 @@ data class Host(
|
||||
result = 31 * result + ownerId.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,5 @@
|
||||
package app.termora
|
||||
|
||||
import java.util.*
|
||||
|
||||
interface HostListener : EventListener {
|
||||
fun hostAdded(host: Host) {}
|
||||
fun hostRemoved(id: String) {}
|
||||
fun hostsChanged() {}
|
||||
}
|
||||
|
||||
|
||||
class HostManager private constructor() {
|
||||
companion object {
|
||||
@@ -17,39 +9,38 @@ class HostManager private constructor() {
|
||||
}
|
||||
|
||||
private val database get() = Database.getDatabase()
|
||||
private val listeners = mutableListOf<HostListener>()
|
||||
private var hosts = mutableMapOf<String, Host>()
|
||||
|
||||
fun addHost(host: Host, notify: Boolean = true) {
|
||||
/**
|
||||
* 修改缓存并存入数据库
|
||||
*/
|
||||
fun addHost(host: Host) {
|
||||
assertEventDispatchThread()
|
||||
database.addHost(host)
|
||||
if (notify) listeners.forEach { it.hostAdded(host) }
|
||||
}
|
||||
|
||||
fun removeHost(id: String) {
|
||||
assertEventDispatchThread()
|
||||
database.removeHost(id)
|
||||
listeners.forEach { it.hostRemoved(id) }
|
||||
|
||||
if (host.deleted) {
|
||||
hosts.entries.removeIf { it.value.id == host.id || it.value.parentId == host.id }
|
||||
} else {
|
||||
hosts[host.id] = host
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 第一次调用从数据库中获取,后续从缓存中获取
|
||||
*/
|
||||
fun hosts(): List<Host> {
|
||||
return database.getHosts()
|
||||
if (hosts.isEmpty()) {
|
||||
database.getHosts().filter { !it.deleted }
|
||||
.forEach { hosts[it.id] = it }
|
||||
}
|
||||
return hosts.values.filter { !it.deleted }
|
||||
.sortedWith(compareBy<Host> { if (it.protocol == Protocol.Folder) 0 else 1 }.thenBy { it.sort })
|
||||
}
|
||||
|
||||
fun removeAll() {
|
||||
assertEventDispatchThread()
|
||||
database.removeAllHost()
|
||||
listeners.forEach { it.hostsChanged() }
|
||||
/**
|
||||
* 从缓存中获取
|
||||
*/
|
||||
fun getHost(id: String): Host? {
|
||||
return hosts[id]
|
||||
}
|
||||
|
||||
fun addHostListener(listener: HostListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeHostListener(listener: HostListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1134,16 +1134,16 @@ open class HostOptionsPane : OptionsPane() {
|
||||
private fun initEvents() {
|
||||
addBtn.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent?) {
|
||||
val dialog = HostTreeDialog(owner) { host ->
|
||||
jumpHosts.none { it.id == host.id } && filter.invoke(host)
|
||||
}
|
||||
|
||||
val dialog = NewHostTreeDialog(owner)
|
||||
dialog.setFilter { node -> jumpHosts.none { it.id == node.host.id } && filter.invoke(node.host) }
|
||||
dialog.setTreeName("HostOptionsPane.JumpHostsOption.Tree")
|
||||
dialog.setLocationRelativeTo(owner)
|
||||
dialog.isVisible = true
|
||||
val hosts = dialog.hosts
|
||||
if (hosts.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
hosts.forEach {
|
||||
val rowCount = model.rowCount
|
||||
jumpHosts.add(it)
|
||||
|
||||
@@ -1,660 +0,0 @@
|
||||
package app.termora
|
||||
|
||||
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.NewHostAction
|
||||
import app.termora.actions.OpenHostAction
|
||||
import app.termora.transport.SFTPAction
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
||||
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.Transferable
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.*
|
||||
import javax.swing.*
|
||||
import javax.swing.event.CellEditorListener
|
||||
import javax.swing.event.ChangeEvent
|
||||
import javax.swing.event.PopupMenuEvent
|
||||
import javax.swing.event.PopupMenuListener
|
||||
import javax.swing.tree.TreePath
|
||||
import javax.swing.tree.TreeSelectionModel
|
||||
|
||||
|
||||
class HostTree : JTree(), Disposable {
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
private val editor = OutlineTextField(64)
|
||||
|
||||
var contextmenu = true
|
||||
|
||||
/**
|
||||
* 双击是否打开连接
|
||||
*/
|
||||
var doubleClickConnection = true
|
||||
|
||||
val model = HostTreeModel()
|
||||
val searchableModel = SearchableHostTreeModel(model)
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
|
||||
private fun initView() {
|
||||
setModel(model)
|
||||
isEditable = true
|
||||
dropMode = DropMode.ON_OR_INSERT
|
||||
dragEnabled = true
|
||||
selectionModel.selectionMode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION
|
||||
editor.preferredSize = Dimension(220, 0)
|
||||
|
||||
setCellRenderer(object : DefaultXTreeCellRenderer() {
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
override fun getTreeCellRendererComponent(
|
||||
tree: JTree,
|
||||
value: Any,
|
||||
sel: Boolean,
|
||||
expanded: Boolean,
|
||||
leaf: Boolean,
|
||||
row: Int,
|
||||
hasFocus: Boolean
|
||||
): Component {
|
||||
val host = value as Host
|
||||
var text = host.name
|
||||
|
||||
// 是否显示更多信息
|
||||
if (properties.getString("HostTree.showMoreInfo", "false").toBoolean()) {
|
||||
val color = if (sel) {
|
||||
if (this@HostTree.hasFocus()) {
|
||||
UIManager.getColor("textHighlightText")
|
||||
} else {
|
||||
this.foreground
|
||||
}
|
||||
} else {
|
||||
UIManager.getColor("textInactiveText")
|
||||
}
|
||||
|
||||
if (host.protocol == Protocol.SSH) {
|
||||
text = """
|
||||
<html>${host.name}
|
||||
|
||||
<font color=rgb(${color.red},${color.green},${color.blue})>${host.username}@${host.host}</font></html>
|
||||
""".trimIndent()
|
||||
} else if (host.protocol == Protocol.Serial) {
|
||||
text = """
|
||||
<html>${host.name}
|
||||
|
||||
<font color=rgb(${color.red},${color.green},${color.blue})>${host.options.serialComm.port}</font></html>
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus)
|
||||
|
||||
icon = when (host.protocol) {
|
||||
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||
Protocol.Serial -> if (sel && this@HostTree.hasFocus()) Icons.plugin.dark else Icons.plugin
|
||||
else -> if (sel && this@HostTree.hasFocus()) Icons.terminal.dark else Icons.terminal
|
||||
}
|
||||
return c
|
||||
}
|
||||
})
|
||||
|
||||
setCellEditor(object : DefaultCellEditor(editor) {
|
||||
override fun isCellEditable(e: EventObject?): Boolean {
|
||||
if (e is MouseEvent) {
|
||||
return false
|
||||
}
|
||||
return super.isCellEditable(e)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
val state = Database.getDatabase().properties.getString("HostTreeExpansionState")
|
||||
if (state != null) {
|
||||
TreeUtils.loadExpansionState(this@HostTree, state)
|
||||
}
|
||||
}
|
||||
|
||||
override fun convertValueToText(
|
||||
value: Any?,
|
||||
selected: Boolean,
|
||||
expanded: Boolean,
|
||||
leaf: Boolean,
|
||||
row: Int,
|
||||
hasFocus: Boolean
|
||||
): String {
|
||||
if (value is Host) {
|
||||
return value.name
|
||||
}
|
||||
return super.convertValueToText(value, selected, expanded, leaf, row, hasFocus)
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
// 右键选中
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (!SwingUtilities.isRightMouseButton(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
requestFocusInWindow()
|
||||
|
||||
val selectionRows = selectionModel.selectionRows
|
||||
|
||||
val selRow = getClosestRowForLocation(e.x, e.y)
|
||||
if (selRow < 0) {
|
||||
selectionModel.clearSelection()
|
||||
return
|
||||
} else if (selectionRows != null && selectionRows.contains(selRow)) {
|
||||
return
|
||||
}
|
||||
|
||||
selectionPath = getPathForLocation(e.x, e.y)
|
||||
|
||||
setSelectionRow(selRow)
|
||||
}
|
||||
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||
val host = lastSelectedPathComponent
|
||||
if (host is Host && host.protocol != Protocol.Folder) {
|
||||
ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
|
||||
?.actionPerformed(OpenHostActionEvent(e.source, host, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// contextmenu
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (!(SwingUtilities.isRightMouseButton(e))) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Objects.isNull(lastSelectedPathComponent)) {
|
||||
return
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater { showContextMenu(e) }
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// rename
|
||||
getCellEditor().addCellEditorListener(object : CellEditorListener {
|
||||
override fun editingStopped(e: ChangeEvent) {
|
||||
val lastHost = lastSelectedPathComponent
|
||||
if (lastHost !is Host || editor.text.isBlank() || editor.text == lastHost.name) {
|
||||
return
|
||||
}
|
||||
runCatchingHost(lastHost.copy(name = editor.text))
|
||||
}
|
||||
|
||||
override fun editingCanceled(e: ChangeEvent) {
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// drag
|
||||
transferHandler = object : TransferHandler() {
|
||||
|
||||
override fun createTransferable(c: JComponent): Transferable {
|
||||
val nodes = selectionModel.selectionPaths
|
||||
.map { it.lastPathComponent }
|
||||
.filterIsInstance<Host>()
|
||||
.toMutableList()
|
||||
|
||||
val iterator = nodes.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val node = iterator.next()
|
||||
val parents = model.getPathToRoot(node).filter { it != node }
|
||||
if (parents.any { nodes.contains(it) }) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
|
||||
return MoveHostTransferable(nodes)
|
||||
}
|
||||
|
||||
override fun getSourceActions(c: JComponent?): Int {
|
||||
return MOVE
|
||||
}
|
||||
|
||||
override fun canImport(support: TransferSupport): Boolean {
|
||||
if (!support.isDrop) {
|
||||
return false
|
||||
}
|
||||
val dropLocation = support.dropLocation
|
||||
if (dropLocation !is JTree.DropLocation || support.component != this@HostTree
|
||||
|| dropLocation.childIndex != -1
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
val lastNode = dropLocation.path.lastPathComponent
|
||||
if (lastNode !is Host || lastNode.protocol != Protocol.Folder) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (support.isDataFlavorSupported(MoveHostTransferable.dataFlavor)) {
|
||||
val nodes = support.transferable.getTransferData(MoveHostTransferable.dataFlavor) as List<*>
|
||||
if (nodes.any { it == lastNode }) {
|
||||
return false
|
||||
}
|
||||
for (parent in model.getPathToRoot(lastNode).filter { it != lastNode }) {
|
||||
if (nodes.any { it == parent }) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
support.setShowDropLocation(true)
|
||||
return support.isDataFlavorSupported(MoveHostTransferable.dataFlavor)
|
||||
}
|
||||
|
||||
override fun importData(support: TransferSupport): Boolean {
|
||||
if (!support.isDrop) {
|
||||
return false
|
||||
}
|
||||
|
||||
val dropLocation = support.dropLocation
|
||||
if (dropLocation !is JTree.DropLocation) {
|
||||
return false
|
||||
}
|
||||
|
||||
val lastNode = dropLocation.path.lastPathComponent
|
||||
if (lastNode !is Host || lastNode.protocol != Protocol.Folder) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!support.isDataFlavorSupported(MoveHostTransferable.dataFlavor)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val hosts = (support.transferable.getTransferData(MoveHostTransferable.dataFlavor) as List<*>)
|
||||
.filterIsInstance<Host>().toMutableList()
|
||||
if (hosts.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 记录展开的节点
|
||||
val expandedHosts = mutableListOf<String>()
|
||||
for (host in hosts) {
|
||||
model.visit(host) {
|
||||
if (it.protocol == Protocol.Folder) {
|
||||
if (isExpanded(TreePath(model.getPathToRoot(it)))) {
|
||||
expandedHosts.addFirst(it.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var now = System.currentTimeMillis()
|
||||
for (host in hosts) {
|
||||
model.removeNodeFromParent(host)
|
||||
val newHost = host.copy(
|
||||
parentId = lastNode.id,
|
||||
sort = ++now,
|
||||
updateDate = now
|
||||
)
|
||||
runCatchingHost(newHost)
|
||||
}
|
||||
|
||||
expandNode(lastNode)
|
||||
|
||||
// 展开
|
||||
for (id in expandedHosts) {
|
||||
model.getHost(id)?.let { expandNode(it) }
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun isPathEditable(path: TreePath?): Boolean {
|
||||
if (path == null) return false
|
||||
if (path.lastPathComponent == model.root) return false
|
||||
return super.isPathEditable(path)
|
||||
}
|
||||
|
||||
override fun getLastSelectedPathComponent(): Any? {
|
||||
val last = super.getLastSelectedPathComponent() ?: return null
|
||||
if (last is Host) {
|
||||
return model.getHost(last.id) ?: last
|
||||
}
|
||||
return last
|
||||
}
|
||||
|
||||
private fun showContextMenu(event: MouseEvent) {
|
||||
if (!contextmenu) return
|
||||
|
||||
val lastHost = lastSelectedPathComponent
|
||||
if (lastHost !is Host) {
|
||||
return
|
||||
}
|
||||
|
||||
val properties = Database.getDatabase().properties
|
||||
val popupMenu = FlatPopupMenu()
|
||||
val newMenu = JMenu(I18n.getString("termora.welcome.contextmenu.new"))
|
||||
val newFolder = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.folder"))
|
||||
val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host"))
|
||||
|
||||
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect"))
|
||||
val openWith = popupMenu.add(JMenu(I18n.getString("termora.welcome.contextmenu.connect-with"))) as JMenu
|
||||
val openWithSFTP = openWith.add("SFTP")
|
||||
val openWithSFTPCommand = openWith.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
|
||||
val openInNewWindow = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open-in-new-window"))
|
||||
popupMenu.addSeparator()
|
||||
val copy = popupMenu.add(I18n.getString("termora.welcome.contextmenu.copy"))
|
||||
val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove"))
|
||||
val rename = popupMenu.add(I18n.getString("termora.welcome.contextmenu.rename"))
|
||||
popupMenu.addSeparator()
|
||||
val expandAll = popupMenu.add(I18n.getString("termora.welcome.contextmenu.expand-all"))
|
||||
val colspanAll = popupMenu.add(I18n.getString("termora.welcome.contextmenu.collapse-all"))
|
||||
popupMenu.addSeparator()
|
||||
popupMenu.add(newMenu)
|
||||
popupMenu.addSeparator()
|
||||
|
||||
val showMoreInfo = JCheckBoxMenuItem(I18n.getString("termora.welcome.contextmenu.show-more-info"))
|
||||
showMoreInfo.isSelected = properties.getString("HostTree.showMoreInfo", "false").toBoolean()
|
||||
showMoreInfo.addActionListener {
|
||||
properties.putString(
|
||||
"HostTree.showMoreInfo",
|
||||
showMoreInfo.isSelected.toString()
|
||||
)
|
||||
SwingUtilities.updateComponentTreeUI(this)
|
||||
}
|
||||
popupMenu.add(showMoreInfo)
|
||||
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
|
||||
|
||||
open.addActionListener { openHosts(it, false) }
|
||||
openWithSFTP.addActionListener { openWithSFTP(it) }
|
||||
openWithSFTPCommand.addActionListener { openWithSFTPCommand(it) }
|
||||
openInNewWindow.addActionListener { openHosts(it, true) }
|
||||
|
||||
// 如果选中了 SSH 服务器,那么才启用
|
||||
openWithSFTP.isEnabled = getSelectionNodes().any { it.protocol == Protocol.SSH }
|
||||
openWithSFTPCommand.isEnabled = openWithSFTP.isEnabled
|
||||
openWith.isEnabled = openWith.menuComponents.any { it is JMenuItem && it.isEnabled }
|
||||
|
||||
rename.addActionListener {
|
||||
startEditingAtPath(TreePath(model.getPathToRoot(lastHost)))
|
||||
}
|
||||
|
||||
expandAll.addActionListener {
|
||||
getSelectionNodes().forEach { expandNode(it, true) }
|
||||
}
|
||||
|
||||
|
||||
colspanAll.addActionListener {
|
||||
selectionModel.selectionPaths.map { it.lastPathComponent }
|
||||
.filterIsInstance<Host>()
|
||||
.filter { it.protocol == Protocol.Folder }
|
||||
.forEach { collapseNode(it) }
|
||||
}
|
||||
|
||||
copy.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val parent = model.getParent(lastHost) ?: return
|
||||
val node = copyNode(parent, lastHost)
|
||||
selectionPath = TreePath(model.getPathToRoot(node))
|
||||
}
|
||||
})
|
||||
|
||||
remove.addActionListener {
|
||||
if (OptionPane.showConfirmDialog(
|
||||
SwingUtilities.getWindowAncestor(this),
|
||||
I18n.getString("termora.keymgr.delete-warning"),
|
||||
I18n.getString("termora.remove"),
|
||||
JOptionPane.YES_NO_OPTION,
|
||||
JOptionPane.QUESTION_MESSAGE
|
||||
) == JOptionPane.YES_OPTION
|
||||
) {
|
||||
var lastParent: Host? = null
|
||||
while (!selectionModel.isSelectionEmpty) {
|
||||
val host = lastSelectedPathComponent ?: break
|
||||
if (host !is Host) {
|
||||
break
|
||||
} else {
|
||||
lastParent = model.getParent(host)
|
||||
}
|
||||
model.visit(host) { hostManager.removeHost(it.id) }
|
||||
}
|
||||
if (lastParent != null) {
|
||||
selectionPath = TreePath(model.getPathToRoot(lastParent))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newFolder.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
if (lastHost.protocol != Protocol.Folder) {
|
||||
return
|
||||
}
|
||||
|
||||
val host = Host(
|
||||
id = UUID.randomUUID().toSimpleString(),
|
||||
protocol = Protocol.Folder,
|
||||
name = I18n.getString("termora.welcome.contextmenu.new.folder.name"),
|
||||
sort = System.currentTimeMillis(),
|
||||
parentId = lastHost.id
|
||||
)
|
||||
|
||||
runCatchingHost(host)
|
||||
|
||||
expandNode(lastHost)
|
||||
selectionPath = TreePath(model.getPathToRoot(host))
|
||||
startEditingAtPath(selectionPath)
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
newHost.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
ActionManager.getInstance().getAction(NewHostAction.NEW_HOST)
|
||||
?.actionPerformed(e)
|
||||
}
|
||||
})
|
||||
|
||||
property.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val dialog = HostDialog(SwingUtilities.getWindowAncestor(this@HostTree), lastHost)
|
||||
dialog.title = lastHost.name
|
||||
dialog.isVisible = true
|
||||
val host = dialog.host ?: return
|
||||
runCatchingHost(host)
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化状态
|
||||
newFolder.isEnabled = lastHost.protocol == Protocol.Folder
|
||||
newHost.isEnabled = newFolder.isEnabled
|
||||
remove.isEnabled = !getSelectionNodes().any { it == model.root }
|
||||
copy.isEnabled = remove.isEnabled
|
||||
rename.isEnabled = remove.isEnabled
|
||||
property.isEnabled = lastHost.protocol != Protocol.Folder
|
||||
|
||||
popupMenu.addPopupMenuListener(object : PopupMenuListener {
|
||||
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
||||
this@HostTree.grabFocus()
|
||||
}
|
||||
|
||||
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {
|
||||
this@HostTree.requestFocusInWindow()
|
||||
}
|
||||
|
||||
override fun popupMenuCanceled(e: PopupMenuEvent) {
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
popupMenu.show(this, event.x, event.y)
|
||||
}
|
||||
|
||||
private fun openHosts(evt: EventObject, openInNewWindow: Boolean) {
|
||||
assertEventDispatchThread()
|
||||
val nodes = getSelectionNodes().filter { it.protocol != Protocol.Folder }
|
||||
if (nodes.isEmpty()) return
|
||||
val openHostAction = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST) ?: return
|
||||
val source = if (openInNewWindow)
|
||||
TermoraFrameManager.getInstance().createWindow().apply { isVisible = true }
|
||||
else evt.source
|
||||
|
||||
nodes.forEach { openHostAction.actionPerformed(OpenHostActionEvent(source, it, evt)) }
|
||||
}
|
||||
|
||||
private fun openWithSFTP(evt: EventObject) {
|
||||
val nodes = getSelectionNodes().filter { it.protocol == Protocol.SSH }
|
||||
if (nodes.isEmpty()) return
|
||||
|
||||
val sftpAction = ActionManager.getInstance().getAction(Actions.SFTP) as SFTPAction? ?: return
|
||||
val tab = sftpAction.openOrCreateSFTPTerminalTab(AnActionEvent(this, StringUtils.EMPTY, evt)) ?: return
|
||||
for (node in nodes) {
|
||||
sftpAction.connectHost(node, tab)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openWithSFTPCommand(evt: EventObject) {
|
||||
val nodes = getSelectionNodes().filter { it.protocol == Protocol.SSH }
|
||||
if (nodes.isEmpty()) return
|
||||
val action = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST) ?: return
|
||||
for (host in nodes) {
|
||||
action.actionPerformed(OpenHostActionEvent(this, host.copy(protocol = Protocol.SFTPPty), evt))
|
||||
}
|
||||
}
|
||||
|
||||
fun expandNode(node: Host, including: Boolean = false) {
|
||||
expandPath(TreePath(model.getPathToRoot(node)))
|
||||
if (including) {
|
||||
model.getChildren(node).forEach { expandNode(it, true) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun copyNode(
|
||||
parent: Host,
|
||||
host: Host,
|
||||
idGenerator: () -> String = { UUID.randomUUID().toSimpleString() }
|
||||
): Host {
|
||||
val now = System.currentTimeMillis()
|
||||
val newHost = host.copy(
|
||||
name = "${host.name} ${I18n.getString("termora.welcome.contextmenu.copy")}",
|
||||
id = idGenerator.invoke(),
|
||||
parentId = parent.id,
|
||||
updateDate = now,
|
||||
createDate = now,
|
||||
sort = now
|
||||
)
|
||||
|
||||
runCatchingHost(newHost)
|
||||
|
||||
if (host.protocol == Protocol.Folder) {
|
||||
for (child in model.getChildren(host)) {
|
||||
copyNode(newHost, child, idGenerator)
|
||||
}
|
||||
if (isExpanded(TreePath(model.getPathToRoot(host)))) {
|
||||
expandNode(newHost)
|
||||
}
|
||||
}
|
||||
|
||||
return newHost
|
||||
|
||||
}
|
||||
|
||||
private fun runCatchingHost(host: Host) {
|
||||
hostManager.addHost(host)
|
||||
}
|
||||
|
||||
private fun collapseNode(node: Host) {
|
||||
model.getChildren(node).forEach { collapseNode(it) }
|
||||
collapsePath(TreePath(model.getPathToRoot(node)))
|
||||
}
|
||||
|
||||
fun getSelectionNodes(): List<Host> {
|
||||
val selectionNodes = selectionModel.selectionPaths.map { it.lastPathComponent }
|
||||
.filterIsInstance<Host>()
|
||||
|
||||
if (selectionNodes.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val nodes = mutableListOf<Host>()
|
||||
val parents = mutableListOf<Host>()
|
||||
|
||||
for (node in selectionNodes) {
|
||||
if (node.protocol == Protocol.Folder) {
|
||||
parents.add(node)
|
||||
}
|
||||
nodes.add(node)
|
||||
}
|
||||
|
||||
while (parents.isNotEmpty()) {
|
||||
val p = parents.removeFirst()
|
||||
for (i in 0 until getModel().getChildCount(p)) {
|
||||
val child = getModel().getChild(p, i) as Host
|
||||
nodes.add(child)
|
||||
parents.add(child)
|
||||
}
|
||||
}
|
||||
|
||||
// 确保是最新的
|
||||
for (i in 0 until nodes.size) {
|
||||
nodes[i] = model.getHost(nodes[i].id) ?: continue
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
Database.getDatabase().properties.putString(
|
||||
"HostTreeExpansionState",
|
||||
TreeUtils.saveExpansionState(this)
|
||||
)
|
||||
}
|
||||
|
||||
private abstract class HostTreeNodeTransferable(val hosts: List<Host>) :
|
||||
Transferable {
|
||||
|
||||
override fun getTransferDataFlavors(): Array<DataFlavor> {
|
||||
return arrayOf(getDataFlavor())
|
||||
}
|
||||
|
||||
override fun isDataFlavorSupported(flavor: DataFlavor): Boolean {
|
||||
return getDataFlavor() == flavor
|
||||
}
|
||||
|
||||
override fun getTransferData(flavor: DataFlavor): Any {
|
||||
return hosts
|
||||
}
|
||||
|
||||
abstract fun getDataFlavor(): DataFlavor
|
||||
}
|
||||
|
||||
private class MoveHostTransferable(hosts: List<Host>) : HostTreeNodeTransferable(hosts) {
|
||||
companion object {
|
||||
val dataFlavor =
|
||||
DataFlavor("${DataFlavor.javaJVMLocalObjectMimeType};class=${MoveHostTransferable::class.java.name}")
|
||||
}
|
||||
|
||||
override fun getDataFlavor(): DataFlavor {
|
||||
return dataFlavor
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package app.termora
|
||||
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import javax.swing.*
|
||||
import javax.swing.tree.TreeSelectionModel
|
||||
|
||||
class HostTreeDialog(
|
||||
owner: Window,
|
||||
private val filter: (host: Host) -> Boolean = { true }
|
||||
) : DialogWrapper(owner) {
|
||||
|
||||
private val tree = HostTree()
|
||||
|
||||
val hosts = mutableListOf<Host>()
|
||||
|
||||
var allowMulti = true
|
||||
set(value) {
|
||||
field = value
|
||||
if (value) {
|
||||
tree.selectionModel.selectionMode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION
|
||||
} else {
|
||||
tree.selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
||||
isModal = true
|
||||
isResizable = false
|
||||
controlsVisible = false
|
||||
title = I18n.getString("termora.transport.sftp.select-host")
|
||||
|
||||
tree.setModel(SearchableHostTreeModel(tree.model) { host ->
|
||||
(host.protocol == Protocol.Folder || host.protocol == Protocol.SSH) && filter.invoke(host)
|
||||
})
|
||||
tree.contextmenu = true
|
||||
tree.doubleClickConnection = false
|
||||
tree.dragEnabled = false
|
||||
|
||||
initEvents()
|
||||
|
||||
init()
|
||||
setLocationRelativeTo(null)
|
||||
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowActivated(e: WindowEvent) {
|
||||
removeWindowListener(this)
|
||||
val state = Database.getDatabase().properties.getString("HostTreeDialog.HostTreeExpansionState")
|
||||
if (state != null) {
|
||||
TreeUtils.loadExpansionState(tree, state)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
tree.addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||
val node = tree.lastSelectedPathComponent ?: return
|
||||
if (node is Host && node.protocol != Protocol.Folder) {
|
||||
doOKAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
tree.setModel(null)
|
||||
Database.getDatabase().properties.putString(
|
||||
"HostTreeDialog.HostTreeExpansionState",
|
||||
TreeUtils.saveExpansionState(tree)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val scrollPane = JScrollPane(tree)
|
||||
scrollPane.border = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor),
|
||||
BorderFactory.createEmptyBorder(4, 6, 4, 6)
|
||||
)
|
||||
|
||||
return scrollPane
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
|
||||
if (allowMulti) {
|
||||
val nodes = tree.getSelectionNodes().filter { it.protocol == Protocol.SSH }
|
||||
if (nodes.isEmpty()) {
|
||||
return
|
||||
}
|
||||
hosts.clear()
|
||||
hosts.addAll(nodes)
|
||||
} else {
|
||||
val node = tree.lastSelectedPathComponent ?: return
|
||||
if (node !is Host || node.protocol != Protocol.SSH) {
|
||||
return
|
||||
}
|
||||
hosts.clear()
|
||||
hosts.add(node)
|
||||
}
|
||||
|
||||
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
override fun doCancelAction() {
|
||||
hosts.clear()
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
package app.termora
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import javax.swing.event.TreeModelEvent
|
||||
import javax.swing.event.TreeModelListener
|
||||
import javax.swing.tree.TreeModel
|
||||
import javax.swing.tree.TreePath
|
||||
|
||||
class HostTreeModel : TreeModel {
|
||||
|
||||
val listeners = mutableListOf<TreeModelListener>()
|
||||
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
private val hosts = mutableMapOf<String, Host>()
|
||||
private val myRoot by lazy {
|
||||
Host(
|
||||
id = "0",
|
||||
protocol = Protocol.Folder,
|
||||
name = I18n.getString("termora.welcome.my-hosts"),
|
||||
host = StringUtils.EMPTY,
|
||||
port = 0,
|
||||
remark = StringUtils.EMPTY,
|
||||
username = StringUtils.EMPTY
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
for (host in hostManager.hosts()) {
|
||||
hosts[host.id] = host
|
||||
}
|
||||
|
||||
hostManager.addHostListener(object : HostListener {
|
||||
override fun hostRemoved(id: String) {
|
||||
val host = hosts[id] ?: return
|
||||
removeNodeFromParent(host)
|
||||
}
|
||||
|
||||
override fun hostAdded(host: Host) {
|
||||
// 如果已经存在,那么是修改
|
||||
if (hosts.containsKey(host.id)) {
|
||||
val oldHost = hosts.getValue(host.id)
|
||||
// 父级结构变了
|
||||
if (oldHost.parentId != host.parentId) {
|
||||
hostRemoved(host.id)
|
||||
hostAdded(host)
|
||||
} else {
|
||||
hosts[host.id] = host
|
||||
val event = TreeModelEvent(this, getPathToRoot(host))
|
||||
listeners.forEach { it.treeStructureChanged(event) }
|
||||
}
|
||||
|
||||
} else {
|
||||
hosts[host.id] = host
|
||||
val parent = getParent(host) ?: return
|
||||
val path = TreePath(getPathToRoot(parent))
|
||||
val event = TreeModelEvent(this, path, intArrayOf(getIndexOfChild(parent, host)), arrayOf(host))
|
||||
listeners.forEach { it.treeNodesInserted(event) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun hostsChanged() {
|
||||
hosts.clear()
|
||||
for (host in hostManager.hosts()) {
|
||||
hosts[host.id] = host
|
||||
}
|
||||
val event = TreeModelEvent(this, getPathToRoot(root), null, null)
|
||||
listeners.forEach { it.treeStructureChanged(event) }
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
override fun getRoot(): Host {
|
||||
return myRoot
|
||||
}
|
||||
|
||||
override fun getChild(parent: Any?, index: Int): Any {
|
||||
return getChildren(parent)[index]
|
||||
}
|
||||
|
||||
override fun getChildCount(parent: Any?): Int {
|
||||
return getChildren(parent).size
|
||||
}
|
||||
|
||||
override fun isLeaf(node: Any?): Boolean {
|
||||
return getChildCount(node) == 0
|
||||
}
|
||||
|
||||
fun getParent(node: Host): Host? {
|
||||
if (node.parentId == root.id || root.id == node.id) {
|
||||
return root
|
||||
}
|
||||
return hosts.values.firstOrNull { it.id == node.parentId }
|
||||
}
|
||||
|
||||
override fun valueForPathChanged(path: TreePath?, newValue: Any?) {
|
||||
|
||||
}
|
||||
|
||||
override fun getIndexOfChild(parent: Any?, child: Any?): Int {
|
||||
return getChildren(parent).indexOf(child)
|
||||
}
|
||||
|
||||
override fun addTreeModelListener(listener: TreeModelListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
override fun removeTreeModelListener(listener: TreeModelListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅从结构中删除
|
||||
*/
|
||||
fun removeNodeFromParent(host: Host) {
|
||||
val parent = getParent(host) ?: return
|
||||
val index = getIndexOfChild(parent, host)
|
||||
val event = TreeModelEvent(this, TreePath(getPathToRoot(parent)), intArrayOf(index), arrayOf(host))
|
||||
hosts.remove(host.id)
|
||||
listeners.forEach { it.treeNodesRemoved(event) }
|
||||
}
|
||||
|
||||
fun visit(host: Host, visitor: (host: Host) -> Unit) {
|
||||
if (host.protocol == Protocol.Folder) {
|
||||
getChildren(host).forEach { visit(it, visitor) }
|
||||
visitor.invoke(host)
|
||||
} else {
|
||||
visitor.invoke(host)
|
||||
}
|
||||
}
|
||||
|
||||
fun getHost(id: String): Host? {
|
||||
return hosts[id]
|
||||
}
|
||||
|
||||
fun getPathToRoot(host: Host): Array<Host> {
|
||||
|
||||
if (host.id == root.id) {
|
||||
return arrayOf(root)
|
||||
}
|
||||
|
||||
val parents = mutableListOf(host)
|
||||
var pId = host.parentId
|
||||
while (pId != root.id) {
|
||||
val e = hosts[(pId)] ?: break
|
||||
parents.addFirst(e)
|
||||
pId = e.parentId
|
||||
}
|
||||
parents.addFirst(root)
|
||||
return parents.toTypedArray()
|
||||
}
|
||||
|
||||
fun getChildren(parent: Any?): List<Host> {
|
||||
val pId = if (parent is Host) parent.id else root.id
|
||||
return hosts.values.filter { it.parentId == pId }
|
||||
.sortedWith(compareBy<Host> { if (it.protocol == Protocol.Folder) 0 else 1 }.thenBy { it.sort })
|
||||
}
|
||||
}
|
||||
97
src/main/kotlin/app/termora/HostTreeNode.kt
Normal file
@@ -0,0 +1,97 @@
|
||||
package app.termora
|
||||
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
import javax.swing.tree.TreeNode
|
||||
|
||||
class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
companion object {
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果要重新赋值,记得修改 [Host.updateDate] 否则下次取出时可能时缓存的
|
||||
*/
|
||||
var host: Host
|
||||
get() {
|
||||
val cacheHost = hostManager.getHost((userObject as Host).id)
|
||||
val myHost = userObject as Host
|
||||
if (cacheHost == null) {
|
||||
return myHost
|
||||
}
|
||||
return if (cacheHost.updateDate > myHost.updateDate) cacheHost else myHost
|
||||
}
|
||||
set(value) = setUserObject(value)
|
||||
|
||||
val folderCount
|
||||
get() = children().toList().count { if (it is HostTreeNode) it.host.protocol == Protocol.Folder else false }
|
||||
|
||||
override fun getParent(): HostTreeNode? {
|
||||
return super.getParent() as HostTreeNode?
|
||||
}
|
||||
|
||||
fun getAllChildren(): List<HostTreeNode> {
|
||||
val children = mutableListOf<HostTreeNode>()
|
||||
for (child in children()) {
|
||||
if (child is HostTreeNode) {
|
||||
children.add(child)
|
||||
children.addAll(child.getAllChildren())
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
fun childrenNode(): List<HostTreeNode> {
|
||||
return children?.map { it as HostTreeNode } ?: emptyList()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 深度克隆
|
||||
* @param scopes 克隆的范围
|
||||
*/
|
||||
fun clone(scopes: Set<Protocol> = emptySet()): HostTreeNode {
|
||||
val newNode = clone() as HostTreeNode
|
||||
deepClone(newNode, this, scopes)
|
||||
return newNode
|
||||
}
|
||||
|
||||
private fun deepClone(newNode: HostTreeNode, oldNode: HostTreeNode, scopes: Set<Protocol> = emptySet()) {
|
||||
for (child in oldNode.childrenNode()) {
|
||||
if (scopes.isNotEmpty() && !scopes.contains(child.host.protocol)) continue
|
||||
val newChildNode = child.clone() as HostTreeNode
|
||||
deepClone(newChildNode, child, scopes)
|
||||
newNode.add(newChildNode)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clone(): Any {
|
||||
val newNode = HostTreeNode(host)
|
||||
newNode.children = null
|
||||
newNode.parent = null
|
||||
return newNode
|
||||
}
|
||||
|
||||
override fun isNodeChild(aNode: TreeNode?): Boolean {
|
||||
if (aNode is HostTreeNode) {
|
||||
for (node in childrenNode()) {
|
||||
if (node.host == aNode.host) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.isNodeChild(aNode)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as HostTreeNode
|
||||
|
||||
return host == other.host
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return host.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ object Icons {
|
||||
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
|
||||
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
|
||||
val close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") }
|
||||
val openInNewWindow by lazy { DynamicIcon("icons/openInNewWindow.svg", "icons/openInNewWindow_dark.svg") }
|
||||
val openInToolWindow by lazy { DynamicIcon("icons/openInToolWindow.svg", "icons/openInToolWindow_dark.svg") }
|
||||
val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
|
||||
val eye by lazy { DynamicIcon("icons/eye.svg", "icons/eye_dark.svg") }
|
||||
val eyeClose by lazy { DynamicIcon("icons/eyeClose.svg", "icons/eyeClose_dark.svg") }
|
||||
@@ -26,6 +28,9 @@ object Icons {
|
||||
val empty by lazy { DynamicIcon("icons/empty.svg") }
|
||||
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") }
|
||||
val add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") }
|
||||
val locate by lazy { DynamicIcon("icons/locate.svg", "icons/locate_dark.svg") }
|
||||
val percentage by lazy { DynamicIcon("icons/percentage.svg", "icons/percentage_dark.svg") }
|
||||
val text by lazy { DynamicIcon("icons/text.svg", "icons/text_dark.svg") }
|
||||
val errorIntroduction by lazy { DynamicIcon("icons/errorIntroduction.svg", "icons/errorIntroduction_dark.svg") }
|
||||
val networkPolicy by lazy { DynamicIcon("icons/networkPolicy.svg", "icons/networkPolicy_dark.svg") }
|
||||
val clusterRole by lazy { DynamicIcon("icons/clusterRole.svg", "icons/clusterRole_dark.svg") }
|
||||
@@ -114,5 +119,6 @@ object Icons {
|
||||
val listKey by lazy { DynamicIcon("icons/listKey.svg", "icons/listKey_dark.svg") }
|
||||
val forwardPorts by lazy { DynamicIcon("icons/forwardPorts.svg", "icons/forwardPorts_dark.svg") }
|
||||
val showWriteAccess by lazy { DynamicIcon("icons/showWriteAccess.svg", "icons/showWriteAccess_dark.svg") }
|
||||
val nvidia by lazy { DynamicIcon("icons/nvidia.svg", "icons/nvidia_dark.svg") }
|
||||
|
||||
}
|
||||
@@ -50,4 +50,9 @@ private fun setupNativeLibraries() {
|
||||
if (jSerialComm.exists()) {
|
||||
System.setProperty("jSerialComm.library.path", jSerialComm.absolutePath)
|
||||
}
|
||||
|
||||
val restart4j = FileUtils.getFile(dylib, "restart4j", "restarter")
|
||||
if (restart4j.exists()) {
|
||||
System.setProperty("restarter.path", restart4j.absolutePath)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviders
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
@@ -13,11 +14,13 @@ import kotlin.math.abs
|
||||
|
||||
class MyTabbedPane : FlatTabbedPane() {
|
||||
|
||||
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||
private val dragMouseAdaptor = DragMouseAdaptor()
|
||||
private val terminalTabbedManager
|
||||
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||
.getData(DataProviders.TerminalTabbedManager)
|
||||
private val owner
|
||||
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||
.getData(DataProviders.TermoraFrame) as TermoraFrame
|
||||
|
||||
init {
|
||||
initEvents()
|
||||
@@ -145,11 +148,11 @@ class MyTabbedPane : FlatTabbedPane() {
|
||||
// 如果等于 null 表示在空地方释放,那么单独一个窗口
|
||||
if (c == null) {
|
||||
val window = TermoraFrameManager.getInstance().createWindow()
|
||||
dragToAnotherWindow(window)
|
||||
dragToAnotherWindow(owner, window)
|
||||
window.location = releasedPoint
|
||||
window.isVisible = true
|
||||
} else if (c != owner && c is TermoraFrame) { // 如果在某个窗口内释放,那么就移动到某个窗口内
|
||||
dragToAnotherWindow(c)
|
||||
dragToAnotherWindow(owner, c)
|
||||
} else {
|
||||
val tab = this.terminalTab
|
||||
val terminalTabbedManager = terminalTabbedManager
|
||||
@@ -224,20 +227,29 @@ class MyTabbedPane : FlatTabbedPane() {
|
||||
}
|
||||
|
||||
|
||||
private fun dragToAnotherWindow(frame: TermoraFrame) {
|
||||
private fun dragToAnotherWindow(oldFrame: TermoraFrame, frame: TermoraFrame) {
|
||||
val tab = this.terminalTab ?: return
|
||||
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
||||
val tabbedManager = frame.getData(DataProviders.TerminalTabbed) ?: return
|
||||
val tabbedPane = frame.getData(DataProviders.TabbedPane) ?: return
|
||||
val windowScope = frame.getData(DataProviders.WindowScope) ?: return
|
||||
val oldWindowScope = oldFrame.getData(DataProviders.WindowScope) ?: return
|
||||
val location = Point(MouseInfo.getPointerInfo().location)
|
||||
SwingUtilities.convertPointFromScreen(location, tabbedPane)
|
||||
val index = tabbedPane.indexAtLocation(location.x, location.y)
|
||||
|
||||
|
||||
moveTab(
|
||||
tabbedManager,
|
||||
tab,
|
||||
index
|
||||
)
|
||||
|
||||
TerminalPanelFactory.getInstance(oldWindowScope).removeTerminalPanel(terminalPanel)
|
||||
TerminalPanelFactory.getInstance(windowScope).addTerminalPanel(terminalPanel)
|
||||
|
||||
|
||||
|
||||
if (frame.hasFocus()) {
|
||||
return
|
||||
}
|
||||
|
||||
1116
src/main/kotlin/app/termora/NewHostTree.kt
Normal file
87
src/main/kotlin/app/termora/NewHostTreeDialog.kt
Normal file
@@ -0,0 +1,87 @@
|
||||
package app.termora
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.util.function.Function
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JScrollPane
|
||||
import javax.swing.UIManager
|
||||
|
||||
class NewHostTreeDialog(
|
||||
owner: Window,
|
||||
) : DialogWrapper(owner) {
|
||||
var hosts = emptyList<Host>()
|
||||
var allowMulti = true
|
||||
|
||||
private var filter: Function<HostTreeNode, Boolean> = Function<HostTreeNode, Boolean> { true }
|
||||
private val tree = NewHostTree()
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
||||
isModal = true
|
||||
isResizable = false
|
||||
controlsVisible = false
|
||||
title = I18n.getString("termora.transport.sftp.select-host")
|
||||
|
||||
tree.contextmenu = false
|
||||
tree.doubleClickConnection = false
|
||||
tree.dragEnabled = false
|
||||
|
||||
|
||||
|
||||
init()
|
||||
setLocationRelativeTo(null)
|
||||
|
||||
}
|
||||
|
||||
fun setFilter(filter: Function<HostTreeNode, Boolean>) {
|
||||
tree.model = FilterableHostTreeModel(tree) { false }.apply {
|
||||
addFilter(filter)
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val scrollPane = JScrollPane(tree)
|
||||
scrollPane.border = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor),
|
||||
BorderFactory.createEmptyBorder(4, 6, 4, 6)
|
||||
)
|
||||
|
||||
return scrollPane
|
||||
}
|
||||
|
||||
|
||||
override fun doCancelAction() {
|
||||
hosts = emptyList()
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
hosts = tree.getSelectionHostTreeNodes(true)
|
||||
.filter { filter.apply(it) }
|
||||
.map { it.host }
|
||||
|
||||
if (hosts.isEmpty()) return
|
||||
if (!allowMulti && hosts.size > 1) return
|
||||
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
fun setTreeName(treeName: String) {
|
||||
Disposer.register(disposable, object : Disposable {
|
||||
private val key = "${treeName}.state"
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
|
||||
init {
|
||||
TreeUtils.loadExpansionState(tree, properties.getString(key, StringUtils.EMPTY))
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
properties.putString(key, TreeUtils.saveExpansionState(tree))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
83
src/main/kotlin/app/termora/NewHostTreeModel.kt
Normal file
@@ -0,0 +1,83 @@
|
||||
package app.termora
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import javax.swing.tree.DefaultTreeModel
|
||||
import javax.swing.tree.MutableTreeNode
|
||||
import javax.swing.tree.TreeNode
|
||||
|
||||
|
||||
class NewHostTreeModel : DefaultTreeModel(
|
||||
HostTreeNode(
|
||||
Host(
|
||||
id = "0",
|
||||
protocol = Protocol.Folder,
|
||||
name = I18n.getString("termora.welcome.my-hosts"),
|
||||
host = StringUtils.EMPTY,
|
||||
port = 0,
|
||||
remark = StringUtils.EMPTY,
|
||||
username = StringUtils.EMPTY
|
||||
)
|
||||
)
|
||||
) {
|
||||
private val Host.isRoot get() = this.parentId == "0" || this.parentId.isBlank()
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
|
||||
init {
|
||||
reload()
|
||||
}
|
||||
|
||||
|
||||
override fun getRoot(): HostTreeNode {
|
||||
return super.getRoot() as HostTreeNode
|
||||
}
|
||||
|
||||
|
||||
override fun reload(parent: TreeNode) {
|
||||
|
||||
if (parent !is HostTreeNode) {
|
||||
super.reload(parent)
|
||||
return
|
||||
}
|
||||
|
||||
parent.removeAllChildren()
|
||||
|
||||
val hosts = hostManager.hosts()
|
||||
val nodes = linkedMapOf<String, HostTreeNode>()
|
||||
|
||||
// 遍历 Host 列表,构建树节点
|
||||
for (host in hosts) {
|
||||
val node = HostTreeNode(host)
|
||||
nodes[host.id] = node
|
||||
}
|
||||
|
||||
for (host in hosts) {
|
||||
val node = nodes[host.id] ?: continue
|
||||
if (host.isRoot) continue
|
||||
val p = nodes[host.parentId] ?: continue
|
||||
p.add(node)
|
||||
}
|
||||
|
||||
for ((_, v) in nodes.entries) {
|
||||
if (parent.host.id == v.host.parentId) {
|
||||
parent.add(v)
|
||||
}
|
||||
}
|
||||
|
||||
super.reload(parent)
|
||||
}
|
||||
|
||||
override fun insertNodeInto(newChild: MutableTreeNode, parent: MutableTreeNode, index: Int) {
|
||||
super.insertNodeInto(newChild, parent, index)
|
||||
// 重置所有排序
|
||||
if (parent is HostTreeNode) {
|
||||
for ((i, c) in parent.children().toList().filterIsInstance<HostTreeNode>().withIndex()) {
|
||||
val sort = i.toLong()
|
||||
if (c.host.sort == sort) continue
|
||||
c.host = c.host.copy(sort = sort, updateDate = System.currentTimeMillis())
|
||||
hostManager.addHost(c.host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -13,12 +13,13 @@ import java.awt.event.ItemEvent
|
||||
import javax.swing.*
|
||||
import kotlin.math.max
|
||||
|
||||
class RequestAuthenticationDialog(owner: Window) : DialogWrapper(owner) {
|
||||
class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(owner) {
|
||||
|
||||
private val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||
private val rememberCheckBox = JCheckBox("Remember")
|
||||
private val passwordPanel = JPanel(BorderLayout())
|
||||
private val passwordPasswordField = OutlinePasswordField()
|
||||
private val usernameTextField = OutlineTextField()
|
||||
private val publicKeyComboBox = OutlineComboBox<OhKeyPair>()
|
||||
private val keyManager get() = KeyManager.getInstance()
|
||||
private var authentication = Authentication.No
|
||||
@@ -64,6 +65,8 @@ class RequestAuthenticationDialog(owner: Window) : DialogWrapper(owner) {
|
||||
}
|
||||
}
|
||||
|
||||
usernameTextField.text = host.username
|
||||
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
@@ -72,7 +75,7 @@ class RequestAuthenticationDialog(owner: Window) : DialogWrapper(owner) {
|
||||
val formMargin = "7dlu"
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow",
|
||||
"pref, $formMargin, pref"
|
||||
"pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
switchPasswordComponent()
|
||||
@@ -81,8 +84,10 @@ class RequestAuthenticationDialog(owner: Window) : DialogWrapper(owner) {
|
||||
.layout(layout)
|
||||
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, 1)
|
||||
.add(authenticationTypeComboBox).xy(3, 1)
|
||||
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, 3)
|
||||
.add(passwordPanel).xy(3, 3)
|
||||
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, 3)
|
||||
.add(usernameTextField).xy(3, 3)
|
||||
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, 5)
|
||||
.add(passwordPanel).xy(3, 5)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -134,7 +139,13 @@ class RequestAuthenticationDialog(owner: Window) : DialogWrapper(owner) {
|
||||
|
||||
fun getAuthentication(): Authentication {
|
||||
isModal = true
|
||||
SwingUtilities.invokeLater { passwordPasswordField.requestFocusInWindow() }
|
||||
SwingUtilities.invokeLater {
|
||||
if (usernameTextField.text.isBlank()) {
|
||||
usernameTextField.requestFocusInWindow()
|
||||
} else {
|
||||
passwordPasswordField.requestFocusInWindow()
|
||||
}
|
||||
}
|
||||
isVisible = true
|
||||
return authentication
|
||||
}
|
||||
@@ -143,4 +154,8 @@ class RequestAuthenticationDialog(owner: Window) : DialogWrapper(owner) {
|
||||
return rememberCheckBox.isSelected
|
||||
}
|
||||
|
||||
fun getUsername(): String {
|
||||
return usernameTextField.text
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,6 +27,7 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
||||
private var sshClient: SshClient? = null
|
||||
private var sshSession: ClientSession? = null
|
||||
private var lastPasswordReporterDataListener: PasswordReporterDataListener? = null
|
||||
private val sftpCommand get() = Database.getDatabase().sftp.sftpCommand
|
||||
|
||||
companion object {
|
||||
val canSupports by lazy {
|
||||
@@ -42,14 +43,14 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
||||
|
||||
override suspend fun openPtyConnector(): PtyConnector {
|
||||
|
||||
|
||||
val useJumpHosts = host.options.jumpHosts.isNotEmpty() || host.proxy.type != ProxyType.No
|
||||
val commands = mutableListOf("sftp")
|
||||
val commands = mutableListOf(StringUtils.defaultIfBlank(sftpCommand, "sftp"))
|
||||
var host = this.host
|
||||
|
||||
// 如果配置了跳板机或者代理,那么通过 SSH 的端口转发到本地
|
||||
if (useJumpHosts) {
|
||||
host = host.copy(
|
||||
updateDate = System.currentTimeMillis(),
|
||||
tunnelings = listOf(
|
||||
Tunneling(
|
||||
type = TunnelingType.Local,
|
||||
@@ -66,7 +67,11 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
||||
// 打开通道
|
||||
for (tunneling in host.tunnelings) {
|
||||
val address = SshClients.openTunneling(sshSession, host, tunneling)
|
||||
host = host.copy(host = address.hostName, port = address.port)
|
||||
host = host.copy(
|
||||
host = address.hostName,
|
||||
port = address.port,
|
||||
updateDate = System.currentTimeMillis(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,9 +133,9 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
||||
private fun setAuthentication(commands: MutableList<String>, host: Host) {
|
||||
// 如果通过公钥连接
|
||||
if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||
val keyPair = keyManager.getOhKeyPair(host.authentication.password)
|
||||
if (keyPair != null) {
|
||||
val keyPair = OhKeyPairKeyPairProvider.generateKeyPair(keyPair)
|
||||
val ohKeyPair = keyManager.getOhKeyPair(host.authentication.password)
|
||||
if (ohKeyPair != null) {
|
||||
val keyPair = OhKeyPairKeyPairProvider.generateKeyPair(ohKeyPair)
|
||||
val privateKeyPath = Application.createSubTemporaryDir()
|
||||
val privateKeyFile = Files.createTempFile(privateKeyPath, Application.getName(), StringUtils.EMPTY)
|
||||
Files.newOutputStream(privateKeyFile)
|
||||
|
||||
@@ -12,10 +12,11 @@ import javax.swing.SwingUtilities
|
||||
|
||||
class SFTPTerminalTab : Disposable, TerminalTab, DataProvider {
|
||||
|
||||
private val transportPanel by lazy {
|
||||
TransportPanel().apply {
|
||||
Disposer.register(this@SFTPTerminalTab, this)
|
||||
}
|
||||
private val sftp get() = Database.getDatabase().sftp
|
||||
private val transportPanel = TransportPanel()
|
||||
|
||||
init {
|
||||
Disposer.register(this, transportPanel)
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
@@ -43,6 +44,11 @@ class SFTPTerminalTab : Disposable, TerminalTab, DataProvider {
|
||||
|
||||
override fun canClose(): Boolean {
|
||||
assertEventDispatchThread()
|
||||
|
||||
if (sftp.pinTab) {
|
||||
return false
|
||||
}
|
||||
|
||||
val transportManager = transportPanel.getData(TransportDataProviders.TransportManager) ?: return true
|
||||
if (transportManager.getTransports().isEmpty()) {
|
||||
return true
|
||||
|
||||
@@ -36,10 +36,13 @@ import javax.swing.SwingUtilities
|
||||
class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
PtyHostTerminalTab(windowScope, host) {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
|
||||
val SSHSession = DataKey(ClientSession::class)
|
||||
|
||||
private val log = LoggerFactory.getLogger(SSHTerminalTab::class.java)
|
||||
}
|
||||
|
||||
private val mutex = Mutex()
|
||||
private val tab = this
|
||||
|
||||
private var sshClient: SshClient? = null
|
||||
private var sshSession: ClientSession? = null
|
||||
@@ -86,7 +89,8 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
terminal.write("SSH client is opening...\r\n")
|
||||
}
|
||||
|
||||
var host = this.host.copy(authentication = this.host.authentication.copy())
|
||||
var host =
|
||||
this.host.copy(authentication = this.host.authentication.copy(), updateDate = System.currentTimeMillis())
|
||||
val owner = SwingUtilities.getWindowAncestor(terminalPanel)
|
||||
val client = SshClients.openClient(host).also { sshClient = it }
|
||||
client.serverKeyVerifier = DialogServerKeyVerifier(owner)
|
||||
@@ -95,12 +99,21 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
|
||||
if (host.authentication.type == AuthenticationType.No) {
|
||||
withContext(Dispatchers.Swing) {
|
||||
val dialog = RequestAuthenticationDialog(owner)
|
||||
val dialog = RequestAuthenticationDialog(owner, host)
|
||||
val authentication = dialog.getAuthentication()
|
||||
host = host.copy(authentication = authentication)
|
||||
host = host.copy(
|
||||
authentication = authentication,
|
||||
username = dialog.getUsername(),
|
||||
updateDate = System.currentTimeMillis(),
|
||||
)
|
||||
// save
|
||||
if (dialog.isRemembered()) {
|
||||
HostManager.getInstance().addHost(this@SSHTerminalTab.host.copy(authentication = authentication))
|
||||
HostManager.getInstance().addHost(
|
||||
tab.host.copy(
|
||||
authentication = authentication,
|
||||
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,7 +168,7 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
if (Database.getDatabase().terminal.autoCloseTabWhenDisconnected) {
|
||||
terminalTabbedManager?.let { manager ->
|
||||
SwingUtilities.invokeLater {
|
||||
manager.closeTerminalTab(this@SSHTerminalTab, true)
|
||||
manager.closeTerminalTab(tab, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,15 +205,30 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
}
|
||||
|
||||
for (tunneling in host.tunnelings) {
|
||||
|
||||
SshClients.openTunneling(session, host, tunneling)
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
terminal.write("Start [${tunneling.name}] port forwarding successfully.\r\n")
|
||||
try {
|
||||
SshClients.openTunneling(session, host, tunneling)
|
||||
withContext(Dispatchers.Swing) {
|
||||
terminal.write("Start [${tunneling.name}] port forwarding successfully.\r\n")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error("Start [${tunneling.name}] port forwarding failed: {}", e.message, e)
|
||||
}
|
||||
withContext(Dispatchers.Swing) {
|
||||
terminal.write("Start [${tunneling.name}] port forwarding failed: ${e.message}\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||
if (dataKey == SSHSession) {
|
||||
return sshSession as T?
|
||||
}
|
||||
return super.getData(dataKey)
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
if (mutex.tryLock()) {
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
package app.termora
|
||||
|
||||
import javax.swing.event.TreeModelEvent
|
||||
import javax.swing.event.TreeModelListener
|
||||
import javax.swing.tree.TreeModel
|
||||
import javax.swing.tree.TreePath
|
||||
|
||||
class SearchableHostTreeModel(
|
||||
private val model: HostTreeModel,
|
||||
private val filter: (host: Host) -> Boolean = { true }
|
||||
) : TreeModel {
|
||||
private var text = String()
|
||||
|
||||
override fun getRoot(): Any {
|
||||
return model.root
|
||||
}
|
||||
|
||||
override fun getChild(parent: Any?, index: Int): Any {
|
||||
return getChildren(parent)[index]
|
||||
}
|
||||
|
||||
override fun getChildCount(parent: Any?): Int {
|
||||
return getChildren(parent).size
|
||||
}
|
||||
|
||||
override fun isLeaf(node: Any?): Boolean {
|
||||
return model.isLeaf(node)
|
||||
}
|
||||
|
||||
override fun valueForPathChanged(path: TreePath?, newValue: Any?) {
|
||||
return model.valueForPathChanged(path, newValue)
|
||||
}
|
||||
|
||||
override fun getIndexOfChild(parent: Any?, child: Any?): Int {
|
||||
return getChildren(parent).indexOf(child)
|
||||
}
|
||||
|
||||
override fun addTreeModelListener(l: TreeModelListener) {
|
||||
model.addTreeModelListener(l)
|
||||
}
|
||||
|
||||
override fun removeTreeModelListener(l: TreeModelListener) {
|
||||
model.removeTreeModelListener(l)
|
||||
}
|
||||
|
||||
|
||||
private fun getChildren(parent: Any?): List<Host> {
|
||||
val children = model.getChildren(parent)
|
||||
if (children.isEmpty()) return emptyList()
|
||||
return children.filter { e ->
|
||||
filter.invoke(e)
|
||||
&& e.name.contains(text, true)
|
||||
|| e.host.contains(text, true)
|
||||
|| TreeUtils.children(model, e, true).filterIsInstance<Host>().any { it.name.contains(text, true) || it.host.contains(text, true) }
|
||||
}
|
||||
}
|
||||
|
||||
fun search(text: String) {
|
||||
this.text = text
|
||||
model.listeners.forEach {
|
||||
it.treeStructureChanged(
|
||||
TreeModelEvent(
|
||||
this, TreePath(root),
|
||||
null, null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import app.termora.AES.encodeBase64String
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.highlight.KeywordHighlight
|
||||
import app.termora.highlight.KeywordHighlightManager
|
||||
import app.termora.keymap.Keymap
|
||||
@@ -22,6 +23,7 @@ import app.termora.terminal.CursorStyle
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.panel.FloatingToolbarPanel
|
||||
import app.termora.terminal.panel.TerminalPanel
|
||||
import app.termora.transport.SFTPAction
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||
@@ -34,7 +36,6 @@ import com.jthemedetecor.OsThemeDetector
|
||||
import com.sun.jna.LastErrorException
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.*
|
||||
import org.apache.commons.codec.binary.Base64
|
||||
import org.apache.commons.io.IOUtils
|
||||
@@ -43,6 +44,7 @@ import org.apache.commons.lang3.SystemUtils
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import org.apache.commons.lang3.time.DateFormatUtils
|
||||
import org.jdesktop.swingx.JXEditorPane
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
@@ -67,6 +69,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
private val keymapManager get() = KeymapManager.getInstance()
|
||||
private val macroManager get() = MacroManager.getInstance()
|
||||
private val actionManager get() = ActionManager.getInstance()
|
||||
private val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
||||
private val keyManager get() = KeyManager.getInstance()
|
||||
|
||||
@@ -195,12 +198,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
appearance.language = languageComboBox.selectedItem as String
|
||||
SwingUtilities.invokeLater {
|
||||
OptionPane.showMessageDialog(
|
||||
owner,
|
||||
I18n.getString("termora.settings.restart.message"),
|
||||
I18n.getString("termora.settings.restart.title"),
|
||||
messageType = JOptionPane.INFORMATION_MESSAGE,
|
||||
)
|
||||
TermoraRestarter.getInstance().scheduleRestart(owner)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,6 +303,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private val cursorStyleComboBox = FlatComboBox<CursorStyle>()
|
||||
private val debugComboBox = YesOrNoComboBox()
|
||||
private val beepComboBox = YesOrNoComboBox()
|
||||
private val cursorBlinkComboBox = YesOrNoComboBox()
|
||||
private val fontComboBox = FlatComboBox<String>()
|
||||
private val shellComboBox = FlatComboBox<String>()
|
||||
private val maxRowsTextField = IntSpinner(0, 0)
|
||||
@@ -390,6 +389,12 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
cursorBlinkComboBox.addItemListener { e ->
|
||||
if (e.stateChange == ItemEvent.SELECTED) {
|
||||
terminalSetting.cursorBlink = cursorBlinkComboBox.selectedItem as Boolean
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
shellComboBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
@@ -478,6 +483,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
fontComboBox.selectedItem = terminalSetting.font
|
||||
debugComboBox.selectedItem = terminalSetting.debug
|
||||
beepComboBox.selectedItem = terminalSetting.beep
|
||||
cursorBlinkComboBox.selectedItem = terminalSetting.cursorBlink
|
||||
cursorStyleComboBox.selectedItem = terminalSetting.cursor
|
||||
selectCopyComboBox.selectedItem = terminalSetting.selectCopy
|
||||
autoCloseTabComboBox.selectedItem = terminalSetting.autoCloseTabWhenDisconnected
|
||||
@@ -499,7 +505,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
val beepBtn = JButton(Icons.run)
|
||||
@@ -526,6 +532,8 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
|
||||
.add(cursorStyleComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.cursor-blink")}:").xy(1, rows)
|
||||
.add(cursorBlinkComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.floating-toolbar")}:").xy(1, rows)
|
||||
.add(floatingToolbarComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.auto-close-tab")}:").xy(1, rows)
|
||||
@@ -1296,8 +1304,11 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
private inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||
|
||||
val editCommandField = OutlineTextField(255)
|
||||
private val editCommandField = OutlineTextField(255)
|
||||
private val sftpCommandField = OutlineTextField(255)
|
||||
private val pinTabComboBox = YesOrNoComboBox()
|
||||
private val sftp get() = database.sftp
|
||||
private val sftpAction get() = actionManager.getAction(Actions.SFTP) as SFTPAction
|
||||
|
||||
init {
|
||||
initView()
|
||||
@@ -1311,6 +1322,33 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
sftp.editCommand = editCommandField.text
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
sftpCommandField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||
override fun changedUpdate(e: DocumentEvent) {
|
||||
sftp.sftpCommand = sftpCommandField.text
|
||||
}
|
||||
})
|
||||
|
||||
pinTabComboBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
sftp.pinTab = pinTabComboBox.selectedItem as Boolean
|
||||
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
||||
val evt = AnActionEvent(window, StringUtils.EMPTY, EventObject(window))
|
||||
if (pinTabComboBox.selectedItem == true) {
|
||||
sftpAction.openOrCreateSFTPTerminalTab(evt)
|
||||
}
|
||||
val tabbed = evt.getData(DataProviders.TabbedPane) ?: continue
|
||||
val manager = evt.getData(DataProviders.TerminalTabbedManager) ?: continue
|
||||
for ((index, tab) in manager.getTerminalTabs().withIndex()) {
|
||||
if (tab is SFTPTerminalTab) {
|
||||
tabbed.setTabClosable(index, pinTabComboBox.selectedItem != true)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1321,7 +1359,15 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
editCommandField.placeholderText = "open -a TextEdit {0}"
|
||||
}
|
||||
|
||||
if (SystemInfo.isWindows) {
|
||||
sftpCommandField.placeholderText = "sftp.exe"
|
||||
} else {
|
||||
sftpCommandField.placeholderText = "sftp"
|
||||
}
|
||||
|
||||
editCommandField.text = sftp.editCommand
|
||||
sftpCommandField.text = sftp.sftpCommand
|
||||
pinTabComboBox.selectedItem = sftp.pinTab
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
@@ -1343,8 +1389,12 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
)
|
||||
|
||||
val builder = FormBuilder.create().layout(layout).debug(false)
|
||||
builder.add("${I18n.getString("termora.settings.sftp.edit-command")}:").xy(1, 1)
|
||||
builder.add(editCommandField).xy(3, 1)
|
||||
builder.add("${I18n.getString("termora.settings.sftp.fixed-tab")}:").xy(1, 1)
|
||||
builder.add(pinTabComboBox).xy(3, 1)
|
||||
builder.add("${I18n.getString("termora.settings.sftp.edit-command")}:").xy(1, 3)
|
||||
builder.add(editCommandField).xy(3, 3)
|
||||
builder.add("${I18n.getString("termora.tabbed.contextmenu.sftp-command")}:").xy(1, 5)
|
||||
builder.add(sftpCommandField).xy(3, 5)
|
||||
|
||||
return builder.build()
|
||||
|
||||
@@ -1568,7 +1618,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
val key = doorman.work(passwordTextField.password)
|
||||
|
||||
hosts.forEach { hostManager.addHost(it, false) }
|
||||
hosts.forEach { hostManager.addHost(it) }
|
||||
keyPairs.forEach { keyManager.addOhKeyPair(it) }
|
||||
for (e in properties) {
|
||||
for ((k, v) in e.second) {
|
||||
|
||||
@@ -2,10 +2,12 @@ package app.termora
|
||||
|
||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||
import app.termora.terminal.TerminalSize
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.sshd.client.ClientBuilder
|
||||
import org.apache.sshd.client.SshClient
|
||||
import org.apache.sshd.client.channel.ChannelShell
|
||||
import org.apache.sshd.client.channel.ClientChannelEvent
|
||||
import org.apache.sshd.client.config.hosts.HostConfigEntry
|
||||
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver
|
||||
import org.apache.sshd.client.config.hosts.KnownHostEntry
|
||||
@@ -31,6 +33,7 @@ import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
|
||||
import org.eclipse.jgit.transport.sshd.ProxyData
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Window
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.net.SocketAddress
|
||||
@@ -38,6 +41,7 @@ import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingUtilities
|
||||
@@ -75,6 +79,34 @@ object SshClients {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行一个命令
|
||||
*
|
||||
* @return first: exitCode , second: response
|
||||
*/
|
||||
fun execChannel(
|
||||
session: ClientSession,
|
||||
command: String
|
||||
): Pair<Int, String> {
|
||||
|
||||
val baos = ByteArrayOutputStream()
|
||||
val channel = session.createExecChannel(command)
|
||||
channel.out = baos
|
||||
|
||||
if (channel.open().verify(timeout).await(timeout)) {
|
||||
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), timeout)
|
||||
}
|
||||
|
||||
IOUtils.closeQuietly(channel)
|
||||
|
||||
if (channel.exitStatus == null) {
|
||||
return Pair(-1, baos.toString())
|
||||
}
|
||||
|
||||
return Pair(channel.exitStatus, baos.toString())
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开一个会话
|
||||
*/
|
||||
@@ -119,7 +151,7 @@ object SshClients {
|
||||
log.info("jump host: ${currentHost.host}:${currentHost.port} , next host: ${nextHost.host}:${nextHost.port} , local address: ${address.hostName}:${address.port}")
|
||||
}
|
||||
// 映射完毕之后修改Host和端口
|
||||
jumpHosts[i + 1] = nextHost.copy(host = address.hostName, port = address.port)
|
||||
jumpHosts[i + 1] = nextHost.copy(host = address.hostName, port = address.port, updateDate = System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +1,67 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.highlight.KeywordHighlightPaintListener
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.PtyConnector
|
||||
import app.termora.terminal.Terminal
|
||||
import app.termora.terminal.panel.TerminalHyperlinkPaintListener
|
||||
import app.termora.terminal.panel.TerminalPanel
|
||||
import kotlinx.coroutines.*
|
||||
import java.awt.event.ComponentEvent
|
||||
import java.awt.event.ComponentListener
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class TerminalPanelFactory {
|
||||
class TerminalPanelFactory : Disposable {
|
||||
private val terminalPanels = mutableListOf<TerminalPanel>()
|
||||
|
||||
companion object {
|
||||
|
||||
private val Factory = DataKey(TerminalPanelFactory::class)
|
||||
|
||||
fun getInstance(scope: Scope): TerminalPanelFactory {
|
||||
return scope.getOrCreate(TerminalPanelFactory::class) { TerminalPanelFactory() }
|
||||
}
|
||||
|
||||
fun getAllTerminalPanel(): List<TerminalPanel> {
|
||||
fun getAllTerminalPanel(): Array<TerminalPanel> {
|
||||
return ApplicationScope.forApplicationScope().windowScopes()
|
||||
.map { getInstance(it) }
|
||||
.flatMap { it.getTerminalPanels() }
|
||||
.flatMap { it.terminalPanels }.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// repaint
|
||||
Painter.getInstance()
|
||||
}
|
||||
|
||||
|
||||
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
||||
val terminalPanel = TerminalPanel(terminal, ptyConnector)
|
||||
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
||||
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
||||
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
||||
terminal.getTerminalModel().setData(Factory, this)
|
||||
|
||||
Disposer.register(terminalPanel, object : Disposable {
|
||||
override fun dispose() {
|
||||
terminalPanels.remove(terminalPanel)
|
||||
if (terminal.getTerminalModel().hasData(Factory)) {
|
||||
terminal.getTerminalModel().getData(Factory).removeTerminalPanel(terminalPanel)
|
||||
}
|
||||
}
|
||||
})
|
||||
terminalPanels.add(terminalPanel)
|
||||
|
||||
addTerminalPanel(terminalPanel)
|
||||
return terminalPanel
|
||||
}
|
||||
|
||||
fun getTerminalPanels(): List<TerminalPanel> {
|
||||
return terminalPanels
|
||||
fun getTerminalPanels(): Array<TerminalPanel> {
|
||||
return terminalPanels.toTypedArray()
|
||||
}
|
||||
|
||||
fun repaintAll() {
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
terminalPanels.forEach { it.repaintImmediate() }
|
||||
getTerminalPanels().forEach { it.repaintImmediate() }
|
||||
} else {
|
||||
SwingUtilities.invokeLater { repaintAll() }
|
||||
}
|
||||
@@ -62,4 +79,35 @@ class TerminalPanelFactory {
|
||||
terminalPanels.remove(terminalPanel)
|
||||
}
|
||||
|
||||
fun addTerminalPanel(terminalPanel: TerminalPanel) {
|
||||
terminalPanels.add(terminalPanel)
|
||||
terminalPanel.terminal.getTerminalModel().setData(Factory, this)
|
||||
}
|
||||
|
||||
private class Painter : Disposable {
|
||||
companion object {
|
||||
fun getInstance(): Painter {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(Painter::class) { Painter() }
|
||||
}
|
||||
}
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
init {
|
||||
coroutineScope.launch {
|
||||
while (coroutineScope.isActive) {
|
||||
delay(500.milliseconds)
|
||||
SwingUtilities.invokeLater {
|
||||
ApplicationScope.forApplicationScope().windowScopes()
|
||||
.map { getInstance(it) }.forEach { it.repaintAll() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -73,12 +73,17 @@ class TerminalTabbed(
|
||||
tabbedPane.addPropertyChangeListener("selectedIndex") { evt ->
|
||||
val oldIndex = evt.oldValue as Int
|
||||
val newIndex = evt.newValue as Int
|
||||
|
||||
if (oldIndex >= 0 && tabs.size > newIndex) {
|
||||
tabs[oldIndex].onLostFocus()
|
||||
}
|
||||
|
||||
if (newIndex >= 0 && tabs.size > newIndex) {
|
||||
tabs[newIndex].onGrabFocus()
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater { tabbedPane.getComponentAt(newIndex).requestFocusInWindow() }
|
||||
|
||||
}
|
||||
|
||||
// 选择变动
|
||||
@@ -174,6 +179,9 @@ class TerminalTabbed(
|
||||
// 新的获取到焦点
|
||||
tabs[tabbedPane.selectedIndex].onGrabFocus()
|
||||
|
||||
// 新的真正获取焦点
|
||||
tabbedPane.getComponentAt(tabbedPane.selectedIndex).requestFocusInWindow()
|
||||
|
||||
if (disposable) {
|
||||
Disposer.dispose(tab)
|
||||
}
|
||||
@@ -272,7 +280,7 @@ class TerminalTabbed(
|
||||
}
|
||||
|
||||
|
||||
close.isEnabled = c !is WelcomePanel
|
||||
close.isEnabled = tab.canClose()
|
||||
rename.isEnabled = close.isEnabled
|
||||
clone.isEnabled = close.isEnabled
|
||||
openInNewWindow.isEnabled = close.isEnabled
|
||||
@@ -298,7 +306,7 @@ class TerminalTabbed(
|
||||
}
|
||||
|
||||
|
||||
private fun addTab(index: Int, tab: TerminalTab) {
|
||||
private fun addTab(index: Int, tab: TerminalTab, selected: Boolean) {
|
||||
val c = tab.getJComponent()
|
||||
val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
|
||||
|
||||
@@ -309,13 +317,20 @@ class TerminalTabbed(
|
||||
StringUtils.EMPTY,
|
||||
index
|
||||
)
|
||||
c.putClientProperty(titleProperty, title)
|
||||
|
||||
// 设置标题
|
||||
c.putClientProperty(titleProperty, title)
|
||||
// 监听 icons 变化
|
||||
tab.addPropertyChangeListener(iconListener)
|
||||
|
||||
tabs.add(index, tab)
|
||||
tabbedPane.selectedIndex = index
|
||||
|
||||
if (selected) {
|
||||
tabbedPane.selectedIndex = index
|
||||
}
|
||||
|
||||
tabbedPane.setTabClosable(index, tab.canClose())
|
||||
|
||||
Disposer.register(this, tab)
|
||||
}
|
||||
|
||||
@@ -341,7 +356,7 @@ class TerminalTabbed(
|
||||
}
|
||||
|
||||
host = host.copy(
|
||||
protocol = Protocol.SFTPPty,
|
||||
protocol = Protocol.SFTPPty, updateDate = System.currentTimeMillis(),
|
||||
options = host.options.copy(env = envs.toPropertiesString())
|
||||
)
|
||||
}
|
||||
@@ -437,12 +452,12 @@ class TerminalTabbed(
|
||||
override fun dispose() {
|
||||
}
|
||||
|
||||
override fun addTerminalTab(tab: TerminalTab) {
|
||||
addTab(tabs.size, tab)
|
||||
override fun addTerminalTab(tab: TerminalTab, selected: Boolean) {
|
||||
addTab(tabs.size, tab, selected)
|
||||
}
|
||||
|
||||
override fun addTerminalTab(index: Int, tab: TerminalTab) {
|
||||
addTab(index, tab)
|
||||
override fun addTerminalTab(index: Int, tab: TerminalTab, selected: Boolean) {
|
||||
addTab(index, tab, selected)
|
||||
}
|
||||
|
||||
override fun getSelectedTerminalTab(): TerminalTab? {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package app.termora
|
||||
|
||||
interface TerminalTabbedManager {
|
||||
fun addTerminalTab(tab: TerminalTab)
|
||||
fun addTerminalTab(index: Int, tab: TerminalTab)
|
||||
fun addTerminalTab(tab: TerminalTab, selected: Boolean = true)
|
||||
fun addTerminalTab(index: Int, tab: TerminalTab, selected: Boolean = true)
|
||||
fun getSelectedTerminalTab(): TerminalTab?
|
||||
fun getTerminalTabs(): List<TerminalTab>
|
||||
fun setSelectedTerminalTab(tab: TerminalTab)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package app.termora
|
||||
|
||||
|
||||
import app.termora.actions.ActionManager
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviderSupport
|
||||
import app.termora.actions.DataProviders
|
||||
@@ -12,7 +11,6 @@ import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jetbrains.JBR
|
||||
import java.awt.Dimension
|
||||
import java.awt.Insets
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.*
|
||||
@@ -32,7 +30,6 @@ fun assertEventDispatchThread() {
|
||||
class TermoraFrame : JFrame(), DataProvider {
|
||||
|
||||
|
||||
private val actionManager get() = ActionManager.getInstance()
|
||||
private val id = UUID.randomUUID().toString()
|
||||
private val windowScope = ApplicationScope.forWindowScope(this)
|
||||
private val titleBar = LogicCustomTitleBar.createCustomTitleBar(this)
|
||||
@@ -42,7 +39,7 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
private val isWindowDecorationsSupported by lazy { JBR.isWindowDecorationsSupported() }
|
||||
private val dataProviderSupport = DataProviderSupport()
|
||||
private val welcomePanel = WelcomePanel(windowScope)
|
||||
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
|
||||
private val sftp get() = Database.getDatabase().sftp
|
||||
|
||||
|
||||
init {
|
||||
@@ -103,6 +100,13 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
minimumSize = Dimension(640, 400)
|
||||
terminalTabbed.addTerminalTab(welcomePanel)
|
||||
|
||||
// 下一次事件循环检测是否固定 SFTP
|
||||
SwingUtilities.invokeLater {
|
||||
if (sftp.pinTab) {
|
||||
terminalTabbed.addTerminalTab(SFTPTerminalTab(), false)
|
||||
}
|
||||
}
|
||||
|
||||
// macOS 要避开左边的控制栏
|
||||
if (SystemInfo.isMacOS) {
|
||||
val left = max(titleBar.leftInset.toInt(), 76)
|
||||
|
||||
@@ -19,6 +19,8 @@ class TermoraFrameManager {
|
||||
}
|
||||
}
|
||||
|
||||
private val frames = mutableListOf<TermoraFrame>()
|
||||
|
||||
fun createWindow(): TermoraFrame {
|
||||
val frame = TermoraFrame()
|
||||
registerCloseCallback(frame)
|
||||
@@ -26,16 +28,26 @@ class TermoraFrameManager {
|
||||
frame.defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
||||
frame.setSize(1280, 800)
|
||||
frame.setLocationRelativeTo(null)
|
||||
frames.add(frame)
|
||||
return frame
|
||||
}
|
||||
|
||||
fun getWindows(): Array<TermoraFrame> {
|
||||
return frames.toTypedArray()
|
||||
}
|
||||
|
||||
|
||||
private fun registerCloseCallback(window: TermoraFrame) {
|
||||
window.addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
|
||||
// 删除
|
||||
frames.remove(window)
|
||||
|
||||
// dispose windowScope
|
||||
Disposer.dispose(ApplicationScope.forWindowScope(e.window))
|
||||
val windowScope = ApplicationScope.forWindowScope(e.window)
|
||||
Disposer.disposeChildren(windowScope, null)
|
||||
Disposer.dispose(windowScope)
|
||||
|
||||
val windowScopes = ApplicationScope.windowScopes()
|
||||
|
||||
@@ -68,7 +80,9 @@ class TermoraFrameManager {
|
||||
try {
|
||||
Disposer.getTree().assertIsEmpty(true)
|
||||
} catch (e: Exception) {
|
||||
log.error(e.message)
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
exitProcess(0)
|
||||
|
||||
155
src/main/kotlin/app/termora/TermoraRestarter.kt
Normal file
@@ -0,0 +1,155 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.github.hstyi.restart4j.Restarter
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Component
|
||||
import java.nio.file.Paths
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
class TermoraRestarter {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(TermoraRestarter::class.java)
|
||||
|
||||
fun getInstance(): TermoraRestarter {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(TermoraRestarter::class) { TermoraRestarter() }
|
||||
}
|
||||
|
||||
init {
|
||||
Restarter.setProcessHandler { ProcessHandle.current().pid().toInt() }
|
||||
Restarter.setExecCommandsHandler { commands ->
|
||||
val pb = ProcessBuilder(commands)
|
||||
if (SystemInfo.isLinux) {
|
||||
// 去掉链接库变量
|
||||
pb.environment().remove("LD_LIBRARY_PATH")
|
||||
}
|
||||
pb.redirectOutput(ProcessBuilder.Redirect.DISCARD)
|
||||
pb.redirectError(ProcessBuilder.Redirect.DISCARD)
|
||||
pb.directory(Paths.get(System.getProperty("user.home")).toFile())
|
||||
pb.start()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val restarting = AtomicBoolean(false)
|
||||
private val isSupported get() = !restarting.get() && checkIsSupported()
|
||||
private val isLinuxAppImage by lazy { System.getenv("LinuxAppImage")?.toBoolean() == true }
|
||||
private val startupCommand by lazy { ProcessHandle.current().info().command().getOrNull() }
|
||||
private val macOSApplicationPath by lazy {
|
||||
StringUtils.removeEndIgnoreCase(
|
||||
Application.getAppPath(),
|
||||
"/Contents/MacOS/Termora"
|
||||
)
|
||||
}
|
||||
|
||||
private fun restart(commands: List<String>) {
|
||||
if (!isSupported) return
|
||||
if (!restarting.compareAndSet(false, true)) return
|
||||
|
||||
SwingUtilities.invokeLater {
|
||||
try {
|
||||
doRestart(commands)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计划重启,如果当前进程支持重启,那么会询问用户是否重启。如果不支持重启,那么弹窗提示需要手动重启。
|
||||
*/
|
||||
fun scheduleRestart(owner: Component?, commands: List<String> = emptyList()) {
|
||||
|
||||
if (isSupported) {
|
||||
if (OptionPane.showConfirmDialog(
|
||||
owner,
|
||||
I18n.getString("termora.settings.restart.message"),
|
||||
I18n.getString("termora.settings.restart.title"),
|
||||
messageType = JOptionPane.QUESTION_MESSAGE,
|
||||
optionType = JOptionPane.YES_NO_OPTION,
|
||||
options = arrayOf(
|
||||
I18n.getString("termora.settings.restart.title"),
|
||||
I18n.getString("termora.cancel")
|
||||
),
|
||||
initialValue = I18n.getString("termora.settings.restart.title")
|
||||
) == JOptionPane.YES_OPTION
|
||||
) {
|
||||
restart(commands)
|
||||
}
|
||||
} else {
|
||||
OptionPane.showMessageDialog(
|
||||
owner,
|
||||
I18n.getString("termora.settings.restart.message"),
|
||||
I18n.getString("termora.settings.restart.title"),
|
||||
messageType = JOptionPane.INFORMATION_MESSAGE,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun doRestart(commands: List<String>) {
|
||||
|
||||
if (commands.isEmpty()) {
|
||||
if (SystemInfo.isMacOS) {
|
||||
Restarter.restart(arrayOf("open", "-n", macOSApplicationPath))
|
||||
} else if (SystemInfo.isWindows && startupCommand != null) {
|
||||
Restarter.restart(arrayOf(startupCommand))
|
||||
} else if (SystemInfo.isLinux) {
|
||||
if (isLinuxAppImage) {
|
||||
Restarter.restart(arrayOf(System.getenv("APPIMAGE")))
|
||||
} else if (startupCommand != null) {
|
||||
Restarter.restart(arrayOf(startupCommand))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Restarter.restart(commands.toTypedArray())
|
||||
}
|
||||
|
||||
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
||||
window.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun checkIsSupported(): Boolean {
|
||||
val appPath = Application.getAppPath()
|
||||
if (appPath.isBlank() || Application.isUnknownVersion()) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn("Restart not supported")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (SystemInfo.isWindows && startupCommand == null) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn("Restart not supported , ProcessHandle#info#command is null.")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (SystemInfo.isLinux) {
|
||||
if (isLinuxAppImage) {
|
||||
val appImage = System.getenv("APPIMAGE") ?: StringUtils.EMPTY
|
||||
return appImage.isNotBlank() && FileUtils.getFile(appImage).exists()
|
||||
}
|
||||
return startupCommand != null
|
||||
}
|
||||
|
||||
if (SystemInfo.isMacOS) {
|
||||
return Application.getAppPath().isNotBlank()
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -70,6 +70,9 @@ class UpdaterManager private constructor() {
|
||||
.build()
|
||||
val response = Application.httpClient.newCall(request).execute()
|
||||
if (!response.isSuccessful) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error("Failed to fetch latest version, response was ${response.code}")
|
||||
}
|
||||
return LatestVersion.self
|
||||
}
|
||||
|
||||
@@ -151,8 +154,4 @@ class UpdaterManager private constructor() {
|
||||
fun ignore(version: String) {
|
||||
properties.putString("ignored.version.$version", "true")
|
||||
}
|
||||
|
||||
private fun doGetLatestVersion() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package app.termora
|
||||
|
||||
|
||||
import app.termora.actions.*
|
||||
import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
|
||||
import app.termora.findeverywhere.FindEverywhereProvider
|
||||
import app.termora.findeverywhere.FindEverywhereResult
|
||||
import app.termora.terminal.DataKey
|
||||
@@ -27,11 +26,15 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
private val rootPanel = JPanel(BorderLayout())
|
||||
private val searchTextField = FlatTextField()
|
||||
private val hostTree = HostTree()
|
||||
private val hostTree = NewHostTree()
|
||||
private val bannerPanel = BannerPanel()
|
||||
private val toggle = FlatButton()
|
||||
private var fullContent = properties.getString("WelcomeFullContent", "false").toBoolean()
|
||||
private val dataProviderSupport = DataProviderSupport()
|
||||
private val hostTreeModel = hostTree.model as NewHostTreeModel
|
||||
private val filterableHostTreeModel = FilterableHostTreeModel(hostTree) {
|
||||
searchTextField.text.isBlank()
|
||||
}
|
||||
|
||||
init {
|
||||
initView()
|
||||
@@ -126,8 +129,6 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
||||
})
|
||||
hostTree.showsRootHandles = true
|
||||
|
||||
Disposer.register(this, hostTree)
|
||||
|
||||
val scrollPane = JScrollPane(hostTree)
|
||||
scrollPane.verticalScrollBar.maximumSize = Dimension(0, 0)
|
||||
scrollPane.verticalScrollBar.preferredSize = Dimension(0, 0)
|
||||
@@ -138,6 +139,11 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
||||
panel.add(scrollPane, BorderLayout.CENTER)
|
||||
panel.border = BorderFactory.createEmptyBorder(10, 0, 0, 0)
|
||||
|
||||
hostTree.model = filterableHostTreeModel
|
||||
TreeUtils.loadExpansionState(
|
||||
hostTree,
|
||||
properties.getString("Welcome.HostTree.state", StringUtils.EMPTY)
|
||||
)
|
||||
|
||||
return panel
|
||||
}
|
||||
@@ -163,37 +169,49 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
||||
})
|
||||
|
||||
|
||||
FindEverywhereProvider.getFindEverywhereProviders(windowScope)
|
||||
.add(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider {
|
||||
override fun find(pattern: String): List<FindEverywhereResult> {
|
||||
return TreeUtils.children(hostTree.model, hostTree.model.root)
|
||||
.filterIsInstance<Host>()
|
||||
.filter { it.protocol != Protocol.Folder }
|
||||
.map { HostFindEverywhereResult(it) }
|
||||
FindEverywhereProvider.getFindEverywhereProviders(windowScope).add(object : FindEverywhereProvider {
|
||||
override fun find(pattern: String): List<FindEverywhereResult> {
|
||||
var filter = hostTreeModel.root.getAllChildren()
|
||||
.map { it.host }
|
||||
.filter { it.protocol != Protocol.Folder }
|
||||
|
||||
if (pattern.isNotBlank()) {
|
||||
filter = filter.filter {
|
||||
if (it.protocol == Protocol.SSH) {
|
||||
it.name.contains(pattern, true) || it.host.contains(pattern, true)
|
||||
} else {
|
||||
it.name.contains(pattern, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun group(): String {
|
||||
return I18n.getString("termora.find-everywhere.groups.open-new-hosts")
|
||||
}
|
||||
return filter.map { HostFindEverywhereResult(it) }
|
||||
}
|
||||
|
||||
override fun order(): Int {
|
||||
return Integer.MIN_VALUE + 2
|
||||
}
|
||||
}))
|
||||
override fun group(): String {
|
||||
return I18n.getString("termora.find-everywhere.groups.open-new-hosts")
|
||||
}
|
||||
|
||||
override fun order(): Int {
|
||||
return Integer.MIN_VALUE + 2
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
filterableHostTreeModel.addFilter {
|
||||
val text = searchTextField.text
|
||||
val host = it.host
|
||||
text.isBlank() || host.name.contains(text, true)
|
||||
|| host.host.contains(text, true)
|
||||
|| host.username.contains(text, true)
|
||||
}
|
||||
|
||||
searchTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||
private var state = StringUtils.EMPTY
|
||||
override fun changedUpdate(e: DocumentEvent) {
|
||||
val text = searchTextField.text
|
||||
if (text.isBlank()) {
|
||||
hostTree.setModel(hostTree.model)
|
||||
TreeUtils.loadExpansionState(hostTree, state)
|
||||
state = String()
|
||||
} else {
|
||||
if (state.isBlank()) state = TreeUtils.saveExpansionState(hostTree)
|
||||
hostTree.setModel(hostTree.searchableModel)
|
||||
hostTree.searchableModel.search(text)
|
||||
TreeUtils.expandAll(hostTree)
|
||||
filterableHostTreeModel.refresh()
|
||||
if (text.isNotBlank()) {
|
||||
hostTree.expandAll()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -241,11 +259,13 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
hostTree.setModel(null)
|
||||
properties.putString("WelcomeFullContent", fullContent.toString())
|
||||
properties.putString("Welcome.HostTree.state", TreeUtils.saveExpansionState(hostTree))
|
||||
}
|
||||
|
||||
private class HostFindEverywhereResult(val host: Host) : FindEverywhereResult {
|
||||
private inner class HostFindEverywhereResult(val host: Host) : FindEverywhereResult {
|
||||
private val showMoreInfo get() = properties.getString("HostTree.showMoreInfo", "false").toBoolean()
|
||||
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
ActionManager.getInstance()
|
||||
.getAction(OpenHostAction.OPEN_HOST)
|
||||
@@ -261,7 +281,18 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
||||
return Icons.terminal
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
override fun getText(isSelected: Boolean): String {
|
||||
if (showMoreInfo) {
|
||||
val color = UIManager.getColor(if (isSelected) "textHighlightText" else "textInactiveText")
|
||||
val moreInfo = when (host.protocol) {
|
||||
Protocol.SSH -> "${host.username}@${host.host}"
|
||||
Protocol.Serial -> host.options.serialComm.port
|
||||
else -> StringUtils.EMPTY
|
||||
}
|
||||
if (moreInfo.isNotBlank()) {
|
||||
return "<html>${host.name} <font color=rgb(${color.red},${color.green},${color.blue})>${moreInfo}</font></html>"
|
||||
}
|
||||
}
|
||||
return host.name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,11 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
||||
addAction(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
|
||||
|
||||
addAction(Actions.MULTIPLE, MultipleAction())
|
||||
addAction(Actions.APP_UPDATE, AppUpdateAction())
|
||||
addAction(Actions.APP_UPDATE, AppUpdateAction.getInstance())
|
||||
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())
|
||||
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
|
||||
addAction(Actions.SFTP, SFTPAction())
|
||||
addAction(SFTPCommandAction.SFTP_COMMAND, SFTPCommandAction())
|
||||
addAction(Actions.MACRO, MacroAction())
|
||||
addAction(Actions.KEY_MANAGER, KeyManagerAction())
|
||||
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.Application.httpClient
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.sun.jna.platform.win32.Advapi32
|
||||
import com.sun.jna.platform.win32.WinError
|
||||
import com.sun.jna.platform.win32.WinNT
|
||||
import com.sun.jna.platform.win32.WinReg
|
||||
import io.github.g00fy2.versioncompare.Version
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import okhttp3.Request
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.JXEditorPane
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Dimension
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.io.File
|
||||
import java.net.ProxySelector
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.JScrollPane
|
||||
@@ -18,11 +32,20 @@ import kotlin.concurrent.fixedRateTimer
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
class AppUpdateAction : AnAction(
|
||||
class AppUpdateAction private constructor() : AnAction(
|
||||
StringUtils.EMPTY,
|
||||
Icons.ideUpdate
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(AppUpdateAction::class.java)
|
||||
private const val PKG_FILE_KEY = "pkgFile"
|
||||
|
||||
fun getInstance(): AppUpdateAction {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(AppUpdateAction::class) { AppUpdateAction() }
|
||||
}
|
||||
}
|
||||
|
||||
private val updaterManager get() = UpdaterManager.getInstance()
|
||||
|
||||
init {
|
||||
@@ -63,11 +86,75 @@ class AppUpdateAction : AnAction(
|
||||
return
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
ActionManager.getInstance()
|
||||
.setEnabled(Actions.APP_UPDATE, true)
|
||||
try {
|
||||
downloadLatestPkg(latestVersion)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
withContext(Dispatchers.Swing) { isEnabled = true }
|
||||
|
||||
}
|
||||
|
||||
|
||||
private suspend fun downloadLatestPkg(latestVersion: UpdaterManager.LatestVersion) {
|
||||
if (SystemInfo.isLinux) return
|
||||
|
||||
super.putValue(PKG_FILE_KEY, null)
|
||||
val arch = if (SystemInfo.isAARCH64) "aarch64" else "x86-64"
|
||||
val osName = if (SystemInfo.isWindows) "windows" else "osx"
|
||||
val suffix = if (SystemInfo.isWindows) "exe" else "dmg"
|
||||
val filename = "termora-${latestVersion.version}-${osName}-${arch}.${suffix}"
|
||||
val asset = latestVersion.assets.find { it.name == filename } ?: return
|
||||
|
||||
val response = httpClient
|
||||
.newBuilder()
|
||||
.callTimeout(15, TimeUnit.MINUTES)
|
||||
.readTimeout(15, TimeUnit.MINUTES)
|
||||
.proxySelector(ProxySelector.getDefault())
|
||||
.build()
|
||||
.newCall(Request.Builder().url(asset.downloadUrl).build())
|
||||
.execute()
|
||||
if (!response.isSuccessful) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.warn("Failed to download latest version ${latestVersion.version}, response code ${response.code}")
|
||||
}
|
||||
IOUtils.closeQuietly(response)
|
||||
return
|
||||
}
|
||||
|
||||
val body = response.body
|
||||
val input = body?.byteStream()
|
||||
val file = FileUtils.getFile(Application.getTemporaryDir(), "${UUID.randomUUID()}-${filename}")
|
||||
val output = file.outputStream()
|
||||
|
||||
val downloaded = runCatching { IOUtils.copy(input, output) }.isSuccess
|
||||
IOUtils.closeQuietly(input, output, body, response)
|
||||
|
||||
if (!downloaded) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error("Failed to download latest version to $filename")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("Successfully downloaded latest version to $file")
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Swing) { setLatestPkgFile(file) }
|
||||
|
||||
}
|
||||
|
||||
private fun setLatestPkgFile(file: File) {
|
||||
putValue(PKG_FILE_KEY, file)
|
||||
}
|
||||
|
||||
private fun getLatestPkgFile(): File? {
|
||||
return getValue(PKG_FILE_KEY) as? File
|
||||
}
|
||||
|
||||
private fun showUpdateDialog() {
|
||||
@@ -106,12 +193,59 @@ class AppUpdateAction : AnAction(
|
||||
if (option == JOptionPane.CANCEL_OPTION) {
|
||||
return
|
||||
} else if (option == JOptionPane.NO_OPTION) {
|
||||
ActionManager.getInstance().setEnabled(Actions.APP_UPDATE, false)
|
||||
updaterManager.ignore(updaterManager.lastVersion.version)
|
||||
isEnabled = false
|
||||
updaterManager.ignore(lastVersion.version)
|
||||
} else if (option == JOptionPane.YES_OPTION) {
|
||||
ActionManager.getInstance()
|
||||
.setEnabled(Actions.APP_UPDATE, false)
|
||||
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${lastVersion.version}"))
|
||||
updateSelf(lastVersion)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelf(latestVersion: UpdaterManager.LatestVersion) {
|
||||
val file = getLatestPkgFile()
|
||||
if (SystemInfo.isLinux || file == null) {
|
||||
isEnabled = false
|
||||
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${latestVersion.version}"))
|
||||
return
|
||||
}
|
||||
|
||||
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||
val commands = if (SystemInfo.isMacOS) listOf("open", "-n", file.absolutePath)
|
||||
// 如果安装过,那么直接静默安装和自动启动
|
||||
else if (isAppInstalled()) listOf(
|
||||
file.absolutePath,
|
||||
"/SILENT",
|
||||
"/AUTOSTART",
|
||||
"/NORESTART",
|
||||
"/FORCECLOSEAPPLICATIONS"
|
||||
)
|
||||
// 没有安装过 则打开安装向导
|
||||
else listOf(file.absolutePath)
|
||||
|
||||
println(commands)
|
||||
TermoraRestarter.getInstance().scheduleRestart(owner, commands)
|
||||
|
||||
}
|
||||
|
||||
private fun isAppInstalled(): Boolean {
|
||||
val keyPath = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${Application.getName()}_is1"
|
||||
val phkKey = WinReg.HKEYByReference()
|
||||
|
||||
// 尝试打开注册表键
|
||||
val result = Advapi32.INSTANCE.RegOpenKeyEx(
|
||||
WinReg.HKEY_LOCAL_MACHINE,
|
||||
keyPath,
|
||||
0,
|
||||
WinNT.KEY_READ,
|
||||
phkKey
|
||||
)
|
||||
|
||||
if (result == WinError.ERROR_SUCCESS) {
|
||||
// 键存在,关闭句柄
|
||||
Advapi32.INSTANCE.RegCloseKey(phkKey.getValue())
|
||||
return true
|
||||
} else {
|
||||
// 键不存在或无权限
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,6 @@ object DataProviders {
|
||||
|
||||
|
||||
object Welcome {
|
||||
val HostTree = DataKey(app.termora.HostTree::class)
|
||||
val HostTree = DataKey(app.termora.NewHostTree::class)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.Host
|
||||
import app.termora.HostDialog
|
||||
import app.termora.HostManager
|
||||
import app.termora.Protocol
|
||||
import app.termora.*
|
||||
import javax.swing.tree.TreePath
|
||||
|
||||
class NewHostAction : AnAction() {
|
||||
@@ -20,27 +17,27 @@ class NewHostAction : AnAction() {
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val tree = evt.getData(DataProviders.Welcome.HostTree) ?: return
|
||||
val model = tree.model
|
||||
var lastHost = tree.lastSelectedPathComponent ?: model.root
|
||||
if (lastHost !is Host) {
|
||||
return
|
||||
}
|
||||
|
||||
if (lastHost.protocol != Protocol.Folder) {
|
||||
val p = model.getParent(lastHost) ?: return
|
||||
lastHost = p
|
||||
var lastNode = (tree.lastSelectedPathComponent ?: tree.model.root) as? HostTreeNode ?: return
|
||||
if (lastNode.host.protocol != Protocol.Folder) {
|
||||
lastNode = lastNode.parent ?: return
|
||||
}
|
||||
|
||||
val lastHost = lastNode.host
|
||||
val dialog = HostDialog(evt.window)
|
||||
dialog.setLocationRelativeTo(evt.window)
|
||||
dialog.isVisible = true
|
||||
val host = (dialog.host ?: return).copy(parentId = lastHost.id)
|
||||
|
||||
hostManager.addHost(host)
|
||||
val newNode = HostTreeNode(host)
|
||||
|
||||
tree.expandNode(lastHost)
|
||||
val model = if (tree.model is FilterableHostTreeModel) (tree.model as FilterableHostTreeModel).getModel()
|
||||
else tree.model
|
||||
|
||||
tree.selectionPath = TreePath(model.getPathToRoot(host))
|
||||
if (model is NewHostTreeModel) {
|
||||
model.insertNodeInto(newNode, lastNode, lastNode.childCount)
|
||||
tree.selectionPath = TreePath(model.getPathToRoot(newNode))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
30
src/main/kotlin/app/termora/actions/SFTPCommandAction.kt
Normal file
@@ -0,0 +1,30 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.HostTerminalTab
|
||||
import app.termora.I18n
|
||||
import app.termora.OpenHostActionEvent
|
||||
import app.termora.Protocol
|
||||
|
||||
class SFTPCommandAction : AnAction() {
|
||||
companion object {
|
||||
/**
|
||||
* 打开 SFTP command
|
||||
*/
|
||||
const val SFTP_COMMAND = "SFTPCommandAction"
|
||||
}
|
||||
|
||||
init {
|
||||
putValue(ACTION_COMMAND_KEY, SFTP_COMMAND)
|
||||
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.open-sftp-command"))
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val actionManager = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST) ?: return
|
||||
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||
val tab = terminalTabbedManager.getSelectedTerminalTab() as? HostTerminalTab ?: return
|
||||
val host = tab.host
|
||||
if (!(host.protocol == Protocol.SSH || host.protocol == Protocol.SFTPPty)) return
|
||||
actionManager.actionPerformed(OpenHostActionEvent(evt.source, host.copy(protocol = Protocol.SFTPPty), evt))
|
||||
evt.consume()
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,6 @@ interface FindEverywhereResult : ActionListener {
|
||||
|
||||
fun getIcon(isSelected: Boolean): Icon = Icons.empty
|
||||
|
||||
fun getText(isSelected: Boolean) = toString()
|
||||
|
||||
}
|
||||
@@ -94,16 +94,16 @@ class FindEverywhereXList(private val model: DefaultListModel<FindEverywhereResu
|
||||
label.font = font.deriveFont(font.size - 2f)
|
||||
val box = Box.createHorizontalBox()
|
||||
box.add(label)
|
||||
/*box.add(object : JComponent() {
|
||||
override fun paintComponent(g: Graphics) {
|
||||
g.color = DynamicColor.BorderColor
|
||||
g.drawLine(10, height / 2, width, height / 2)
|
||||
}
|
||||
})*/
|
||||
return box
|
||||
}
|
||||
|
||||
val c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus)
|
||||
val c = super.getListCellRendererComponent(
|
||||
list,
|
||||
if (value is FindEverywhereResult) value.getText(isSelected) else value,
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
if (isSelected) {
|
||||
background = UIManager.getColor("List.selectionBackground")
|
||||
foreground = UIManager.getColor("List.selectionForeground")
|
||||
|
||||
@@ -76,6 +76,12 @@ class KeymapImpl(private val menuShortcutKeyMaskEx: Int) : Keymap("Keymap", null
|
||||
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_R, menuShortcutKeyMaskEx or InputEvent.SHIFT_DOWN_MASK))
|
||||
)
|
||||
|
||||
// Command + Shift + P
|
||||
addShortcut(
|
||||
SFTPCommandAction.SFTP_COMMAND,
|
||||
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_P, menuShortcutKeyMaskEx or InputEvent.SHIFT_DOWN_MASK))
|
||||
)
|
||||
|
||||
|
||||
// switch map
|
||||
for (i in KeyEvent.VK_1..KeyEvent.VK_9) {
|
||||
|
||||
@@ -27,10 +27,11 @@ class KeymapTableModel : DefaultTableModel() {
|
||||
TerminalZoomOutAction.ZOOM_OUT,
|
||||
TerminalZoomResetAction.ZOOM_RESET,
|
||||
OpenLocalTerminalAction.LOCAL_TERMINAL,
|
||||
TerminalClearScreenAction.CLEAR_SCREEN,
|
||||
FindEverywhereAction.FIND_EVERYWHERE,
|
||||
NewWindowAction.NEW_WINDOW,
|
||||
TabReconnectAction.RECONNECT_TAB,
|
||||
TerminalClearScreenAction.CLEAR_SCREEN,
|
||||
SFTPCommandAction.SFTP_COMMAND,
|
||||
SwitchTabAction.SWITCH_TAB,
|
||||
)) {
|
||||
val action = actionManager.getAction(id) ?: continue
|
||||
|
||||
@@ -211,9 +211,9 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
||||
}
|
||||
|
||||
val owner = SwingUtilities.getWindowAncestor(this) ?: return
|
||||
val hostTreeDialog = HostTreeDialog(owner) {
|
||||
it.protocol == Protocol.SSH
|
||||
}
|
||||
val hostTreeDialog = NewHostTreeDialog(owner)
|
||||
hostTreeDialog.setFilter { it.host.protocol == Protocol.SSH }
|
||||
hostTreeDialog.setTreeName("KeyManagerPanel.SSHCopyIdTree")
|
||||
hostTreeDialog.isVisible = true
|
||||
val hosts = hostTreeDialog.hosts
|
||||
if (hosts.isEmpty()) {
|
||||
|
||||
@@ -42,6 +42,7 @@ class SSHCopyIdDialog(
|
||||
}
|
||||
private val terminalPanel by lazy {
|
||||
terminalPanelFactory.createTerminalPanel(terminal, PtyConnectorDelegate())
|
||||
.apply { enableFloatingToolbar = false }
|
||||
}
|
||||
private val coroutineScope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
|
||||
@@ -152,7 +153,7 @@ class SSHCopyIdDialog(
|
||||
val baos = ByteArrayOutputStream()
|
||||
channel.out = baos
|
||||
if (channel.open().verify(timeout).await(timeout)) {
|
||||
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), timeout);
|
||||
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), timeout)
|
||||
}
|
||||
if (channel.exitStatus != 0) {
|
||||
throw IllegalStateException("Server response: ${channel.exitStatus}")
|
||||
|
||||
@@ -3,8 +3,11 @@ package app.termora.terminal.panel
|
||||
import app.termora.*
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.panel.vw.NvidiaSMIVisualWindow
|
||||
import app.termora.terminal.panel.vw.SystemInformationVisualWindow
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
import com.formdev.flatlaf.ui.FlatRoundBorder
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
@@ -70,6 +73,11 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
initActions()
|
||||
}
|
||||
|
||||
override fun updateUI() {
|
||||
super.updateUI()
|
||||
border = FlatRoundBorder()
|
||||
}
|
||||
|
||||
fun triggerShow() {
|
||||
if (!floatingToolbarEnable || closed) {
|
||||
return
|
||||
@@ -98,6 +106,12 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
// Pin
|
||||
add(initPinActionButton())
|
||||
|
||||
// 服务器信息
|
||||
add(initServerInfoActionButton())
|
||||
|
||||
// Nvidia 显卡信息
|
||||
add(initNvidiaSMIActionButton())
|
||||
|
||||
// 重连
|
||||
add(initReconnectActionButton())
|
||||
|
||||
@@ -105,6 +119,62 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
add(initCloseActionButton())
|
||||
}
|
||||
|
||||
private fun initServerInfoActionButton(): JButton {
|
||||
val btn = JButton(Icons.infoOutline)
|
||||
btn.toolTipText = I18n.getString("termora.visual-window.system-information")
|
||||
btn.addActionListener(object : AnAction() {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
||||
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
||||
|
||||
if (tab !is SSHTerminalTab) {
|
||||
terminalPanel.toast(I18n.getString("termora.floating-toolbar.not-supported"))
|
||||
return
|
||||
}
|
||||
|
||||
for (window in terminalPanel.getVisualWindows()) {
|
||||
if (window is SystemInformationVisualWindow) {
|
||||
terminalPanel.moveToFront(window)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val visualWindowPanel = SystemInformationVisualWindow(tab, terminalPanel)
|
||||
terminalPanel.addVisualWindow(visualWindowPanel)
|
||||
|
||||
}
|
||||
})
|
||||
return btn
|
||||
}
|
||||
|
||||
private fun initNvidiaSMIActionButton(): JButton {
|
||||
val btn = JButton(Icons.nvidia)
|
||||
btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi")
|
||||
btn.addActionListener(object : AnAction() {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
||||
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
||||
|
||||
if (tab !is SSHTerminalTab) {
|
||||
terminalPanel.toast(I18n.getString("termora.floating-toolbar.not-supported"))
|
||||
return
|
||||
}
|
||||
|
||||
for (window in terminalPanel.getVisualWindows()) {
|
||||
if (window is NvidiaSMIVisualWindow) {
|
||||
terminalPanel.moveToFront(window)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val visualWindowPanel = NvidiaSMIVisualWindow(tab, terminalPanel)
|
||||
terminalPanel.addVisualWindow(visualWindowPanel)
|
||||
|
||||
}
|
||||
})
|
||||
return btn
|
||||
}
|
||||
|
||||
private fun initPinActionButton(): JButton {
|
||||
val btn = JButton(Icons.pin)
|
||||
btn.isSelected = pinAction.isSelected
|
||||
|
||||
119
src/main/kotlin/app/termora/terminal/panel/TerminalBlink.kt
Normal file
@@ -0,0 +1,119 @@
|
||||
package app.termora.terminal.panel
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.Database
|
||||
import app.termora.Disposable
|
||||
import app.termora.terminal.*
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class TerminalBlink(terminal: Terminal) : Disposable {
|
||||
|
||||
|
||||
private var cursorBlinkJob: Job? = null
|
||||
private val terminalSettings get() = Database.getDatabase().terminal
|
||||
private val isDisposed = AtomicBoolean(false)
|
||||
private val globalBlink get() = GlobalBlink.getInstance()
|
||||
private val coroutineScope get() = globalBlink.coroutineScope
|
||||
|
||||
/**
|
||||
* 返回 true 表示可以显示某些内容 [TextStyle.blink]
|
||||
*/
|
||||
val blink get() = globalBlink.blink
|
||||
|
||||
/**
|
||||
* 这个与 [blink] 不同的是它是控制光标的
|
||||
*/
|
||||
@Volatile
|
||||
var cursorBlink = true
|
||||
private set
|
||||
|
||||
init {
|
||||
|
||||
reset()
|
||||
|
||||
// 如果有写入,那么显示光标 N 秒
|
||||
terminal.getTerminalModel().addDataListener(object : DataListener {
|
||||
override fun onChanged(key: DataKey<*>, data: Any) {
|
||||
// 写入后,重置光标
|
||||
if (key == VisualTerminal.Written) {
|
||||
reset()
|
||||
} else if (key == TerminalPanel.Focused) {
|
||||
// 获取焦点的一瞬间则立即重置
|
||||
if (data == true) {
|
||||
reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private fun reset() {
|
||||
if (isDisposed.get()) {
|
||||
return
|
||||
}
|
||||
|
||||
cursorBlink = true
|
||||
cursorBlinkJob?.cancel()
|
||||
cursorBlinkJob = coroutineScope.launch {
|
||||
while (coroutineScope.isActive) {
|
||||
|
||||
delay(500.milliseconds)
|
||||
|
||||
if (isDisposed.get()) {
|
||||
break
|
||||
}
|
||||
|
||||
// 如果开启了光标闪烁才闪速
|
||||
cursorBlink = if (terminalSettings.cursorBlink) {
|
||||
!cursorBlink
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
if (isDisposed.compareAndSet(false, true)) {
|
||||
cursorBlinkJob?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class GlobalBlink : Disposable {
|
||||
|
||||
companion object {
|
||||
fun getInstance(): GlobalBlink {
|
||||
return ApplicationScope.forApplicationScope()
|
||||
.getOrCreate(GlobalBlink::class) { GlobalBlink() }
|
||||
}
|
||||
}
|
||||
|
||||
val coroutineScope by lazy { CoroutineScope(Dispatchers.IO) }
|
||||
|
||||
/**
|
||||
* 返回 true 表示可以显示某些内容 [TextStyle.blink]
|
||||
*/
|
||||
@Volatile
|
||||
var blink = true
|
||||
private set
|
||||
|
||||
|
||||
init {
|
||||
coroutineScope.launch {
|
||||
while (coroutineScope.isActive) {
|
||||
delay(500)
|
||||
blink = !blink
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import kotlin.time.Duration
|
||||
class TerminalDisplay(
|
||||
private val terminalPanel: TerminalPanel,
|
||||
private val terminal: Terminal,
|
||||
private val terminalBlink: TerminalBlink
|
||||
) : JComponent() {
|
||||
|
||||
companion object {
|
||||
@@ -132,31 +133,30 @@ class TerminalDisplay(
|
||||
g.fillRect(0, 0, width, height)
|
||||
}
|
||||
|
||||
private fun drawCursor(g: Graphics, xOffset: Int, width: Int) {
|
||||
private fun drawCursor(g: Graphics, y: Int, xOffset: Int, width: Int) {
|
||||
val lineHeight = getLineHeight()
|
||||
val position = terminal.getCursorModel().getPosition()
|
||||
val row = position.y
|
||||
val style = if (inputMethodData.isNoTyping)
|
||||
terminal.getTerminalModel().getData(DataKey.CursorStyle) else CursorStyle.Bar
|
||||
val hasFocus = terminal.getTerminalModel().getData(TerminalPanel.Focused, false)
|
||||
|
||||
// background
|
||||
g.color = Color(colorPalette.getColor(TerminalColor.Cursor.BACKGROUND))
|
||||
|
||||
if (style == CursorStyle.Block) {
|
||||
if (terminalPanel.hasFocus()) {
|
||||
g.fillRect(xOffset, (row - 1) * lineHeight, width, lineHeight)
|
||||
if (hasFocus) {
|
||||
g.fillRect(xOffset, (y - 1) * lineHeight, width, lineHeight)
|
||||
} else {
|
||||
g.drawRect(xOffset, (row - 1) * lineHeight, width, lineHeight)
|
||||
g.drawRect(xOffset, (y - 1) * lineHeight, width, lineHeight)
|
||||
}
|
||||
} else if (style == CursorStyle.Underline) {
|
||||
val h = ceil(lineHeight / 10.0).toInt()
|
||||
g.fillRect(xOffset, row * lineHeight - h / 2, width, h)
|
||||
g.fillRect(xOffset, y * lineHeight - h / 2, width, h)
|
||||
} else if (style == CursorStyle.Bar) {
|
||||
if (inputMethodData.isTyping) {
|
||||
val w = ceil(width / 3.5).toInt()
|
||||
g.fillRect(xOffset, (row - 1) * lineHeight, w, lineHeight)
|
||||
g.fillRect(xOffset, (y - 1) * lineHeight, w, lineHeight)
|
||||
} else {
|
||||
g.drawLine(xOffset, row * lineHeight - lineHeight, xOffset, row * lineHeight)
|
||||
g.drawLine(xOffset, y * lineHeight - lineHeight, xOffset, y * lineHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,19 +219,23 @@ class TerminalDisplay(
|
||||
}
|
||||
|
||||
private fun drawCharacters(g: Graphics2D) {
|
||||
val reverseVideo = terminal.getTerminalModel().getData(DataKey.ReverseVideo, false)
|
||||
val rows = terminal.getTerminalModel().getRows()
|
||||
val cols = terminal.getTerminalModel().getCols()
|
||||
val buffer = terminal.getDocument().getCurrentTerminalLineBuffer()
|
||||
val terminalModel = terminal.getTerminalModel()
|
||||
val reverseVideo = terminalModel.getData(DataKey.ReverseVideo, false)
|
||||
val rows = terminalModel.getRows()
|
||||
val cols = terminalModel.getCols()
|
||||
val triple = Triple(Char.Space.toString(), TextStyle.Default, 1)
|
||||
val cursorPosition = terminal.getCursorModel().getPosition()
|
||||
val averageCharWidth = getAverageCharWidth()
|
||||
val maxVerticalScrollOffset = terminal.getScrollingModel().getMaxVerticalScrollOffset()
|
||||
val verticalScrollOffset = terminal.getScrollingModel().getVerticalScrollOffset()
|
||||
val selectionModel = terminal.getSelectionModel()
|
||||
val cursorStyle = terminal.getTerminalModel().getData(DataKey.CursorStyle)
|
||||
val showCursor = terminal.getTerminalModel().getData(DataKey.ShowCursor)
|
||||
val cursorStyle = terminalModel.getData(DataKey.CursorStyle)
|
||||
val showCursor = terminalModel.getData(DataKey.ShowCursor)
|
||||
val markupModel = terminal.getMarkupModel()
|
||||
val lineHeight = getLineHeight()
|
||||
val blink = terminalBlink.blink
|
||||
val cursorBlink = terminalBlink.cursorBlink
|
||||
val hasFocus = terminalModel.getData(TerminalPanel.Focused, false)
|
||||
|
||||
|
||||
for (i in 1..rows) {
|
||||
@@ -242,7 +246,7 @@ class TerminalDisplay(
|
||||
while (j <= cols) {
|
||||
val position = Position(row + 1, j)
|
||||
val caret = showCursor && j == cursorPosition.x + inputMethodData.offset
|
||||
&& row + 1 == cursorPosition.y + buffer.getBufferCount()
|
||||
&& i == cursorPosition.y + (maxVerticalScrollOffset - verticalScrollOffset)
|
||||
|
||||
val (text, style, length) = if (characters.hasNext()) characters.next() else triple
|
||||
var textStyle = style
|
||||
@@ -271,6 +275,13 @@ class TerminalDisplay(
|
||||
background = colorPalette.getColor(TerminalColor.Basic.SELECTION_BACKGROUND)
|
||||
}
|
||||
|
||||
// 如果启用了闪烁
|
||||
if (textStyle.blink) {
|
||||
if (!blink) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 设置字体
|
||||
g.font = getDisplayFont(text, textStyle)
|
||||
val charWidth = min(
|
||||
@@ -312,12 +323,15 @@ class TerminalDisplay(
|
||||
|
||||
// 渲染光标
|
||||
if (caret) {
|
||||
drawCursor(g, xOffset, charWidth)
|
||||
// 如果是获取焦点状态,那么颜色互换
|
||||
if (terminalPanel.hasFocus() && cursorStyle == CursorStyle.Block && inputMethodData.isNoTyping) {
|
||||
g.color = Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND))
|
||||
} else {
|
||||
g.color = Color(foreground)
|
||||
// 这几种情况光标才会渲染:输入中、闪烁中、没有焦点
|
||||
if (inputMethodData.isTyping || cursorBlink || !hasFocus) {
|
||||
drawCursor(g, i, xOffset, charWidth)
|
||||
// 如果是获取焦点状态,那么颜色互换
|
||||
if (hasFocus && cursorStyle == CursorStyle.Block && inputMethodData.isNoTyping) {
|
||||
g.color = Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND))
|
||||
} else {
|
||||
g.color = Color(foreground)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviderSupport
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.terminal.*
|
||||
import app.termora.terminal.panel.vw.VisualWindow
|
||||
import app.termora.terminal.panel.vw.VisualWindowManager
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
import java.awt.*
|
||||
@@ -32,20 +35,33 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnector) :
|
||||
JPanel(BorderLayout()), DataProvider, Disposable {
|
||||
JPanel(BorderLayout()), DataProvider, Disposable, VisualWindowManager {
|
||||
|
||||
companion object {
|
||||
val Debug = DataKey(Boolean::class)
|
||||
val Finding = DataKey(Boolean::class)
|
||||
val Focused = DataKey(Boolean::class)
|
||||
val SelectCopy = DataKey(Boolean::class)
|
||||
}
|
||||
|
||||
private val terminalBlink = TerminalBlink(terminal)
|
||||
private val terminalFindPanel = TerminalFindPanel(this, terminal)
|
||||
private val floatingToolbar = FloatingToolbarPanel()
|
||||
private val terminalDisplay = TerminalDisplay(this, terminal)
|
||||
private val terminalDisplay = TerminalDisplay(this, terminal, terminalBlink)
|
||||
private val dataProviderSupport = DataProviderSupport()
|
||||
private val layeredPane = TerminalLayeredPane()
|
||||
private var visualWindows = emptyArray<VisualWindow>()
|
||||
|
||||
val scrollBar = TerminalScrollBar(this@TerminalPanel, terminalFindPanel, terminal)
|
||||
var enableFloatingToolbar = true
|
||||
set(value) {
|
||||
field = value
|
||||
if (value) {
|
||||
layeredPane.add(floatingToolbar, JLayeredPane.POPUP_LAYER as Any)
|
||||
} else {
|
||||
layeredPane.remove(floatingToolbar)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@@ -116,11 +132,11 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
||||
scrollBar.blockIncrement = 1
|
||||
background = Color.black
|
||||
|
||||
|
||||
val layeredPane = TerminalLayeredPane()
|
||||
layeredPane.add(terminalDisplay, JLayeredPane.DEFAULT_LAYER as Any)
|
||||
layeredPane.add(terminalFindPanel, JLayeredPane.POPUP_LAYER as Any)
|
||||
layeredPane.add(floatingToolbar, JLayeredPane.POPUP_LAYER as Any)
|
||||
if (enableFloatingToolbar) {
|
||||
layeredPane.add(floatingToolbar, JLayeredPane.POPUP_LAYER as Any)
|
||||
}
|
||||
add(layeredPane, BorderLayout.CENTER)
|
||||
add(scrollBar, BorderLayout.EAST)
|
||||
|
||||
@@ -140,10 +156,12 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
||||
|
||||
this.addFocusListener(object : FocusAdapter() {
|
||||
override fun focusLost(e: FocusEvent) {
|
||||
terminal.getTerminalModel().setData(Focused, false)
|
||||
repaintImmediate()
|
||||
}
|
||||
|
||||
override fun focusGained(e: FocusEvent) {
|
||||
terminal.getTerminalModel().setData(Focused, true)
|
||||
repaintImmediate()
|
||||
}
|
||||
})
|
||||
@@ -386,6 +404,7 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
Disposer.dispose(terminalBlink)
|
||||
Disposer.dispose(floatingToolbar)
|
||||
}
|
||||
|
||||
@@ -498,6 +517,23 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
||||
height
|
||||
)
|
||||
}
|
||||
|
||||
is VisualWindow -> {
|
||||
var location = c.location
|
||||
val dimension = getDimension()
|
||||
if (location.x > dimension.width) {
|
||||
location = Point(dimension.width - c.preferredSize.width, location.y)
|
||||
}
|
||||
if (location.y > dimension.height) {
|
||||
location = Point(location.x, dimension.height - c.preferredSize.height)
|
||||
}
|
||||
c.setBounds(
|
||||
location.x,
|
||||
location.y,
|
||||
c.width,
|
||||
c.height
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -507,4 +543,41 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||
return dataProviderSupport.getData(dataKey)
|
||||
}
|
||||
|
||||
override fun moveToFront(visualWindow: VisualWindow) {
|
||||
if (visualWindow.isWindow()) {
|
||||
visualWindow.getWindow()?.requestFocus()
|
||||
return
|
||||
}
|
||||
layeredPane.moveToFront(visualWindow.getJComponent())
|
||||
}
|
||||
|
||||
override fun getVisualWindows(): Array<VisualWindow> {
|
||||
return visualWindows
|
||||
}
|
||||
|
||||
override fun addVisualWindow(visualWindow: VisualWindow) {
|
||||
visualWindows = ArrayUtils.add(visualWindows, visualWindow)
|
||||
layeredPane.add(visualWindow.getJComponent(), JLayeredPane.DRAG_LAYER as Any)
|
||||
layeredPane.moveToFront(visualWindow.getJComponent())
|
||||
}
|
||||
|
||||
override fun removeVisualWindow(visualWindow: VisualWindow) {
|
||||
rebaseVisualWindow(visualWindow)
|
||||
visualWindows = ArrayUtils.removeElement(visualWindows, visualWindow)
|
||||
}
|
||||
|
||||
override fun rebaseVisualWindow(visualWindow: VisualWindow) {
|
||||
layeredPane.remove(visualWindow.getJComponent())
|
||||
layeredPane.revalidate()
|
||||
layeredPane.repaint()
|
||||
requestFocusInWindow()
|
||||
}
|
||||
|
||||
override fun getDimension(): Dimension {
|
||||
return Dimension(
|
||||
terminalDisplay.size.width + padding.left + padding.right,
|
||||
terminalDisplay.size.height + padding.bottom + padding.top
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,8 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
|
||||
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
|
||||
terminalPanel.requestFocusInWindow()
|
||||
|
||||
if (isMouseTracking) {
|
||||
return
|
||||
}
|
||||
@@ -77,7 +79,6 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
|
||||
mousePressedPoint.y = e.y
|
||||
}
|
||||
|
||||
terminalPanel.requestFocusInWindow()
|
||||
|
||||
// 如果只有 Shift 键按下,那么应该追加选中
|
||||
if (selectionModel.hasSelection() && SwingUtilities.isLeftMouseButton(e) && e.modifiersEx == 1088) {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import app.termora.Disposable
|
||||
import kotlinx.coroutines.*
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.swing.JPanel
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
abstract class AutoRefreshPanel : JPanel(), Disposable {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(AutoRefreshPanel::class.java)
|
||||
}
|
||||
|
||||
protected val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
protected abstract suspend fun refresh(isFirst: Boolean)
|
||||
|
||||
init {
|
||||
coroutineScope.launch {
|
||||
var isFirst = true
|
||||
while (coroutineScope.isActive) {
|
||||
try {
|
||||
refresh(isFirst)
|
||||
isFirst = false
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
if (isFirst) {
|
||||
break
|
||||
}
|
||||
} finally {
|
||||
delay(1000.milliseconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import app.termora.*
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.JXBusyLabel
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.xml.sax.EntityResolver
|
||||
import org.xml.sax.InputSource
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.CardLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.GridLayout
|
||||
import java.io.StringReader
|
||||
import javax.swing.*
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.xpath.XPathFactory
|
||||
|
||||
class NvidiaSMIVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
||||
SSHVisualWindow(tab, "NVIDIA-SMI", visualWindowManager) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(NvidiaSMIVisualWindow::class.java)
|
||||
}
|
||||
|
||||
private val nvidiaSMIPanel by lazy { NvidiaSMIPanel() }
|
||||
private val busyLabel = JXBusyLabel()
|
||||
private val errorPanel = FormBuilder.create().layout(FormLayout("pref:grow", "20dlu, pref, 5dlu, pref"))
|
||||
.add(JLabel(FlatSVGIcon(Icons.warningDialog.name, 60, 60))).xy(1, 2, "center, fill")
|
||||
.add(JLabel("Not supported")).xy(1, 4, "center, fill")
|
||||
.build()
|
||||
private val loadingPanel = FormBuilder.create().layout(FormLayout("pref:grow", "20dlu, pref"))
|
||||
.add(busyLabel).xy(1, 2, "center, fill")
|
||||
.build()
|
||||
private val cardLayout = CardLayout()
|
||||
private val rootPanel = JPanel(cardLayout)
|
||||
private var isPercentage
|
||||
get() = properties.getString("VisualWindow.${id}.isPercentage", "false").toBoolean()
|
||||
set(value) = properties.putString("VisualWindow.${id}.isPercentage", value.toString())
|
||||
|
||||
private val percentageBtn by lazy { JButton(if (isPercentage) Icons.text else Icons.percentage) }
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
initVisualWindowPanel()
|
||||
}
|
||||
|
||||
override fun toolbarButtons(): List<JButton> {
|
||||
return listOf(percentageBtn)
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
title = I18n.getString("termora.visual-window.nvidia-smi")
|
||||
busyLabel.isBusy = true
|
||||
|
||||
rootPanel.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
|
||||
rootPanel.add(errorPanel, "ErrorPanel")
|
||||
rootPanel.add(loadingPanel, "LoadingPanel")
|
||||
rootPanel.add(nvidiaSMIPanel, "NvidiaSMIPanel")
|
||||
|
||||
add(rootPanel, BorderLayout.CENTER)
|
||||
|
||||
cardLayout.show(rootPanel, "LoadingPanel")
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
percentageBtn.addActionListener {
|
||||
isPercentage = !isPercentage
|
||||
percentageBtn.icon = if (isPercentage) Icons.text else Icons.percentage
|
||||
nvidiaSMIPanel.refreshPanel()
|
||||
}
|
||||
|
||||
Disposer.register(this, nvidiaSMIPanel)
|
||||
}
|
||||
|
||||
private data class GPU(
|
||||
/**
|
||||
* 名称 product_name
|
||||
*/
|
||||
val productName: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 序号 minor_number
|
||||
*/
|
||||
val minorNumber: Int = 0,
|
||||
|
||||
/**
|
||||
* 温度 temperature.gpu_temp
|
||||
*
|
||||
* 单位:C
|
||||
*/
|
||||
var temp: Double = 0.0,
|
||||
var tempText: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 使用的功率 gpu_power_readings.power_draw
|
||||
*
|
||||
* 单位:W
|
||||
*/
|
||||
var powerUsage: Double = 0.0,
|
||||
var powerUsageText: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 功率大小 gpu_power_readings.max_power_limit
|
||||
*
|
||||
* 单位:W
|
||||
*/
|
||||
var powerCap: Double = 0.0,
|
||||
var powerCapText: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 使用的显存 fb_memory_usage.used
|
||||
*
|
||||
* 单位:Mib
|
||||
*/
|
||||
var memoryUsage: Double = 0.0,
|
||||
var memoryUsageText: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 显存大小 fb_memory_usage.total
|
||||
*
|
||||
* 单位:Mib
|
||||
*/
|
||||
var memoryCap: Double = 0.0,
|
||||
var memoryCapText: String = StringUtils.EMPTY,
|
||||
|
||||
|
||||
/**
|
||||
* GPU 利用率 utilization.gpu_util
|
||||
*
|
||||
* 单位:%
|
||||
*/
|
||||
var gpu: Double = 0.0
|
||||
)
|
||||
|
||||
private class NvidiaSMI(
|
||||
val driverVersion: String = StringUtils.EMPTY,
|
||||
val cudaVersion: String = StringUtils.EMPTY,
|
||||
val gpus: MutableList<GPU> = mutableListOf<GPU>(),
|
||||
)
|
||||
|
||||
private class GPUPanel(val minorNumber: Int, title: String) : JPanel(BorderLayout()) {
|
||||
val gpuProgressBar = SmartProgressBar()
|
||||
val tempProgressBar = SmartProgressBar()
|
||||
val memProgressBar = SmartProgressBar()
|
||||
val powerProgressBar = SmartProgressBar()
|
||||
|
||||
init {
|
||||
val formMargin = "4dlu"
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val p = FormBuilder.create().debug(false)
|
||||
.layout(
|
||||
FormLayout(
|
||||
"left:pref, $formMargin, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
)
|
||||
.border(
|
||||
BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createTitledBorder(title),
|
||||
BorderFactory.createEmptyBorder(4, 4, 4, 4),
|
||||
)
|
||||
)
|
||||
.add("GPU: ").xy(1, rows)
|
||||
.add(gpuProgressBar).xy(3, rows).apply { rows += step }
|
||||
.add("Temp: ").xy(1, rows)
|
||||
.add(tempProgressBar).xy(3, rows).apply { rows += step }
|
||||
.add("Mem: ").xy(1, rows)
|
||||
.add(memProgressBar).xy(3, rows).apply { rows += step }
|
||||
.add("Power: ").xy(1, rows)
|
||||
.add(powerProgressBar).xy(3, rows).apply { rows += step }
|
||||
.build()
|
||||
add(p, BorderLayout.CENTER)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class NvidiaSMIPanel : AutoRefreshPanel() {
|
||||
|
||||
private val xPath by lazy { XPathFactory.newInstance().newXPath() }
|
||||
private val db by lazy {
|
||||
val factory = DocumentBuilderFactory.newInstance()
|
||||
factory.isValidating = false
|
||||
factory.isXIncludeAware = false
|
||||
factory.isNamespaceAware = false
|
||||
val db = factory.newDocumentBuilder()
|
||||
db.setEntityResolver(object : EntityResolver {
|
||||
override fun resolveEntity(
|
||||
publicId: String?,
|
||||
systemId: String?
|
||||
): InputSource? {
|
||||
return if (StringUtils.contains(systemId, ".dtd")) {
|
||||
InputSource(StringReader(StringUtils.EMPTY))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
db
|
||||
}
|
||||
|
||||
private var nvidiaSMI = NvidiaSMI()
|
||||
private val gpuRootPanel = JPanel()
|
||||
private val driverVersionLabel = JLabel()
|
||||
private val cudaVersionLabel = JLabel()
|
||||
private val gpusLabel = JLabel()
|
||||
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
|
||||
private fun initViews() {
|
||||
|
||||
layout = BorderLayout()
|
||||
|
||||
add(
|
||||
FormBuilder.create().debug(false)
|
||||
.layout(
|
||||
FormLayout(
|
||||
"default:grow, pref, default:grow, 4dlu, pref, default:grow, 4dlu, pref, default:grow, default:grow",
|
||||
"pref, 4dlu"
|
||||
)
|
||||
)
|
||||
.add(Box.createHorizontalGlue()).xy(1, 1)
|
||||
.add("Driver: ").xy(2, 1)
|
||||
.add(driverVersionLabel).xy(3, 1)
|
||||
.add("CUDA: ").xy(5, 1)
|
||||
.add(cudaVersionLabel).xy(6, 1)
|
||||
.add("GPUS: ").xy(8, 1)
|
||||
.add(gpusLabel).xy(9, 1)
|
||||
.add(Box.createHorizontalGlue()).xy(10, 1)
|
||||
.build(), BorderLayout.NORTH
|
||||
)
|
||||
|
||||
add(JScrollPane(gpuRootPanel).apply {
|
||||
verticalScrollBar.maximumSize = Dimension(0, 0)
|
||||
verticalScrollBar.preferredSize = Dimension(0, 0)
|
||||
verticalScrollBar.minimumSize = Dimension(0, 0)
|
||||
border = BorderFactory.createEmptyBorder()
|
||||
}, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
override suspend fun refresh(isFirst: Boolean) {
|
||||
val session = tab.getData(SSHTerminalTab.SSHSession) ?: return
|
||||
|
||||
val doc = try {
|
||||
val (code, text) = SshClients.execChannel(session, "nvidia-smi -x -q")
|
||||
if (StringUtils.isNotBlank(text)) {
|
||||
db.parse(InputSource(StringReader(text)))
|
||||
} else {
|
||||
throw IllegalStateException("exit code: $code")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
|
||||
if (doc == null) {
|
||||
if (isFirst) {
|
||||
withContext(Dispatchers.Swing) {
|
||||
cardLayout.show(rootPanel, "ErrorPanel")
|
||||
}
|
||||
// 直接取消轮训
|
||||
SwingUtilities.invokeLater { coroutineScope.cancel() }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
nvidiaSMI = NvidiaSMI(
|
||||
driverVersion = xPath.compile("/nvidia_smi_log/driver_version/text()").evaluate(doc),
|
||||
cudaVersion = xPath.compile("/nvidia_smi_log/cuda_version/text()").evaluate(doc),
|
||||
)
|
||||
val attachedGPUs = xPath.compile("/nvidia_smi_log/attached_gpus/text()").evaluate(doc).toIntOrNull() ?: 0
|
||||
|
||||
for (i in 1..attachedGPUs) {
|
||||
val gpu = GPU(
|
||||
productName = xPath.compile("/nvidia_smi_log/gpu[${i}]/product_name/text()").evaluate(doc),
|
||||
minorNumber = xPath.compile("/nvidia_smi_log/gpu[${i}]/minor_number/text()").evaluate(doc)
|
||||
.toIntOrNull() ?: 0,
|
||||
tempText = xPath.compile("/nvidia_smi_log/gpu[${i}]/temperature/gpu_temp/text()").evaluate(doc),
|
||||
powerUsageText = xPath.compile("/nvidia_smi_log/gpu[${i}]/gpu_power_readings/power_draw/text()")
|
||||
.evaluate(doc),
|
||||
powerCapText = xPath.compile("/nvidia_smi_log/gpu[${i}]/gpu_power_readings/max_power_limit/text()")
|
||||
.evaluate(doc),
|
||||
memoryUsageText = xPath.compile("/nvidia_smi_log/gpu[${i}]/fb_memory_usage/used/text()")
|
||||
.evaluate(doc),
|
||||
memoryCapText = xPath.compile("/nvidia_smi_log/gpu[${i}]/fb_memory_usage/total/text()")
|
||||
.evaluate(doc),
|
||||
gpu = xPath.compile("/nvidia_smi_log/gpu[${i}]/utilization/gpu_util/text()")
|
||||
.evaluate(doc).split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
)
|
||||
|
||||
nvidiaSMI.gpus.add(
|
||||
gpu.copy(
|
||||
temp = gpu.tempText.split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
powerUsage = gpu.powerUsageText.split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
powerCap = gpu.powerCapText.split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
memoryUsage = gpu.memoryUsageText.split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
memoryCap = gpu.memoryCapText.split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
if (isFirst) {
|
||||
initPanel()
|
||||
cardLayout.show(rootPanel, "NvidiaSMIPanel")
|
||||
}
|
||||
|
||||
refreshPanel()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun initPanel() {
|
||||
gpuRootPanel.layout = GridLayout(
|
||||
if (nvidiaSMI.gpus.size % 2 == 0) nvidiaSMI.gpus.size / 2 else nvidiaSMI.gpus.size / 2 + 1,
|
||||
2, 4, 4
|
||||
)
|
||||
for (e in nvidiaSMI.gpus) {
|
||||
gpuRootPanel.add(GPUPanel(e.minorNumber, "${e.minorNumber} ${e.productName}"))
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshPanel() {
|
||||
cudaVersionLabel.text = nvidiaSMI.cudaVersion
|
||||
driverVersionLabel.text = nvidiaSMI.driverVersion
|
||||
gpusLabel.text = nvidiaSMI.gpus.size.toString()
|
||||
|
||||
for (c in gpuRootPanel.components) {
|
||||
if (c is GPUPanel) {
|
||||
for (g in nvidiaSMI.gpus) {
|
||||
if (c.minorNumber == g.minorNumber) {
|
||||
refreshGPUPanel(g, c)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshGPUPanel(gpu: GPU, g: GPUPanel) {
|
||||
g.gpuProgressBar.value = gpu.gpu.toInt()
|
||||
|
||||
g.tempProgressBar.value = gpu.temp.toInt()
|
||||
g.tempProgressBar.string = if (isPercentage) "${g.tempProgressBar.value}%" else gpu.tempText
|
||||
|
||||
g.powerProgressBar.value = (gpu.powerUsage / gpu.powerCap * 100.0).toInt()
|
||||
g.powerProgressBar.string = if (isPercentage) "${g.powerProgressBar.value}%"
|
||||
else "${gpu.powerUsageText}/${gpu.powerCapText}"
|
||||
|
||||
g.memProgressBar.value = (gpu.memoryUsage / gpu.memoryCap * 100.0).toInt()
|
||||
g.memProgressBar.string = if (isPercentage) "${g.memProgressBar.value}%"
|
||||
else "${gpu.memoryUsageText}/${gpu.memoryCapText}"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun dispose() {
|
||||
busyLabel.isBusy = false
|
||||
super.dispose()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import app.termora.Disposer
|
||||
import app.termora.SSHTerminalTab
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProviders
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.util.*
|
||||
|
||||
abstract class SSHVisualWindow(
|
||||
protected val tab: SSHTerminalTab,
|
||||
id: String,
|
||||
visualWindowManager: VisualWindowManager
|
||||
) : VisualWindowPanel(id, visualWindowManager) {
|
||||
|
||||
init {
|
||||
Disposer.register(tab, this)
|
||||
}
|
||||
|
||||
override fun toggleWindow() {
|
||||
val evt = AnActionEvent(tab.getJComponent(), StringUtils.EMPTY, EventObject(this))
|
||||
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||
|
||||
super.toggleWindow()
|
||||
|
||||
if (!isWindow()) {
|
||||
terminalTabbedManager.setSelectedTerminalTab(tab)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun getWindowTitle(): String {
|
||||
return tab.getTitle() + " - " + title
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import com.formdev.flatlaf.extras.components.FlatProgressBar
|
||||
import java.awt.Dimension
|
||||
import javax.swing.UIManager
|
||||
|
||||
class SmartProgressBar : FlatProgressBar() {
|
||||
init {
|
||||
preferredSize = Dimension(-1, UIManager.getInt("Table.rowHeight") - 6)
|
||||
isStringPainted = true
|
||||
maximum = 100
|
||||
minimum = 0
|
||||
}
|
||||
|
||||
override fun setValue(n: Int) {
|
||||
super.setValue(n)
|
||||
|
||||
foreground = if (value < 60) {
|
||||
UIManager.getColor("Component.accentColor")
|
||||
} else if (value < 85) {
|
||||
UIManager.getColor("Component.warning.focusedBorderColor")
|
||||
} else {
|
||||
UIManager.getColor("Component.error.focusedBorderColor")
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateUI() {
|
||||
super.updateUI()
|
||||
value = value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import app.termora.*
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.sshd.client.session.ClientSession
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.BorderLayout
|
||||
import javax.swing.*
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
import javax.swing.table.DefaultTableModel
|
||||
|
||||
|
||||
class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
||||
SSHVisualWindow(tab, "SystemInformation", visualWindowManager) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(SystemInformationVisualWindow::class.java)
|
||||
}
|
||||
|
||||
private val systemInformationPanel by lazy { SystemInformationPanel() }
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
initVisualWindowPanel()
|
||||
}
|
||||
|
||||
|
||||
private fun initViews() {
|
||||
title = I18n.getString("termora.visual-window.system-information")
|
||||
add(systemInformationPanel, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
Disposer.register(this, systemInformationPanel)
|
||||
}
|
||||
|
||||
private inner class SystemInformationPanel : AutoRefreshPanel() {
|
||||
|
||||
|
||||
private val cpuProgressBar = SmartProgressBar()
|
||||
private val memoryProgressBar = SmartProgressBar()
|
||||
private val swapProgressBar = SmartProgressBar()
|
||||
private val mem = Mem()
|
||||
private val cpu = CPU()
|
||||
private val swap = Swap()
|
||||
private val tableModel = object : DefaultTableModel() {
|
||||
override fun isCellEditable(row: Int, column: Int): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
|
||||
private fun initViews() {
|
||||
layout = BorderLayout()
|
||||
add(createPanel(), BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
private fun createPanel(): JComponent {
|
||||
val formMargin = "4dlu"
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val p = JPanel(BorderLayout())
|
||||
val n = FormBuilder.create().debug(false).layout(
|
||||
FormLayout(
|
||||
"left:pref, $formMargin, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin"
|
||||
)
|
||||
)
|
||||
.add("CPU: ").xy(1, rows)
|
||||
.add(cpuProgressBar).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.visual-window.system-information.mem")}: ").xy(1, rows)
|
||||
.add(memoryProgressBar).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.visual-window.system-information.swap")}: ").xy(1, rows)
|
||||
.add(swapProgressBar).xy(3, rows).apply { rows += step }
|
||||
.build()
|
||||
|
||||
val table = JTable(tableModel)
|
||||
table.tableHeader.isEnabled = false
|
||||
table.showVerticalLines = true
|
||||
table.showHorizontalLines = true
|
||||
table.fillsViewportHeight = true
|
||||
|
||||
tableModel.addColumn(I18n.getString("termora.visual-window.system-information.filesystem"))
|
||||
tableModel.addColumn(I18n.getString("termora.visual-window.system-information.used-total"))
|
||||
|
||||
val centerRenderer = DefaultTableCellRenderer()
|
||||
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
|
||||
table.columnModel.getColumn(1).cellRenderer = centerRenderer
|
||||
|
||||
|
||||
p.add(n, BorderLayout.NORTH)
|
||||
p.add(JScrollPane(table).apply {
|
||||
border = BorderFactory.createMatteBorder(1, 1, 1, 1, DynamicColor.BorderColor)
|
||||
}, BorderLayout.CENTER)
|
||||
p.border = BorderFactory.createEmptyBorder(6, 6, 6, 6)
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
}
|
||||
|
||||
override suspend fun refresh(isFirst: Boolean) {
|
||||
val session = tab.getData(SSHTerminalTab.SSHSession) ?: return
|
||||
|
||||
try {
|
||||
// 刷新 CPU 和 内存
|
||||
refreshCPUAndMem(session)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error("refreshCPUAndMem", e)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 刷新磁盘
|
||||
refreshDisk(session)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error("refreshDisk", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshCPUAndMem(session: ClientSession) {
|
||||
|
||||
// top
|
||||
var pair = SshClients.execChannel(session, "top -bn1")
|
||||
if (pair.first != 0) {
|
||||
return
|
||||
}
|
||||
|
||||
val regex = """\d+\.?\d*""".toRegex()
|
||||
val lines = pair.second.split(StringUtils.LF)
|
||||
for (line in lines) {
|
||||
val isCPU = line.startsWith("%Cpu(s):", true)
|
||||
val isMibMem = line.startsWith("MiB Mem :", true)
|
||||
val isKibMem = line.startsWith("KiB Mem :", true)
|
||||
val isMibSwap = line.startsWith("MiB Swap:", true)
|
||||
val isKibSwap = line.startsWith("KiB Swap:", true)
|
||||
val unit = if (isKibSwap || isKibMem) 'K' else 'M'
|
||||
|
||||
if (isCPU) {
|
||||
val parts = StringUtils.removeStartIgnoreCase(line, "%Cpu(s):").split(",").map { it.trim() }
|
||||
for (part in parts) {
|
||||
val value = regex.find(part)?.value?.toDoubleOrNull() ?: 0.0
|
||||
if (part.contains("us")) {
|
||||
cpu.us = value
|
||||
} else if (part.contains("sy")) {
|
||||
cpu.sy = value
|
||||
} else if (part.contains("ni")) {
|
||||
cpu.ni = value
|
||||
} else if (part.contains("id")) {
|
||||
cpu.id = value
|
||||
} else if (part.contains("wa")) {
|
||||
cpu.wa = value
|
||||
} else if (part.contains("hi")) {
|
||||
cpu.hi = value
|
||||
} else if (part.contains("si")) {
|
||||
cpu.si = value
|
||||
} else if (part.contains("st")) {
|
||||
cpu.st = value
|
||||
}
|
||||
}
|
||||
} else if (isMibMem || isKibMem) {
|
||||
val parts = StringUtils.removeStartIgnoreCase(line, "${unit}iB Mem :")
|
||||
.split(",")
|
||||
.map { it.trim() }
|
||||
for (part in parts) {
|
||||
val value = regex.find(part)?.value?.toDoubleOrNull() ?: 0.0
|
||||
if (part.contains("total")) {
|
||||
mem.total = value
|
||||
} else if (part.contains("free")) {
|
||||
mem.free = value
|
||||
} else if (part.contains("used")) {
|
||||
mem.used = value
|
||||
} else if (part.contains("buff/cache")) {
|
||||
mem.buffCache = value
|
||||
}
|
||||
}
|
||||
|
||||
if (isKibMem) {
|
||||
mem.total = mem.total / 1024.0
|
||||
mem.free = mem.free / 1024.0
|
||||
mem.used = mem.used / 1024.0
|
||||
mem.buffCache = mem.buffCache / 1024.0
|
||||
}
|
||||
} else if (isMibSwap || isKibSwap) {
|
||||
val parts = StringUtils.removeStartIgnoreCase(line, "${unit}iB Swap:")
|
||||
.split(",")
|
||||
.map { it.trim() }
|
||||
|
||||
for (part in parts) {
|
||||
val value = regex.find(part)?.value?.toDoubleOrNull() ?: 0.0
|
||||
if (part.contains("total")) {
|
||||
swap.total = value
|
||||
} else if (part.contains("free")) {
|
||||
swap.free = value
|
||||
} else if (part.contains("used.")) {
|
||||
swap.used = value
|
||||
}
|
||||
}
|
||||
|
||||
if (isKibSwap) {
|
||||
swap.total = swap.total / 1024.0
|
||||
swap.free = swap.free / 1024.0
|
||||
swap.used = swap.used / 1024.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
cpuProgressBar.value = (100.0 - cpu.id).toInt()
|
||||
memoryProgressBar.value = (mem.used / mem.total * 100.0).toInt()
|
||||
memoryProgressBar.string =
|
||||
"${formatBytes((mem.used * 1024 * 1024).toLong())} / ${formatBytes((mem.total * 1024 * 1024).toLong())}"
|
||||
|
||||
swapProgressBar.value = (swap.used / swap.total * 100.0).toInt()
|
||||
swapProgressBar.string =
|
||||
"${formatBytes((swap.used * 1024 * 1024).toLong())} / ${formatBytes((swap.total * 1024 * 1024).toLong())}"
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshDisk(session: ClientSession) {
|
||||
|
||||
// df -h
|
||||
var pair = SshClients.execChannel(session, "df -B1")
|
||||
if (pair.first != 0) {
|
||||
return
|
||||
}
|
||||
|
||||
val disks = mutableListOf<Disk>()
|
||||
val lines = pair.second.split(StringUtils.LF)
|
||||
for (line in lines) {
|
||||
if (!line.startsWith("/dev/")) {
|
||||
continue
|
||||
}
|
||||
|
||||
val parts = line.split("\\s+".toRegex())
|
||||
if (parts.size < 6) {
|
||||
continue
|
||||
}
|
||||
|
||||
disks.add(
|
||||
Disk(
|
||||
filesystem = parts[0],
|
||||
size = parts[1].toLong(),
|
||||
used = parts[2].toLong(),
|
||||
avail = parts[3].toLong(),
|
||||
usePercentage = StringUtils.removeEnd(parts[4], "%").toIntOrNull() ?: 0,
|
||||
mountedOn = parts[5],
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
while (tableModel.rowCount > 0) {
|
||||
tableModel.removeRow(0)
|
||||
}
|
||||
|
||||
for (disk in disks) {
|
||||
tableModel.addRow(
|
||||
arrayOf(
|
||||
" ${disk.filesystem}",
|
||||
formatBytes(disk.used) + " / " + formatBytes(disk.size),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private data class Mem(
|
||||
/**
|
||||
* 总内存
|
||||
*/
|
||||
var total: Double = 0.0,
|
||||
/**
|
||||
* 空闲内存
|
||||
*/
|
||||
var free: Double = 0.0,
|
||||
/**
|
||||
* 已用内存
|
||||
*/
|
||||
var used: Double = 0.0,
|
||||
/**
|
||||
* 缓存和缓冲区占用的内存
|
||||
*/
|
||||
var buffCache: Double = 0.0,
|
||||
)
|
||||
|
||||
private data class Swap(
|
||||
/**
|
||||
* 交换空间的总大小
|
||||
*/
|
||||
var total: Double = 0.0,
|
||||
/**
|
||||
* 已使用的交换空间
|
||||
*/
|
||||
var free: Double = 0.0,
|
||||
/**
|
||||
* 未使用的交换空间
|
||||
*/
|
||||
var used: Double = 0.0,
|
||||
)
|
||||
|
||||
private data class CPU(
|
||||
/**
|
||||
* 用户空间 CPU 占用时间百分比。
|
||||
* 该值表示 CPU 用于执行用户进程的时间比例。
|
||||
* 示例:如果系统中 CPU 用于执行用户程序的时间占总 CPU 时间的 40%,则该值为 40.0。
|
||||
*/
|
||||
var us: Double = 0.0,
|
||||
|
||||
/**
|
||||
* 系统空间 CPU 占用时间百分比。
|
||||
* 该值表示 CPU 用于执行内核进程的时间比例。
|
||||
* 示例:如果内核进程占用 CPU 时间的 20%,则该值为 20.0。
|
||||
*/
|
||||
var sy: Double = 0.0,
|
||||
|
||||
/**
|
||||
* 优先级调整过的进程(Nice)占用的 CPU 时间百分比。
|
||||
* 该值表示 CPU 用于执行“优先级较低的”进程的时间比例。
|
||||
* 示例:如果优先级调整过的进程占用 CPU 时间的 10%,则该值为 10.0。
|
||||
*/
|
||||
var ni: Double = 0.0,
|
||||
|
||||
/**
|
||||
* CPU 空闲时间百分比。
|
||||
* 该值表示 CPU 在空闲状态下没有执行任何任务的时间比例。
|
||||
* 示例:如果 CPU 95% 处于空闲状态,该值为 95.0。
|
||||
*/
|
||||
var id: Double = 0.0,
|
||||
|
||||
/**
|
||||
* I/O 等待时间百分比。
|
||||
* 该值表示 CPU 正在等待 I/O 操作完成的时间比例。
|
||||
* 示例:如果 CPU 由于 I/O 操作等待占用 5% 的时间,则该值为 5.0。
|
||||
*/
|
||||
var wa: Double = 0.0,
|
||||
|
||||
/**
|
||||
* 硬件中断处理时间百分比。
|
||||
* 该值表示 CPU 用于处理中断请求的时间比例,通常由硬件触发。
|
||||
* 示例:如果 CPU 处理硬件中断占用 2% 的时间,则该值为 2.0。
|
||||
*/
|
||||
var hi: Double = 0.0,
|
||||
|
||||
/**
|
||||
* 软件中断处理时间百分比。
|
||||
* 该值表示 CPU 用于处理由软件触发的中断的时间比例。
|
||||
* 示例:如果 CPU 处理软件中断占用 3% 的时间,则该值为 3.0。
|
||||
*/
|
||||
var si: Double = 0.0,
|
||||
|
||||
/**
|
||||
* 虚拟化环境中的 CPU 抢占时间百分比。
|
||||
* 该值表示 CPU 在虚拟化环境中被其他虚拟机抢占的时间比例。
|
||||
* 示例:如果虚拟化环境中的 CPU 抢占占用 0.5% 的时间,则该值为 0.5。
|
||||
*/
|
||||
var st: Double = 0.0,
|
||||
)
|
||||
|
||||
private data class Disk(
|
||||
var filesystem: String = StringUtils.EMPTY,
|
||||
/**
|
||||
* 总大小
|
||||
*/
|
||||
var size: Long = 0L,
|
||||
/**
|
||||
* 已经使用的空间
|
||||
*/
|
||||
var used: Long = 0L,
|
||||
/**
|
||||
* 可用空间
|
||||
*/
|
||||
var avail: Long = 0L,
|
||||
/**
|
||||
* 已经使用的百分比
|
||||
*/
|
||||
var usePercentage: Int = 0,
|
||||
|
||||
/**
|
||||
* 挂载点
|
||||
*/
|
||||
var mountedOn: String = StringUtils.EMPTY
|
||||
)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import app.termora.Disposable
|
||||
import java.awt.Window
|
||||
import javax.swing.JComponent
|
||||
|
||||
/**
|
||||
* 虚拟窗口
|
||||
*/
|
||||
interface VisualWindow : Disposable {
|
||||
|
||||
/**
|
||||
* 虚拟窗口内容
|
||||
*/
|
||||
fun getJComponent(): JComponent
|
||||
|
||||
/**
|
||||
* 是否是独立窗口(独立成一个 Window)
|
||||
*/
|
||||
fun isWindow(): Boolean
|
||||
|
||||
/**
|
||||
* 如果是独立窗口,那么可以返回
|
||||
*/
|
||||
fun getWindow(): Window? = null
|
||||
|
||||
/**
|
||||
* 切换独立模式
|
||||
*/
|
||||
fun toggleWindow()
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import java.awt.Dimension
|
||||
|
||||
interface VisualWindowManager {
|
||||
|
||||
/**
|
||||
* 将窗口移动到最前面
|
||||
*/
|
||||
fun moveToFront(visualWindow: VisualWindow)
|
||||
|
||||
/**
|
||||
* 添加虚拟窗口
|
||||
*/
|
||||
fun addVisualWindow(visualWindow: VisualWindow)
|
||||
|
||||
/**
|
||||
* 移除虚拟窗口
|
||||
*/
|
||||
fun removeVisualWindow(visualWindow: VisualWindow)
|
||||
|
||||
/**
|
||||
* 变基,仅仅从 LayeredPane 移除,但是不从 [getVisualWindows] 中移除
|
||||
*/
|
||||
fun rebaseVisualWindow(visualWindow: VisualWindow)
|
||||
|
||||
/**
|
||||
* 获取管理的所有窗口
|
||||
*/
|
||||
fun getVisualWindows(): Array<VisualWindow>
|
||||
|
||||
/**
|
||||
* 获取管理器的宽高
|
||||
*/
|
||||
fun getDimension(): Dimension
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import app.termora.*
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
import java.awt.*
|
||||
import java.awt.event.*
|
||||
import java.beans.PropertyChangeEvent
|
||||
import java.beans.PropertyChangeListener
|
||||
import javax.swing.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
open class VisualWindowPanel(protected val id: String, protected val visualWindowManager: VisualWindowManager) :
|
||||
JPanel(BorderLayout()), VisualWindow {
|
||||
|
||||
protected val properties get() = Database.getDatabase().properties
|
||||
private val titleLabel = JLabel()
|
||||
private val toolbar = FlatToolBar()
|
||||
private val visualWindow = this
|
||||
private val resizer = VisualWindowResizer(this) { !isWindow }
|
||||
private var isWindow = false
|
||||
set(value) {
|
||||
val oldValue = field
|
||||
field = value
|
||||
firePropertyChange("isWindow", oldValue, value)
|
||||
}
|
||||
private var dialog: VisualWindowDialog? = null
|
||||
private var oldBounds = Rectangle()
|
||||
private var toggleWindowBtn = JButton(Icons.openInNewWindow)
|
||||
private var isAlwaysTop
|
||||
get() = properties.getString("VisualWindow.${id}.dialog.isAlwaysTop", "false").toBoolean()
|
||||
set(value) = properties.putString("VisualWindow.${id}.dialog.isAlwaysTop", value.toString())
|
||||
|
||||
private val alwaysTopBtn = JButton(Icons.moveUp)
|
||||
private val closeWindowListener = object : WindowAdapter() {
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var title: String
|
||||
set(value) {
|
||||
titleLabel.text = value
|
||||
}
|
||||
get() = titleLabel.text
|
||||
|
||||
|
||||
protected fun initVisualWindowPanel() {
|
||||
initViews()
|
||||
initEvents()
|
||||
initToolBar()
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
border = BorderFactory.createMatteBorder(1, 1, 1, 1, DynamicColor.BorderColor)
|
||||
|
||||
val x = properties.getString("VisualWindow.${id}.location.x", "-1").toIntOrNull() ?: -1
|
||||
val y = properties.getString("VisualWindow.${id}.location.y", "-1").toIntOrNull() ?: -1
|
||||
val w = properties.getString("VisualWindow.${id}.location.width", "-1").toIntOrNull() ?: -1
|
||||
val h = properties.getString("VisualWindow.${id}.location.height", "-1").toIntOrNull() ?: -1
|
||||
|
||||
if (x >= 0 && y >= 0) {
|
||||
setLocation(x, y)
|
||||
} else {
|
||||
setLocation(200, 200)
|
||||
}
|
||||
|
||||
if (w > 0 && h > 0) setSize(w, h) else setSize(400, 200)
|
||||
|
||||
alwaysTopBtn.isSelected = isAlwaysTop
|
||||
alwaysTopBtn.isVisible = false
|
||||
}
|
||||
|
||||
protected open fun toolbarButtons(): List<JButton> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
val dragListener = DragListener()
|
||||
toolbar.addMouseListener(dragListener)
|
||||
toolbar.addMouseMotionListener(dragListener)
|
||||
|
||||
// 监听全局事件
|
||||
Toolkit.getDefaultToolkit().addAWTEventListener(object : AWTEventListener {
|
||||
override fun eventDispatched(event: AWTEvent) {
|
||||
if (event is MouseEvent) {
|
||||
if (event.id == MouseEvent.MOUSE_PRESSED) {
|
||||
val c = event.component ?: return
|
||||
if (SwingUtilities.isDescendingFrom(c, visualWindow)) {
|
||||
visualWindowManager.moveToFront(visualWindow)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}, MouseEvent.MOUSE_EVENT_MASK)
|
||||
|
||||
// 阻止事件穿透
|
||||
addMouseListener(object : MouseAdapter() {})
|
||||
|
||||
toggleWindowBtn.addActionListener { toggleWindow() }
|
||||
|
||||
addPropertyChangeListener("isWindow", object : PropertyChangeListener {
|
||||
override fun propertyChange(evt: PropertyChangeEvent) {
|
||||
if (isWindow) {
|
||||
border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||
toggleWindowBtn.icon = Icons.openInToolWindow
|
||||
} else {
|
||||
border = BorderFactory.createMatteBorder(1, 1, 1, 1, DynamicColor.BorderColor)
|
||||
toggleWindowBtn.icon = Icons.openInNewWindow
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
alwaysTopBtn.addActionListener {
|
||||
isAlwaysTop = !isAlwaysTop
|
||||
alwaysTopBtn.isSelected = isAlwaysTop
|
||||
|
||||
if (isWindow()) {
|
||||
dialog?.isAlwaysOnTop = isAlwaysTop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initToolBar() {
|
||||
val btns = toolbarButtons()
|
||||
val count = 2 + btns.size
|
||||
toolbar.add(alwaysTopBtn)
|
||||
toolbar.add(Box.createHorizontalStrut(count * 26))
|
||||
toolbar.add(JLabel(Icons.empty))
|
||||
toolbar.add(Box.createHorizontalGlue())
|
||||
toolbar.add(titleLabel)
|
||||
toolbar.add(Box.createHorizontalGlue())
|
||||
|
||||
btns.forEach { toolbar.add(it) }
|
||||
|
||||
toolbar.add(toggleWindowBtn)
|
||||
toolbar.add(JButton(Icons.close).apply { addActionListener { Disposer.dispose(visualWindow) } })
|
||||
toolbar.border = BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor)
|
||||
add(toolbar, BorderLayout.NORTH)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
|
||||
val bounds = if (isWindow) oldBounds else bounds
|
||||
properties.putString("VisualWindow.${id}.location.x", bounds.x.toString())
|
||||
properties.putString("VisualWindow.${id}.location.y", bounds.y.toString())
|
||||
properties.putString("VisualWindow.${id}.location.width", bounds.width.toString())
|
||||
properties.putString("VisualWindow.${id}.location.height", bounds.height.toString())
|
||||
|
||||
resizer.uninstall()
|
||||
|
||||
this.close()
|
||||
|
||||
}
|
||||
|
||||
final override fun getJComponent(): JComponent {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun isWindow(): Boolean {
|
||||
return isWindow
|
||||
}
|
||||
|
||||
override fun getWindow(): Window? {
|
||||
return dialog
|
||||
}
|
||||
|
||||
protected open fun getWindowTitle(): String {
|
||||
return id
|
||||
}
|
||||
|
||||
override fun toggleWindow() {
|
||||
|
||||
if (isWindow) {
|
||||
// 提前移除 dialog 的关闭事件
|
||||
dialog?.removeWindowListener(closeWindowListener)
|
||||
}
|
||||
|
||||
isWindow = !isWindow
|
||||
dialog?.dispose()
|
||||
dialog = null
|
||||
|
||||
alwaysTopBtn.isVisible = isWindow
|
||||
|
||||
if (isWindow) {
|
||||
oldBounds = bounds
|
||||
// 变基
|
||||
visualWindowManager.rebaseVisualWindow(this)
|
||||
|
||||
val dialog = VisualWindowDialog().apply { dialog = this }
|
||||
dialog.addWindowListener(closeWindowListener)
|
||||
dialog.isVisible = true
|
||||
|
||||
} else {
|
||||
bounds = oldBounds
|
||||
visualWindowManager.removeVisualWindow(visualWindow)
|
||||
visualWindowManager.addVisualWindow(visualWindow)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class DragListener() : MouseAdapter() {
|
||||
private var startPoint: Point? = null
|
||||
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (isWindow) {
|
||||
startPoint = null
|
||||
return
|
||||
}
|
||||
startPoint = SwingUtilities.convertPoint(visualWindow, e.getPoint(), visualWindow.getParent())
|
||||
}
|
||||
|
||||
override fun mouseDragged(e: MouseEvent) {
|
||||
val startPoint = this.startPoint ?: return
|
||||
val newPoint = SwingUtilities.convertPoint(visualWindow, e.getPoint(), visualWindow.getParent())
|
||||
val dimension = visualWindowManager.getDimension()
|
||||
|
||||
val x = min(
|
||||
visualWindow.getX() + (newPoint.x - startPoint.x),
|
||||
dimension.width - visualWindow.width
|
||||
)
|
||||
|
||||
val y = min(
|
||||
visualWindow.getY() + (newPoint.y - startPoint.y),
|
||||
dimension.height - visualWindow.height
|
||||
)
|
||||
|
||||
visualWindow.setBounds(max(x, 0), max(y, 0), visualWindow.getWidth(), visualWindow.getHeight())
|
||||
|
||||
this.startPoint = newPoint
|
||||
}
|
||||
|
||||
override fun mouseReleased(e: MouseEvent) {
|
||||
visualWindowManager.moveToFront(visualWindow)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected open fun close() {
|
||||
if (isWindow()) {
|
||||
dialog?.dispose()
|
||||
dialog = null
|
||||
}
|
||||
visualWindowManager.removeVisualWindow(visualWindow)
|
||||
}
|
||||
|
||||
private inner class VisualWindowDialog : DialogWrapper(null) {
|
||||
|
||||
init {
|
||||
isModal = false
|
||||
controlsVisible = false
|
||||
isResizable = true
|
||||
title = getWindowTitle()
|
||||
isAlwaysOnTop = isAlwaysTop
|
||||
|
||||
initEvents()
|
||||
|
||||
init()
|
||||
|
||||
|
||||
val x = properties.getString("VisualWindow.${id}.dialog.location.x", "-1").toIntOrNull() ?: -1
|
||||
val y = properties.getString("VisualWindow.${id}.dialog.location.y", "-1").toIntOrNull() ?: -1
|
||||
val w = properties.getString("VisualWindow.${id}.dialog.location.width", "-1").toIntOrNull() ?: -1
|
||||
val h = properties.getString("VisualWindow.${id}.dialog.location.height", "-1").toIntOrNull() ?: -1
|
||||
|
||||
if (w > 0 && h > 0) setSize(w, h) else pack()
|
||||
|
||||
if (x >= 0 && y >= 0) {
|
||||
setLocation(x, y)
|
||||
} else {
|
||||
setLocationRelativeTo(null)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
properties.putString("VisualWindow.${id}.dialog.location.x", x.toString())
|
||||
properties.putString("VisualWindow.${id}.dialog.location.y", y.toString())
|
||||
properties.putString("VisualWindow.${id}.dialog.location.width", width.toString())
|
||||
properties.putString("VisualWindow.${id}.dialog.location.height", height.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
return getJComponent()
|
||||
}
|
||||
|
||||
override fun createSouthPanel(): JComponent? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import com.formdev.flatlaf.ui.FlatWindowResizer
|
||||
import java.awt.Dimension
|
||||
import java.awt.Rectangle
|
||||
import javax.swing.JComponent
|
||||
|
||||
class VisualWindowResizer(resizeComp: JComponent, private val windowResizable: () -> Boolean = { true }) :
|
||||
FlatWindowResizer(resizeComp) {
|
||||
|
||||
override fun isWindowResizable(): Boolean {
|
||||
return windowResizable.invoke()
|
||||
}
|
||||
|
||||
override fun getWindowBounds(): Rectangle {
|
||||
return resizeComp.bounds
|
||||
}
|
||||
|
||||
override fun setWindowBounds(r: Rectangle) {
|
||||
resizeComp.bounds = r
|
||||
resizeComp.revalidate()
|
||||
resizeComp.repaint()
|
||||
}
|
||||
|
||||
override fun limitToParentBounds(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getParentBounds(): Rectangle {
|
||||
return resizeComp.getParent().bounds
|
||||
}
|
||||
|
||||
override fun honorMinimumSizeOnResize(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun honorMaximumSizeOnResize(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getWindowMinimumSize(): Dimension {
|
||||
return resizeComp.minimumSize
|
||||
}
|
||||
|
||||
override fun getWindowMaximumSize(): Dimension {
|
||||
return resizeComp.maximumSize
|
||||
}
|
||||
}
|
||||
@@ -57,11 +57,13 @@ class FileSystemTabbed(
|
||||
|
||||
private fun initEvents() {
|
||||
addBtn.addActionListener {
|
||||
val dialog = HostTreeDialog(SwingUtilities.getWindowAncestor(this))
|
||||
val dialog = NewHostTreeDialog(SwingUtilities.getWindowAncestor(this))
|
||||
dialog.location = Point(
|
||||
max(0, addBtn.locationOnScreen.x - dialog.width / 2 + addBtn.width / 2),
|
||||
addBtn.locationOnScreen.y + max(tabHeight, addBtn.height)
|
||||
)
|
||||
dialog.setFilter { it.host.protocol == Protocol.SSH }
|
||||
dialog.setTreeName("FileSystemTabbed.Tree")
|
||||
dialog.isVisible = true
|
||||
|
||||
for (host in dialog.hosts) {
|
||||
|
||||
@@ -14,7 +14,7 @@ class SFTPAction : AnAction("SFTP", Icons.folder) {
|
||||
val tab = openOrCreateSFTPTerminalTab(evt) ?: return
|
||||
|
||||
if (host != null) {
|
||||
connectHost(host.copy(protocol = Protocol.SSH), tab)
|
||||
connectHost(host.copy(protocol = Protocol.SSH, updateDate = System.currentTimeMillis()), tab)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,20 +23,22 @@ class SFTPAction : AnAction("SFTP", Icons.folder) {
|
||||
*
|
||||
* @return null 表示当前条件下无法创建
|
||||
*/
|
||||
fun openOrCreateSFTPTerminalTab(evt: AnActionEvent): SFTPTerminalTab? {
|
||||
fun openOrCreateSFTPTerminalTab(evt: AnActionEvent, selected: Boolean = true): SFTPTerminalTab? {
|
||||
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return null
|
||||
|
||||
val tabs = terminalTabbedManager.getTerminalTabs()
|
||||
for (tab in tabs) {
|
||||
if (tab is SFTPTerminalTab) {
|
||||
terminalTabbedManager.setSelectedTerminalTab(tab)
|
||||
if (selected) {
|
||||
terminalTabbedManager.setSelectedTerminalTab(tab)
|
||||
}
|
||||
return tab
|
||||
}
|
||||
}
|
||||
|
||||
// 创建一个新的
|
||||
val tab = SFTPTerminalTab()
|
||||
terminalTabbedManager.addTerminalTab(tab)
|
||||
terminalTabbedManager.addTerminalTab(tab, selected)
|
||||
|
||||
return tab
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package app.termora.transport
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||
import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon
|
||||
import com.formdev.flatlaf.icons.FlatOptionPaneInformationIcon
|
||||
@@ -105,7 +107,7 @@ class SftpFileSystemPanel(
|
||||
private suspend fun doConnect() {
|
||||
|
||||
val thisHost = this.host ?: return
|
||||
var host = thisHost.copy(authentication = thisHost.authentication.copy())
|
||||
var host = thisHost.copy(authentication = thisHost.authentication.copy(), updateDate = System.currentTimeMillis())
|
||||
|
||||
closeIO()
|
||||
|
||||
@@ -117,13 +119,20 @@ class SftpFileSystemPanel(
|
||||
client.serverKeyVerifier = DialogServerKeyVerifier(owner)
|
||||
// 弹出授权框
|
||||
if (host.authentication.type == AuthenticationType.No) {
|
||||
val dialog = RequestAuthenticationDialog(owner)
|
||||
val dialog = RequestAuthenticationDialog(owner, host)
|
||||
val authentication = dialog.getAuthentication()
|
||||
host = host.copy(authentication = authentication)
|
||||
host = host.copy(
|
||||
authentication = authentication,
|
||||
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
||||
)
|
||||
// save
|
||||
if (dialog.isRemembered()) {
|
||||
HostManager.getInstance()
|
||||
.addHost(host.copy(authentication = authentication))
|
||||
HostManager.getInstance().addHost(
|
||||
host.copy(
|
||||
authentication = authentication,
|
||||
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,9 +300,11 @@ class SftpFileSystemPanel(
|
||||
val builder = FormBuilder.create().layout(layout).debug(false)
|
||||
builder.add(FlatOptionPaneInformationIcon()).xy(2, 2)
|
||||
builder.add(errorInfo).xyw(1, 4, 3, "fill, center")
|
||||
builder.add(JXHyperlink(object : AbstractAction(I18n.getString("termora.transport.sftp.select-host")) {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val dialog = HostTreeDialog(SwingUtilities.getWindowAncestor(this@SftpFileSystemPanel))
|
||||
builder.add(JXHyperlink(object : AnAction(I18n.getString("termora.transport.sftp.select-host")) {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val dialog = NewHostTreeDialog(evt.window)
|
||||
dialog.setFilter { it.host.protocol == Protocol.SSH }
|
||||
dialog.setTreeName("SftpFileSystemPanel.SelectHostTree")
|
||||
dialog.allowMulti = false
|
||||
dialog.setLocationRelativeTo(this@SelectHostPanel)
|
||||
dialog.isVisible = true
|
||||
|
||||
@@ -69,6 +69,7 @@ termora.settings.terminal.debug=Debug mode
|
||||
termora.settings.terminal.beep=Beep
|
||||
termora.settings.terminal.select-copy=Select copy
|
||||
termora.settings.terminal.cursor-style=Cursor type
|
||||
termora.settings.terminal.cursor-blink=Cursor blink
|
||||
termora.settings.terminal.local-shell=Local shell
|
||||
termora.settings.terminal.floating-toolbar=Floating Toolbar
|
||||
termora.settings.terminal.auto-close-tab=Auto Close Tab
|
||||
@@ -109,6 +110,7 @@ termora.settings.keymap.already-exists=The shortcut [{0}] is already in use by [
|
||||
|
||||
|
||||
termora.settings.sftp.edit-command=Edit Command
|
||||
termora.settings.sftp.fixed-tab=Fixed tab
|
||||
|
||||
|
||||
termora.settings.restart.title=Restart
|
||||
@@ -123,25 +125,31 @@ termora.find-everywhere.groups.opened-hosts=Opened hosts
|
||||
termora.find-everywhere.groups.tools=Tools
|
||||
termora.find-everywhere.groups.settings=${termora.setting}
|
||||
termora.find-everywhere.quick-command.local-terminal=Local Terminal
|
||||
termora.find-everywhere.double-shift-deprecated=The double-click Shift shortcut will be removed in a future version
|
||||
termora.find-everywhere.double-shift-deprecated-instead=${termora.find-everywhere.double-shift-deprecated}, use {0} instead
|
||||
|
||||
# Welcome
|
||||
termora.welcome.my-hosts=My hosts
|
||||
termora.welcome.contextmenu.connect=Connect
|
||||
termora.welcome.contextmenu.connect-with=Connect with
|
||||
termora.welcome.contextmenu.open-in-new-window=${termora.tabbed.contextmenu.open-in-new-window}
|
||||
termora.welcome.contextmenu.refresh=${termora.transport.table.contextmenu.refresh}
|
||||
termora.welcome.contextmenu.copy=${termora.copy}
|
||||
termora.welcome.contextmenu.remove=${termora.remove}
|
||||
termora.welcome.contextmenu.rename=Rename
|
||||
termora.welcome.contextmenu.expand-all=Expand all
|
||||
termora.welcome.contextmenu.collapse-all=Collapse all
|
||||
termora.welcome.contextmenu.new=New
|
||||
termora.welcome.contextmenu.import=${termora.keymgr.import}
|
||||
termora.welcome.contextmenu.new.folder=${termora.folder}
|
||||
termora.welcome.contextmenu.new.host=Host
|
||||
termora.welcome.contextmenu.new.folder.name=New Folder
|
||||
termora.welcome.contextmenu.property=Properties
|
||||
termora.welcome.contextmenu.show-more-info=Show more info
|
||||
termora.welcome.contextmenu.download=Download
|
||||
termora.welcome.contextmenu.import.csv.download-template=Do you want to import or download the template?
|
||||
termora.welcome.contextmenu.import.csv.download-template-done=Download the template successfully
|
||||
termora.welcome.contextmenu.import.csv.download-template-done-open-folder=Download the template successfully, Do you want to open the folder?
|
||||
termora.welcome.contextmenu.import.xshell-folder-empty=The folder does not contain any *.xsh files, Please select the correct Xshell Sessions directory
|
||||
termora.welcome.contextmenu.import.finalshell-folder-empty=The folder does not contain any *_connect_config.json files, Please select the correct FinalShell directory
|
||||
|
||||
# New Host
|
||||
termora.new-host.title=Create a new host
|
||||
@@ -335,6 +343,7 @@ termora.actions.open-local-terminal=Open Local Terminal
|
||||
termora.actions.open-find-everywhere=Open FindEverywhere
|
||||
termora.actions.open-new-window=Open new Window
|
||||
termora.actions.clear-screen=Clear Terminal Screen
|
||||
termora.actions.open-sftp-command=Open SFTP Command
|
||||
termora.actions.switch-tab=Switch to specific Tab [1..9]
|
||||
|
||||
# Terminal
|
||||
@@ -343,6 +352,19 @@ termora.terminal.copied=Copied
|
||||
termora.terminal.channel-disconnected=Channel has been disconnected.\u0020
|
||||
termora.terminal.channel-reconnect=Type {0} to reconnect.
|
||||
|
||||
# Visual Window
|
||||
termora.visual-window.system-information=System information
|
||||
termora.visual-window.system-information.mem=Mem
|
||||
termora.visual-window.system-information.swap=Swap
|
||||
termora.visual-window.system-information.filesystem=Filesystem
|
||||
termora.visual-window.system-information.used-total=Used / Total
|
||||
|
||||
|
||||
termora.visual-window.nvidia-smi=NVIDIA SMI
|
||||
|
||||
|
||||
termora.floating-toolbar.not-supported=This action is not supported
|
||||
|
||||
|
||||
# zmodem
|
||||
termora.addons.zmodem.skip=SKIP
|
||||
@@ -65,8 +65,6 @@ termora.find-everywhere.groups.opened-hosts=已打开的主机
|
||||
termora.find-everywhere.groups.tools=工具
|
||||
termora.find-everywhere.groups.settings=${termora.setting}
|
||||
termora.find-everywhere.quick-command.local-terminal=本地终端
|
||||
termora.find-everywhere.double-shift-deprecated=双击 Shift 快捷键将会在未来的版本中移除
|
||||
termora.find-everywhere.double-shift-deprecated-instead=${termora.find-everywhere.double-shift-deprecated},请使用 {0} 代替
|
||||
|
||||
termora.settings.terminal=终端
|
||||
termora.settings.terminal.font=字体
|
||||
@@ -76,6 +74,7 @@ termora.settings.terminal.debug=调试模式
|
||||
termora.settings.terminal.beep=蜂鸣声
|
||||
termora.settings.terminal.select-copy=选中复制
|
||||
termora.settings.terminal.cursor-style=光标样式
|
||||
termora.settings.terminal.cursor-blink=光标闪烁
|
||||
termora.settings.terminal.local-shell=本地终端
|
||||
termora.settings.terminal.floating-toolbar=悬浮工具栏
|
||||
termora.settings.terminal.auto-close-tab=自动关闭标签
|
||||
@@ -114,6 +113,7 @@ termora.settings.keymap.already-exists=快捷键 [{0}] 已经被 [{1}] 占用
|
||||
|
||||
|
||||
termora.settings.sftp.edit-command=编辑命令
|
||||
termora.settings.sftp.fixed-tab=固定标签
|
||||
|
||||
|
||||
# Welcome
|
||||
@@ -132,6 +132,13 @@ termora.welcome.contextmenu.new.folder.name=新建文件夹
|
||||
termora.welcome.contextmenu.property=属性
|
||||
termora.welcome.contextmenu.show-more-info=显示更多信息
|
||||
|
||||
termora.welcome.contextmenu.download=下载
|
||||
termora.welcome.contextmenu.import.csv.download-template=您要导入还是下载模板?
|
||||
termora.welcome.contextmenu.import.csv.download-template-done=下载成功
|
||||
termora.welcome.contextmenu.import.csv.download-template-done-open-folder=下载成功, 是否需要打开所在文件夹?
|
||||
termora.welcome.contextmenu.import.xshell-folder-empty=该文件夹不包含 *.xsh 文件,请选择正确的 Xshell 会话目录
|
||||
termora.welcome.contextmenu.import.finalshell-folder-empty=该文件夹不包含 *_connect_config.json 文件,请选择正确的 FinalShell 配置目录
|
||||
|
||||
# New Host
|
||||
termora.new-host.title=新建主机
|
||||
termora.new-host.general=属性
|
||||
@@ -327,7 +334,19 @@ termora.actions.open-local-terminal=打开本地终端
|
||||
termora.actions.open-find-everywhere=打开全局查找
|
||||
termora.actions.open-new-window=打开新窗口
|
||||
termora.actions.clear-screen=清除终端屏幕
|
||||
termora.actions.open-sftp-command=打开 SFTP 终端
|
||||
termora.actions.switch-tab=切换到特定标签页 [1..9]
|
||||
|
||||
|
||||
# Visual Window
|
||||
termora.visual-window.system-information=系统信息
|
||||
termora.visual-window.system-information.mem=内存
|
||||
termora.visual-window.system-information.swap=交换
|
||||
termora.visual-window.system-information.filesystem=文件系统
|
||||
termora.visual-window.system-information.used-total=使用 / 大小
|
||||
|
||||
termora.floating-toolbar.not-supported=不允许此操作
|
||||
|
||||
|
||||
# zmodem
|
||||
termora.addons.zmodem.skip=跳过
|
||||
@@ -63,6 +63,7 @@ termora.settings.keymap.action=操作
|
||||
termora.settings.keymap.already-exists=快捷鍵 [{0}] 已經被 [{1}] 占用
|
||||
|
||||
termora.settings.sftp.edit-command=編輯命令
|
||||
termora.settings.sftp.fixed-tab=固定標籤
|
||||
|
||||
|
||||
# Find everywhere
|
||||
@@ -74,8 +75,6 @@ termora.find-everywhere.groups.opened-hosts=已開啟的主機
|
||||
termora.find-everywhere.groups.tools=工具
|
||||
termora.find-everywhere.groups.settings=${termora.setting}
|
||||
termora.find-everywhere.quick-command.local-terminal=本地端
|
||||
termora.find-everywhere.double-shift-deprecated=雙擊 Shift 快捷鍵將會在未來的版本中移除
|
||||
termora.find-everywhere.double-shift-deprecated-instead=${termora.find-everywhere.double-shift-deprecated},請使用 {0} 代替
|
||||
|
||||
termora.settings.terminal=終端
|
||||
termora.settings.terminal.font=字體
|
||||
@@ -85,6 +84,7 @@ termora.settings.terminal.debug=偵錯模式
|
||||
termora.settings.terminal.beep=蜂鳴聲
|
||||
termora.settings.terminal.select-copy=選取複製
|
||||
termora.settings.terminal.cursor-style=遊標風格
|
||||
termora.settings.terminal.cursor-blink=遊標閃爍
|
||||
termora.settings.terminal.local-shell=本地端
|
||||
termora.settings.terminal.floating-toolbar=懸浮工具列
|
||||
termora.settings.terminal.auto-close-tab=自動關閉標籤
|
||||
@@ -130,6 +130,12 @@ termora.welcome.contextmenu.new.host=主機
|
||||
termora.welcome.contextmenu.new.folder.name=新建資料夾
|
||||
termora.welcome.contextmenu.property=屬性
|
||||
termora.welcome.contextmenu.show-more-info=顯示更多信息
|
||||
termora.welcome.contextmenu.download=下載
|
||||
termora.welcome.contextmenu.import.csv.download-template=您要匯入還是下載範本?
|
||||
termora.welcome.contextmenu.import.csv.download-template-done=下載成功
|
||||
termora.welcome.contextmenu.import.csv.download-template-done-open-folder=下載成功, 是否需要開啟所在資料夾?
|
||||
termora.welcome.contextmenu.import.xshell-folder-empty=該資料夾不包含 *.xsh 文件,請選擇正確的 Xshell 會話目錄
|
||||
termora.welcome.contextmenu.import.finalshell-folder-empty=該資料夾不包含 *_connect_config.json 文件,請選擇正確的 FinalShell 設定目錄
|
||||
|
||||
# New Host
|
||||
termora.new-host.title=新主機
|
||||
@@ -308,8 +314,17 @@ termora.actions.open-local-terminal=開啟本地終端
|
||||
termora.actions.open-find-everywhere=開啟全域搜尋
|
||||
termora.actions.open-new-window=開啟新視窗
|
||||
termora.actions.clear-screen=清除終端機螢幕
|
||||
termora.actions.open-sftp-command=打開 SFTP 終端
|
||||
termora.actions.switch-tab=切換到特定分頁 [1..9]
|
||||
|
||||
# Visual Window
|
||||
termora.visual-window.system-information=系統訊息
|
||||
termora.visual-window.system-information.mem=內存
|
||||
termora.visual-window.system-information.swap=交換
|
||||
termora.visual-window.system-information.filesystem=檔案系統
|
||||
termora.visual-window.system-information.used-total=使用 / 大小
|
||||
|
||||
termora.floating-toolbar.not-supported=不允許此操作
|
||||
|
||||
# zmodem
|
||||
termora.addons.zmodem.skip=跳過
|
||||
4
src/main/resources/icons/locate.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.5 5V2.02054C11.4149 2.26101 13.739 4.5851 13.9795 7.5H11C10.7239 7.5 10.5 7.72386 10.5 8C10.5 8.27614 10.7239 8.5 11 8.5H13.9795C13.739 11.4149 11.4149 13.739 8.5 13.9795V11C8.5 10.7239 8.27614 10.5 8 10.5C7.72386 10.5 7.5 10.7239 7.5 11V13.9795C4.5851 13.739 2.26101 11.4149 2.02054 8.5H5C5.27614 8.5 5.5 8.27614 5.5 8C5.5 7.72386 5.27614 7.5 5 7.5H2.02054C2.26101 4.5851 4.5851 2.26101 7.5 2.02054V5C7.5 5.27614 7.72386 5.5 8 5.5C8.27614 5.5 8.5 5.27614 8.5 5ZM1 8C1 4.13401 4.13401 1 8 1C11.866 1 15 4.13401 15 8C15 11.866 11.866 15 8 15C4.13401 15 1 11.866 1 8Z" fill="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 866 B |
4
src/main/resources/icons/locate_dark.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.5 5V2.02054C11.4149 2.26101 13.739 4.5851 13.9795 7.5H11C10.7239 7.5 10.5 7.72386 10.5 8C10.5 8.27614 10.7239 8.5 11 8.5H13.9795C13.739 11.4149 11.4149 13.739 8.5 13.9795V11C8.5 10.7239 8.27614 10.5 8 10.5C7.72386 10.5 7.5 10.7239 7.5 11V13.9795C4.5851 13.739 2.26101 11.4149 2.02054 8.5H5C5.27614 8.5 5.5 8.27614 5.5 8C5.5 7.72386 5.27614 7.5 5 7.5H2.02054C2.26101 4.5851 4.5851 2.26101 7.5 2.02054V5C7.5 5.27614 7.72386 5.5 8 5.5C8.27614 5.5 8.5 5.27614 8.5 5ZM1 8C1 4.13401 4.13401 1 8 1C11.866 1 15 4.13401 15 8C15 11.866 11.866 15 8 15C4.13401 15 1 11.866 1 8Z" fill="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 866 B |
5
src/main/resources/icons/nvidia.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg t="1740039296619" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9516"
|
||||
width="16" height="16">
|
||||
<path d="M381.781333 375.381333v-61.013333a285.866667 285.866667 0 0 1 18.090667-0.768c167.338667-5.290667 277.034667 143.957333 277.034667 143.957333s-118.357333 164.309333-245.333334 164.309334a156.586667 156.586667 0 0 1-49.408-7.893334v-185.429333c65.194667 7.893333 78.378667 36.565333 117.205334 101.76l87.04-73.130667s-63.658667-83.285333-170.666667-83.285333a256.682667 256.682667 0 0 0-33.962667 1.493333m0-202.026666v91.221333l18.090667-1.152c232.533333-7.893333 384.426667 190.72 384.426667 190.72s-174.08 211.797333-355.413334 211.797333a275.626667 275.626667 0 0 1-46.72-4.138666v56.533333c12.8 1.493333 26.026667 2.645333 38.826667 2.645333 168.832 0 290.986667-86.314667 409.301333-188.074666 19.584 15.829333 99.84 53.888 116.48 70.485333-112.341333 94.208-374.272 169.984-522.794666 169.984-14.293333 0-27.861333-0.768-41.429334-2.261333v79.530666H1024V173.354667z m0 440.576v48.256c-156.032-27.904-199.381333-190.293333-199.381333-190.293334s75.008-82.944 199.381333-96.512v52.778667H381.44c-65.194667-7.936-116.48 53.12-116.48 53.12s29.013333 102.912 116.864 132.693333M104.789333 465.066667s92.330667-136.405333 277.333334-150.741334V264.576C177.194667 281.173333 0 454.528 0 454.528s100.266667 290.218667 381.781333 316.586667v-52.778667c-206.506667-25.6-276.992-253.269333-276.992-253.269333z"
|
||||
p-id="9517" fill="#6C707E"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
5
src/main/resources/icons/nvidia_dark.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg t="1740039296619" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9516"
|
||||
width="16" height="16">
|
||||
<path d="M381.781333 375.381333v-61.013333a285.866667 285.866667 0 0 1 18.090667-0.768c167.338667-5.290667 277.034667 143.957333 277.034667 143.957333s-118.357333 164.309333-245.333334 164.309334a156.586667 156.586667 0 0 1-49.408-7.893334v-185.429333c65.194667 7.893333 78.378667 36.565333 117.205334 101.76l87.04-73.130667s-63.658667-83.285333-170.666667-83.285333a256.682667 256.682667 0 0 0-33.962667 1.493333m0-202.026666v91.221333l18.090667-1.152c232.533333-7.893333 384.426667 190.72 384.426667 190.72s-174.08 211.797333-355.413334 211.797333a275.626667 275.626667 0 0 1-46.72-4.138666v56.533333c12.8 1.493333 26.026667 2.645333 38.826667 2.645333 168.832 0 290.986667-86.314667 409.301333-188.074666 19.584 15.829333 99.84 53.888 116.48 70.485333-112.341333 94.208-374.272 169.984-522.794666 169.984-14.293333 0-27.861333-0.768-41.429334-2.261333v79.530666H1024V173.354667z m0 440.576v48.256c-156.032-27.904-199.381333-190.293333-199.381333-190.293334s75.008-82.944 199.381333-96.512v52.778667H381.44c-65.194667-7.936-116.48 53.12-116.48 53.12s29.013333 102.912 116.864 132.693333M104.789333 465.066667s92.330667-136.405333 277.333334-150.741334V264.576C177.194667 281.173333 0 454.528 0 454.528s100.266667 290.218667 381.781333 316.586667v-52.778667c-206.506667-25.6-276.992-253.269333-276.992-253.269333z"
|
||||
p-id="9517" fill="#CED0D6"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
4
src/main/resources/icons/openInNewWindow.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 14C2.89543 14 2 13.1046 2 12L2 4C2 2.89543 2.89543 2 4 2L6.5 2C6.77614 2 7 2.22386 7 2.5C7 2.77614 6.77614 3 6.5 3L4 3C3.44772 3 3 3.44772 3 4L3 12C3 12.5523 3.44772 13 4 13H12C12.5523 13 13 12.5523 13 12V9.5C13 9.22386 13.2239 9 13.5 9C13.7761 9 14 9.22386 14 9.5V12C14 13.1046 13.1046 14 12 14H4Z" fill="#6C707E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 2V6.5C14 6.77614 13.7761 7 13.5 7C13.2239 7 13 6.77614 13 6.5V3.70711L8.85355 7.85355C8.65829 8.04882 8.34171 8.04882 8.14645 7.85355C7.95118 7.65829 7.95118 7.34171 8.14645 7.14645L12.2929 3L9.5 3C9.22386 3 9 2.77614 9 2.5C9 2.22386 9.22386 2 9.5 2L14 2Z" fill="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 799 B |
4
src/main/resources/icons/openInNewWindow_dark.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 14C2.89543 14 2 13.1046 2 12L2 4C2 2.89543 2.89543 2 4 2L6.5 2C6.77614 2 7 2.22386 7 2.5C7 2.77614 6.77614 3 6.5 3L4 3C3.44772 3 3 3.44772 3 4L3 12C3 12.5523 3.44772 13 4 13H12C12.5523 13 13 12.5523 13 12V9.5C13 9.22386 13.2239 9 13.5 9C13.7761 9 14 9.22386 14 9.5V12C14 13.1046 13.1046 14 12 14H4Z" fill="#CED0D6"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 2V6.5C14 6.77614 13.7761 7 13.5 7C13.2239 7 13 6.77614 13 6.5V3.70711L8.85355 7.85355C8.65829 8.04882 8.34171 8.04882 8.14645 7.85355C7.95118 7.65829 7.95118 7.34171 8.14645 7.14645L12.2929 3L9.5 3C9.22386 3 9 2.77614 9 2.5C9 2.22386 9.22386 2 9.5 2L14 2Z" fill="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 799 B |
6
src/main/resources/icons/openInToolWindow.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 7L5.5 10.5" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.5 10.5L5.5 10.5L5.5 7.5" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 508 B |
6
src/main/resources/icons/openInToolWindow_dark.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 7L5.5 10.5" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.5 10.5L5.5 10.5L5.5 7.5" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 496 B |
5
src/main/resources/icons/percentage.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg t="1740037914635" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5141"
|
||||
width="16" height="16">
|
||||
<path d="M855.7 210.8l-42.4-42.4c-3.1-3.1-8.2-3.1-11.3 0L168.3 801.9c-3.1 3.1-3.1 8.2 0 11.3l42.4 42.4c3.1 3.1 8.2 3.1 11.3 0L855.6 222c3.2-3 3.2-8.1 0.1-11.2zM304 448c79.4 0 144-64.6 144-144s-64.6-144-144-144-144 64.6-144 144 64.6 144 144 144z m0-216c39.7 0 72 32.3 72 72s-32.3 72-72 72-72-32.3-72-72 32.3-72 72-72zM720 576c-79.4 0-144 64.6-144 144s64.6 144 144 144 144-64.6 144-144-64.6-144-144-144z m0 216c-39.7 0-72-32.3-72-72s32.3-72 72-72 72 32.3 72 72-32.3 72-72 72z"
|
||||
p-id="5142" fill="#6C707E"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 680 B |
5
src/main/resources/icons/percentage_dark.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg t="1740037914635" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5141"
|
||||
width="16" height="16">
|
||||
<path d="M855.7 210.8l-42.4-42.4c-3.1-3.1-8.2-3.1-11.3 0L168.3 801.9c-3.1 3.1-3.1 8.2 0 11.3l42.4 42.4c3.1 3.1 8.2 3.1 11.3 0L855.6 222c3.2-3 3.2-8.1 0.1-11.2zM304 448c79.4 0 144-64.6 144-144s-64.6-144-144-144-144 64.6-144 144 64.6 144 144 144z m0-216c39.7 0 72 32.3 72 72s-32.3 72-72 72-72-32.3-72-72 32.3-72 72-72zM720 576c-79.4 0-144 64.6-144 144s64.6 144 144 144 144-64.6 144-144-64.6-144-144-144z m0 216c-39.7 0-72-32.3-72-72s32.3-72 72-72 72 32.3 72 72-32.3 72-72 72z"
|
||||
p-id="5142" fill="#CED0D6"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 680 B |
5
src/main/resources/icons/text.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg t="1740038430861" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7036"
|
||||
width="16" height="16">
|
||||
<path d="M853.333333 138.666667H170.666667c-17.066667 0-32 14.933333-32 32v128c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V202.666667h277.333333v618.666666H384c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h256c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32h-96v-618.666666h277.333333V298.666667c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V170.666667c0-17.066667-14.933333-32-32-32z"
|
||||
fill="#6C707E" p-id="7037"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 609 B |
5
src/main/resources/icons/text_dark.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg t="1740038430861" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7036"
|
||||
width="16" height="16">
|
||||
<path d="M853.333333 138.666667H170.666667c-17.066667 0-32 14.933333-32 32v128c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V202.666667h277.333333v618.666666H384c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h256c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32h-96v-618.666666h277.333333V298.666667c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V170.666667c0-17.066667-14.933333-32-32-32z"
|
||||
fill="#CED0D6" p-id="7037"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 609 B |
85
src/main/resources/termora.iss
Normal file
@@ -0,0 +1,85 @@
|
||||
; Script generated by the Inno Setup Script Wizard.
|
||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
|
||||
#define MyAppPublisher "TermoraDev"
|
||||
#define MyAppURL "https://github.com/TermoraDev/termora"
|
||||
#define MyAppSupportURL "https://github.com/TermoraDev/termora/issues"
|
||||
#define MyAppUpdatesURL "https://github.com/TermoraDev/termora/releases"
|
||||
#define MyAppExeName "Termora.exe"
|
||||
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||
AppId={#MyAppId}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
AppVerName={#MyAppName} {#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
AppPublisherURL={#MyAppURL}
|
||||
AppSupportURL={#MyAppSupportURL}
|
||||
AppUpdatesURL={#MyAppUpdatesURL}
|
||||
DefaultDirName={autopf}\{#MyAppName}
|
||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||
; "ArchitecturesAllowed=x64compatible" specifies that Setup cannot run
|
||||
; on anything but x64 and Windows 11 on Arm.
|
||||
ArchitecturesAllowed=x64compatible
|
||||
; "ArchitecturesInstallIn64BitMode=x64compatible" requests that the
|
||||
; install be done in "64-bit mode" on x64 or Windows 11 on Arm,
|
||||
; meaning it should use the native 64-bit Program Files directory and
|
||||
; the 64-bit view of the registry.
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
DisableProgramGroupPage=yes
|
||||
; Uncomment the following line to run in non administrative install mode (install for current user only).
|
||||
;PrivilegesRequired=lowest
|
||||
OutputDir={#MyOutputDir}
|
||||
OutputBaseFilename={#MyAppName}-{#MyAppVersion}
|
||||
SolidCompression=yes
|
||||
WizardStyle=classic
|
||||
;WizardStyle=modern
|
||||
SetupIconFile={#MySetupIconFile}
|
||||
;WizardSmallImageFile=
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
|
||||
[Files]
|
||||
Source: "{#MySourceDir}\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#MySourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall; Check: ShouldPromptStart
|
||||
Filename: "{app}\{#MyAppExeName}"; Flags: nowait runhidden; Check: ShouldAutoStart
|
||||
|
||||
[Code]
|
||||
function CmdLineParamExists(const Value: string): Boolean;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
for I := 1 to ParamCount do
|
||||
if CompareText(ParamStr(I), Value) = 0 then
|
||||
begin
|
||||
Result := True;
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
|
||||
function ShouldAutoStart: Boolean;
|
||||
begin
|
||||
// 静默模式下且包含 /AUTOSTART 参数时自动启动
|
||||
Result := WizardSilent and CmdLineParamExists('/AUTOSTART');
|
||||
end;
|
||||
|
||||
function ShouldPromptStart: Boolean;
|
||||
begin
|
||||
Result := not WizardSilent;
|
||||
end;
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM linuxserver/openssh-server
|
||||
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 inetutils-telnet && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
|
||||
&& apk update && apk add wget gcc g++ git make zsh htop stress-ng inetutils-telnet && 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 \
|
||||
&& ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz
|
||||
|
||||
|
||||