mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-15 18:02:58 +08:00
Init Commit
This commit is contained in:
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
data/
|
||||||
|
dist/
|
||||||
|
certs/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
!**/src/main/**/out/
|
||||||
|
!**/src/test/**/out/
|
||||||
|
|
||||||
|
### Kotlin ###
|
||||||
|
.kotlin
|
||||||
|
|
||||||
|
### Eclipse ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
bin/
|
||||||
|
!**/src/main/**/bin/
|
||||||
|
!**/src/test/**/bin/
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### Mac OS ###
|
||||||
|
.DS_Store
|
||||||
45
README.md
Normal file
45
README.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Termora
|
||||||
|
|
||||||
|
**Termora** 是一个终端模拟器和 SSH 客户端,支持 Windows,macOS 和 Linux。
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="./docs/readme.png" alt="termora" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
**Termora** 采用 [Kotlin/JVM](https://kotlinlang.org/) 开发并实现了 [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) 协议(尚未完全实现),它的最终目标是通过 [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) 实现全平台(含 Android、iOS、iPadOS 等)。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 支持 SSH 和本地终端
|
||||||
|
- 支持 Windows、macOS、Linux 平台
|
||||||
|
- 支持 Zmodem 协议
|
||||||
|
- 支持 SSH 端口转发
|
||||||
|
- 支持配置同步到 [Gist](https://gist.github.com)
|
||||||
|
- 支持宏(录制脚本并回放)
|
||||||
|
- 支持关键词高亮
|
||||||
|
- 支持密钥管理器
|
||||||
|
- 支持将命令发送到多个会话
|
||||||
|
- 支持 [Find Everywhere](./docs/findeverywhere.png) 快速跳转
|
||||||
|
- 支持数据加密
|
||||||
|
- ...
|
||||||
|
|
||||||
|
## 下载
|
||||||
|
|
||||||
|
- [releases](https://github.com/TermoraDev/termora/releases/latest)
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
由于苹果开发者证书正在申请中,所以 macOS 用户需要执行 `sudo xattr -r -d com.apple.quarantine /Applications/Termora.app` 后才可以运行程序。
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) 的 JDK 版本,通过 `./gradlew :run`即可运行程序。
|
||||||
|
|
||||||
|
通过 `./gradlew dist` 可以自动构建适用于本机的版本。在 macOS 上是:`dmg`,在 Windows 上是:`zip`,在 Linux 上是:`tar.gz`。
|
||||||
|
|
||||||
|
## 协议
|
||||||
|
|
||||||
|
本软件采用双重许可模式,您可以选择以下任意一种许可方式:
|
||||||
|
|
||||||
|
- AGPL-3.0:根据 [AGPL-3.0](https://opensource.org/license/agpl-v3) 的条款,您可以自由使用、分发和修改本软件。
|
||||||
|
- 专有许可:如果希望在闭源或专有环境中使用,请联系作者获取许可。
|
||||||
231
THIRDPARTY
Normal file
231
THIRDPARTY
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
annotations 24.0.1
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/java-annotations/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
bip39-lib-jvm 1.0.8
|
||||||
|
MIT License
|
||||||
|
https://github.com/Electric-Coin-Company/kotlin-bip39/blob/main/LICENSE
|
||||||
|
|
||||||
|
colorpicker 2.0.1
|
||||||
|
BSD 3-Clause "New" or "Revised" License
|
||||||
|
https://github.com/dheid/colorpicker/blob/main/LICENSE
|
||||||
|
|
||||||
|
commonmark 0.24.0
|
||||||
|
BSD 2-Clause "Simplified" License
|
||||||
|
https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt
|
||||||
|
|
||||||
|
commons-codec 1.17.1
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/apache/commons-codec/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
commons-compress 1.27.1
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/apache/commons-compress/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
commons-io 2.18.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/apache/commons-io/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
commons-lang3 3.17.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/apache/commons-lang/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
commons-net 3.11.1
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/apache/commons-net/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
commons-text 1.12.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/apache/commons-text/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
eddsa 0.3.0
|
||||||
|
Creative Commons Zero v1.0 Universal
|
||||||
|
https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
flatlaf 3.5.4
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
|
|
||||||
|
flatlaf-extras 3.5.4
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
|
|
||||||
|
flatlaf-swingx 3.5.4
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
|
|
||||||
|
JavaEWAH 1.2.3
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/lemire/javaewah/blob/master/LICENSE
|
||||||
|
|
||||||
|
jbr-api 17.1.10.1
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/JetBrainsRuntimeApi/blob/main/LICENSE
|
||||||
|
|
||||||
|
jcl-over-slf4j 1.7.36
|
||||||
|
Apache License 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0.txt
|
||||||
|
|
||||||
|
jfa 1.2.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/0x4a616e/jfa/blob/main/LICENSE
|
||||||
|
|
||||||
|
jgoodies-common 1.8.1
|
||||||
|
BSD-2-Clause License
|
||||||
|
http://www.opensource.org/licenses/bsd-license.html
|
||||||
|
|
||||||
|
jgoodies-forms 1.9.0
|
||||||
|
BSD-2-Clause License
|
||||||
|
http://www.opensource.org/licenses/bsd-license.html
|
||||||
|
|
||||||
|
jna 5.16.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/java-native-access/jna/blob/master/AL2.0
|
||||||
|
|
||||||
|
jna-platform 5.16.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/java-native-access/jna/blob/master/AL2.0
|
||||||
|
|
||||||
|
jnafilechooser-api 1.1.2
|
||||||
|
BSD 3-Clause "New" or "Revised" License
|
||||||
|
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
||||||
|
|
||||||
|
jnafilechooser-win32 1.1.2
|
||||||
|
BSD 3-Clause "New" or "Revised" License
|
||||||
|
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
||||||
|
|
||||||
|
jsvg 1.4.0
|
||||||
|
MIT License
|
||||||
|
https://github.com/weisJ/jsvg/blob/master/LICENSE
|
||||||
|
|
||||||
|
jSystemThemeDetector 3.9.1
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/Dansoftowner/jSystemThemeDetector/blob/master/LICENSE
|
||||||
|
|
||||||
|
kotlin-logging 1.7.9
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/oshai/kotlin-logging/blob/master/LICENSE
|
||||||
|
|
||||||
|
kotlin-stdlib 2.1.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
|
kotlin-stdlib-jdk7 1.9.10
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
|
kotlin-stdlib-jdk8 1.9.10
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
|
kotlin-stdlib-jdk8 1.9.10
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
|
kotlinx-coroutines-core-jvm 1.10.1
|
||||||
|
Apache License 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
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
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
kotlinx-serialization-json-jvm 1.7.3
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
logging-interceptor 4.12.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
okhttp 4.12.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
okio-jvm 3.6.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
org.eclipse.jgit.ssh.apache 7.1.0.202411261347-r
|
||||||
|
Eclipse Distribution License
|
||||||
|
https://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
|
||||||
|
org.eclipse.jgit 7.1.0.202411261347-r
|
||||||
|
Eclipse Distribution License
|
||||||
|
https://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
|
||||||
|
oshi-core 6.6.5
|
||||||
|
MIT License
|
||||||
|
https://github.com/oshi/oshi/blob/master/LICENSE
|
||||||
|
|
||||||
|
pty4j 0.13.2
|
||||||
|
Eclipse Public License 1.0
|
||||||
|
https://github.com/JetBrains/pty4j/blob/master/LICENSE
|
||||||
|
|
||||||
|
slf4j-api 2.0.16
|
||||||
|
MIT License
|
||||||
|
https://github.com/qos-ch/slf4j/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
slf4j-tinylog 2.7.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||||
|
|
||||||
|
sshd-common 2.14.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
sshd-core 2.14.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
sshd-osgi 2.14.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
sshd-sftp 2.14.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
swingx-all 1.6.5-1
|
||||||
|
GNU LESSER GENERAL PUBLIC LICENSE v3
|
||||||
|
https://www.gnu.org/licenses/lgpl-3.0
|
||||||
|
|
||||||
|
tinylog-api 2.7.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||||
|
|
||||||
|
tinylog-impl 2.7.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||||
|
|
||||||
|
versioncompare 1.4.1
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/G00fY2/version-compare/blob/main/LICENSE
|
||||||
|
|
||||||
|
xodus-compress 2.0.1
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
xodus-environment 2.0.1
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
xodus-openAPI 2.0.1
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
xodus-utils 2.0.1
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
xodus-vfs 2.0.1
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
jediterm
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
|
||||||
293
build.gradle.kts
Normal file
293
build.gradle.kts
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
import org.gradle.internal.jvm.Jvm
|
||||||
|
import org.gradle.kotlin.dsl.support.uppercaseFirstChar
|
||||||
|
import org.gradle.nativeplatform.platform.internal.ArchitectureInternal
|
||||||
|
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||||
|
import org.gradle.nativeplatform.platform.internal.DefaultOperatingSystem
|
||||||
|
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
java
|
||||||
|
application
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
alias(libs.plugins.kotlinx.serialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
group = "app.termora"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
val os: DefaultOperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||||
|
var arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||||
|
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies")
|
||||||
|
maven("https://www.jitpack.io")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
testImplementation(libs.hutool)
|
||||||
|
testImplementation(libs.sshj)
|
||||||
|
testImplementation(platform(libs.koin.bom))
|
||||||
|
testImplementation(libs.koin.core)
|
||||||
|
testImplementation(libs.jsch)
|
||||||
|
testImplementation(libs.rhino)
|
||||||
|
testImplementation(libs.delight.rhino.sandbox)
|
||||||
|
|
||||||
|
implementation(libs.slf4j.api)
|
||||||
|
implementation(libs.pty4j)
|
||||||
|
implementation(libs.slf4j.tinylog)
|
||||||
|
implementation(libs.tinylog.impl)
|
||||||
|
implementation(libs.commons.codec)
|
||||||
|
implementation(libs.commons.io)
|
||||||
|
implementation(libs.commons.lang3)
|
||||||
|
implementation(libs.commons.net)
|
||||||
|
implementation(libs.commons.text)
|
||||||
|
implementation(libs.commons.compress)
|
||||||
|
implementation(libs.kotlinx.coroutines.swing)
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
implementation(libs.flatlaf)
|
||||||
|
implementation(libs.flatlaf.extras)
|
||||||
|
implementation(libs.flatlaf.swingx)
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
implementation(libs.swingx)
|
||||||
|
implementation(libs.jgoodies.forms)
|
||||||
|
implementation(libs.jna)
|
||||||
|
implementation(libs.jna.platform)
|
||||||
|
implementation(libs.versioncompare)
|
||||||
|
implementation(libs.oshi.core)
|
||||||
|
implementation(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
||||||
|
implementation(libs.jfa) { exclude(group = "*", module = "*") }
|
||||||
|
implementation(libs.jbr.api)
|
||||||
|
implementation(libs.okhttp)
|
||||||
|
implementation(libs.okhttp.logging)
|
||||||
|
implementation(libs.sshd.core)
|
||||||
|
implementation(libs.commonmark)
|
||||||
|
implementation(libs.jgit)
|
||||||
|
implementation(libs.jgit.sshd)
|
||||||
|
implementation(libs.jnafilechooser)
|
||||||
|
implementation(libs.xodus.vfs)
|
||||||
|
implementation(libs.xodus.openAPI)
|
||||||
|
implementation(libs.xodus.environment)
|
||||||
|
implementation(libs.bip39)
|
||||||
|
implementation(libs.colorpicker)
|
||||||
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
val args = mutableListOf(
|
||||||
|
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
||||||
|
)
|
||||||
|
|
||||||
|
if (os.isMacOsX) {
|
||||||
|
args.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||||
|
args.add("-Dsun.java2d.metal=true")
|
||||||
|
args.add("-Dapple.awt.application.appearance=system")
|
||||||
|
}
|
||||||
|
|
||||||
|
args.add("-Dapp-version=${project.version}")
|
||||||
|
|
||||||
|
if (os.isLinux) {
|
||||||
|
args.add("-Dsun.java2d.opengl=true")
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationDefaultJvmArgs = args
|
||||||
|
mainClass = "app.termora.MainKt"
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Copy>("copy-dependencies") {
|
||||||
|
from(configurations.runtimeClasspath)
|
||||||
|
.into("${layout.buildDirectory.get()}/libs")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Exec>("jlink") {
|
||||||
|
val modules = listOf(
|
||||||
|
"java.base",
|
||||||
|
"java.desktop",
|
||||||
|
"java.logging",
|
||||||
|
"java.management",
|
||||||
|
"java.rmi",
|
||||||
|
"java.security.jgss",
|
||||||
|
"jdk.crypto.ec",
|
||||||
|
"jdk.unsupported",
|
||||||
|
)
|
||||||
|
|
||||||
|
commandLine(
|
||||||
|
"${Jvm.current().javaHome}/bin/jlink",
|
||||||
|
"--verbose",
|
||||||
|
"--strip-java-debug-attributes",
|
||||||
|
"--strip-native-commands",
|
||||||
|
"--strip-debug",
|
||||||
|
"--compress=zip-9",
|
||||||
|
"--no-header-files",
|
||||||
|
"--no-man-pages",
|
||||||
|
"--add-modules",
|
||||||
|
modules.joinToString(","),
|
||||||
|
"--output",
|
||||||
|
"${layout.buildDirectory.get()}/jlink"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Exec>("jpackage") {
|
||||||
|
val buildDir = layout.buildDirectory.get()
|
||||||
|
val options = mutableListOf(
|
||||||
|
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
||||||
|
"-Xmx2g",
|
||||||
|
"-XX:+HeapDumpOnOutOfMemoryError",
|
||||||
|
"-Dlogger.console.level=off",
|
||||||
|
"-Dkotlinx.coroutines.debug=off",
|
||||||
|
"-Dapp-version=${project.version}",
|
||||||
|
)
|
||||||
|
|
||||||
|
if (os.isMacOsX) {
|
||||||
|
options.add("-Dsun.java2d.metal=true")
|
||||||
|
options.add("-Dapple.awt.application.appearance=system")
|
||||||
|
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||||
|
} else {
|
||||||
|
options.add("-Dsun.java2d.opengl=true")
|
||||||
|
}
|
||||||
|
|
||||||
|
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage", "--verbose")
|
||||||
|
arguments.addAll(listOf("--runtime-image", "${buildDir}/jlink"))
|
||||||
|
arguments.addAll(listOf("--name", project.name.uppercaseFirstChar()))
|
||||||
|
arguments.addAll(listOf("--app-version", "${project.version}"))
|
||||||
|
arguments.addAll(listOf("--main-jar", tasks.jar.get().archiveFileName.get()))
|
||||||
|
arguments.addAll(listOf("--main-class", application.mainClass.get()))
|
||||||
|
arguments.addAll(listOf("--input", "$buildDir/libs"))
|
||||||
|
arguments.addAll(listOf("--temp", "$buildDir/jpackage"))
|
||||||
|
arguments.addAll(listOf("--dest", "$buildDir/distributions"))
|
||||||
|
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE)))
|
||||||
|
|
||||||
|
|
||||||
|
if (os.isMacOsX) {
|
||||||
|
arguments.addAll(listOf("--mac-package-name", project.name.uppercaseFirstChar()))
|
||||||
|
arguments.addAll(listOf("--mac-app-category", "developer-tools"))
|
||||||
|
arguments.addAll(listOf("--mac-package-identifier", "${project.group}"))
|
||||||
|
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.icns"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (os.isWindows) {
|
||||||
|
arguments.add("--win-dir-chooser")
|
||||||
|
arguments.add("--win-shortcut")
|
||||||
|
arguments.add("--win-shortcut-prompt")
|
||||||
|
arguments.addAll(listOf("--win-upgrade-uuid", "E1D93CAD-5BF8-442E-93BA-6E90DE601E4C"))
|
||||||
|
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.ico"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
arguments.add("--type")
|
||||||
|
if (os.isMacOsX) {
|
||||||
|
arguments.add("dmg")
|
||||||
|
} else if (os.isWindows) {
|
||||||
|
arguments.add("msi")
|
||||||
|
} else if (os.isLinux) {
|
||||||
|
arguments.add("app-image")
|
||||||
|
} else {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
commandLine(arguments)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("dist") {
|
||||||
|
doLast {
|
||||||
|
val vendor = Jvm.current().vendor ?: StringUtils.EMPTY
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
if (!JvmVendorSpec.JETBRAINS.matches(vendor)) {
|
||||||
|
throw GradleException("JVM: $vendor is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
val distributionDir = layout.buildDirectory.dir("distributions").get()
|
||||||
|
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
|
||||||
|
|
||||||
|
// 清空目录
|
||||||
|
exec { commandLine(gradlew, "clean") }
|
||||||
|
|
||||||
|
// 打包并复制依赖
|
||||||
|
exec { commandLine(gradlew, "jar", "copy-dependencies") }
|
||||||
|
|
||||||
|
// 检查依赖的开源协议
|
||||||
|
exec { commandLine(gradlew, "check-license") }
|
||||||
|
|
||||||
|
// jlink
|
||||||
|
exec { commandLine(gradlew, "jlink") }
|
||||||
|
|
||||||
|
// 打包
|
||||||
|
exec { commandLine(gradlew, "jpackage") }
|
||||||
|
|
||||||
|
// pack
|
||||||
|
exec {
|
||||||
|
if (os.isWindows) { // zip
|
||||||
|
commandLine(
|
||||||
|
"tar", "-vacf",
|
||||||
|
distributionDir.file("${project.name}-${project.version}-windows-${arch.name}.zip").asFile.absolutePath,
|
||||||
|
project.name.uppercaseFirstChar()
|
||||||
|
)
|
||||||
|
workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
|
||||||
|
} else if (os.isLinux) { // tar.gz
|
||||||
|
commandLine(
|
||||||
|
"tar", "-czvf",
|
||||||
|
distributionDir.file("${project.name}-${project.version}-linux-${arch.name}.tar.gz").asFile.absolutePath,
|
||||||
|
project.name.uppercaseFirstChar()
|
||||||
|
)
|
||||||
|
workingDir = distributionDir.asFile
|
||||||
|
} else if (os.isMacOsX) { // rename
|
||||||
|
commandLine(
|
||||||
|
"mv",
|
||||||
|
distributionDir.file("${project.name.uppercaseFirstChar()}-${project.version}.dmg").asFile.absolutePath,
|
||||||
|
distributionDir.file("${project.name}-${project.version}-osx-${arch.name}.dmg").asFile.absolutePath,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw GradleException("${os.name} is not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("check-license") {
|
||||||
|
doLast {
|
||||||
|
val thirdParty = mutableMapOf<String, String>()
|
||||||
|
val iterator = File(projectDir, "THIRDPARTY").readLines().iterator()
|
||||||
|
val thirdPartyNames = mutableSetOf<String>()
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val nameWithVersion = iterator.next()
|
||||||
|
if (nameWithVersion.isBlank()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore license name
|
||||||
|
iterator.next()
|
||||||
|
|
||||||
|
val license = iterator.next()
|
||||||
|
thirdParty[nameWithVersion.replace(StringUtils.SPACE, "-")] = license
|
||||||
|
thirdPartyNames.add(nameWithVersion.split(StringUtils.SPACE).first())
|
||||||
|
}
|
||||||
|
|
||||||
|
for (file in configurations.runtimeClasspath.get()) {
|
||||||
|
val name = file.nameWithoutExtension
|
||||||
|
if (!thirdParty.containsKey(name)) {
|
||||||
|
if (logger.isWarnEnabled) {
|
||||||
|
logger.warn("$name does not exist in third-party")
|
||||||
|
}
|
||||||
|
if (!thirdPartyNames.contains(name)) {
|
||||||
|
throw GradleException("$name No license found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
vendor = JvmVendorSpec.JETBRAINS
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
docs/findeverywhere.png
Normal file
BIN
docs/findeverywhere.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
BIN
docs/readme.png
Normal file
BIN
docs/readme.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
3
gradle.properties
Normal file
3
gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.caching=true
|
||||||
|
org.gradle.parallel=true
|
||||||
|
kotlin.code.style=official
|
||||||
98
gradle/libs.versions.toml
Normal file
98
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
[versions]
|
||||||
|
kotlin = "2.1.0"
|
||||||
|
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"
|
||||||
|
commons-lang3 = "3.17.0"
|
||||||
|
commons-net = "3.11.1"
|
||||||
|
commons-text = "1.12.0"
|
||||||
|
commons-compress = "1.27.1"
|
||||||
|
koin-bom = "4.0.0"
|
||||||
|
swingx = "1.6.5-1"
|
||||||
|
jgoodies-forms = "1.9.0"
|
||||||
|
jfa = "1.2.0"
|
||||||
|
oshi = "6.6.5"
|
||||||
|
versioncompare = "1.4.1"
|
||||||
|
jna = "5.16.0"
|
||||||
|
jSystemThemeDetector = "3.9.1"
|
||||||
|
commons-io = "2.18.0"
|
||||||
|
jbr-api = "17.1.10.1"
|
||||||
|
leveldb = "0.12"
|
||||||
|
guava = "33.3.1-jre"
|
||||||
|
credential-secure-storage = "1.0.3"
|
||||||
|
hutool = "5.8.34"
|
||||||
|
jsch = "0.2.21"
|
||||||
|
okhttp = "4.12.0"
|
||||||
|
bcprov = "1.79"
|
||||||
|
sshj = "0.39.0"
|
||||||
|
sshd-core = "2.14.0"
|
||||||
|
jgit = "7.1.0.202411261347-r"
|
||||||
|
commonmark = "0.24.0"
|
||||||
|
jnafilechooser = "1.1.2"
|
||||||
|
xodus = "2.0.1"
|
||||||
|
bip39 = "1.0.8"
|
||||||
|
colorpicker = "2.0.1"
|
||||||
|
rhino = "1.7.15"
|
||||||
|
delight-rhino-sandbox = "0.0.17"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||||
|
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
||||||
|
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
|
||||||
|
slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
|
||||||
|
slf4j-tinylog = { group = "org.tinylog", name = "slf4j-tinylog", version.ref = "tinylog" }
|
||||||
|
tinylog-impl = { group = "org.tinylog", name = "tinylog-impl", version.ref = "tinylog" }
|
||||||
|
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-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" }
|
||||||
|
flatlaf = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
|
||||||
|
flatlaf-extras = { group = "com.formdev", name = "flatlaf-extras", version.ref = "flatlaf" }
|
||||||
|
trove4j = { group = "org.jetbrains.intellij.deps", name = "trove4j", version.ref = "trove4j" }
|
||||||
|
koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
|
||||||
|
koin-core = { module = "io.insert-koin:koin-core" }
|
||||||
|
swingx = { module = "org.swinglabs.swingx:swingx-all", version.ref = "swingx" }
|
||||||
|
jgoodies-forms = { module = "com.jgoodies:jgoodies-forms", version.ref = "jgoodies-forms" }
|
||||||
|
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
||||||
|
jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
|
||||||
|
jSystemThemeDetector = { module = "com.github.Dansoftowner:jSystemThemeDetector", version.ref = "jSystemThemeDetector" }
|
||||||
|
versioncompare = { module = "io.github.g00fy2:versioncompare", version.ref = "versioncompare" }
|
||||||
|
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" }
|
||||||
|
jbr-api = { module = "com.jetbrains:jbr-api", version.ref = "jbr-api" }
|
||||||
|
flatlaf-swingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
|
||||||
|
leveldb = { module = "org.iq80.leveldb:leveldb", version.ref = "leveldb" }
|
||||||
|
guava = { module = "com.google.guava:guava", version.ref = "guava" }
|
||||||
|
hutool = { module = "cn.hutool:hutool-all", version.ref = "hutool" }
|
||||||
|
credential-secure-storage = { module = "com.microsoft:credential-secure-storage", version.ref = "credential-secure-storage" }
|
||||||
|
jsch = { module = "com.github.mwiede:jsch", version.ref = "jsch" }
|
||||||
|
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||||
|
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
||||||
|
bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bcprov" }
|
||||||
|
sshj = { module = "com.hierynomus:sshj", version.ref = "sshj" }
|
||||||
|
sshd-core = { module = "org.apache.sshd:sshd-core", version.ref = "sshd-core" }
|
||||||
|
jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" }
|
||||||
|
commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
|
||||||
|
jgit-sshd = { module = "org.eclipse.jgit:org.eclipse.jgit.ssh.apache", version.ref = "jgit" }
|
||||||
|
xodus-openAPI = { module = "org.jetbrains.xodus:xodus-openAPI", version.ref = "xodus" }
|
||||||
|
xodus-entity-store = { module = "org.jetbrains.xodus:xodus-entity-store", version.ref = "xodus" }
|
||||||
|
xodus-environment = { module = "org.jetbrains.xodus:xodus-environment", version.ref = "xodus" }
|
||||||
|
xodus-crypto = { module = "org.jetbrains.xodus:xodus-crypto", version.ref = "xodus" }
|
||||||
|
xodus-vfs = { module = "org.jetbrains.xodus:xodus-vfs", version.ref = "xodus" }
|
||||||
|
jnafilechooser = { module = "com.github.steos.jnafilechooser:jnafilechooser-api", version.ref = "jnafilechooser" }
|
||||||
|
bip39 = { module = "cash.z.ecc.android:kotlin-bip39-jvm", version.ref = "bip39" }
|
||||||
|
rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
||||||
|
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
|
||||||
|
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||||
|
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.10.2-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
234
gradlew
vendored
Executable file
234
gradlew
vendored
Executable file
@@ -0,0 +1,234 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
89
gradlew.bat
vendored
Normal file
89
gradlew.bat
vendored
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
5
settings.gradle.kts
Normal file
5
settings.gradle.kts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
plugins {
|
||||||
|
id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0"
|
||||||
|
}
|
||||||
|
rootProject.name = "termora"
|
||||||
|
|
||||||
14
src/main/java/app/termora/Disposable.java
Normal file
14
src/main/java/app/termora/Disposable.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||||
|
package app.termora;
|
||||||
|
|
||||||
|
public interface Disposable {
|
||||||
|
|
||||||
|
default void dispose() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Parent extends Disposable {
|
||||||
|
void beforeTreeDispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
192
src/main/java/app/termora/Disposer.java
Normal file
192
src/main/java/app/termora/Disposer.java
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||||
|
package app.termora;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.*;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Manages a parent-child relation of chained objects requiring cleanup.</p>
|
||||||
|
*
|
||||||
|
* <p>A root node can be created via {@link #newDisposable()}, to which children are attached via subsequent calls to {@link #register(Disposable, Disposable)}.
|
||||||
|
* Invoking {@link #dispose(Disposable)} will process all its registered children's {@link Disposable#dispose()} method.</p>
|
||||||
|
* <p>
|
||||||
|
* See <a href="https://www.jetbrains.org/intellij/sdk/docs/basics/disposers.html">Disposer and Disposable</a> in SDK Docs.
|
||||||
|
*
|
||||||
|
* @see Disposable
|
||||||
|
*/
|
||||||
|
public final class Disposer {
|
||||||
|
private static final ObjectTree ourTree = new ObjectTree();
|
||||||
|
|
||||||
|
public static boolean isDebugDisposerOn() {
|
||||||
|
return "on".equals(System.getProperty("idea.disposer.debug"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean ourDebugMode;
|
||||||
|
|
||||||
|
private Disposer() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Contract(pure = true, value = "->new")
|
||||||
|
public static Disposable newDisposable() {
|
||||||
|
// must not be lambda because we care about identity in ObjectTree.myObject2NodeMap
|
||||||
|
return newDisposable("");
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Contract(pure = true, value = "_->new")
|
||||||
|
public static Disposable newDisposable(@NotNull @NonNls String debugName) {
|
||||||
|
// must not be lambda because we care about identity in ObjectTree.myObject2NodeMap
|
||||||
|
return new Disposable() {
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return debugName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Contract(pure = true, value = "_,_->new")
|
||||||
|
public static @NotNull Disposable newDisposable(@NotNull Disposable parentDisposable, @NotNull String debugName) {
|
||||||
|
Disposable result = newDisposable(debugName);
|
||||||
|
register(parentDisposable, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Map<String, Disposable> ourKeyDisposables = Collections.synchronizedMap(new WeakHashMap<>());
|
||||||
|
|
||||||
|
|
||||||
|
public static void register(@NotNull Disposable parent, @NotNull Disposable child) {
|
||||||
|
RuntimeException e = ourTree.register(parent, child);
|
||||||
|
if (e != null) throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers child disposable under parent unless the parent has already been disposed
|
||||||
|
*
|
||||||
|
* @return whether the registration succeeded
|
||||||
|
*/
|
||||||
|
public static boolean tryRegister(@NotNull Disposable parent, @NotNull Disposable child) {
|
||||||
|
return ourTree.register(parent, child) == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void register(@NotNull Disposable parent, @NotNull Disposable child, @NonNls @NotNull final String key) {
|
||||||
|
register(parent, child);
|
||||||
|
Disposable v = get(key);
|
||||||
|
if (v != null) throw new IllegalArgumentException("Key " + key + " already registered: " + v);
|
||||||
|
ourKeyDisposables.put(key, child);
|
||||||
|
register(child, new KeyDisposable(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class KeyDisposable implements Disposable {
|
||||||
|
@NotNull
|
||||||
|
private final String myKey;
|
||||||
|
|
||||||
|
KeyDisposable(@NotNull String key) {
|
||||||
|
myKey = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
ourKeyDisposables.remove(myKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "KeyDisposable (" + myKey + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if {@code disposable} is disposed or being disposed (i.e. its {@link Disposable#dispose()} method is executing).
|
||||||
|
*/
|
||||||
|
public static boolean isDisposed(@NotNull Disposable disposable) {
|
||||||
|
return ourTree.getDisposalInfo(disposable) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use {@link #isDisposed(Disposable)} instead
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public static boolean isDisposing(@NotNull Disposable disposable) {
|
||||||
|
return isDisposed(disposable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Disposable get(@NotNull String key) {
|
||||||
|
return ourKeyDisposables.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void dispose(@NotNull Disposable disposable) {
|
||||||
|
dispose(disposable, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code predicate} is used only for direct children.
|
||||||
|
*/
|
||||||
|
@ApiStatus.Internal
|
||||||
|
public static void disposeChildren(@NotNull Disposable disposable, @Nullable Predicate<? super Disposable> predicate) {
|
||||||
|
ourTree.executeAllChildren(disposable, predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void dispose(@NotNull Disposable disposable, boolean processUnregistered) {
|
||||||
|
ourTree.executeAll(disposable, processUnregistered);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public static ObjectTree getTree() {
|
||||||
|
return ourTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void assertIsEmpty() {
|
||||||
|
assertIsEmpty(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void assertIsEmpty(boolean throwError) {
|
||||||
|
if (ourDebugMode) {
|
||||||
|
ourTree.assertIsEmpty(throwError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return old value
|
||||||
|
*/
|
||||||
|
public static boolean setDebugMode(boolean debugMode) {
|
||||||
|
if (debugMode) {
|
||||||
|
debugMode = !"off".equals(System.getProperty("idea.disposer.debug"));
|
||||||
|
}
|
||||||
|
boolean oldValue = ourDebugMode;
|
||||||
|
ourDebugMode = debugMode;
|
||||||
|
return oldValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isDebugMode() {
|
||||||
|
return ourDebugMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return object registered on {@code parentDisposable} which is equal to object, or {@code null} if not found
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static <T extends Disposable> T findRegisteredObject(@NotNull Disposable parentDisposable, @NotNull T object) {
|
||||||
|
return ourTree.findRegisteredObject(parentDisposable, object);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Throwable getDisposalTrace(@NotNull Disposable disposable) {
|
||||||
|
if (getTree().getDisposalInfo(disposable) instanceof Throwable) {
|
||||||
|
return (Throwable) disposable;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiStatus.Internal
|
||||||
|
public static void clearDisposalTraces() {
|
||||||
|
ourTree.clearDisposedObjectTraces();
|
||||||
|
}
|
||||||
|
}
|
||||||
128
src/main/java/app/termora/ObjectNode.java
Normal file
128
src/main/java/app/termora/ObjectNode.java
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||||
|
package app.termora;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NonNls;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.jetbrains.annotations.TestOnly;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
final class ObjectNode {
|
||||||
|
private final ObjectTree myTree;
|
||||||
|
|
||||||
|
ObjectNode myParent; // guarded by myTree.treeLock
|
||||||
|
private final Disposable myObject;
|
||||||
|
|
||||||
|
private List<ObjectNode> myChildren; // guarded by myTree.treeLock
|
||||||
|
private Throwable myTrace; // guarded by myTree.treeLock
|
||||||
|
|
||||||
|
ObjectNode(@NotNull ObjectTree tree,
|
||||||
|
@Nullable ObjectNode parentNode,
|
||||||
|
@NotNull Disposable object) {
|
||||||
|
myTree = tree;
|
||||||
|
myParent = parentNode;
|
||||||
|
myObject = object;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void addChild(@NotNull ObjectNode child) {
|
||||||
|
List<ObjectNode> children = myChildren;
|
||||||
|
if (children == null) {
|
||||||
|
myChildren = new ArrayList<>();
|
||||||
|
myChildren.add(child);
|
||||||
|
} else {
|
||||||
|
children.add(child);
|
||||||
|
}
|
||||||
|
child.myParent = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeChild(@NotNull ObjectNode child) {
|
||||||
|
List<ObjectNode> children = myChildren;
|
||||||
|
if (children != null) {
|
||||||
|
// optimisation: iterate backwards
|
||||||
|
for (int i = children.size() - 1; i >= 0; i--) {
|
||||||
|
ObjectNode node = children.get(i);
|
||||||
|
if (node.equals(child)) {
|
||||||
|
children.remove(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
child.myParent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectNode getParent() {
|
||||||
|
return myParent;
|
||||||
|
}
|
||||||
|
|
||||||
|
void getAndRemoveRecursively(@NotNull List<? super Disposable> result) {
|
||||||
|
getAndRemoveChildrenRecursively(result, null);
|
||||||
|
myTree.removeObjectFromTree(this);
|
||||||
|
// already disposed. may happen when someone does `register(obj, ()->Disposer.dispose(t));` abomination
|
||||||
|
if (myTree.rememberDisposedTrace(myObject) == null) {
|
||||||
|
result.add(myObject);
|
||||||
|
}
|
||||||
|
myChildren = null;
|
||||||
|
myParent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code predicate} is used only for direct children.
|
||||||
|
*/
|
||||||
|
void getAndRemoveChildrenRecursively(@NotNull List<? super Disposable> result, @Nullable Predicate<? super Disposable> predicate) {
|
||||||
|
if (myChildren != null) {
|
||||||
|
for (int i = myChildren.size() - 1; i >= 0; i--) {
|
||||||
|
ObjectNode childNode = myChildren.get(i);
|
||||||
|
if (predicate == null || predicate.test(childNode.getObject())) {
|
||||||
|
childNode.getAndRemoveRecursively(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
Disposable getObject() {
|
||||||
|
return myObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNls
|
||||||
|
public String toString() {
|
||||||
|
return "Node: " + myObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
Throwable getTrace() {
|
||||||
|
return myTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearTrace() {
|
||||||
|
myTrace = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestOnly
|
||||||
|
void assertNoReferencesKept(@NotNull Disposable aDisposable) {
|
||||||
|
assert getObject() != aDisposable;
|
||||||
|
if (myChildren != null) {
|
||||||
|
for (ObjectNode node : myChildren) {
|
||||||
|
node.assertNoReferencesKept(aDisposable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<D extends Disposable> D findChildEqualTo(@NotNull D object) {
|
||||||
|
List<ObjectNode> children = myChildren;
|
||||||
|
if (children != null) {
|
||||||
|
for (ObjectNode node : children) {
|
||||||
|
Disposable nodeObject = node.getObject();
|
||||||
|
if (nodeObject.equals(object)) {
|
||||||
|
//noinspection unchecked
|
||||||
|
return (D) nodeObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
246
src/main/java/app/termora/ObjectTree.java
Normal file
246
src/main/java/app/termora/ObjectTree.java
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||||
|
package app.termora;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.jetbrains.annotations.TestOnly;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
final class ObjectTree {
|
||||||
|
private static final ThreadLocal<Throwable> ourTopmostDisposeTrace = new ThreadLocal<>();
|
||||||
|
|
||||||
|
private final Set<Disposable> myRootObjects = new HashSet<>();
|
||||||
|
// guarded by treeLock
|
||||||
|
private final Map<Disposable, ObjectNode> myObject2NodeMap = new HashMap<>();
|
||||||
|
// Disposable -> trace or boolean marker (if trace unavailable)
|
||||||
|
private final Map<Disposable, Object> myDisposedObjects = new WeakHashMap<>(); // guarded by treeLock
|
||||||
|
|
||||||
|
private final Object treeLock = new Object();
|
||||||
|
|
||||||
|
private ObjectNode getNode(@NotNull Disposable object) {
|
||||||
|
return myObject2NodeMap.get(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putNode(@NotNull Disposable object, @Nullable("null means remove") ObjectNode node) {
|
||||||
|
if (node == null) {
|
||||||
|
myObject2NodeMap.remove(object);
|
||||||
|
} else {
|
||||||
|
myObject2NodeMap.put(object, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
final RuntimeException register(@NotNull Disposable parent, @NotNull Disposable child) {
|
||||||
|
if (parent == child) return new IllegalArgumentException("Cannot register to itself: " + parent);
|
||||||
|
synchronized (treeLock) {
|
||||||
|
Object wasDisposed = getDisposalInfo(parent);
|
||||||
|
if (wasDisposed != null) {
|
||||||
|
return new IllegalStateException("Sorry but parent: " + parent + " has already been disposed " +
|
||||||
|
"(see the cause for stacktrace) so the child: " + child + " will never be disposed",
|
||||||
|
wasDisposed instanceof Throwable ? (Throwable) wasDisposed : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
myDisposedObjects.remove(child); // if we dispose thing and then register it back it means it's not disposed anymore
|
||||||
|
ObjectNode parentNode = getNode(parent);
|
||||||
|
if (parentNode == null) parentNode = createNodeFor(parent, null);
|
||||||
|
|
||||||
|
ObjectNode childNode = getNode(child);
|
||||||
|
if (childNode == null) {
|
||||||
|
childNode = createNodeFor(child, parentNode);
|
||||||
|
} else {
|
||||||
|
ObjectNode oldParent = childNode.getParent();
|
||||||
|
if (oldParent != null) {
|
||||||
|
oldParent.removeChild(childNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
myRootObjects.remove(child);
|
||||||
|
|
||||||
|
RuntimeException e = checkWasNotAddedAlready(parentNode, childNode);
|
||||||
|
if (e != null) return e;
|
||||||
|
|
||||||
|
parentNode.addChild(childNode);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object getDisposalInfo(@NotNull Disposable object) {
|
||||||
|
synchronized (treeLock) {
|
||||||
|
return myDisposedObjects.get(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RuntimeException checkWasNotAddedAlready(@NotNull ObjectNode childNode, @NotNull ObjectNode parentNode) {
|
||||||
|
for (ObjectNode node = childNode; node != null; node = node.getParent()) {
|
||||||
|
if (node == parentNode) {
|
||||||
|
return new IllegalStateException("'" + childNode.getObject() + "' was already added as a child of '" + parentNode.getObject() + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private ObjectNode createNodeFor(@NotNull Disposable object, @Nullable ObjectNode parentNode) {
|
||||||
|
final ObjectNode newNode = new ObjectNode(this, parentNode, object);
|
||||||
|
if (parentNode == null) {
|
||||||
|
myRootObjects.add(object);
|
||||||
|
}
|
||||||
|
putNode(object, newNode);
|
||||||
|
return newNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runWithTrace(@NotNull Supplier<? extends @NotNull List<Disposable>> removeFromTreeAction) {
|
||||||
|
boolean needTrace = Disposer.isDebugMode() && ourTopmostDisposeTrace.get() == null;
|
||||||
|
if (needTrace) {
|
||||||
|
ourTopmostDisposeTrace.set(new Throwable());
|
||||||
|
}
|
||||||
|
|
||||||
|
// first, atomically remove disposables from the tree to avoid "register during dispose" race conditions
|
||||||
|
List<Disposable> disposables;
|
||||||
|
synchronized (treeLock) {
|
||||||
|
disposables = removeFromTreeAction.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// second, call "beforeTreeDispose" in pre-order (some clients are hardcoded to see parents-then-children order in "beforeTreeDispose")
|
||||||
|
List<Throwable> exceptions = null;
|
||||||
|
for (int i = disposables.size() - 1; i >= 0; i--) {
|
||||||
|
Disposable disposable = disposables.get(i);
|
||||||
|
if (disposable instanceof Disposable.Parent) {
|
||||||
|
try {
|
||||||
|
((Disposable.Parent) disposable).beforeTreeDispose();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
if (exceptions == null) exceptions = new ArrayList<>();
|
||||||
|
exceptions.add(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// third, dispose in post-order (bottom-up)
|
||||||
|
for (Disposable disposable : disposables) {
|
||||||
|
try {
|
||||||
|
//noinspection SSBasedInspection
|
||||||
|
disposable.dispose();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
if (exceptions == null) exceptions = new ArrayList<>();
|
||||||
|
exceptions.add(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needTrace) {
|
||||||
|
ourTopmostDisposeTrace.remove();
|
||||||
|
}
|
||||||
|
if (exceptions != null) {
|
||||||
|
handleExceptions(exceptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void executeAllChildren(@NotNull Disposable object, @Nullable Predicate<? super Disposable> predicate) {
|
||||||
|
runWithTrace(() -> {
|
||||||
|
ObjectNode node = getNode(object);
|
||||||
|
if (node == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Disposable> disposables = new ArrayList<>();
|
||||||
|
node.getAndRemoveChildrenRecursively(disposables, predicate);
|
||||||
|
return disposables;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void executeAll(@NotNull Disposable object, boolean processUnregistered) {
|
||||||
|
runWithTrace(() -> {
|
||||||
|
ObjectNode node = getNode(object);
|
||||||
|
if (node == null && !processUnregistered) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<Disposable> disposables = new ArrayList<>();
|
||||||
|
if (node == null) {
|
||||||
|
if (rememberDisposedTrace(object) == null) {
|
||||||
|
disposables.add(object);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node.getAndRemoveRecursively(disposables);
|
||||||
|
}
|
||||||
|
return disposables;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleExceptions(@NotNull List<? extends Throwable> exceptions) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestOnly
|
||||||
|
void assertNoReferenceKeptInTree(@NotNull Disposable disposable) {
|
||||||
|
synchronized (treeLock) {
|
||||||
|
for (Map.Entry<Disposable, ObjectNode> entry : myObject2NodeMap.entrySet()) {
|
||||||
|
Disposable key = entry.getKey();
|
||||||
|
assert key != disposable;
|
||||||
|
ObjectNode node = entry.getValue();
|
||||||
|
node.assertNoReferencesKept(disposable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void assertIsEmpty(boolean throwError) {
|
||||||
|
synchronized (treeLock) {
|
||||||
|
for (Disposable object : myRootObjects) {
|
||||||
|
if (object == null) continue;
|
||||||
|
ObjectNode objectNode = getNode(object);
|
||||||
|
if (objectNode == null) continue;
|
||||||
|
while (objectNode.getParent() != null) {
|
||||||
|
objectNode = objectNode.getParent();
|
||||||
|
}
|
||||||
|
final Throwable trace = objectNode.getTrace();
|
||||||
|
RuntimeException exception = new RuntimeException("Memory leak detected: '" + object + "' of " + object.getClass()
|
||||||
|
+ "\nSee the cause for the corresponding Disposer.register() stacktrace:\n",
|
||||||
|
trace);
|
||||||
|
if (throwError) {
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// return old value
|
||||||
|
Object rememberDisposedTrace(@NotNull Disposable object) {
|
||||||
|
synchronized (treeLock) {
|
||||||
|
Throwable trace = ourTopmostDisposeTrace.get();
|
||||||
|
return myDisposedObjects.put(object, trace != null ? trace : Boolean.TRUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearDisposedObjectTraces() {
|
||||||
|
synchronized (treeLock) {
|
||||||
|
myDisposedObjects.clear();
|
||||||
|
for (ObjectNode value : myObject2NodeMap.values()) {
|
||||||
|
value.clearTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
<D extends Disposable> D findRegisteredObject(@NotNull Disposable parentDisposable, @NotNull D object) {
|
||||||
|
synchronized (treeLock) {
|
||||||
|
ObjectNode parentNode = getNode(parentDisposable);
|
||||||
|
if (parentNode == null) return null;
|
||||||
|
return parentNode.findChildEqualTo(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeObjectFromTree(@NotNull ObjectNode node) {
|
||||||
|
synchronized (treeLock) {
|
||||||
|
Disposable myObject = node.getObject();
|
||||||
|
putNode(myObject, null);
|
||||||
|
ObjectNode parent = node.getParent();
|
||||||
|
if (parent == null) {
|
||||||
|
myRootObjects.remove(myObject);
|
||||||
|
} else {
|
||||||
|
parent.removeChild(node);
|
||||||
|
}
|
||||||
|
node.myParent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/main/java/zmodem/FileCopyStreamEvent.kt
Normal file
34
src/main/java/zmodem/FileCopyStreamEvent.kt
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package zmodem
|
||||||
|
|
||||||
|
import org.apache.commons.net.io.CopyStreamEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果一共两个文件,并且传输第一个文件时:
|
||||||
|
*
|
||||||
|
* remaining = 1
|
||||||
|
* index = 1
|
||||||
|
*/
|
||||||
|
class FileCopyStreamEvent(
|
||||||
|
source: Any,
|
||||||
|
// 本次传输的文件名
|
||||||
|
val filename: String,
|
||||||
|
// 剩余未传输的文件数量
|
||||||
|
val remaining: Int,
|
||||||
|
// 第几个文件
|
||||||
|
val index: Int,
|
||||||
|
// 总字节数
|
||||||
|
totalBytesTransferred: Long,
|
||||||
|
// 已经传输完成的字节数
|
||||||
|
bytesTransferred: Int,
|
||||||
|
// 本次传输的字节数
|
||||||
|
streamSize: Long,
|
||||||
|
/**
|
||||||
|
* 这个文件被跳过了
|
||||||
|
*/
|
||||||
|
val skip: Boolean = false,
|
||||||
|
) :
|
||||||
|
CopyStreamEvent(
|
||||||
|
source, totalBytesTransferred,
|
||||||
|
bytesTransferred,
|
||||||
|
streamSize
|
||||||
|
)
|
||||||
24
src/main/java/zmodem/XModem.java
Normal file
24
src/main/java/zmodem/XModem.java
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package zmodem;
|
||||||
|
|
||||||
|
import zmodem.xfer.zm.util.Modem;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class XModem {
|
||||||
|
private Modem modem;
|
||||||
|
|
||||||
|
public XModem(InputStream inputStream, OutputStream outputStream) {
|
||||||
|
this.modem = new Modem(inputStream, outputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void send(Path file, boolean useBlock1K) throws IOException, InterruptedException {
|
||||||
|
modem.send(file, useBlock1K);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void receive(Path file) throws IOException {
|
||||||
|
modem.receive(file, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
207
src/main/java/zmodem/YModem.java
Normal file
207
src/main/java/zmodem/YModem.java
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
package zmodem;
|
||||||
|
|
||||||
|
import zmodem.xfer.util.CRC16;
|
||||||
|
import zmodem.xfer.util.CRC8;
|
||||||
|
import zmodem.xfer.util.XCRC;
|
||||||
|
import zmodem.xfer.zm.util.Modem;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* YModem.<br/>
|
||||||
|
* Block 0 contain minimal file information (only filename)<br/>
|
||||||
|
* <p>
|
||||||
|
* Created by Anton Sirotinkin (aesirot@mail.ru), Moscow 2014<br/>
|
||||||
|
* I hope you will find this program useful.<br/>
|
||||||
|
* You are free to use/modify the code for any purpose, but please leave a reference to me.<br/>
|
||||||
|
* <br/>
|
||||||
|
*/
|
||||||
|
public class YModem {
|
||||||
|
private Modem modem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param inputStream stream for reading received data from other side
|
||||||
|
* @param outputStream stream for writing data to other side
|
||||||
|
*/
|
||||||
|
public YModem(InputStream inputStream, OutputStream outputStream) {
|
||||||
|
this.modem = new Modem(inputStream, outputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a file.<br/>
|
||||||
|
* <p>
|
||||||
|
* This method support correct thread interruption, when thread is interrupted "cancel of transmission" will be send.
|
||||||
|
* So you can move long transmission to other thread and interrupt it according to your algorithm.
|
||||||
|
*
|
||||||
|
* @param file
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void send(Path file) throws IOException {
|
||||||
|
//check filename
|
||||||
|
// if (!file.getFileName().toString().matches("\\w{1,8}\\.\\w{1,3}")) {
|
||||||
|
// throw new IOException("Filename must be in DOS style (no spaces, max 8.3)");
|
||||||
|
// }
|
||||||
|
|
||||||
|
//open file
|
||||||
|
try (DataInputStream dataStream = new DataInputStream(Files.newInputStream(file))) {
|
||||||
|
|
||||||
|
boolean useCRC16 = modem.waitReceiverRequest();
|
||||||
|
XCRC crc;
|
||||||
|
if (useCRC16)
|
||||||
|
crc = new CRC16();
|
||||||
|
else
|
||||||
|
crc = new CRC8();
|
||||||
|
|
||||||
|
//send block 0
|
||||||
|
BasicFileAttributes readAttributes = Files.readAttributes(file, BasicFileAttributes.class);
|
||||||
|
String fileNameString = file.getFileName().toString() + (char) 0 + ((Long) Files.size(file)).toString() + " " + Long.toOctalString(readAttributes.lastModifiedTime().toMillis() / 1000);
|
||||||
|
byte[] fileNameBytes = Arrays.copyOf(fileNameString.getBytes(), 128);
|
||||||
|
modem.sendBlock(0, Arrays.copyOf(fileNameBytes, 128), 128, crc);
|
||||||
|
|
||||||
|
modem.waitReceiverRequest();
|
||||||
|
//send data
|
||||||
|
byte[] block = new byte[1024];
|
||||||
|
modem.sendDataBlocks(dataStream, 1, crc, block);
|
||||||
|
|
||||||
|
modem.sendEOT();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send files in batch mode.<br/>
|
||||||
|
* <p>
|
||||||
|
* This method support correct thread interruption, when thread is interrupted "cancel of transmission" will be send.
|
||||||
|
* So you can move long transmission to other thread and interrupt it according to your algorithm.
|
||||||
|
*
|
||||||
|
* @param files
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void batchSend(Path... files) throws IOException {
|
||||||
|
for (Path file : files) {
|
||||||
|
send(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendBatchStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendBatchStop() throws IOException {
|
||||||
|
boolean useCRC16 = modem.waitReceiverRequest();
|
||||||
|
XCRC crc;
|
||||||
|
if (useCRC16)
|
||||||
|
crc = new CRC16();
|
||||||
|
else
|
||||||
|
crc = new CRC8();
|
||||||
|
|
||||||
|
//send block 0
|
||||||
|
byte[] bytes = new byte[128];
|
||||||
|
modem.sendBlock(0, bytes, bytes.length, crc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive single file <br/>
|
||||||
|
* <p>
|
||||||
|
* This method support correct thread interruption, when thread is interrupted "cancel of transmission" will be send.
|
||||||
|
* So you can move long transmission to other thread and interrupt it according to your algorithm.
|
||||||
|
*
|
||||||
|
* @param directory directory where file will be saved
|
||||||
|
* @return path to created file
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public Path receiveSingleFileInDirectory(Path directory) throws IOException {
|
||||||
|
return receive(directory, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive files in batch mode <br/>
|
||||||
|
* <p>
|
||||||
|
* This method support correct thread interruption, when thread is interrupted "cancel of transmission" will be send.
|
||||||
|
* So you can move long transmission to other thread and interrupt it according to your algorithm.
|
||||||
|
*
|
||||||
|
* @param directory directory where files will be saved
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void receiveFilesInDirectory(Path directory) throws IOException {
|
||||||
|
while (receive(directory, true) != null) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive path <br/>
|
||||||
|
* <p>
|
||||||
|
* This method support correct thread interruption, when thread is interrupted "cancel of transmission" will be send.
|
||||||
|
* So you can move long transmission to other thread and interrupt it according to your algorithm.
|
||||||
|
*
|
||||||
|
* @param path path to file where data will be saved
|
||||||
|
* @return path to file
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public Path receive(Path path) throws IOException {
|
||||||
|
return receive(path, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path receive(Path path, boolean inDirectory) throws IOException {
|
||||||
|
DataOutputStream dataOutput = null;
|
||||||
|
Path filePath;
|
||||||
|
try {
|
||||||
|
XCRC crc = new CRC16();
|
||||||
|
int errorCount = 0;
|
||||||
|
|
||||||
|
// process block 0
|
||||||
|
byte[] block;
|
||||||
|
int character;
|
||||||
|
while (true) {
|
||||||
|
character = modem.requestTransmissionStart(true);
|
||||||
|
try {
|
||||||
|
// read file name from zero block
|
||||||
|
block = modem.readBlock(0, (character == Modem.SOH), crc);
|
||||||
|
|
||||||
|
if (inDirectory) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (block[0] == 0) {
|
||||||
|
//this is stop block of batch file transfer
|
||||||
|
modem.sendByte(Modem.ACK);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < block.length; i++) {
|
||||||
|
if (block[i] == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sb.append((char) block[i]);
|
||||||
|
}
|
||||||
|
filePath = path.resolve(sb.toString());
|
||||||
|
} else {
|
||||||
|
filePath = path;
|
||||||
|
}
|
||||||
|
dataOutput = new DataOutputStream(Files.newOutputStream(filePath));
|
||||||
|
modem.sendByte(Modem.ACK);
|
||||||
|
break;
|
||||||
|
} catch (Modem.InvalidBlockException e) {
|
||||||
|
errorCount++;
|
||||||
|
if (errorCount == Modem.MAXERRORS) {
|
||||||
|
modem.interruptTransmission();
|
||||||
|
throw new IOException("Transmission aborted, error count exceeded max");
|
||||||
|
}
|
||||||
|
modem.sendByte(Modem.NAK);
|
||||||
|
} catch (Modem.RepeatedBlockException | Modem.SynchronizationLostException e) {
|
||||||
|
//fatal transmission error
|
||||||
|
modem.interruptTransmission();
|
||||||
|
throw new IOException("Fatal transmission error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//receive data blocks
|
||||||
|
modem.receive(filePath, true);
|
||||||
|
} finally {
|
||||||
|
if (dataOutput != null) {
|
||||||
|
dataOutput.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/main/java/zmodem/ZModem.java
Normal file
46
src/main/java/zmodem/ZModem.java
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package zmodem;
|
||||||
|
|
||||||
|
|
||||||
|
import org.apache.commons.net.io.CopyStreamListener;
|
||||||
|
import zmodem.util.FileAdapter;
|
||||||
|
import zmodem.xfer.zm.util.ZModemReceive;
|
||||||
|
import zmodem.xfer.zm.util.ZModemSend;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
|
||||||
|
public class ZModem {
|
||||||
|
|
||||||
|
private final InputStream netIs;
|
||||||
|
private final OutputStream netOs;
|
||||||
|
private final AtomicBoolean isCancelled = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
|
||||||
|
public ZModem(InputStream netin, OutputStream netout) {
|
||||||
|
netIs = netin;
|
||||||
|
netOs = netout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void receive(Supplier<FileAdapter> destDir, CopyStreamListener listener) throws IOException {
|
||||||
|
ZModemReceive sender = new ZModemReceive(destDir, netIs, netOs);
|
||||||
|
sender.addCopyStreamListener(listener);
|
||||||
|
sender.receive(isCancelled::get);
|
||||||
|
netOs.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void send(Supplier<List<FileAdapter>> filesSupplier, CopyStreamListener listener) throws IOException {
|
||||||
|
ZModemSend sender = new ZModemSend(filesSupplier, netIs, netOs);
|
||||||
|
sender.addCopyStreamListener(listener);
|
||||||
|
sender.send(isCancelled::get);
|
||||||
|
netOs.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel() {
|
||||||
|
isCancelled.compareAndSet(false, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/main/java/zmodem/package-info.java
Normal file
4
src/main/java/zmodem/package-info.java
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* https://github.com/scraymer/Zmodem-in-Java
|
||||||
|
*/
|
||||||
|
package zmodem;
|
||||||
65
src/main/java/zmodem/util/CustomFile.java
Normal file
65
src/main/java/zmodem/util/CustomFile.java
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package zmodem.util;
|
||||||
|
|
||||||
|
import org.apache.commons.io.input.RandomAccessFileInputStream;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
|
||||||
|
public class CustomFile implements FileAdapter {
|
||||||
|
File file = null;
|
||||||
|
|
||||||
|
public CustomFile(File file) {
|
||||||
|
super();
|
||||||
|
this.file = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return file.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() throws IOException {
|
||||||
|
return RandomAccessFileInputStream.builder()
|
||||||
|
.setCloseOnClose(true)
|
||||||
|
.setRandomAccessFile(new RandomAccessFile(file, "r"))
|
||||||
|
.setBufferSize(1024 * 8)
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream getOutputStream() throws IOException {
|
||||||
|
return getOutputStream(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream getOutputStream(boolean append) throws IOException {
|
||||||
|
return new BufferedOutputStream(new FileOutputStream(file, append));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileAdapter getChild(String name) {
|
||||||
|
if (name.equals(file.getName())) {
|
||||||
|
return this;
|
||||||
|
} else if (file.isDirectory()) {
|
||||||
|
return new CustomFile(new File(file.getAbsolutePath(), name));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long length() {
|
||||||
|
return file.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDirectory() {
|
||||||
|
return file.isDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean exists() {
|
||||||
|
return file.exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
42
src/main/java/zmodem/util/EmptyFileAdapter.kt
Normal file
42
src/main/java/zmodem/util/EmptyFileAdapter.kt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package zmodem.util
|
||||||
|
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
class EmptyFileAdapter private constructor() : FileAdapter {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { EmptyFileAdapter() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInputStream(): InputStream {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOutputStream(): OutputStream {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOutputStream(append: Boolean): OutputStream {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChild(name: String?): FileAdapter {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun length(): Long {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isDirectory(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun exists(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/main/java/zmodem/util/FileAdapter.java
Normal file
25
src/main/java/zmodem/util/FileAdapter.java
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package zmodem.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public interface FileAdapter {
|
||||||
|
public String getName();
|
||||||
|
|
||||||
|
public InputStream getInputStream() throws IOException;
|
||||||
|
|
||||||
|
public OutputStream getOutputStream() throws IOException;
|
||||||
|
|
||||||
|
public OutputStream getOutputStream(boolean append) throws IOException;
|
||||||
|
|
||||||
|
public FileAdapter getChild(String name);
|
||||||
|
|
||||||
|
public long length();
|
||||||
|
|
||||||
|
public boolean isDirectory();
|
||||||
|
|
||||||
|
public boolean exists();
|
||||||
|
|
||||||
|
public String toString();
|
||||||
|
}
|
||||||
7
src/main/java/zmodem/xfer/io/ObjectInputStream.java
Normal file
7
src/main/java/zmodem/xfer/io/ObjectInputStream.java
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package zmodem.xfer.io;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public abstract class ObjectInputStream<T> {
|
||||||
|
public abstract T read() throws IOException;
|
||||||
|
}
|
||||||
7
src/main/java/zmodem/xfer/io/ObjectOutputStream.java
Normal file
7
src/main/java/zmodem/xfer/io/ObjectOutputStream.java
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package zmodem.xfer.io;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public abstract class ObjectOutputStream<T> {
|
||||||
|
public abstract void write(T o) throws IOException;
|
||||||
|
}
|
||||||
27
src/main/java/zmodem/xfer/util/ASCII.java
Normal file
27
src/main/java/zmodem/xfer/util/ASCII.java
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package zmodem.xfer.util;
|
||||||
|
|
||||||
|
public enum ASCII {
|
||||||
|
|
||||||
|
SOH((byte) 0x01),
|
||||||
|
STX((byte) 0x02),
|
||||||
|
EOT((byte) 0x04),
|
||||||
|
ENQ((byte) 0x05),
|
||||||
|
ACK((byte) 0x06),
|
||||||
|
BS((byte) 0x08),
|
||||||
|
LF((byte) 0x0a),
|
||||||
|
CR((byte) 0x0d),
|
||||||
|
XON((byte) 0x11),
|
||||||
|
XOFF((byte) 0x13),
|
||||||
|
NAK((byte) 0x15),
|
||||||
|
CAN((byte) 0x18);
|
||||||
|
|
||||||
|
private byte value;
|
||||||
|
|
||||||
|
private ASCII(byte b) {
|
||||||
|
value = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte value() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/main/java/zmodem/xfer/util/Arrays.java
Normal file
91
src/main/java/zmodem/xfer/util/Arrays.java
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package zmodem.xfer.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* copyOf is not in java 5 java.util.Array
|
||||||
|
*
|
||||||
|
* @author justin
|
||||||
|
*/
|
||||||
|
public class Arrays {
|
||||||
|
public enum Endianness {Little, Big;}
|
||||||
|
|
||||||
|
public static byte[] copyOf(byte[] original, int newLength) {
|
||||||
|
byte[] copy = new byte[newLength];
|
||||||
|
System.arraycopy(original, 0, copy, 0,
|
||||||
|
Math.min(original.length, newLength));
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean equals(byte[] a, byte[] a2) {
|
||||||
|
return java.util.Arrays.equals(a, a2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long toInteger(byte[] array, int size, Endianness endian) {
|
||||||
|
long n = 0;
|
||||||
|
int offset = 0, increment = 1;
|
||||||
|
switch (endian) {
|
||||||
|
case Little:
|
||||||
|
increment = 1;
|
||||||
|
offset = 0;
|
||||||
|
break;
|
||||||
|
case Big:
|
||||||
|
increment = -1;
|
||||||
|
offset = size - 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
n += (0xff & array[offset]) * (0x1 << i * 8);
|
||||||
|
offset += increment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static short toShort(byte[] array, Endianness endian) {
|
||||||
|
return (short) toInteger(array, 2, endian);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int toInt(byte[] array, Endianness endian) {
|
||||||
|
return (int) toInteger(array, 4, endian);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long toLong(byte[] array, Endianness endian) {
|
||||||
|
return toInteger(array, 8, endian);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] fromInteger(long n, int size, Endianness endian) {
|
||||||
|
byte[] ret = new byte[size];
|
||||||
|
int offset = 0, increment = 1;
|
||||||
|
|
||||||
|
switch (endian) {
|
||||||
|
case Big:
|
||||||
|
increment = -1;
|
||||||
|
offset = size - 1;
|
||||||
|
break;
|
||||||
|
case Little:
|
||||||
|
increment = 1;
|
||||||
|
offset = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
ret[offset] = (byte) ((n >> (i * 8)) & 0xFF);
|
||||||
|
offset += increment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] fromShort(short s, Endianness endian) {
|
||||||
|
return fromInteger(s, 2, endian);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] fromInt(int i, Endianness endian) {
|
||||||
|
return fromInteger(i, 4, endian);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] fromLong(long i, Endianness endian) {
|
||||||
|
return fromInteger(i, 8, endian);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/main/java/zmodem/xfer/util/Buffer.java
Normal file
38
src/main/java/zmodem/xfer/util/Buffer.java
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package zmodem.xfer.util;
|
||||||
|
|
||||||
|
public interface Buffer {
|
||||||
|
public byte get();
|
||||||
|
|
||||||
|
public Buffer get(byte[] dst, int offset, int len);
|
||||||
|
|
||||||
|
public Buffer get(byte[] dst);
|
||||||
|
|
||||||
|
public Buffer put(byte b);
|
||||||
|
|
||||||
|
public Buffer put(byte[] dst, int offset, int len);
|
||||||
|
|
||||||
|
public Buffer put(byte[] dst);
|
||||||
|
|
||||||
|
public byte get(int index);
|
||||||
|
|
||||||
|
public Buffer get(int index, byte[] dst, int offset, int len);
|
||||||
|
|
||||||
|
public Buffer get(int index, byte[] dst);
|
||||||
|
|
||||||
|
public Buffer put(int index, byte b);
|
||||||
|
|
||||||
|
public Buffer put(int index, byte[] dst, int offset, int len);
|
||||||
|
|
||||||
|
public Buffer put(int index, byte[] dst);
|
||||||
|
|
||||||
|
public void flip();
|
||||||
|
|
||||||
|
public int remaining();
|
||||||
|
|
||||||
|
public boolean hasRemaining();
|
||||||
|
|
||||||
|
public HexBuffer asHexBuffer();
|
||||||
|
|
||||||
|
public ByteBuffer asByteBuffer();
|
||||||
|
|
||||||
|
}
|
||||||
193
src/main/java/zmodem/xfer/util/ByteBuffer.java
Normal file
193
src/main/java/zmodem/xfer/util/ByteBuffer.java
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package zmodem.xfer.util;
|
||||||
|
|
||||||
|
|
||||||
|
public class ByteBuffer implements Buffer {
|
||||||
|
|
||||||
|
private java.nio.ByteBuffer _wrapped;
|
||||||
|
|
||||||
|
public ByteBuffer(java.nio.ByteBuffer b) {
|
||||||
|
_wrapped = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteBuffer allocate(int capacity) {
|
||||||
|
return new ByteBuffer(java.nio.ByteBuffer.allocate(capacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteBuffer allocateDirect(int capacity) {
|
||||||
|
return new ByteBuffer(java.nio.ByteBuffer.allocateDirect(capacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer slice() {
|
||||||
|
return new ByteBuffer(_wrapped.slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer duplicate() {
|
||||||
|
return new ByteBuffer(_wrapped.duplicate());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer asReadOnlyBuffer() {
|
||||||
|
return new ByteBuffer(_wrapped.asReadOnlyBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte get() {
|
||||||
|
return _wrapped.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer get(byte[] dst, int offset, int len) {
|
||||||
|
for (; offset < len; offset++)
|
||||||
|
dst[offset] = get();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer get(byte[] dst) {
|
||||||
|
return get(dst, 0, dst.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer put(byte b) {
|
||||||
|
_wrapped.put(b);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer put(byte[] dst, int offset, int len) {
|
||||||
|
for (; offset < len; offset++)
|
||||||
|
put(dst[offset]);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer put(byte[] dst) {
|
||||||
|
return put(dst, 0, dst.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte get(int index) {
|
||||||
|
return _wrapped.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer get(int index, byte[] dst, int offset, int len) {
|
||||||
|
for (; offset < len; offset++)
|
||||||
|
dst[offset] = get(index++);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer get(int index, byte[] dst) {
|
||||||
|
return get(index, dst, 0, dst.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer put(int index, byte b) {
|
||||||
|
_wrapped.put(index, b);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer put(int index, byte[] dst, int offset, int len) {
|
||||||
|
for (; offset < len; offset++)
|
||||||
|
put(index++, dst[offset]);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer put(int index, byte[] dst) {
|
||||||
|
return put(index, dst, 0, dst.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer compact() {
|
||||||
|
_wrapped.compact();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDirect() {
|
||||||
|
return _wrapped.isDirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
public char getChar() {
|
||||||
|
return (char) get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer putChar(char value) {
|
||||||
|
return put((byte) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public char getChar(int index) {
|
||||||
|
return (char) get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer putChar(int index, char value) {
|
||||||
|
return put(index, (byte) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HexBuffer asHexBuffer() {
|
||||||
|
return new HexBuffer(_wrapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getShort() {
|
||||||
|
return Arrays.toShort(new byte[]{get(), get()}, Arrays.Endianness.Little);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer putShort(short value) {
|
||||||
|
return put(Arrays.fromShort(value, Arrays.Endianness.Little));
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getShort(int index) {
|
||||||
|
return Arrays.toShort(new byte[]{get(index), get(index + 1)}, Arrays.Endianness.Little);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer putShort(int index, short value) {
|
||||||
|
return put(index, Arrays.fromShort(value, Arrays.Endianness.Little));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getInt() {
|
||||||
|
return Arrays.toInt(new byte[]{get(), get(), get(), get()}, Arrays.Endianness.Little);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer putInt(int value) {
|
||||||
|
return put(Arrays.fromInt(value, Arrays.Endianness.Little));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getInt(int index) {
|
||||||
|
return Arrays.toInt(new byte[]{get(index), get(index + 1), get(index + 2), get(index + 3)}, Arrays.Endianness.Little);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer putInt(int index, int value) {
|
||||||
|
return put(index, Arrays.fromInt(value, Arrays.Endianness.Little));
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLong() {
|
||||||
|
return Arrays.toLong(new byte[]{get(), get(), get(), get(), get(), get(), get(), get()}, Arrays.Endianness.Little);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer putLong(long value) {
|
||||||
|
return put(Arrays.fromLong(value, Arrays.Endianness.Little));
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLong(int index) {
|
||||||
|
return Arrays.toLong(new byte[]{get(index), get(index + 1), get(index + 2), get(index + 3), get(index + 4), get(index + 5), get(index + 6), get(index + 7)}, Arrays.Endianness.Little);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer putLong(int index, long value) {
|
||||||
|
return put(index, Arrays.fromLong(value, Arrays.Endianness.Little));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReadOnly() {
|
||||||
|
return _wrapped.isReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void flip() {
|
||||||
|
_wrapped.flip();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int remaining() {
|
||||||
|
return _wrapped.remaining();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasRemaining() {
|
||||||
|
return remaining() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer asByteBuffer() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
232
src/main/java/zmodem/xfer/util/CRC.java
Normal file
232
src/main/java/zmodem/xfer/util/CRC.java
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
package zmodem.xfer.util;
|
||||||
|
|
||||||
|
|
||||||
|
public class CRC {
|
||||||
|
|
||||||
|
public static enum Type {
|
||||||
|
CRC16(2, 0),
|
||||||
|
CRC32(4, 0xffffffff);
|
||||||
|
private int numbytes;
|
||||||
|
private int initial;
|
||||||
|
|
||||||
|
private Type(int s, int i) {
|
||||||
|
numbytes = s;
|
||||||
|
initial = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return numbytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int initial() {
|
||||||
|
return initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copied from the original zmodem source
|
||||||
|
*/
|
||||||
|
private static final int crctab[] = {
|
||||||
|
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
|
||||||
|
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
|
||||||
|
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
|
||||||
|
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
|
||||||
|
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
|
||||||
|
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
|
||||||
|
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
|
||||||
|
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
|
||||||
|
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
|
||||||
|
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
|
||||||
|
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
|
||||||
|
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
|
||||||
|
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
|
||||||
|
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
|
||||||
|
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
|
||||||
|
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
|
||||||
|
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
|
||||||
|
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
|
||||||
|
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
|
||||||
|
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
|
||||||
|
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
|
||||||
|
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
|
||||||
|
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
|
||||||
|
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
|
||||||
|
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
|
||||||
|
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
|
||||||
|
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
|
||||||
|
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
|
||||||
|
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
|
||||||
|
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
|
||||||
|
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
|
||||||
|
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Copied from the original zmodem source
|
||||||
|
*/
|
||||||
|
private static final int cr3tab[] = { /* CRC polynomial 0xedb88320 */
|
||||||
|
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
|
||||||
|
0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
|
||||||
|
0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
|
||||||
|
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
|
||||||
|
0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
|
||||||
|
0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
|
||||||
|
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
|
||||||
|
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
|
||||||
|
0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
|
||||||
|
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
|
||||||
|
0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
|
||||||
|
0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
|
||||||
|
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
|
||||||
|
0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
|
||||||
|
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
|
||||||
|
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
|
||||||
|
0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
|
||||||
|
0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
|
||||||
|
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
|
||||||
|
0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
|
||||||
|
0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
|
||||||
|
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
|
||||||
|
0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
|
||||||
|
0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
|
||||||
|
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
|
||||||
|
0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
|
||||||
|
0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
|
||||||
|
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
|
||||||
|
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
|
||||||
|
0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
|
||||||
|
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
|
||||||
|
0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
|
||||||
|
};
|
||||||
|
|
||||||
|
private static int updcrc(int cp, int crc) {
|
||||||
|
//System.out.printf("updcrc: %08x / %08x\n",cp,crc);
|
||||||
|
return (crctab[(crc >> 8) & 0xff] ^ (crc << 8) ^ cp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int updcrc32(int b, int c) {
|
||||||
|
return (cr3tab[(c ^ b) & 0xff] ^ ((c >> 8) & 0x00FFFFFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static byte[] arrayCRC(Type t, byte[] bytes) {
|
||||||
|
switch (t) {
|
||||||
|
case CRC16:
|
||||||
|
return arrayCRC16(bytes);
|
||||||
|
case CRC32:
|
||||||
|
return arrayCRC32(bytes);
|
||||||
|
}
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bytes
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static byte[] arrayCRC16(byte[] bytes) {
|
||||||
|
byte[] bb = new byte[2];
|
||||||
|
int value = CRC16(bytes);
|
||||||
|
bb[1] = (byte) (value & 0xFF);
|
||||||
|
bb[0] = (byte) ((value >> 8) & 0xFF);
|
||||||
|
return bb;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int CRC16(byte[] bytes) {
|
||||||
|
int crc = 0;
|
||||||
|
|
||||||
|
for (byte b : bytes)
|
||||||
|
crc = updcrc(0xff & b, crc);
|
||||||
|
|
||||||
|
crc = updcrc(0, updcrc(0, crc));
|
||||||
|
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bytes
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static byte[] arrayCRC32(byte[] bytes) {
|
||||||
|
byte[] bb = new byte[4];
|
||||||
|
long value = CRC32(bytes);
|
||||||
|
bb[3] = (byte) (value & 0xFF);
|
||||||
|
bb[2] = (byte) ((value >> 8) & 0xFF);
|
||||||
|
bb[1] = (byte) ((value >> 16) & 0xFF);
|
||||||
|
bb[0] = (byte) ((value >> 24) & 0xFF);
|
||||||
|
return bb;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int CRC32(byte[] bytes) {
|
||||||
|
int crc = 0xFFFFFFFF;
|
||||||
|
|
||||||
|
for (byte b : bytes)
|
||||||
|
crc = updcrc32(b, crc);
|
||||||
|
|
||||||
|
return ~crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Type type;
|
||||||
|
private int crc;
|
||||||
|
|
||||||
|
public CRC(Type t) {
|
||||||
|
type = t;
|
||||||
|
crc = type.initial();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(byte b) {
|
||||||
|
switch (type) {
|
||||||
|
case CRC16:
|
||||||
|
crc = updcrc(0xff & b, crc);
|
||||||
|
break;
|
||||||
|
case CRC32:
|
||||||
|
crc = updcrc32((0xff & b), crc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(byte[] array) {
|
||||||
|
for (byte b : array)
|
||||||
|
update(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void finalized() {
|
||||||
|
switch (type) {
|
||||||
|
case CRC16:
|
||||||
|
crc = 0xffff & updcrc(0, updcrc(0, crc));
|
||||||
|
break;
|
||||||
|
case CRC32:
|
||||||
|
crc = ~crc;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
//System.out.printf("crc: %08x\n",crc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return type.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getBytes() {
|
||||||
|
byte[] bb;
|
||||||
|
switch (type) {
|
||||||
|
case CRC16:
|
||||||
|
bb = new byte[2];
|
||||||
|
bb[1] = (byte) (crc & 0xFF);
|
||||||
|
bb[0] = (byte) ((crc >> 8) & 0xFF);
|
||||||
|
return bb;
|
||||||
|
case CRC32:
|
||||||
|
bb = new byte[4];
|
||||||
|
bb[3] = (byte) (crc & 0xFF);
|
||||||
|
bb[2] = (byte) ((crc >> 8) & 0xFF);
|
||||||
|
bb[1] = (byte) ((crc >> 16) & 0xFF);
|
||||||
|
bb[0] = (byte) ((crc >> 24) & 0xFF);
|
||||||
|
return bb;
|
||||||
|
}
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type type() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
58
src/main/java/zmodem/xfer/util/CRC16.java
Normal file
58
src/main/java/zmodem/xfer/util/CRC16.java
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package zmodem.xfer.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses table for irreducible polynomial: 1 + x^2 + x^15 + x^16
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class CRC16 implements XCRC {
|
||||||
|
|
||||||
|
private static int[] table = {
|
||||||
|
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
|
||||||
|
0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
|
||||||
|
0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
|
||||||
|
0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
|
||||||
|
0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,
|
||||||
|
0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D,
|
||||||
|
0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
|
||||||
|
0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,
|
||||||
|
0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
|
||||||
|
0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,
|
||||||
|
0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
|
||||||
|
0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,
|
||||||
|
0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
|
||||||
|
0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
|
||||||
|
0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,
|
||||||
|
0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78,
|
||||||
|
0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F,
|
||||||
|
0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,
|
||||||
|
0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,
|
||||||
|
0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256,
|
||||||
|
0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
|
||||||
|
0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
|
||||||
|
0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,
|
||||||
|
0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
|
||||||
|
0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,
|
||||||
|
0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
|
||||||
|
0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
|
||||||
|
0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
|
||||||
|
0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9,
|
||||||
|
0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
|
||||||
|
0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8,
|
||||||
|
0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0,
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCRCLength() {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long calcCRC(byte[] block) {
|
||||||
|
int crc = 0x0000;
|
||||||
|
for (byte b : block) {
|
||||||
|
crc = ((crc << 8) ^ table[((crc >> 8) ^ (0xff & b))]) & 0xFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/main/java/zmodem/xfer/util/CRC8.java
Normal file
21
src/main/java/zmodem/xfer/util/CRC8.java
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package zmodem.xfer.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by asirotinkin on 11.11.2014.
|
||||||
|
*/
|
||||||
|
public class CRC8 implements XCRC {
|
||||||
|
@Override
|
||||||
|
public int getCRCLength() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long calcCRC(byte[] block) {
|
||||||
|
byte checkSumma = 0;
|
||||||
|
for (int i = 0; i < block.length; i++) {
|
||||||
|
checkSumma += block[i];
|
||||||
|
}
|
||||||
|
return checkSumma;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
235
src/main/java/zmodem/xfer/util/HexBuffer.java
Normal file
235
src/main/java/zmodem/xfer/util/HexBuffer.java
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
package zmodem.xfer.util;
|
||||||
|
|
||||||
|
|
||||||
|
import zmodem.xfer.util.Arrays.Endianness;
|
||||||
|
|
||||||
|
public class HexBuffer implements Buffer {
|
||||||
|
|
||||||
|
private static final byte[] hx = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
|
||||||
|
|
||||||
|
public static byte[] binToHex(byte[] bin) {
|
||||||
|
byte[] hex = new byte[bin.length * 2];
|
||||||
|
for (int i = 0; i < bin.length; i++)
|
||||||
|
System.arraycopy(toHex(bin[i]), 0, hex, i * 2, 2);
|
||||||
|
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] hexToBin(byte[] hex) {
|
||||||
|
byte[] bin = new byte[hex.length / 2];
|
||||||
|
for (int i = 0; i < bin.length; i++) {
|
||||||
|
byte[] bn = new byte[2];
|
||||||
|
System.arraycopy(hex, i * 2, bn, 0, 2);
|
||||||
|
bin[i] = toByte(bn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte toByte(byte[] array) {
|
||||||
|
int d;
|
||||||
|
|
||||||
|
d = java.util.Arrays.binarySearch(hx, array[0]) * 16;
|
||||||
|
d += java.util.Arrays.binarySearch(hx, array[1]);
|
||||||
|
|
||||||
|
return (byte) d;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] toHex(byte b) {
|
||||||
|
byte[] array = new byte[2];
|
||||||
|
|
||||||
|
array[0] = hx[((b >> 4) & 0xF)];
|
||||||
|
array[1] = hx[(b & 0xF)];
|
||||||
|
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
private java.nio.ByteBuffer _wrapped;
|
||||||
|
|
||||||
|
protected HexBuffer(java.nio.ByteBuffer b) {
|
||||||
|
_wrapped = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HexBuffer allocate(int capacity) {
|
||||||
|
return new HexBuffer(java.nio.ByteBuffer.allocate(capacity * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HexBuffer allocateDirect(int capacity) {
|
||||||
|
return new HexBuffer(java.nio.ByteBuffer.allocateDirect(capacity * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer slice() {
|
||||||
|
return new HexBuffer(_wrapped.slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer duplicate() {
|
||||||
|
return new HexBuffer(_wrapped.duplicate());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer asReadOnlyBuffer() {
|
||||||
|
return new HexBuffer(_wrapped.asReadOnlyBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte get() {
|
||||||
|
return toByte(new byte[]{_wrapped.get(), _wrapped.get()});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer get(byte[] dst, int offset, int len) {
|
||||||
|
for (; offset < len; offset++)
|
||||||
|
dst[offset] = get();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer get(byte[] dst) {
|
||||||
|
return get(dst, 0, dst.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer put(byte b) {
|
||||||
|
_wrapped.put(toHex(b));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer put(byte[] dst, int offset, int len) {
|
||||||
|
for (; offset < len; offset++)
|
||||||
|
put(dst[offset]);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer put(byte[] dst) {
|
||||||
|
return put(dst, 0, dst.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte get(int index) {
|
||||||
|
return toByte(new byte[]{_wrapped.get(index * 2), _wrapped.get(index * 2 + 1)});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer get(int index, byte[] dst, int offset, int len) {
|
||||||
|
for (; offset < len; offset++)
|
||||||
|
dst[offset] = get(index++);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer get(int index, byte[] dst) {
|
||||||
|
return get(index, dst, 0, dst.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer put(int index, byte b) {
|
||||||
|
byte[] array = toHex(b);
|
||||||
|
_wrapped.put(index * 2, array[0]);
|
||||||
|
_wrapped.put(index * 2 + 1, array[1]);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer put(int index, byte[] dst, int offset, int len) {
|
||||||
|
for (; offset < len; offset++)
|
||||||
|
put(index++, dst[offset]);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer put(int index, byte[] dst) {
|
||||||
|
return put(index, dst, 0, dst.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer compact() {
|
||||||
|
_wrapped.compact();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDirect() {
|
||||||
|
return _wrapped.isDirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
public char getChar() {
|
||||||
|
return (char) get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer putChar(char value) {
|
||||||
|
return put((byte) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public char getChar(int index) {
|
||||||
|
return (char) get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer putChar(int index, char value) {
|
||||||
|
return put(index, (byte) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer asByteBuffer() {
|
||||||
|
return new ByteBuffer(_wrapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getShort() {
|
||||||
|
return Arrays.toShort(new byte[]{get(), get()}, Endianness.Little);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer putShort(short value) {
|
||||||
|
return put(Arrays.fromShort(value, Endianness.Little));
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getShort(int index) {
|
||||||
|
return Arrays.toShort(new byte[]{get(index), get(index + 1)}, Endianness.Little);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer putShort(int index, short value) {
|
||||||
|
return put(index, Arrays.fromShort(value, Endianness.Little));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getInt() {
|
||||||
|
return Arrays.toInt(new byte[]{get(), get(), get(), get()}, Endianness.Little);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer putInt(int value) {
|
||||||
|
return put(Arrays.fromInt(value, Endianness.Little));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getInt(int index) {
|
||||||
|
return Arrays.toInt(new byte[]{get(index), get(index + 1), get(index + 2), get(index + 3)}, Endianness.Little);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer putInt(int index, int value) {
|
||||||
|
return put(index, Arrays.fromInt(value, Endianness.Little));
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLong() {
|
||||||
|
return Arrays.toLong(new byte[]{get(), get(), get(), get(), get(), get(), get(), get()}, Endianness.Little);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer putLong(long value) {
|
||||||
|
return put(Arrays.fromLong(value, Endianness.Little));
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLong(int index) {
|
||||||
|
return Arrays.toLong(new byte[]{get(index), get(index + 1), get(index + 2), get(index + 3), get(index + 4), get(index + 5), get(index + 6), get(index + 7)}, Endianness.Little);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buffer putLong(int index, long value) {
|
||||||
|
return put(index, Arrays.fromLong(value, Endianness.Little));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReadOnly() {
|
||||||
|
return _wrapped.isReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void flip() {
|
||||||
|
_wrapped.flip();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int remaining() {
|
||||||
|
double rem = ((double) _wrapped.remaining() / 2.0d);
|
||||||
|
return (int) Math.floor(rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasRemaining() {
|
||||||
|
return (_wrapped.remaining() > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HexBuffer asHexBuffer() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package zmodem.xfer.util;
|
||||||
|
|
||||||
|
public class InvalidChecksumException extends RuntimeException {
|
||||||
|
private static final long serialVersionUID = 3864874377147160043L;
|
||||||
|
|
||||||
|
}
|
||||||
7
src/main/java/zmodem/xfer/util/TimeoutException.java
Normal file
7
src/main/java/zmodem/xfer/util/TimeoutException.java
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package zmodem.xfer.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by asirotinkin on 12.11.2014.
|
||||||
|
*/
|
||||||
|
class TimeoutException extends Exception {
|
||||||
|
}
|
||||||
10
src/main/java/zmodem/xfer/util/XCRC.java
Normal file
10
src/main/java/zmodem/xfer/util/XCRC.java
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package zmodem.xfer.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Muzeffer on 2016/6/30.
|
||||||
|
*/
|
||||||
|
public interface XCRC {
|
||||||
|
int getCRCLength();
|
||||||
|
|
||||||
|
long calcCRC(byte[] block);
|
||||||
|
}
|
||||||
29
src/main/java/zmodem/xfer/zm/packet/Cancel.java
Normal file
29
src/main/java/zmodem/xfer/zm/packet/Cancel.java
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package zmodem.xfer.zm.packet;
|
||||||
|
|
||||||
|
|
||||||
|
import zmodem.xfer.util.ASCII;
|
||||||
|
import zmodem.xfer.util.Buffer;
|
||||||
|
import zmodem.xfer.util.ByteBuffer;
|
||||||
|
import zmodem.xfer.zm.util.ZMPacket;
|
||||||
|
|
||||||
|
public class Cancel extends ZMPacket {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Buffer marshall() {
|
||||||
|
ByteBuffer buff = ByteBuffer.allocate(16);
|
||||||
|
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
buff.put(ASCII.CAN.value());
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
buff.put(ASCII.BS.value());
|
||||||
|
|
||||||
|
buff.flip();
|
||||||
|
|
||||||
|
return buff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Cancel: CAN * 8 + BS * 8";
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/main/java/zmodem/xfer/zm/packet/DataPacket.java
Normal file
92
src/main/java/zmodem/xfer/zm/packet/DataPacket.java
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package zmodem.xfer.zm.packet;
|
||||||
|
|
||||||
|
|
||||||
|
import zmodem.xfer.util.*;
|
||||||
|
import zmodem.xfer.zm.util.ZDLEEncoder;
|
||||||
|
import zmodem.xfer.zm.util.ZMPacket;
|
||||||
|
import zmodem.xfer.zm.util.ZModemCharacter;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
public class DataPacket extends ZMPacket {
|
||||||
|
|
||||||
|
public static DataPacket unmarshall(Buffer buff, CRC crc) {
|
||||||
|
byte[] data = new byte[buff.remaining() - crc.size() - 1];
|
||||||
|
|
||||||
|
buff.get(data);
|
||||||
|
|
||||||
|
ZModemCharacter type;
|
||||||
|
type = ZModemCharacter.forbyte(buff.get());
|
||||||
|
|
||||||
|
|
||||||
|
byte[] netCrc = new byte[crc.size()];
|
||||||
|
buff.get(netCrc);
|
||||||
|
|
||||||
|
if (!Arrays.equals(netCrc, crc.getBytes()))
|
||||||
|
throw new InvalidChecksumException();
|
||||||
|
|
||||||
|
return new DataPacket(type, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private final ZModemCharacter type;
|
||||||
|
private byte[] data = new byte[0];
|
||||||
|
|
||||||
|
public DataPacket(ZModemCharacter fe) {
|
||||||
|
type = fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataPacket(ZModemCharacter fr, byte[] d) {
|
||||||
|
this(fr);
|
||||||
|
data = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ZModemCharacter type() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] data() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setData(byte[] d) {
|
||||||
|
data = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void copyData(byte[] d) {
|
||||||
|
data = Arrays.copyOf(d, d.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Buffer marshall() {
|
||||||
|
ZDLEEncoder encoder;
|
||||||
|
ByteBuffer buff = ByteBuffer.allocate(data.length * 2 + 64);
|
||||||
|
|
||||||
|
CRC crc = new CRC(CRC.Type.CRC16);
|
||||||
|
|
||||||
|
encoder = new ZDLEEncoder(data);
|
||||||
|
|
||||||
|
crc.update(data);
|
||||||
|
buff.put(encoder.zdle(), 0, encoder.zdleLen());
|
||||||
|
|
||||||
|
buff.put(ZModemCharacter.ZDLE.value());
|
||||||
|
|
||||||
|
crc.update(type.value());
|
||||||
|
buff.put(type.value());
|
||||||
|
|
||||||
|
crc.finalized();
|
||||||
|
|
||||||
|
encoder = new ZDLEEncoder(crc.getBytes());
|
||||||
|
buff.put(encoder.zdle(), 0, encoder.zdleLen());
|
||||||
|
|
||||||
|
buff.flip();
|
||||||
|
|
||||||
|
return buff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return type + ":" + data.length + " bytes";
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/main/java/zmodem/xfer/zm/packet/Finish.java
Normal file
27
src/main/java/zmodem/xfer/zm/packet/Finish.java
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package zmodem.xfer.zm.packet;
|
||||||
|
|
||||||
|
|
||||||
|
import zmodem.xfer.util.Buffer;
|
||||||
|
import zmodem.xfer.util.ByteBuffer;
|
||||||
|
import zmodem.xfer.zm.util.ZMPacket;
|
||||||
|
|
||||||
|
public class Finish extends ZMPacket {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Buffer marshall() {
|
||||||
|
ByteBuffer buff = ByteBuffer.allocate(16);
|
||||||
|
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
buff.put((byte) 'O');
|
||||||
|
|
||||||
|
buff.flip();
|
||||||
|
|
||||||
|
return buff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Finish: OO";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
48
src/main/java/zmodem/xfer/zm/packet/Format.java
Normal file
48
src/main/java/zmodem/xfer/zm/packet/Format.java
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package zmodem.xfer.zm.packet;
|
||||||
|
|
||||||
|
|
||||||
|
import zmodem.xfer.util.CRC;
|
||||||
|
import zmodem.xfer.zm.util.ZModemCharacter;
|
||||||
|
|
||||||
|
public enum Format {
|
||||||
|
|
||||||
|
BIN32(1, CRC.Type.CRC32, ZModemCharacter.ZBIN32),
|
||||||
|
BIN(1, CRC.Type.CRC16, ZModemCharacter.ZBIN),
|
||||||
|
HEX(2, CRC.Type.CRC16, ZModemCharacter.ZHEX);
|
||||||
|
|
||||||
|
|
||||||
|
public static Format fromByte(byte b) {
|
||||||
|
for (Format ft : values()) {
|
||||||
|
if (ft.character() == b)
|
||||||
|
return ft;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int width;
|
||||||
|
private CRC.Type crc;
|
||||||
|
private ZModemCharacter character;
|
||||||
|
|
||||||
|
private Format(int bw, CRC.Type crct, ZModemCharacter fmt) {
|
||||||
|
width = bw;
|
||||||
|
crc = crct;
|
||||||
|
character = fmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CRC.Type crc() {
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte character() {
|
||||||
|
return character.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int width() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hex() {
|
||||||
|
return (this == HEX);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
130
src/main/java/zmodem/xfer/zm/packet/Header.java
Normal file
130
src/main/java/zmodem/xfer/zm/packet/Header.java
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package zmodem.xfer.zm.packet;
|
||||||
|
|
||||||
|
|
||||||
|
import zmodem.xfer.util.*;
|
||||||
|
import zmodem.xfer.zm.util.ZDLEEncoder;
|
||||||
|
import zmodem.xfer.zm.util.ZMPacket;
|
||||||
|
import zmodem.xfer.zm.util.ZModemCharacter;
|
||||||
|
|
||||||
|
public class Header extends ZMPacket {
|
||||||
|
|
||||||
|
public static Header unmarshall(Buffer buff) {
|
||||||
|
|
||||||
|
Format fmt = null;
|
||||||
|
|
||||||
|
while (fmt == null)
|
||||||
|
fmt = Format.fromByte(buff.get());
|
||||||
|
|
||||||
|
if (fmt.hex())
|
||||||
|
buff = buff.asHexBuffer();
|
||||||
|
|
||||||
|
CRC crc = new CRC(fmt.crc());
|
||||||
|
byte b;
|
||||||
|
|
||||||
|
b = buff.get();
|
||||||
|
crc.update(b);
|
||||||
|
ZModemCharacter type = ZModemCharacter.forbyte(b);
|
||||||
|
|
||||||
|
byte[] data = new byte[4];
|
||||||
|
for (int i = 0; i < data.length; i++) {
|
||||||
|
b = buff.get();
|
||||||
|
crc.update(b);
|
||||||
|
data[i] = b;
|
||||||
|
}
|
||||||
|
crc.finalized();
|
||||||
|
|
||||||
|
byte[] netCrc = new byte[crc.size()];
|
||||||
|
buff.get(netCrc);
|
||||||
|
|
||||||
|
if (!Arrays.equals(netCrc, crc.getBytes()))
|
||||||
|
throw new InvalidChecksumException();
|
||||||
|
|
||||||
|
return new Header(fmt, type, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Format format;
|
||||||
|
private ZModemCharacter type;
|
||||||
|
private byte[] data = {0, 0, 0, 0};
|
||||||
|
|
||||||
|
private Header(Format fFmt) {
|
||||||
|
format = fFmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Header(Format fFmt, ZModemCharacter fType) {
|
||||||
|
this(fFmt);
|
||||||
|
type = fType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Header(Format fFmt, ZModemCharacter fType, byte[] flags) {
|
||||||
|
this(fFmt, fType);
|
||||||
|
setFlags(flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Header(Format fFmt, ZModemCharacter fType, int pos) {
|
||||||
|
this(fFmt, fType);
|
||||||
|
setPos(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public ZModemCharacter type() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Format format() {
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFlags(byte[] flags) {
|
||||||
|
data = Arrays.copyOf(flags, flags.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getFlags() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPos(int num) {
|
||||||
|
data = Arrays.fromInt(num, Arrays.Endianness.Little);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPos() {
|
||||||
|
return Arrays.toInt(data, Arrays.Endianness.Little);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Buffer marshall() {
|
||||||
|
ZDLEEncoder encoder;
|
||||||
|
|
||||||
|
Buffer buff;
|
||||||
|
if (format.hex())
|
||||||
|
buff = HexBuffer.allocate(16);
|
||||||
|
else
|
||||||
|
buff = ByteBuffer.allocate(32);
|
||||||
|
|
||||||
|
CRC crc = new CRC(format.crc());
|
||||||
|
|
||||||
|
crc.update(type.value());
|
||||||
|
buff.put(type.value());
|
||||||
|
|
||||||
|
crc.update(data);
|
||||||
|
encoder = new ZDLEEncoder(data, format);
|
||||||
|
buff.put(encoder.zdle(), 0, encoder.zdleLen());
|
||||||
|
|
||||||
|
|
||||||
|
crc.finalized();
|
||||||
|
|
||||||
|
encoder = new ZDLEEncoder(crc.getBytes(), format);
|
||||||
|
buff.put(encoder.zdle(), 0, encoder.zdleLen());
|
||||||
|
|
||||||
|
buff.flip();
|
||||||
|
|
||||||
|
return buff.asByteBuffer();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return type + ", " + format + ", " + "{" + data[0] + "," + data[1] + "," + data[2] + "," + data[3] + "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package zmodem.xfer.zm.packet;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class InvalidPacketException extends IOException {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 6436104259898858243L;
|
||||||
|
|
||||||
|
}
|
||||||
5
src/main/java/zmodem/xfer/zm/proto/Action.java
Normal file
5
src/main/java/zmodem/xfer/zm/proto/Action.java
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package zmodem.xfer.zm.proto;
|
||||||
|
|
||||||
|
public enum Action {
|
||||||
|
ESCAPE, DATA, HEADER, CANCEL, FINISH;
|
||||||
|
}
|
||||||
99
src/main/java/zmodem/xfer/zm/proto/Escape.java
Normal file
99
src/main/java/zmodem/xfer/zm/proto/Escape.java
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package zmodem.xfer.zm.proto;
|
||||||
|
|
||||||
|
import zmodem.xfer.zm.util.ZModemCharacter;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
|
public class Escape {
|
||||||
|
|
||||||
|
private int len = 0;
|
||||||
|
private Action action = Action.ESCAPE;
|
||||||
|
|
||||||
|
|
||||||
|
public Escape(Action a) {
|
||||||
|
this(a, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Escape(Action a, int l) {
|
||||||
|
len = l;
|
||||||
|
action = a;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action action() {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int len() {
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Map<Byte, Escape> _specials = new HashMap<Byte, Escape>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
_specials.put(ZModemCharacter.ZBIN.value(), new Escape(Action.HEADER, 7));
|
||||||
|
_specials.put(ZModemCharacter.ZHEX.value(), new Escape(Action.HEADER, 16));
|
||||||
|
_specials.put(ZModemCharacter.ZBIN32.value(), new Escape(Action.HEADER, 9));
|
||||||
|
_specials.put(ZModemCharacter.ZCRCE.value(), new Escape(Action.DATA, 2));
|
||||||
|
_specials.put(ZModemCharacter.ZCRCG.value(), new Escape(Action.DATA, 2));
|
||||||
|
_specials.put(ZModemCharacter.ZCRCQ.value(), new Escape(Action.DATA, 2));
|
||||||
|
_specials.put(ZModemCharacter.ZCRCW.value(), new Escape(Action.DATA, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Escape detect(byte b, boolean acceptsHeader) {
|
||||||
|
Escape r = _specials.get(b);
|
||||||
|
|
||||||
|
|
||||||
|
if (r == null || ((!acceptsHeader) && r.action() == Action.HEADER))
|
||||||
|
return new Escape(Action.ESCAPE);
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean mustEscape(byte b, byte previous, boolean escapeCtl) {
|
||||||
|
switch (b) {
|
||||||
|
case 0xd:
|
||||||
|
case (byte) 0x8d:
|
||||||
|
if (escapeCtl && previous == '@')
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
|
case 0x18:
|
||||||
|
case 0x10:
|
||||||
|
case 0x11:
|
||||||
|
case 0x13:
|
||||||
|
case (byte) 0x7f:
|
||||||
|
case (byte) 0x90:
|
||||||
|
case (byte) 0x91:
|
||||||
|
case (byte) 0x93:
|
||||||
|
case (byte) 0xff:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
if (escapeCtl && ((b & 0x60) == 0))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte escapeIt(byte b) {
|
||||||
|
if (b == (byte) 0x7f)
|
||||||
|
return ZModemCharacter.ZRUB0.value();
|
||||||
|
if (b == (byte) 0xff)
|
||||||
|
return ZModemCharacter.ZRUB1.value();
|
||||||
|
if (b == (byte) ZModemCharacter.ZRUB0.value())
|
||||||
|
return 0x7f;
|
||||||
|
if (b == (byte) ZModemCharacter.ZRUB1.value())
|
||||||
|
return (byte) 0xff;
|
||||||
|
|
||||||
|
return (byte) (b ^ 0x40);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Action=" + action + ", len=" + len;
|
||||||
|
}
|
||||||
|
}
|
||||||
401
src/main/java/zmodem/xfer/zm/util/Modem.java
Normal file
401
src/main/java/zmodem/xfer/zm/util/Modem.java
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
package zmodem.xfer.zm.util;
|
||||||
|
|
||||||
|
import zmodem.xfer.util.CRC16;
|
||||||
|
import zmodem.xfer.util.CRC8;
|
||||||
|
import zmodem.xfer.util.XCRC;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is core Modem class supporting XModem (and some extensions XModem-1K, XModem-CRC), and YModem.<br/>
|
||||||
|
* YModem support is limited (currently block 0 is ignored).<br/>
|
||||||
|
* <br/>
|
||||||
|
* Created by Anton Sirotinkin (aesirot@mail.ru), Moscow 2014 <br/>
|
||||||
|
* I hope you will find this program useful.<br/>
|
||||||
|
* You are free to use/modify the code for any purpose, but please leave a reference to me.<br/>
|
||||||
|
*/
|
||||||
|
public class Modem {
|
||||||
|
|
||||||
|
public static final byte SOH = 0x01; /* Start Of Header */
|
||||||
|
public static final byte STX = 0x02; /* Start Of Text (used like SOH but means 1024 block size) */
|
||||||
|
public static final byte EOT = 0x04; /* End Of Transmission */
|
||||||
|
public static final byte ACK = 0x06; /* ACKnowlege */
|
||||||
|
public static final byte NAK = 0x15; /* Negative AcKnowlege */
|
||||||
|
public static final byte CAN = 0x18; /* CANcel character */
|
||||||
|
|
||||||
|
public static final byte CPMEOF = 0x1A;
|
||||||
|
public static final byte ST_C = 'C';
|
||||||
|
|
||||||
|
public static final int MAXERRORS = 10;
|
||||||
|
|
||||||
|
public static final int BLOCK_TIMEOUT = 1000;
|
||||||
|
public static final int REQUEST_TIMEOUT = 3000;
|
||||||
|
public static final int WAIT_FOR_RECEIVER_TIMEOUT = 60_000;
|
||||||
|
public static final int SEND_BLOCK_TIMEOUT = 10_000;
|
||||||
|
|
||||||
|
private final InputStream inputStream;
|
||||||
|
private final OutputStream outputStream;
|
||||||
|
|
||||||
|
private final byte[] shortBlockBuffer;
|
||||||
|
private final byte[] longBlockBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param inputStream stream for reading received data from other side
|
||||||
|
* @param outputStream stream for writing data to other side
|
||||||
|
*/
|
||||||
|
public Modem(InputStream inputStream, OutputStream outputStream) {
|
||||||
|
this.inputStream = inputStream;
|
||||||
|
this.outputStream = outputStream;
|
||||||
|
shortBlockBuffer = new byte[128];
|
||||||
|
longBlockBuffer = new byte[1024];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for receiver request for transmission
|
||||||
|
*
|
||||||
|
* @return TRUE if receiver requested CRC-16 checksum, FALSE if 8bit checksum
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public boolean waitReceiverRequest() throws IOException {
|
||||||
|
int character;
|
||||||
|
while (true) {
|
||||||
|
character = readByte();
|
||||||
|
if (character == NAK)
|
||||||
|
return false;
|
||||||
|
if (character == ST_C) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a file. <br/>
|
||||||
|
* <p>
|
||||||
|
* This method support correct thread interruption, when thread is interrupted "cancel of transmission" will be send.
|
||||||
|
* So you can move long transmission to other thread and interrupt it according to your algorithm.
|
||||||
|
*
|
||||||
|
* @param file
|
||||||
|
* @param useBlock1K
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void send(Path file, boolean useBlock1K) throws IOException {
|
||||||
|
//open file
|
||||||
|
try (DataInputStream dataStream = new DataInputStream(Files.newInputStream(file))) {
|
||||||
|
|
||||||
|
boolean useCRC16 = waitReceiverRequest();
|
||||||
|
XCRC crc;
|
||||||
|
if (useCRC16)
|
||||||
|
crc = new CRC16();
|
||||||
|
else
|
||||||
|
crc = new CRC8();
|
||||||
|
|
||||||
|
byte[] block;
|
||||||
|
if (useBlock1K)
|
||||||
|
block = new byte[1024];
|
||||||
|
else
|
||||||
|
block = new byte[128];
|
||||||
|
sendDataBlocks(dataStream, 1, crc, block);
|
||||||
|
|
||||||
|
sendEOT();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendDataBlocks(DataInputStream dataStream, int blockNumber, XCRC crc, byte[] block) throws IOException {
|
||||||
|
int dataLength;
|
||||||
|
while ((dataLength = dataStream.read(block)) != -1) {
|
||||||
|
sendBlock(blockNumber++, block, dataLength, crc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendEOT() throws IOException {
|
||||||
|
int errorCount = 0;
|
||||||
|
int character;
|
||||||
|
while (errorCount < 10) {
|
||||||
|
sendByte(EOT);
|
||||||
|
character = readByte();
|
||||||
|
|
||||||
|
if (character == ACK) {
|
||||||
|
return;
|
||||||
|
} else if (character == CAN) {
|
||||||
|
throw new IOException("Transmission terminated");
|
||||||
|
}
|
||||||
|
errorCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendBlock(int blockNumber, byte[] block, int dataLength, XCRC crc) throws IOException {
|
||||||
|
int errorCount;
|
||||||
|
int character;
|
||||||
|
|
||||||
|
if (dataLength < block.length) {
|
||||||
|
block[dataLength] = CPMEOF;
|
||||||
|
}
|
||||||
|
errorCount = 0;
|
||||||
|
|
||||||
|
while (errorCount < MAXERRORS) {
|
||||||
|
|
||||||
|
if (block.length == 1024)
|
||||||
|
outputStream.write(STX);
|
||||||
|
else //128
|
||||||
|
outputStream.write(SOH);
|
||||||
|
outputStream.write(blockNumber);
|
||||||
|
outputStream.write(~blockNumber);
|
||||||
|
|
||||||
|
outputStream.write(block);
|
||||||
|
writeCRC(block, crc);
|
||||||
|
outputStream.flush();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
character = readByte();
|
||||||
|
if (character == ACK) {
|
||||||
|
return;
|
||||||
|
} else if (character == NAK) {
|
||||||
|
errorCount++;
|
||||||
|
break;
|
||||||
|
} else if (character == CAN) {
|
||||||
|
throw new IOException("Transmission terminated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IOException("Too many errors caught, abandoning transfer");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeCRC(byte[] block, XCRC crc) throws IOException {
|
||||||
|
byte[] crcBytes = new byte[crc.getCRCLength()];
|
||||||
|
long crcValue = crc.calcCRC(block);
|
||||||
|
for (int i = 0; i < crc.getCRCLength(); i++) {
|
||||||
|
crcBytes[crc.getCRCLength() - i - 1] = (byte) ((crcValue >> (8 * i)) & 0xFF);
|
||||||
|
}
|
||||||
|
outputStream.write(crcBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive file <br/>
|
||||||
|
* <p>
|
||||||
|
* This method support correct thread interruption, when thread is interrupted "cancel of transmission" will be send.
|
||||||
|
* So you can move long transmission to other thread and interrupt it according to your algorithm.
|
||||||
|
*
|
||||||
|
* @param file file path for storing
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void receive(Path file, boolean useCRC16) throws IOException {
|
||||||
|
try (DataOutputStream dataOutput = new DataOutputStream(Files.newOutputStream(file))) {
|
||||||
|
int available;
|
||||||
|
// clean input stream
|
||||||
|
if ((available = inputStream.available()) > 0) {
|
||||||
|
inputStream.skip(available);
|
||||||
|
}
|
||||||
|
|
||||||
|
int character = requestTransmissionStart(useCRC16);
|
||||||
|
|
||||||
|
XCRC crc;
|
||||||
|
if (useCRC16)
|
||||||
|
crc = new CRC16();
|
||||||
|
else
|
||||||
|
crc = new CRC8();
|
||||||
|
|
||||||
|
|
||||||
|
processDataBlocks(crc, 1, character, dataOutput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processDataBlocks(XCRC crc, int blockNumber, int blockInitialCharacter, DataOutputStream dataOutput) throws IOException {
|
||||||
|
// read blocks until EOT
|
||||||
|
boolean result = false;
|
||||||
|
boolean shortBlock;
|
||||||
|
byte[] block;
|
||||||
|
while (true) {
|
||||||
|
int errorCount = 0;
|
||||||
|
if (blockInitialCharacter == EOT) {
|
||||||
|
// end of transmission
|
||||||
|
sendByte(ACK);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//read and process block
|
||||||
|
shortBlock = (blockInitialCharacter == SOH);
|
||||||
|
try {
|
||||||
|
block = readBlock(blockNumber, shortBlock, crc);
|
||||||
|
dataOutput.write(block);
|
||||||
|
blockNumber++;
|
||||||
|
errorCount = 0;
|
||||||
|
result = true;
|
||||||
|
sendByte(ACK);
|
||||||
|
} catch (InvalidBlockException e) {
|
||||||
|
errorCount++;
|
||||||
|
if (errorCount == MAXERRORS) {
|
||||||
|
interruptTransmission();
|
||||||
|
throw new IOException("Transmission aborted, error count exceeded max");
|
||||||
|
}
|
||||||
|
sendByte(NAK);
|
||||||
|
result = false;
|
||||||
|
} catch (RepeatedBlockException e) {
|
||||||
|
//thats ok, accept and wait for next block
|
||||||
|
sendByte(ACK);
|
||||||
|
} catch (SynchronizationLostException e) {
|
||||||
|
//fatal transmission error
|
||||||
|
interruptTransmission();
|
||||||
|
throw new IOException("Fatal transmission error", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
//wait for next block
|
||||||
|
blockInitialCharacter = readNextBlockStart(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendByte(byte b) throws IOException {
|
||||||
|
outputStream.write(b);
|
||||||
|
outputStream.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request transmission start and return first byte of "first" block from sender (block 1 for XModem, block 0 for YModem)
|
||||||
|
*
|
||||||
|
* @param useCRC16
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public int requestTransmissionStart(boolean useCRC16) throws IOException {
|
||||||
|
int character;
|
||||||
|
int errorCount = 0;
|
||||||
|
byte requestStartByte;
|
||||||
|
if (!useCRC16) {
|
||||||
|
requestStartByte = NAK;
|
||||||
|
} else {
|
||||||
|
requestStartByte = ST_C;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for first block start
|
||||||
|
// request transmission start (will be repeated after 10 second timeout for 10 times)
|
||||||
|
sendByte(requestStartByte);
|
||||||
|
while (true) {
|
||||||
|
character = readByte();
|
||||||
|
|
||||||
|
if (character == SOH || character == STX) {
|
||||||
|
return character;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int readNextBlockStart(boolean lastBlockResult) throws IOException {
|
||||||
|
int character;
|
||||||
|
int errorCount = 0;
|
||||||
|
while (true) {
|
||||||
|
while (true) {
|
||||||
|
character = readByte();
|
||||||
|
if (character == SOH || character == STX || character == EOT) {
|
||||||
|
return character;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// repeat last block result and wait for next block one more time
|
||||||
|
// if (++errorCount < MAXERRORS) {
|
||||||
|
// sendByte(lastBlockResult ? ACK : NAK);
|
||||||
|
// } else {
|
||||||
|
// interruptTransmission();
|
||||||
|
// throw new RuntimeException("Timeout, no data received from transmitter");
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void shortSleep() {
|
||||||
|
try {
|
||||||
|
Thread.sleep(10);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
try {
|
||||||
|
interruptTransmission();
|
||||||
|
} catch (IOException ignore) {
|
||||||
|
}
|
||||||
|
throw new RuntimeException("Transmission was interrupted", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* send CAN to interrupt seance
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void interruptTransmission() throws IOException {
|
||||||
|
sendByte(CAN);
|
||||||
|
sendByte(CAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] readBlock(int blockNumber, boolean shortBlock, XCRC crc) throws IOException, RepeatedBlockException, SynchronizationLostException, InvalidBlockException {
|
||||||
|
byte[] block;
|
||||||
|
|
||||||
|
if (shortBlock) {
|
||||||
|
block = shortBlockBuffer;
|
||||||
|
} else {
|
||||||
|
block = longBlockBuffer;
|
||||||
|
}
|
||||||
|
byte character;
|
||||||
|
|
||||||
|
character = readByte();
|
||||||
|
|
||||||
|
if (character == blockNumber - 1) {
|
||||||
|
// this is repeating of last block, possible ACK lost
|
||||||
|
throw new RepeatedBlockException();
|
||||||
|
}
|
||||||
|
if (character != blockNumber) {
|
||||||
|
// wrong block - fatal loss of synchronization
|
||||||
|
throw new SynchronizationLostException();
|
||||||
|
}
|
||||||
|
|
||||||
|
character = readByte();
|
||||||
|
|
||||||
|
if (character != ~blockNumber) {
|
||||||
|
throw new InvalidBlockException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// data
|
||||||
|
for (int i = 0; i < block.length; i++) {
|
||||||
|
block[i] = readByte();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (inputStream.available() >= crc.getCRCLength()) {
|
||||||
|
if (crc.calcCRC(block) != readCRC(crc)) {
|
||||||
|
throw new InvalidBlockException();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
shortSleep();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long readCRC(XCRC crc) throws IOException {
|
||||||
|
long checkSumma = 0;
|
||||||
|
for (int j = 0; j < crc.getCRCLength(); j++) {
|
||||||
|
checkSumma = (checkSumma << 8) + inputStream.read();
|
||||||
|
}
|
||||||
|
return checkSumma;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte readByte() throws IOException {
|
||||||
|
while (true) {
|
||||||
|
if (inputStream.available() > 0) {
|
||||||
|
int b = inputStream.read();
|
||||||
|
return (byte) b;
|
||||||
|
}
|
||||||
|
shortSleep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RepeatedBlockException extends Exception {
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SynchronizationLostException extends Exception {
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InvalidBlockException extends Exception {
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/main/java/zmodem/xfer/zm/util/ZDLEEncoder.java
Normal file
58
src/main/java/zmodem/xfer/zm/util/ZDLEEncoder.java
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package zmodem.xfer.zm.util;
|
||||||
|
|
||||||
|
|
||||||
|
import zmodem.xfer.zm.packet.Format;
|
||||||
|
import zmodem.xfer.zm.proto.Escape;
|
||||||
|
|
||||||
|
public class ZDLEEncoder {
|
||||||
|
|
||||||
|
private byte[] raw;
|
||||||
|
private byte[] zdle;
|
||||||
|
private int zdleLen;
|
||||||
|
private Format format;
|
||||||
|
|
||||||
|
|
||||||
|
public ZDLEEncoder(byte[] data) {
|
||||||
|
this(data, Format.BIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ZDLEEncoder(byte[] data, Format fmt) {
|
||||||
|
raw = data;
|
||||||
|
format = fmt;
|
||||||
|
zdle = new byte[raw.length * 2];
|
||||||
|
encode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putZdle(byte b) {
|
||||||
|
zdle[zdleLen] = b;
|
||||||
|
zdleLen++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void encode() {
|
||||||
|
byte previous = 0;
|
||||||
|
for (byte b : raw) {
|
||||||
|
|
||||||
|
if ((!format.hex()) && Escape.mustEscape(b, previous, false)) {
|
||||||
|
putZdle(ZModemCharacter.ZDLE.value());
|
||||||
|
b = Escape.escapeIt(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
putZdle(b);
|
||||||
|
previous = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] raw() {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int zdleLen() {
|
||||||
|
return zdleLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] zdle() {
|
||||||
|
return zdle;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
49
src/main/java/zmodem/xfer/zm/util/ZMOptions.java
Normal file
49
src/main/java/zmodem/xfer/zm/util/ZMOptions.java
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package zmodem.xfer.zm.util;
|
||||||
|
|
||||||
|
public enum ZMOptions {
|
||||||
|
|
||||||
|
CANFDX(0x01), /* Rx can send and receive true FDX */
|
||||||
|
CANOVIO(0x02), /* Rx can receive data during disk I/O */
|
||||||
|
CANBRK(0x04), /* Rx can send a break signal */
|
||||||
|
CANCRY(0x08), /* Receiver can decrypt */
|
||||||
|
CANLZW(0x10), /* Receiver can uncompress */
|
||||||
|
CANFC32(0x20), /* Receiver can use 32 bit Frame Check */
|
||||||
|
ESCCTL(0x40), /* Receiver expects ctl chars to be escaped */
|
||||||
|
ESC8(0x80), /* Receiver expects 8th bit to be escaped */
|
||||||
|
ZCBIN(0x01);
|
||||||
|
|
||||||
|
private byte value;
|
||||||
|
|
||||||
|
private ZMOptions(char b) {
|
||||||
|
value = (byte) b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ZMOptions(int b) {
|
||||||
|
value = (byte) b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ZMOptions(byte b) {
|
||||||
|
value = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public byte value() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte with(ZMOptions... oo) {
|
||||||
|
byte r = 0;
|
||||||
|
for (ZMOptions o : oo)
|
||||||
|
r = (byte) (r | o.value());
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ZMOptions forbyte(byte b) {
|
||||||
|
for (ZMOptions zb : values()) {
|
||||||
|
if (zb.value() == b)
|
||||||
|
return zb;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
9
src/main/java/zmodem/xfer/zm/util/ZMPacket.java
Normal file
9
src/main/java/zmodem/xfer/zm/util/ZMPacket.java
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package zmodem.xfer.zm.util;
|
||||||
|
|
||||||
|
|
||||||
|
import zmodem.xfer.util.Buffer;
|
||||||
|
|
||||||
|
public abstract class ZMPacket {
|
||||||
|
public abstract Buffer marshall();
|
||||||
|
|
||||||
|
}
|
||||||
39
src/main/java/zmodem/xfer/zm/util/ZMPacketFactory.java
Normal file
39
src/main/java/zmodem/xfer/zm/util/ZMPacketFactory.java
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package zmodem.xfer.zm.util;
|
||||||
|
|
||||||
|
|
||||||
|
import zmodem.xfer.zm.packet.DataPacket;
|
||||||
|
|
||||||
|
public class ZMPacketFactory {
|
||||||
|
|
||||||
|
public ZMPacketFactory() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataPacket createZFilePacket(String pathname, long flen) {
|
||||||
|
return createZFilePacket(pathname, flen, 0, "0", 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataPacket createZFilePacket(String pathname, long flen, long ts, String mode/*octal*/
|
||||||
|
, int remainingfiles, long remainingBytes) {
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
|
||||||
|
builder.append(pathname);
|
||||||
|
builder.append('\0');
|
||||||
|
builder.append(flen);
|
||||||
|
builder.append(' ');
|
||||||
|
builder.append(ts);
|
||||||
|
builder.append(' ');
|
||||||
|
builder.append(mode);
|
||||||
|
builder.append(' ');
|
||||||
|
builder.append('0');
|
||||||
|
builder.append(' ');
|
||||||
|
builder.append(remainingfiles);
|
||||||
|
builder.append(' ');
|
||||||
|
builder.append(remainingBytes);
|
||||||
|
builder.append('0');
|
||||||
|
|
||||||
|
return new DataPacket(ZModemCharacter.ZCRCW, builder.toString().getBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
64
src/main/java/zmodem/xfer/zm/util/ZModemCharacter.java
Normal file
64
src/main/java/zmodem/xfer/zm/util/ZModemCharacter.java
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package zmodem.xfer.zm.util;
|
||||||
|
|
||||||
|
public enum ZModemCharacter {
|
||||||
|
ZPAD('*'),
|
||||||
|
ZDLE(0x18),
|
||||||
|
ZDLEE(ZDLE.value() ^ 0x40),
|
||||||
|
ZBIN('A'),
|
||||||
|
ZHEX('B'),
|
||||||
|
ZBIN32('C'),
|
||||||
|
ZCRCE('h'),
|
||||||
|
ZCRCG('i'),
|
||||||
|
ZCRCQ('j'),
|
||||||
|
ZCRCW('k'),
|
||||||
|
ZRUB0('l'),
|
||||||
|
ZRUB1('m'),
|
||||||
|
ZRQINIT(0),
|
||||||
|
ZRINIT(1),
|
||||||
|
ZSINIT(2),
|
||||||
|
ZACK(3),
|
||||||
|
ZFILE(4),
|
||||||
|
ZSKIP(5),
|
||||||
|
ZNAK(6),
|
||||||
|
ZABORT(7),
|
||||||
|
ZFIN(8),
|
||||||
|
ZRPOS(9),
|
||||||
|
ZDATA(10),
|
||||||
|
ZEOF(11),
|
||||||
|
ZFERR(12),
|
||||||
|
ZCRC(13),
|
||||||
|
ZCHALLENGE(14),
|
||||||
|
ZCOMPL(15),
|
||||||
|
ZCAN(16),
|
||||||
|
ZFREECNT(17),
|
||||||
|
ZCOMMAND(18),
|
||||||
|
ZSTDERR(19);
|
||||||
|
|
||||||
|
private byte value;
|
||||||
|
|
||||||
|
private ZModemCharacter(char b) {
|
||||||
|
value = (byte) b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ZModemCharacter(int b) {
|
||||||
|
value = (byte) b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ZModemCharacter(byte b) {
|
||||||
|
value = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public byte value() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ZModemCharacter forbyte(byte b) {
|
||||||
|
for (ZModemCharacter zb : values()) {
|
||||||
|
if (zb.value() == b)
|
||||||
|
return zb;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
259
src/main/java/zmodem/xfer/zm/util/ZModemReceive.java
Normal file
259
src/main/java/zmodem/xfer/zm/util/ZModemReceive.java
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
package zmodem.xfer.zm.util;
|
||||||
|
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.commons.lang3.math.NumberUtils;
|
||||||
|
import org.apache.commons.net.io.CopyStreamAdapter;
|
||||||
|
import org.apache.commons.net.io.CopyStreamListener;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import zmodem.FileCopyStreamEvent;
|
||||||
|
import zmodem.util.EmptyFileAdapter;
|
||||||
|
import zmodem.util.FileAdapter;
|
||||||
|
import zmodem.xfer.util.InvalidChecksumException;
|
||||||
|
import zmodem.xfer.zm.packet.*;
|
||||||
|
import zmodem.zm.io.ZMPacketInputStream;
|
||||||
|
import zmodem.zm.io.ZMPacketOutputStream;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
|
||||||
|
public class ZModemReceive {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ZModemReceive.class);
|
||||||
|
|
||||||
|
private final CopyStreamAdapter adapter = new CopyStreamAdapter();
|
||||||
|
private final Supplier<FileAdapter> destinationSupplier;
|
||||||
|
private FileAdapter destination;
|
||||||
|
|
||||||
|
private FileAdapter file;
|
||||||
|
private int fOffset = 0;
|
||||||
|
private Long filesize;
|
||||||
|
private int remaining = 0;
|
||||||
|
private int index = 0;
|
||||||
|
|
||||||
|
private OutputStream fileOs = null;
|
||||||
|
|
||||||
|
private final InputStream netIs;
|
||||||
|
private final OutputStream netOs;
|
||||||
|
|
||||||
|
private enum Expect {
|
||||||
|
FILENAME, DATA, NOTHING;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public ZModemReceive(Supplier<FileAdapter> destDir, InputStream netin, OutputStream netout) throws IOException {
|
||||||
|
destinationSupplier = destDir;
|
||||||
|
netIs = netin;
|
||||||
|
netOs = netout;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void open(int offset) throws IOException {
|
||||||
|
boolean append = false;
|
||||||
|
|
||||||
|
if (offset != 0) {
|
||||||
|
if (file.exists() && file.length() == offset)
|
||||||
|
append = true;
|
||||||
|
else
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
IOUtils.closeQuietly(fileOs);
|
||||||
|
|
||||||
|
fileOs = file.getOutputStream(append);
|
||||||
|
fOffset = offset;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decodeFileNameData(DataPacket p) {
|
||||||
|
ByteArrayOutputStream filename = new ByteArrayOutputStream();
|
||||||
|
StringBuilder extract = new StringBuilder();
|
||||||
|
byte[] data = p.data();
|
||||||
|
for (int i = 0; i < data.length; i++) {
|
||||||
|
byte b = data[i];
|
||||||
|
if (b == 0) {
|
||||||
|
for (int j = i + 1; j < data.length; j++) {
|
||||||
|
b = data[j];
|
||||||
|
if (b == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
extract.append((char) b);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
filename.write(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String[] segments = extract.toString().split(StringUtils.SPACE);
|
||||||
|
if (ArrayUtils.isNotEmpty(segments)) {
|
||||||
|
// filesize
|
||||||
|
if (segments.length >= 1) {
|
||||||
|
this.filesize = NumberUtils.toLong(segments[0]);
|
||||||
|
}
|
||||||
|
// remaining
|
||||||
|
if (segments.length >= 5) {
|
||||||
|
this.remaining = NumberUtils.toInt(segments[4]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file = destination.getChild(filename.toString());
|
||||||
|
fOffset = 0;
|
||||||
|
|
||||||
|
index++;
|
||||||
|
|
||||||
|
adapter.bytesTransferred(new FileCopyStreamEvent(this, file.getName(), remaining - index, index,
|
||||||
|
this.filesize, fOffset, 0, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCopyStreamListener(CopyStreamListener listener) {
|
||||||
|
adapter.addCopyStreamListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeCopyStreamListener(CopyStreamListener listener) {
|
||||||
|
adapter.removeCopyStreamListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeData(DataPacket p) throws IOException {
|
||||||
|
final byte[] data = p.data();
|
||||||
|
|
||||||
|
fileOs.write(data);
|
||||||
|
fOffset += data.length;
|
||||||
|
|
||||||
|
// 开始传输
|
||||||
|
adapter.bytesTransferred(new FileCopyStreamEvent(this, file.getName(), remaining, index,
|
||||||
|
this.filesize, fOffset, 0, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean initDestination() {
|
||||||
|
if (destination != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
destination = destinationSupplier.get();
|
||||||
|
return !(destination instanceof EmptyFileAdapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void receive(Supplier<Boolean> isCancelled) {
|
||||||
|
ZMPacketInputStream is = new ZMPacketInputStream(netIs);
|
||||||
|
ZMPacketOutputStream os = new ZMPacketOutputStream(netOs);
|
||||||
|
|
||||||
|
Expect expect = Expect.NOTHING;
|
||||||
|
|
||||||
|
byte[] recvOpt = {0, 4, 0, ZMOptions.with(ZMOptions.ESCCTL, ZMOptions.ESC8)};
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
boolean end = false;
|
||||||
|
int errorCount = 0;
|
||||||
|
ZMPacket packet = null;
|
||||||
|
while (!end) {
|
||||||
|
try {
|
||||||
|
packet = is.read();
|
||||||
|
} catch (InvalidChecksumException ice) {
|
||||||
|
if (log.isErrorEnabled()) {
|
||||||
|
log.error(ice.getMessage(), ice);
|
||||||
|
}
|
||||||
|
++errorCount;
|
||||||
|
if (errorCount >= 3) {
|
||||||
|
os.write(new Cancel());
|
||||||
|
end = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet instanceof Cancel) {
|
||||||
|
end = true;
|
||||||
|
} else if (packet instanceof Finish) {
|
||||||
|
end = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCancelled.get()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果重定向为空,则终止传输
|
||||||
|
if (destination instanceof EmptyFileAdapter) {
|
||||||
|
os.write(new Cancel());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet instanceof Header header) {
|
||||||
|
switch (header.type()) {
|
||||||
|
case ZRQINIT:
|
||||||
|
os.write(new Header(Format.HEX, ZModemCharacter.ZRINIT, recvOpt));
|
||||||
|
break;
|
||||||
|
case ZFILE:
|
||||||
|
expect = Expect.FILENAME;
|
||||||
|
break;
|
||||||
|
case ZEOF:
|
||||||
|
os.write(new Header(Format.HEX, ZModemCharacter.ZRINIT, recvOpt));
|
||||||
|
expect = Expect.NOTHING;
|
||||||
|
file = null;
|
||||||
|
fileOs.flush();
|
||||||
|
IOUtils.closeQuietly(fileOs);
|
||||||
|
fileOs = null;
|
||||||
|
break;
|
||||||
|
case ZDATA:
|
||||||
|
open(header.getPos());
|
||||||
|
expect = Expect.DATA;
|
||||||
|
break;
|
||||||
|
case ZFIN:
|
||||||
|
os.write(new Header(Format.HEX, ZModemCharacter.ZFIN));
|
||||||
|
end = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
end = true;
|
||||||
|
os.write(new Cancel());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet instanceof DataPacket data) {
|
||||||
|
switch (expect) {
|
||||||
|
case NOTHING:
|
||||||
|
os.write(new Header(Format.HEX, ZModemCharacter.ZRINIT, recvOpt));
|
||||||
|
break;
|
||||||
|
case FILENAME:
|
||||||
|
if (!initDestination()) {
|
||||||
|
end = true;
|
||||||
|
os.write(new Cancel());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
decodeFileNameData(data);
|
||||||
|
if (file.length() == filesize) {
|
||||||
|
os.write(new Header(Format.HEX, ZModemCharacter.ZSKIP));
|
||||||
|
adapter.bytesTransferred(new FileCopyStreamEvent(this, file.getName(), remaining, index,
|
||||||
|
this.filesize, fOffset, 0, true));
|
||||||
|
} else {
|
||||||
|
os.write(new Header(Format.HEX, ZModemCharacter.ZRPOS, (int) file.length()));
|
||||||
|
}
|
||||||
|
expect = Expect.NOTHING;
|
||||||
|
break;
|
||||||
|
case DATA:
|
||||||
|
writeData(data);
|
||||||
|
switch (data.type()) {
|
||||||
|
case ZCRCW:
|
||||||
|
expect = Expect.NOTHING;
|
||||||
|
case ZCRCQ:
|
||||||
|
os.write(new Header(Format.HEX, ZModemCharacter.ZACK, fOffset));
|
||||||
|
break;
|
||||||
|
case ZCRCE:
|
||||||
|
expect = Expect.NOTHING;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (log.isErrorEnabled()) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
IOUtils.closeQuietly(fileOs);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
213
src/main/java/zmodem/xfer/zm/util/ZModemSend.java
Normal file
213
src/main/java/zmodem/xfer/zm/util/ZModemSend.java
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
package zmodem.xfer.zm.util;
|
||||||
|
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
import org.apache.commons.net.io.CopyStreamAdapter;
|
||||||
|
import org.apache.commons.net.io.CopyStreamListener;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import zmodem.FileCopyStreamEvent;
|
||||||
|
import zmodem.util.FileAdapter;
|
||||||
|
import zmodem.xfer.util.InvalidChecksumException;
|
||||||
|
import zmodem.xfer.zm.packet.*;
|
||||||
|
import zmodem.zm.io.ZMPacketInputStream;
|
||||||
|
import zmodem.zm.io.ZMPacketOutputStream;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
|
||||||
|
public class ZModemSend {
|
||||||
|
|
||||||
|
private static final int packLen = 1024 * 8;
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ZModemSend.class);
|
||||||
|
|
||||||
|
private final byte[] data = new byte[packLen];
|
||||||
|
private final CopyStreamAdapter adapter = new CopyStreamAdapter();
|
||||||
|
private final Supplier<List<FileAdapter>> destinationSupplier;
|
||||||
|
private final InputStream netIs;
|
||||||
|
private final OutputStream netOs;
|
||||||
|
|
||||||
|
private List<FileAdapter> files;
|
||||||
|
private Iterator<FileAdapter> iter;
|
||||||
|
|
||||||
|
private FileAdapter file;
|
||||||
|
private int fOffset = 0;
|
||||||
|
private int index = 0;
|
||||||
|
private int filesize = 0;
|
||||||
|
private boolean atEof = false;
|
||||||
|
private InputStream fileIs;
|
||||||
|
|
||||||
|
|
||||||
|
public ZModemSend(Supplier<List<FileAdapter>> destinationSupplier, InputStream netin, OutputStream netout) throws IOException {
|
||||||
|
this.destinationSupplier = destinationSupplier;
|
||||||
|
netIs = netin;
|
||||||
|
netOs = netout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean nextFile() throws IOException {
|
||||||
|
|
||||||
|
IOUtils.closeQuietly(fileIs);
|
||||||
|
|
||||||
|
if (files == null) {
|
||||||
|
files = destinationSupplier.get();
|
||||||
|
iter = files.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!iter.hasNext())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
|
||||||
|
file = iter.next();
|
||||||
|
fileIs = file.getInputStream();
|
||||||
|
filesize = fileIs.available();
|
||||||
|
fOffset = 0;
|
||||||
|
atEof = false;
|
||||||
|
index++;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void addCopyStreamListener(CopyStreamListener listener) {
|
||||||
|
adapter.addCopyStreamListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeCopyStreamListener(CopyStreamListener listener) {
|
||||||
|
adapter.removeCopyStreamListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void position(int offset) throws IOException {
|
||||||
|
if (offset != fOffset) {
|
||||||
|
fileIs.skipNBytes(offset);
|
||||||
|
fOffset = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getNextBlock() throws IOException {
|
||||||
|
final int len = fileIs.read(data);
|
||||||
|
|
||||||
|
/* we know it is a file: all the data is locally available.*/
|
||||||
|
if (len < data.length)
|
||||||
|
atEof = true;
|
||||||
|
else if (fileIs.available() == 0)
|
||||||
|
atEof = true;
|
||||||
|
|
||||||
|
if (len == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fOffset += len;
|
||||||
|
|
||||||
|
if (len != data.length)
|
||||||
|
return ArrayUtils.subarray(data, 0, len);
|
||||||
|
else
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataPacket getNextDataPacket() throws IOException {
|
||||||
|
byte[] data = getNextBlock();
|
||||||
|
|
||||||
|
ZModemCharacter fe = ZModemCharacter.ZCRCW;
|
||||||
|
if (atEof) {
|
||||||
|
fe = ZModemCharacter.ZCRCE;
|
||||||
|
fileIs.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data == null) {
|
||||||
|
return new DataPacket(fe);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DataPacket(fe, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void send(Supplier<Boolean> isCancelled) {
|
||||||
|
ZMPacketFactory factory = new ZMPacketFactory();
|
||||||
|
|
||||||
|
ZMPacketInputStream is = new ZMPacketInputStream(netIs);
|
||||||
|
ZMPacketOutputStream os = new ZMPacketOutputStream(netOs);
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
boolean end = false;
|
||||||
|
int errorCount = 0;
|
||||||
|
ZMPacket packet = null;
|
||||||
|
|
||||||
|
while (!end) {
|
||||||
|
try {
|
||||||
|
packet = is.read();
|
||||||
|
} catch (InvalidChecksumException ice) {
|
||||||
|
++errorCount;
|
||||||
|
if (errorCount > 20) {
|
||||||
|
os.write(new Cancel());
|
||||||
|
end = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet instanceof Cancel) {
|
||||||
|
end = true;
|
||||||
|
} else if (isCancelled.get()) {
|
||||||
|
os.write(new Cancel());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet instanceof Header header) {
|
||||||
|
switch (header.type()) {
|
||||||
|
case ZSKIP:
|
||||||
|
fireBytesTransferred(true);
|
||||||
|
case ZRINIT:
|
||||||
|
if (!nextFile()) {
|
||||||
|
os.write(new Header(Format.BIN, ZModemCharacter.ZFIN));
|
||||||
|
} else {
|
||||||
|
os.write(new Header(Format.BIN, ZModemCharacter.ZFILE, new byte[]{0, 0, 0, ZMOptions.with(ZMOptions.ZCBIN)}));
|
||||||
|
os.write(factory.createZFilePacket(file.getName(), filesize));
|
||||||
|
fireBytesTransferred(false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ZRPOS:
|
||||||
|
if (!atEof)
|
||||||
|
position(header.getPos());
|
||||||
|
case ZACK:
|
||||||
|
os.write(new Header(Format.BIN, ZModemCharacter.ZDATA, fOffset));
|
||||||
|
os.write(getNextDataPacket());
|
||||||
|
if (atEof) {
|
||||||
|
os.write(new Header(Format.HEX, ZModemCharacter.ZEOF, fOffset));
|
||||||
|
}
|
||||||
|
fireBytesTransferred(false);
|
||||||
|
break;
|
||||||
|
case ZFIN:
|
||||||
|
end = true;
|
||||||
|
os.write(new Finish());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
end = true;
|
||||||
|
os.write(new Cancel());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (log.isErrorEnabled()) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
IOUtils.closeQuietly(fileIs);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fireBytesTransferred(boolean skip) {
|
||||||
|
if (this.filesize == fOffset) {
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
adapter.bytesTransferred(new FileCopyStreamEvent(this, file.getName(), files.size() - index + 1, index,
|
||||||
|
this.filesize, fOffset, 0, skip));
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/main/java/zmodem/zm/io/ZMPacketInputStream.java
Normal file
145
src/main/java/zmodem/zm/io/ZMPacketInputStream.java
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package zmodem.zm.io;
|
||||||
|
|
||||||
|
import zmodem.xfer.io.ObjectInputStream;
|
||||||
|
import zmodem.xfer.util.ByteBuffer;
|
||||||
|
import zmodem.xfer.util.CRC;
|
||||||
|
import zmodem.xfer.zm.packet.*;
|
||||||
|
import zmodem.xfer.zm.proto.Action;
|
||||||
|
import zmodem.xfer.zm.proto.Escape;
|
||||||
|
import zmodem.xfer.zm.util.ZMPacket;
|
||||||
|
import zmodem.xfer.zm.util.ZModemCharacter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
|
||||||
|
public class ZMPacketInputStream extends ObjectInputStream<ZMPacket> {
|
||||||
|
|
||||||
|
private final InputStream netIs;
|
||||||
|
private CRC dataCRC = new CRC(CRC.Type.CRC16);
|
||||||
|
private boolean gotFIN = false;
|
||||||
|
private boolean acceptsHeader = true;
|
||||||
|
|
||||||
|
public ZMPacketInputStream(InputStream is) {
|
||||||
|
netIs = is;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean ignored(int b) {
|
||||||
|
return b == 0x11 || b == 0x13 || b == 0x91 || b == 0x93;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte implRead() throws IOException {
|
||||||
|
int n;
|
||||||
|
do {
|
||||||
|
n = netIs.read();
|
||||||
|
} while (ignored(n));
|
||||||
|
|
||||||
|
if (n == -1) {
|
||||||
|
throw new IOException("Closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (byte) n;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ZMPacket read() throws IOException {
|
||||||
|
ByteBuffer zbuff = ByteBuffer.allocate(1024 * 10);
|
||||||
|
boolean doread = true;
|
||||||
|
Action action = Action.ESCAPE;
|
||||||
|
|
||||||
|
int beforeStop = -1;
|
||||||
|
int countCan = 0;
|
||||||
|
|
||||||
|
while (doread) {
|
||||||
|
byte n = implRead();
|
||||||
|
|
||||||
|
if (gotFIN && n == 'O') {
|
||||||
|
n = implRead();
|
||||||
|
if (n == 'O') {
|
||||||
|
return new Finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n == ZModemCharacter.ZDLE.value()) {
|
||||||
|
n = (byte) netIs.read();
|
||||||
|
|
||||||
|
if (n == ZModemCharacter.ZDLE.value())
|
||||||
|
countCan += 2;
|
||||||
|
else
|
||||||
|
countCan = 0;
|
||||||
|
|
||||||
|
Escape escape = Escape.detect(n, acceptsHeader);
|
||||||
|
|
||||||
|
if (escape.action() != Action.ESCAPE && beforeStop < 0) {
|
||||||
|
action = escape.action();
|
||||||
|
|
||||||
|
if (escape.action() == Action.DATA)
|
||||||
|
beforeStop = dataCRC.size();
|
||||||
|
else
|
||||||
|
beforeStop = escape.len();
|
||||||
|
|
||||||
|
dataCRC.update(n);
|
||||||
|
} else {
|
||||||
|
n = Escape.escapeIt(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
zbuff.put(n);
|
||||||
|
|
||||||
|
if (beforeStop < 0)
|
||||||
|
dataCRC.update(n);
|
||||||
|
|
||||||
|
if (beforeStop == 0)
|
||||||
|
doread = false;
|
||||||
|
|
||||||
|
if (beforeStop > 0)
|
||||||
|
beforeStop--;
|
||||||
|
|
||||||
|
if (countCan >= 5) {
|
||||||
|
doread = false;
|
||||||
|
action = Action.CANCEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
zbuff.flip();
|
||||||
|
|
||||||
|
ZMPacket r = null;
|
||||||
|
switch (action) {
|
||||||
|
case HEADER:
|
||||||
|
r = Header.unmarshall(zbuff);
|
||||||
|
|
||||||
|
|
||||||
|
if (((Header) r).format() == Format.BIN32)
|
||||||
|
dataCRC = new CRC(CRC.Type.CRC32);
|
||||||
|
else
|
||||||
|
dataCRC = new CRC(CRC.Type.CRC16);
|
||||||
|
|
||||||
|
if (((Header) r).type() == ZModemCharacter.ZFIN)
|
||||||
|
gotFIN = true;
|
||||||
|
if (((Header) r).type() == ZModemCharacter.ZDATA || ((Header) r).type() == ZModemCharacter.ZFILE)
|
||||||
|
acceptsHeader = false;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case DATA:
|
||||||
|
dataCRC.finalized();
|
||||||
|
|
||||||
|
r = DataPacket.unmarshall(zbuff, dataCRC);
|
||||||
|
|
||||||
|
dataCRC = new CRC(dataCRC.type());
|
||||||
|
|
||||||
|
if (((DataPacket) r).type() == ZModemCharacter.ZCRCG)
|
||||||
|
acceptsHeader = false;
|
||||||
|
else
|
||||||
|
acceptsHeader = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case CANCEL:
|
||||||
|
r = new Cancel();
|
||||||
|
dataCRC = new CRC(dataCRC.type());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
67
src/main/java/zmodem/zm/io/ZMPacketOutputStream.java
Normal file
67
src/main/java/zmodem/zm/io/ZMPacketOutputStream.java
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package zmodem.zm.io;
|
||||||
|
|
||||||
|
import zmodem.xfer.io.ObjectOutputStream;
|
||||||
|
import zmodem.xfer.util.ASCII;
|
||||||
|
import zmodem.xfer.util.Buffer;
|
||||||
|
import zmodem.xfer.zm.packet.DataPacket;
|
||||||
|
import zmodem.xfer.zm.packet.Format;
|
||||||
|
import zmodem.xfer.zm.packet.Header;
|
||||||
|
import zmodem.xfer.zm.util.ZMPacket;
|
||||||
|
import zmodem.xfer.zm.util.ZModemCharacter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
|
||||||
|
public class ZMPacketOutputStream extends ObjectOutputStream<ZMPacket> {
|
||||||
|
|
||||||
|
private final OutputStream os;
|
||||||
|
|
||||||
|
public ZMPacketOutputStream(OutputStream netOs) {
|
||||||
|
os = netOs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void implWrite(byte b) throws IOException {
|
||||||
|
//System.out.printf("%02x",b);
|
||||||
|
os.write(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(ZMPacket o) throws IOException {
|
||||||
|
Buffer buff = o.marshall();
|
||||||
|
Format fmt = null;
|
||||||
|
|
||||||
|
if (o instanceof Header)
|
||||||
|
fmt = ((Header) o).format();
|
||||||
|
|
||||||
|
|
||||||
|
if (fmt != null) {
|
||||||
|
for (int i = 0; i < fmt.width(); i++)
|
||||||
|
implWrite(ZModemCharacter.ZPAD.value());
|
||||||
|
|
||||||
|
implWrite(ZModemCharacter.ZDLE.value());
|
||||||
|
implWrite(fmt.character());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (buff.hasRemaining()) {
|
||||||
|
byte[] buf = new byte[buff.remaining()];
|
||||||
|
buff.get(buf);
|
||||||
|
os.write(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fmt != null) if (fmt.hex()) {
|
||||||
|
implWrite(ASCII.CR.value());
|
||||||
|
implWrite(ASCII.LF.value());
|
||||||
|
implWrite(ASCII.XON.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o instanceof DataPacket) if (((DataPacket) o).type() == ZModemCharacter.ZCRCW)
|
||||||
|
implWrite(ASCII.XON.value());
|
||||||
|
|
||||||
|
|
||||||
|
os.flush();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
50
src/main/kotlin/app/termora/Actions.kt
Normal file
50
src/main/kotlin/app/termora/Actions.kt
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
object Actions {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开设置
|
||||||
|
*/
|
||||||
|
const val SETTING = "SettingAction"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将命令发送到多个会话
|
||||||
|
*/
|
||||||
|
const val MULTIPLE = "MultipleAction"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找
|
||||||
|
*/
|
||||||
|
const val FIND_EVERYWHERE = "FindEverywhereAction"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关键词高亮
|
||||||
|
*/
|
||||||
|
const val KEYWORD_HIGHLIGHT_EVERYWHERE = "KeywordHighlightAction"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key manager
|
||||||
|
*/
|
||||||
|
const val KEY_MANAGER = "KeyManagerAction"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新
|
||||||
|
*/
|
||||||
|
const val APP_UPDATE = "AppUpdateAction"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 宏
|
||||||
|
*/
|
||||||
|
const val MACRO = "MacroAction"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加主机对话框
|
||||||
|
*/
|
||||||
|
const val ADD_HOST = "AddHostAction"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开一个主机
|
||||||
|
*/
|
||||||
|
const val OPEN_HOST = "OpenHostAction"
|
||||||
|
}
|
||||||
16
src/main/kotlin/app/termora/AnAction.kt
Normal file
16
src/main/kotlin/app/termora/AnAction.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import org.jdesktop.swingx.action.BoundAction
|
||||||
|
import javax.swing.Icon
|
||||||
|
|
||||||
|
abstract class AnAction : BoundAction {
|
||||||
|
|
||||||
|
constructor() : super()
|
||||||
|
constructor(icon: Icon) : super() {
|
||||||
|
super.putValue(SMALL_ICON, icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(name: String?) : super(name)
|
||||||
|
constructor(name: String?, icon: Icon?) : super(name, icon)
|
||||||
|
|
||||||
|
}
|
||||||
146
src/main/kotlin/app/termora/Application.kt
Normal file
146
src/main/kotlin/app/termora/Application.kt
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import com.jthemedetecor.util.OsInfo
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.SystemUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Desktop
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URI
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
object Application {
|
||||||
|
private val services = Collections.synchronizedMap(mutableMapOf<KClass<*>, Any>())
|
||||||
|
private lateinit var baseDataDir: File
|
||||||
|
|
||||||
|
|
||||||
|
val ohMyJson = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
// 默认值不输出
|
||||||
|
encodeDefaults = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val httpClient by lazy {
|
||||||
|
OkHttpClient.Builder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(10))
|
||||||
|
.callTimeout(Duration.ofSeconds(60))
|
||||||
|
.writeTimeout(Duration.ofSeconds(60))
|
||||||
|
.readTimeout(Duration.ofSeconds(60))
|
||||||
|
.addInterceptor(
|
||||||
|
HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
|
||||||
|
private val log = LoggerFactory.getLogger(HttpLoggingInterceptor::class.java)
|
||||||
|
override fun log(message: String) {
|
||||||
|
if (log.isDebugEnabled) log.debug(message)
|
||||||
|
}
|
||||||
|
}).setLevel(HttpLoggingInterceptor.Level.BASIC)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDefaultShell(): String {
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
return "cmd.exe"
|
||||||
|
} else {
|
||||||
|
val shell = System.getenv("SHELL")
|
||||||
|
if (shell != null && shell.isNotBlank()) {
|
||||||
|
return shell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "/bin/bash"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBaseDataDir(): File {
|
||||||
|
if (::baseDataDir.isInitialized) {
|
||||||
|
return baseDataDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从启动参数取
|
||||||
|
var baseDataDir = System.getProperty("${getName()}.base-data-dir".lowercase())
|
||||||
|
// 取不到从环境取
|
||||||
|
if (StringUtils.isBlank(baseDataDir)) {
|
||||||
|
baseDataDir = System.getenv("${getName()}-BASE-DATA-DIR".uppercase())
|
||||||
|
}
|
||||||
|
|
||||||
|
var dir = File(SystemUtils.getUserHome(), ".${getName()}".lowercase())
|
||||||
|
if (StringUtils.isNotBlank(baseDataDir)) {
|
||||||
|
dir = File(baseDataDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FileUtils.forceMkdir(dir)
|
||||||
|
Application.baseDataDir = dir
|
||||||
|
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDatabaseFile(): File {
|
||||||
|
return FileUtils.getFile(getBaseDataDir(), "storage")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getVersion(): String {
|
||||||
|
var version = System.getProperty("jpackage.app-version")
|
||||||
|
if (version.isNullOrBlank()) {
|
||||||
|
version = System.getProperty("app-version")
|
||||||
|
}
|
||||||
|
if (version.isNullOrBlank()) {
|
||||||
|
version = "unknown"
|
||||||
|
}
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAppPath(): String {
|
||||||
|
return StringUtils.defaultString(System.getProperty("jpackage.app-path"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getName(): String {
|
||||||
|
return "Termora"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun browse(uri: URI, async: Boolean = true) {
|
||||||
|
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||||
|
Desktop.getDesktop().browse(uri)
|
||||||
|
} else if (async) {
|
||||||
|
@Suppress("OPT_IN_USAGE")
|
||||||
|
GlobalScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
||||||
|
} else {
|
||||||
|
tryBrowse(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <T : Any> getService(clazz: KClass<T>): T {
|
||||||
|
if (services.containsKey(clazz)) {
|
||||||
|
return services[clazz] as T
|
||||||
|
}
|
||||||
|
throw IllegalStateException("$clazz does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun registerService(clazz: KClass<*>, service: Any) {
|
||||||
|
if (services.containsKey(clazz)) {
|
||||||
|
throw IllegalStateException("$clazz already registered")
|
||||||
|
}
|
||||||
|
services[clazz] = service
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryBrowse(uri: URI) {
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
ProcessBuilder("explorer", uri.toString()).start()
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
ProcessBuilder("open", uri.toString()).start()
|
||||||
|
} else if (SystemInfo.isLinux && OsInfo.isGnome()) {
|
||||||
|
ProcessBuilder("xdg-open", uri.toString()).start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main/kotlin/app/termora/ApplicationDisposable.kt
Normal file
10
src/main/kotlin/app/termora/ApplicationDisposable.kt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将在 JVM 进程退出时释放
|
||||||
|
*/
|
||||||
|
class ApplicationDisposable : Disposable {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { ApplicationDisposable() }
|
||||||
|
}
|
||||||
|
}
|
||||||
254
src/main/kotlin/app/termora/ApplicationRunner.kt
Normal file
254
src/main/kotlin/app/termora/ApplicationRunner.kt
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.db.Database
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.FlatSystemProperties
|
||||||
|
import com.formdev.flatlaf.extras.FlatInspector
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import com.jthemedetecor.OsThemeDetector
|
||||||
|
import com.sun.jna.platform.WindowUtils
|
||||||
|
import com.sun.jna.platform.win32.User32
|
||||||
|
import com.sun.jna.ptr.IntByReference
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.lang3.LocaleUtils
|
||||||
|
import org.apache.commons.lang3.SystemUtils
|
||||||
|
import org.apache.commons.lang3.math.NumberUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.tinylog.configuration.Configuration
|
||||||
|
import java.io.File
|
||||||
|
import java.io.RandomAccessFile
|
||||||
|
import java.nio.channels.FileChannel
|
||||||
|
import java.nio.channels.FileLock
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.*
|
||||||
|
import javax.swing.WindowConstants.DISPOSE_ON_CLOSE
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
class ApplicationRunner {
|
||||||
|
private lateinit var singletonLock: FileLock
|
||||||
|
private val log by lazy {
|
||||||
|
if (!::singletonLock.isInitialized) {
|
||||||
|
throw UnsupportedOperationException("Singleton lock is not initialized")
|
||||||
|
}
|
||||||
|
LoggerFactory.getLogger("Main")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun run() {
|
||||||
|
// 覆盖 tinylog 配置
|
||||||
|
setupTinylog()
|
||||||
|
|
||||||
|
// 是否单例
|
||||||
|
checkSingleton()
|
||||||
|
|
||||||
|
// 打印系统信息
|
||||||
|
printSystemInfo()
|
||||||
|
|
||||||
|
SwingUtilities.invokeAndWait {
|
||||||
|
// 打开数据库
|
||||||
|
openDatabase()
|
||||||
|
|
||||||
|
// 加载设置
|
||||||
|
loadSettings()
|
||||||
|
|
||||||
|
// 设置 LAF
|
||||||
|
setupLaf()
|
||||||
|
|
||||||
|
// 解密数据
|
||||||
|
openDoor()
|
||||||
|
|
||||||
|
// 启动主窗口
|
||||||
|
startMainFrame()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun openDoor() {
|
||||||
|
if (Doorman.instance.isWorking()) {
|
||||||
|
if (!DoormanDialog(null).open()) {
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startMainFrame() {
|
||||||
|
val frame = TermoraFrame()
|
||||||
|
frame.title = if (SystemInfo.isLinux) null else Application.getName()
|
||||||
|
frame.defaultCloseOperation = DISPOSE_ON_CLOSE
|
||||||
|
frame.setSize(1280, 800)
|
||||||
|
frame.setLocationRelativeTo(null)
|
||||||
|
frame.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun loadSettings() {
|
||||||
|
val language = Database.instance.appearance.language
|
||||||
|
val locale = runCatching { LocaleUtils.toLocale(language) }.getOrElse { Locale.getDefault() }
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("Language: {} , Locale: {}", language, locale)
|
||||||
|
}
|
||||||
|
Locale.setDefault(locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun setupLaf() {
|
||||||
|
|
||||||
|
System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux}")
|
||||||
|
System.setProperty(FlatSystemProperties.USE_ROUNDED_POPUP_BORDER, "false")
|
||||||
|
|
||||||
|
if (SystemInfo.isLinux) {
|
||||||
|
JFrame.setDefaultLookAndFeelDecorated(true)
|
||||||
|
JDialog.setDefaultLookAndFeelDecorated(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val themeManager = ThemeManager.instance
|
||||||
|
val settings = Database.instance
|
||||||
|
var theme = settings.appearance.theme
|
||||||
|
|
||||||
|
// 如果是跟随系统或者不存在样式,那么使用默认的
|
||||||
|
if (settings.appearance.followSystem || !themeManager.themes.containsKey(theme)) {
|
||||||
|
theme = if (OsThemeDetector.getDetector().isDark) {
|
||||||
|
"Dark"
|
||||||
|
} else {
|
||||||
|
"Light"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
themeManager.change(theme, true)
|
||||||
|
|
||||||
|
FlatInspector.install("ctrl shift alt X");
|
||||||
|
|
||||||
|
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
||||||
|
UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false)
|
||||||
|
UIManager.put("TitlePane.useWindowDecorations", false)
|
||||||
|
|
||||||
|
UIManager.put("Component.arc", 5)
|
||||||
|
UIManager.put("TextComponent.arc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("Component.hideMnemonics", false)
|
||||||
|
|
||||||
|
UIManager.put("TitleBar.height", 36)
|
||||||
|
|
||||||
|
UIManager.put("Dialog.width", 650)
|
||||||
|
UIManager.put("Dialog.height", 550)
|
||||||
|
|
||||||
|
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
UIManager.put("TabbedPane.tabHeight", UIManager.getInt("TitleBar.height"))
|
||||||
|
} else if (SystemInfo.isLinux) {
|
||||||
|
UIManager.put("TabbedPane.tabHeight", UIManager.getInt("TitleBar.height") - 4)
|
||||||
|
} else {
|
||||||
|
UIManager.put("TabbedPane.tabHeight", UIManager.getInt("TitleBar.height") - 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SystemInfo.isLinux) {
|
||||||
|
UIManager.put("TitlePane.centerTitle", true)
|
||||||
|
UIManager.put("TitlePane.showIcon", false)
|
||||||
|
UIManager.put("TitlePane.showIconInDialogs", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
UIManager.put("Table.rowHeight", 24)
|
||||||
|
UIManager.put("Table.cellNoFocusBorder", BorderFactory.createEmptyBorder())
|
||||||
|
UIManager.put("Table.focusCellHighlightBorder", BorderFactory.createEmptyBorder())
|
||||||
|
UIManager.put("Table.focusSelectedCellHighlightBorder", BorderFactory.createEmptyBorder())
|
||||||
|
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
|
||||||
|
UIManager.put("Tree.rowHeight", 24)
|
||||||
|
UIManager.put("Tree.background", DynamicColor("window"))
|
||||||
|
UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("Tree.showCellFocusIndicator", false)
|
||||||
|
UIManager.put("Tree.repaintWholeRow", true)
|
||||||
|
|
||||||
|
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun printSystemInfo() {
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("Welcome to ${Application.getName()} ${Application.getVersion()}!")
|
||||||
|
log.info(
|
||||||
|
"JVM name: {} , vendor: {} , version: {}",
|
||||||
|
SystemUtils.JAVA_VM_NAME,
|
||||||
|
SystemUtils.JAVA_VM_VENDOR,
|
||||||
|
SystemUtils.JAVA_VM_VERSION,
|
||||||
|
)
|
||||||
|
log.info(
|
||||||
|
"OS name: {} , version: {} , arch: {}",
|
||||||
|
SystemUtils.OS_NAME,
|
||||||
|
SystemUtils.OS_VERSION,
|
||||||
|
SystemUtils.OS_ARCH
|
||||||
|
)
|
||||||
|
log.info("Base config dir: ${Application.getBaseDataDir().absolutePath}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Windows 情况覆盖
|
||||||
|
*/
|
||||||
|
private fun setupTinylog() {
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
val dir = File(Application.getBaseDataDir(), "logs")
|
||||||
|
FileUtils.forceMkdir(dir)
|
||||||
|
Configuration.set("writer_file.latest", "${dir.absolutePath}/${Application.getName().lowercase()}.log")
|
||||||
|
Configuration.set("writer_file.file", "${dir.absolutePath}/{date:yyyy}-{date:MM}-{date:dd}.log")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun checkSingleton() {
|
||||||
|
val file = File(Application.getBaseDataDir(), "lock")
|
||||||
|
val pidFile = File(Application.getBaseDataDir(), "pid")
|
||||||
|
|
||||||
|
|
||||||
|
val raf = RandomAccessFile(file, "rw")
|
||||||
|
val lock = raf.channel.tryLock()
|
||||||
|
|
||||||
|
if (lock != null) {
|
||||||
|
pidFile.writeText(ProcessHandle.current().pid().toString())
|
||||||
|
pidFile.deleteOnExit()
|
||||||
|
file.deleteOnExit()
|
||||||
|
} else {
|
||||||
|
if (SystemInfo.isWindows && pidFile.exists()) {
|
||||||
|
val pid = NumberUtils.toLong(pidFile.readText())
|
||||||
|
for (window in WindowUtils.getAllWindows(false)) {
|
||||||
|
if (pid > 0) {
|
||||||
|
val processId = IntByReference()
|
||||||
|
User32.INSTANCE.GetWindowThreadProcessId(window.hwnd, processId)
|
||||||
|
if (processId.value.toLong() != pid) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if (window.title != Application.getName() || window.filePath.endsWith("explorer.exe")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
User32.INSTANCE.ShowWindow(window.hwnd, User32.SW_SHOWNOACTIVATE)
|
||||||
|
User32.INSTANCE.SetForegroundWindow(window.hwnd)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.err.println("Program is already running")
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
singletonLock = lock
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun openDatabase() {
|
||||||
|
val dir = Application.getDatabaseFile()
|
||||||
|
try {
|
||||||
|
Database.open(dir)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
JOptionPane.showMessageDialog(
|
||||||
|
null, "Unable to open database",
|
||||||
|
I18n.getString("termora.title"), JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
68
src/main/kotlin/app/termora/BannerPanel.kt
Normal file
68
src/main/kotlin/app/termora/BannerPanel.kt
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.FlatLaf
|
||||||
|
import org.apache.commons.lang3.RandomUtils
|
||||||
|
import java.awt.*
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.UIManager
|
||||||
|
|
||||||
|
class BannerPanel(fontSize: Int = 11, val beautiful: Boolean = false) : JComponent() {
|
||||||
|
private val banner = """
|
||||||
|
______
|
||||||
|
/_ __/__ _________ ___ ____ _________ _
|
||||||
|
/ / / _ \/ ___/ __ `__ \/ __ \/ ___/ __ `/
|
||||||
|
/ / / __/ / / / / / / / /_/ / / / /_/ /
|
||||||
|
/_/ \___/_/ /_/ /_/ /_/\____/_/ \__,_/
|
||||||
|
""".trimIndent().lines()
|
||||||
|
|
||||||
|
private val colors = mutableListOf<Color>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
font = Font("JetBrains Mono", Font.PLAIN, fontSize)
|
||||||
|
preferredSize = Dimension(width, getFontMetrics(font).height * banner.size)
|
||||||
|
size = preferredSize
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun paintComponent(g: Graphics) {
|
||||||
|
if (g is Graphics2D) {
|
||||||
|
g.setRenderingHints(
|
||||||
|
RenderingHints(
|
||||||
|
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.font = font
|
||||||
|
g.color = UIManager.getColor("TextField.placeholderForeground")
|
||||||
|
|
||||||
|
val height = g.fontMetrics.height
|
||||||
|
val descent = g.fontMetrics.descent
|
||||||
|
val offset = width / 2 - g.fontMetrics.stringWidth(banner.maxBy { it.length }) / 2
|
||||||
|
val insecure = RandomUtils.insecure()
|
||||||
|
var index = 0
|
||||||
|
|
||||||
|
for (i in banner.indices) {
|
||||||
|
var x = offset
|
||||||
|
val y = height * (i + 1) - descent
|
||||||
|
val chars = banner[i].toCharArray()
|
||||||
|
for (j in chars.indices) {
|
||||||
|
if (beautiful) {
|
||||||
|
if (colors.size <= index) {
|
||||||
|
colors.add(
|
||||||
|
Color(
|
||||||
|
insecure.randomInt(0, 255),
|
||||||
|
insecure.randomInt(0, 255),
|
||||||
|
insecure.randomInt(0, 255)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val color = colors[index++]
|
||||||
|
g.color = if (FlatLaf.isLafDark()) color.brighter() else color.darker()
|
||||||
|
}
|
||||||
|
g.drawChars(chars, j, 1, x, y)
|
||||||
|
x += g.fontMetrics.charWidth(chars[j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/main/kotlin/app/termora/ChannelShellPtyConnector.kt
Normal file
41
src/main/kotlin/app/termora/ChannelShellPtyConnector.kt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.terminal.StreamPtyConnector
|
||||||
|
import org.apache.sshd.client.channel.ChannelShell
|
||||||
|
import org.apache.sshd.client.channel.ClientChannelEvent
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
|
class ChannelShellPtyConnector(
|
||||||
|
private val channel: ChannelShell,
|
||||||
|
private val charset: Charset = Charsets.UTF_8
|
||||||
|
) : StreamPtyConnector(channel.invertedOut, channel.invertedIn) {
|
||||||
|
|
||||||
|
private val reader = InputStreamReader(input, charset)
|
||||||
|
|
||||||
|
override fun read(buffer: CharArray): Int {
|
||||||
|
return reader.read(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(buffer: ByteArray, offset: Int, len: Int) {
|
||||||
|
output.write(buffer, offset, len)
|
||||||
|
output.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(buffer: String) {
|
||||||
|
write(buffer.toByteArray(charset))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resize(rows: Int, cols: Int) {
|
||||||
|
channel.sendWindowChange(cols, rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun waitFor(): Int {
|
||||||
|
channel.waitFor(listOf(ClientChannelEvent.CLOSED), Long.MAX_VALUE)
|
||||||
|
return channel.exitStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
channel.close(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
155
src/main/kotlin/app/termora/Crypto.kt
Normal file
155
src/main/kotlin/app/termora/Crypto.kt
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Base64
|
||||||
|
import org.apache.commons.lang3.RandomUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.security.*
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
import java.security.spec.X509EncodedKeySpec
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.SecretKeyFactory
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.PBEKeySpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
import kotlin.time.measureTime
|
||||||
|
|
||||||
|
object AES {
|
||||||
|
private const val ALGORITHM = "AES"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ECB 没有 IV
|
||||||
|
*/
|
||||||
|
object ECB {
|
||||||
|
private const val TRANSFORMATION = "AES/ECB/PKCS5Padding"
|
||||||
|
|
||||||
|
fun encrypt(key: ByteArray, data: ByteArray): ByteArray {
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, ALGORITHM))
|
||||||
|
return cipher.doFinal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decrypt(key: ByteArray, data: ByteArray): ByteArray {
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, ALGORITHM))
|
||||||
|
return cipher.doFinal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 携带 IV
|
||||||
|
*/
|
||||||
|
object CBC {
|
||||||
|
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
|
||||||
|
|
||||||
|
fun encrypt(key: ByteArray, iv: ByteArray, data: ByteArray): ByteArray {
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, ALGORITHM), IvParameterSpec(iv))
|
||||||
|
return cipher.doFinal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decrypt(key: ByteArray, iv: ByteArray, data: ByteArray): ByteArray {
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, ALGORITHM), IvParameterSpec(iv))
|
||||||
|
return cipher.doFinal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun String.aesCBCEncrypt(key: ByteArray, iv: ByteArray): ByteArray {
|
||||||
|
return encrypt(key, iv, toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.aesCBCEncrypt(key: ByteArray, iv: ByteArray): ByteArray {
|
||||||
|
return encrypt(key, iv, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.aesCBCDecrypt(key: ByteArray, iv: ByteArray): ByteArray {
|
||||||
|
return decrypt(key, iv, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun randomBytes(size: Int = 32): ByteArray {
|
||||||
|
return RandomUtils.secureStrong().randomBytes(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.encodeBase64String(): String {
|
||||||
|
return Base64.encodeBase64String(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.decodeBase64(): ByteArray {
|
||||||
|
return Base64.decodeBase64(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
object PBKDF2 {
|
||||||
|
|
||||||
|
private const val ALGORITHM = "PBKDF2WithHmacSHA512"
|
||||||
|
private val log = LoggerFactory.getLogger(PBKDF2::class.java)
|
||||||
|
|
||||||
|
fun generateSecret(
|
||||||
|
password: CharArray,
|
||||||
|
salt: ByteArray,
|
||||||
|
iterationCount: Int = 150000,
|
||||||
|
keyLength: Int = 256
|
||||||
|
): ByteArray {
|
||||||
|
val bytes: ByteArray
|
||||||
|
val time = measureTime {
|
||||||
|
bytes = SecretKeyFactory.getInstance(ALGORITHM)
|
||||||
|
.generateSecret(PBEKeySpec(password, salt, iterationCount, keyLength))
|
||||||
|
.encoded
|
||||||
|
}
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Secret generated $time")
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
object RSA {
|
||||||
|
|
||||||
|
private const val TRANSFORMATION = "RSA"
|
||||||
|
|
||||||
|
fun encrypt(publicKey: PublicKey, data: ByteArray): ByteArray {
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
|
||||||
|
return cipher.doFinal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decrypt(privateKey: PrivateKey, data: ByteArray): ByteArray {
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, privateKey)
|
||||||
|
return cipher.doFinal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encrypt(privateKey: PrivateKey, data: ByteArray): ByteArray {
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, privateKey)
|
||||||
|
return cipher.doFinal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decrypt(publicKey: PublicKey, data: ByteArray): ByteArray {
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, publicKey)
|
||||||
|
return cipher.doFinal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generatePublic(publicKey: ByteArray): PublicKey {
|
||||||
|
return KeyFactory.getInstance(TRANSFORMATION)
|
||||||
|
.generatePublic(X509EncodedKeySpec(publicKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generatePrivate(privateKey: ByteArray): PrivateKey {
|
||||||
|
return KeyFactory.getInstance(TRANSFORMATION)
|
||||||
|
.generatePrivate(PKCS8EncodedKeySpec(privateKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateKeyPair(keySize: Int = 2048): KeyPair {
|
||||||
|
val generator = KeyPairGenerator.getInstance(TRANSFORMATION)
|
||||||
|
generator.initialize(keySize)
|
||||||
|
return generator.generateKeyPair()
|
||||||
|
}
|
||||||
|
}
|
||||||
200
src/main/kotlin/app/termora/DialogWrapper.kt
Normal file
200
src/main/kotlin/app/termora/DialogWrapper.kt
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.FlatLaf
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import com.jetbrains.JBR
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.event.ActionEvent
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
|
import java.awt.event.WindowAdapter
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
||||||
|
private val rootPanel = JPanel(BorderLayout())
|
||||||
|
private val titleLabel = JLabel()
|
||||||
|
private val titleBar by lazy { LogicCustomTitleBar.createCustomTitleBar(this) }
|
||||||
|
val disposable = Disposer.newDisposable()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val DEFAULT_ACTION = "DEFAULT_ACTION"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected var controlsVisible = true
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
titleBar.putProperty("controls.visible", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected var titleBarHeight = UIManager.getInt("TabbedPane.tabHeight").toFloat()
|
||||||
|
set(value) {
|
||||||
|
titleBar.height = value
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
protected var lostFocusDispose = false
|
||||||
|
protected var escapeDispose = true
|
||||||
|
|
||||||
|
protected fun init() {
|
||||||
|
|
||||||
|
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
|
||||||
|
|
||||||
|
initTitleBar()
|
||||||
|
initEvents()
|
||||||
|
|
||||||
|
if (JBR.isWindowDecorationsSupported()) {
|
||||||
|
if (rootPane.getClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE) != false) {
|
||||||
|
val titlePanel = createTitlePanel()
|
||||||
|
if (titlePanel != null) {
|
||||||
|
rootPanel.add(titlePanel, BorderLayout.NORTH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootPanel.add(createCenterPanel(), BorderLayout.CENTER)
|
||||||
|
|
||||||
|
val southPanel = createSouthPanel()
|
||||||
|
if (southPanel != null) {
|
||||||
|
rootPanel.add(southPanel, BorderLayout.SOUTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootPane.contentPane = rootPanel
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun createSouthPanel(): JComponent? {
|
||||||
|
val box = Box.createHorizontalBox()
|
||||||
|
box.border = BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor),
|
||||||
|
BorderFactory.createEmptyBorder(8, 12, 8, 12)
|
||||||
|
)
|
||||||
|
|
||||||
|
val okButton = createJButtonForAction(createOkAction())
|
||||||
|
box.add(Box.createHorizontalGlue())
|
||||||
|
box.add(createJButtonForAction(CancelAction()))
|
||||||
|
box.add(Box.createHorizontalStrut(8))
|
||||||
|
box.add(okButton)
|
||||||
|
|
||||||
|
|
||||||
|
return box
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun createOkAction(): AbstractAction {
|
||||||
|
return OkAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun createJButtonForAction(action: Action): JButton {
|
||||||
|
val button = JButton(action)
|
||||||
|
val value = action.getValue(DEFAULT_ACTION)
|
||||||
|
if (value is Boolean && value) {
|
||||||
|
rootPane.defaultButton = button
|
||||||
|
}
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun createTitlePanel(): JPanel? {
|
||||||
|
titleLabel.horizontalAlignment = SwingConstants.CENTER
|
||||||
|
titleLabel.verticalAlignment = SwingConstants.CENTER
|
||||||
|
titleLabel.text = title
|
||||||
|
titleLabel.putClientProperty("FlatLaf.style", "font: bold")
|
||||||
|
|
||||||
|
val panel = JPanel(BorderLayout())
|
||||||
|
panel.add(titleLabel, BorderLayout.CENTER)
|
||||||
|
panel.preferredSize = Dimension(-1, titleBar.height.toInt())
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setTitle(title: String?) {
|
||||||
|
super.setTitle(title)
|
||||||
|
titleLabel.text = title
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun createCenterPanel(): JComponent
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
val inputMap = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
|
||||||
|
if (escapeDispose) {
|
||||||
|
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "close")
|
||||||
|
}
|
||||||
|
|
||||||
|
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, toolkit.menuShortcutKeyMaskEx), "close")
|
||||||
|
|
||||||
|
rootPane.actionMap.put("close", object : AnAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
doCancelAction()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
addWindowFocusListener(object : WindowAdapter() {
|
||||||
|
override fun windowLostFocus(e: WindowEvent) {
|
||||||
|
if (lostFocusDispose) {
|
||||||
|
SwingUtilities.invokeLater { doCancelAction() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowClosed(e: WindowEvent) {
|
||||||
|
Disposer.dispose(disposable)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
addWindowListener(object : WindowAdapter(), ThemeChangeListener {
|
||||||
|
override fun windowClosed(e: WindowEvent) {
|
||||||
|
ThemeManager.instance.removeThemeChangeListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun windowOpened(e: WindowEvent) {
|
||||||
|
onChanged()
|
||||||
|
ThemeManager.instance.addThemeChangeListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChanged() {
|
||||||
|
titleBar.putProperty("controls.dark", FlatLaf.isLafDark())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initTitleBar() {
|
||||||
|
titleBar.height = titleBarHeight
|
||||||
|
titleBar.putProperty("controls.visible", controlsVisible)
|
||||||
|
if (JBR.isWindowDecorationsSupported()) {
|
||||||
|
JBR.getWindowDecorations().setCustomTitleBar(this, titleBar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun doOKAction() {
|
||||||
|
dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun doCancelAction() {
|
||||||
|
dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected inner class OkAction(text: String = I18n.getString("termora.confirm")) : AnAction(text) {
|
||||||
|
init {
|
||||||
|
putValue(DEFAULT_ACTION, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
doOKAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected inner class CancelAction : AnAction(I18n.getString("termora.cancel")) {
|
||||||
|
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
doCancelAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main/kotlin/app/termora/DocumentAdaptor.kt
Normal file
18
src/main/kotlin/app/termora/DocumentAdaptor.kt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import javax.swing.event.DocumentEvent
|
||||||
|
import javax.swing.event.DocumentListener
|
||||||
|
|
||||||
|
abstract class DocumentAdaptor : DocumentListener {
|
||||||
|
override fun insertUpdate(e: DocumentEvent) {
|
||||||
|
changedUpdate(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeUpdate(e: DocumentEvent) {
|
||||||
|
changedUpdate(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/main/kotlin/app/termora/Doorman.kt
Normal file
85
src/main/kotlin/app/termora/Doorman.kt
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.AES.decodeBase64
|
||||||
|
import app.termora.AES.encodeBase64String
|
||||||
|
import app.termora.db.Database
|
||||||
|
|
||||||
|
class PasswordWrongException : RuntimeException()
|
||||||
|
|
||||||
|
class Doorman private constructor() {
|
||||||
|
private val properties get() = Database.instance.properties
|
||||||
|
private var key = byteArrayOf()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { Doorman() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isWorking(): Boolean {
|
||||||
|
return properties.getString("doorman", "false").toBoolean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encrypt(text: String): String {
|
||||||
|
checkIsWorking()
|
||||||
|
return AES.ECB.encrypt(key, text.toByteArray()).encodeBase64String()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun decrypt(text: String): String {
|
||||||
|
checkIsWorking()
|
||||||
|
return AES.ECB.decrypt(key, text.decodeBase64()).decodeToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return 返回钥匙
|
||||||
|
*/
|
||||||
|
fun work(password: CharArray): ByteArray {
|
||||||
|
if (key.isNotEmpty()) {
|
||||||
|
throw IllegalStateException("Working")
|
||||||
|
}
|
||||||
|
return work(convertKey(password))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun work(key: ByteArray): ByteArray {
|
||||||
|
val verify = properties.getString("doorman-verify")
|
||||||
|
if (verify == null) {
|
||||||
|
properties.putString(
|
||||||
|
"doorman-verify",
|
||||||
|
AES.ECB.encrypt(key, factor()).encodeBase64String()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
if (!AES.ECB.decrypt(key, verify.decodeBase64()).contentEquals(factor())) {
|
||||||
|
throw PasswordWrongException()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw PasswordWrongException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.key = key
|
||||||
|
properties.putString("doorman", "true")
|
||||||
|
|
||||||
|
return this.key
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun convertKey(password: CharArray): ByteArray {
|
||||||
|
return PBKDF2.generateSecret(password, factor())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun checkIsWorking() {
|
||||||
|
if (key.isEmpty() || !isWorking()) {
|
||||||
|
throw UnsupportedOperationException("Doorman is not working")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun factor(): ByteArray {
|
||||||
|
return Application.getName().toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test(password: CharArray): Boolean {
|
||||||
|
checkIsWorking()
|
||||||
|
return key.contentEquals(convertKey(password))
|
||||||
|
}
|
||||||
|
}
|
||||||
309
src/main/kotlin/app/termora/DoormanDialog.kt
Normal file
309
src/main/kotlin/app/termora/DoormanDialog.kt
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.AES.decodeBase64
|
||||||
|
import app.termora.db.Database
|
||||||
|
import app.termora.terminal.ControlCharacters
|
||||||
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatButton
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatLabel
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.jdesktop.swingx.JXHyperlink
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.datatransfer.DataFlavor
|
||||||
|
import java.awt.event.ActionEvent
|
||||||
|
import java.awt.event.KeyAdapter
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
import javax.swing.*
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(DoormanDialog::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val formMargin = "7dlu"
|
||||||
|
private val label = FlatLabel()
|
||||||
|
private val icon = JLabel()
|
||||||
|
private val passwordTextField = OutlinePasswordField()
|
||||||
|
private val tip = FlatLabel()
|
||||||
|
private val safeBtn = FlatButton()
|
||||||
|
|
||||||
|
var isOpened = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
||||||
|
isModal = true
|
||||||
|
isResizable = false
|
||||||
|
controlsVisible = false
|
||||||
|
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
title = I18n.getString("termora.doorman.safe")
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
val sizes = listOf(16, 20, 24, 28, 32, 48, 64)
|
||||||
|
val loader = TermoraFrame::class.java.classLoader
|
||||||
|
val images = sizes.mapNotNull { e ->
|
||||||
|
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
|
||||||
|
}
|
||||||
|
iconImages = images
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocationRelativeTo(null)
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
label.text = I18n.getString("termora.doorman.safe")
|
||||||
|
tip.text = I18n.getString("termora.doorman.unlock-data")
|
||||||
|
icon.icon = FlatSVGIcon(Icons.role.name, 80, 80)
|
||||||
|
safeBtn.icon = Icons.unlocked
|
||||||
|
|
||||||
|
|
||||||
|
label.labelType = FlatLabel.LabelType.h2
|
||||||
|
label.horizontalAlignment = SwingConstants.CENTER
|
||||||
|
safeBtn.isFocusable = false
|
||||||
|
tip.foreground = UIManager.getColor("TextField.placeholderForeground")
|
||||||
|
icon.horizontalAlignment = SwingConstants.CENTER
|
||||||
|
|
||||||
|
|
||||||
|
safeBtn.addActionListener { doOKAction() }
|
||||||
|
passwordTextField.addActionListener { doOKAction() }
|
||||||
|
|
||||||
|
var rows = 2
|
||||||
|
val step = 2
|
||||||
|
return FormBuilder.create().debug(false)
|
||||||
|
.layout(
|
||||||
|
FormLayout(
|
||||||
|
"$formMargin, default:grow, 4dlu, pref, $formMargin",
|
||||||
|
"${if (SystemInfo.isWindows) "20dlu" else "0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.add(icon).xyw(2, rows, 4).apply { rows += step }
|
||||||
|
.add(label).xyw(2, rows, 4).apply { rows += step }
|
||||||
|
.add(passwordTextField).xy(2, rows)
|
||||||
|
.add(safeBtn).xy(4, rows).apply { rows += step }
|
||||||
|
.add(tip).xyw(2, rows, 4, "center, fill").apply { rows += step }
|
||||||
|
.add(JXHyperlink(object : AnAction(I18n.getString("termora.doorman.forget-password")) {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val option = OptionPane.showConfirmDialog(
|
||||||
|
this@DoormanDialog, I18n.getString("termora.doorman.forget-password-message"),
|
||||||
|
options = arrayOf(
|
||||||
|
I18n.getString("termora.doorman.have-a-mnemonic"),
|
||||||
|
I18n.getString("termora.doorman.dont-have-a-mnemonic"),
|
||||||
|
),
|
||||||
|
optionType = JOptionPane.YES_NO_OPTION,
|
||||||
|
messageType = JOptionPane.INFORMATION_MESSAGE,
|
||||||
|
initialValue = I18n.getString("termora.doorman.have-a-mnemonic")
|
||||||
|
)
|
||||||
|
if (option == JOptionPane.YES_OPTION) {
|
||||||
|
showMnemonicsDialog()
|
||||||
|
} else if (option == JOptionPane.NO_OPTION) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
this@DoormanDialog,
|
||||||
|
I18n.getString("termora.doorman.delete-data"),
|
||||||
|
messageType = JOptionPane.WARNING_MESSAGE
|
||||||
|
)
|
||||||
|
Application.browse(Application.getDatabaseFile().toURI())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).apply { isFocusable = false }).xyw(2, rows, 4, "center, fill")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showMnemonicsDialog() {
|
||||||
|
val dialog = MnemonicsDialog(this@DoormanDialog)
|
||||||
|
dialog.isVisible = true
|
||||||
|
val entropy = dialog.entropy
|
||||||
|
if (entropy.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val keyBackup = Database.instance.properties.getString("doorman-key-backup")
|
||||||
|
?: throw IllegalStateException("doorman-key-backup is null")
|
||||||
|
val key = AES.ECB.decrypt(entropy, keyBackup.decodeBase64())
|
||||||
|
Doorman.instance.work(key)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
passwordTextField.outline = "error"
|
||||||
|
passwordTextField.requestFocus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isOpened = true
|
||||||
|
super.doOKAction()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doOKAction() {
|
||||||
|
if (passwordTextField.password.isEmpty()) {
|
||||||
|
passwordTextField.outline = "error"
|
||||||
|
passwordTextField.requestFocus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Doorman.instance.work(passwordTextField.password)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is PasswordWrongException) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
this, I18n.getString("termora.doorman.password-wrong"),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
passwordTextField.outline = "error"
|
||||||
|
passwordTextField.requestFocus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isOpened = true
|
||||||
|
|
||||||
|
super.doOKAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun open(): Boolean {
|
||||||
|
isModal = true
|
||||||
|
isVisible = true
|
||||||
|
return isOpened
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class MnemonicsDialog(owner: Window) : DialogWrapper(owner) {
|
||||||
|
|
||||||
|
private val textFields = (1..12).map { PasteTextField(it) }
|
||||||
|
var entropy = byteArrayOf()
|
||||||
|
private set
|
||||||
|
|
||||||
|
init {
|
||||||
|
isModal = true
|
||||||
|
isResizable = true
|
||||||
|
controlsVisible = false
|
||||||
|
title = I18n.getString("termora.doorman.mnemonic.title")
|
||||||
|
init()
|
||||||
|
pack()
|
||||||
|
size = Dimension(max(size.width, UIManager.getInt("Dialog.width") - 250), size.height)
|
||||||
|
setLocationRelativeTo(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getWords(): List<String> {
|
||||||
|
val words = mutableListOf<String>()
|
||||||
|
for (e in textFields) {
|
||||||
|
if (e.text.isBlank()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
words.add(e.text)
|
||||||
|
}
|
||||||
|
return words
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
val formMargin = "4dlu"
|
||||||
|
val layout = FormLayout(
|
||||||
|
"default:grow, $formMargin, default:grow, $formMargin, default:grow, $formMargin, default:grow",
|
||||||
|
"pref, $formMargin, pref, $formMargin, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
|
||||||
|
.layout(layout).debug(true)
|
||||||
|
val iterator = textFields.iterator()
|
||||||
|
for (i in 1..5 step 2) {
|
||||||
|
for (j in 1..7 step 2) {
|
||||||
|
builder.add(iterator.next()).xy(j, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doOKAction() {
|
||||||
|
for (textField in textFields) {
|
||||||
|
if (textField.text.isBlank()) {
|
||||||
|
textField.outline = "error"
|
||||||
|
textField.requestFocusInWindow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Mnemonics.MnemonicCode(getWords().joinToString(StringUtils.SPACE)).use {
|
||||||
|
it.validate()
|
||||||
|
entropy = it.toEntropy()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
this,
|
||||||
|
I18n.getString("termora.doorman.mnemonic.incorrect"),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
super.doOKAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doCancelAction() {
|
||||||
|
entropy = byteArrayOf()
|
||||||
|
super.doCancelAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class PasteTextField(private val index: Int) : OutlineTextField() {
|
||||||
|
init {
|
||||||
|
addKeyListener(object : KeyAdapter() {
|
||||||
|
override fun keyPressed(e: KeyEvent) {
|
||||||
|
if (e.keyCode == KeyEvent.VK_BACK_SPACE) {
|
||||||
|
if (text.isEmpty() && index != 1) {
|
||||||
|
textFields[index - 2].requestFocusInWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun paste() {
|
||||||
|
if (!toolkit.systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val text = toolkit.systemClipboard.getData(DataFlavor.stringFlavor)?.toString() ?: return
|
||||||
|
if (text.isBlank()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val words = mutableListOf<String>()
|
||||||
|
if (text.count { it == ControlCharacters.SP } > text.count { it == ControlCharacters.LF }) {
|
||||||
|
words.addAll(text.split(StringUtils.SPACE))
|
||||||
|
} else {
|
||||||
|
words.addAll(text.split(ControlCharacters.LF))
|
||||||
|
}
|
||||||
|
val iterator = words.iterator()
|
||||||
|
for (i in index..textFields.size) {
|
||||||
|
if (iterator.hasNext()) {
|
||||||
|
textFields[i - 1].text = iterator.next()
|
||||||
|
textFields[i - 1].requestFocusInWindow()
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun createSouthPanel(): JComponent? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/main/kotlin/app/termora/DynamicColor.kt
Normal file
63
src/main/kotlin/app/termora/DynamicColor.kt
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.FlatLaf
|
||||||
|
import java.awt.Color
|
||||||
|
import javax.swing.UIManager
|
||||||
|
|
||||||
|
open class DynamicColor : Color {
|
||||||
|
private var regular: Color?
|
||||||
|
private val dark: Color?
|
||||||
|
private var colorKey: String? = null
|
||||||
|
private val color: Color
|
||||||
|
get() {
|
||||||
|
val r = regular
|
||||||
|
val d = dark
|
||||||
|
if (r == null || d == null) {
|
||||||
|
return UIManager.getColor(colorKey)
|
||||||
|
}
|
||||||
|
return if (FlatLaf.isLafDark()) d else r
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(regular: Color, dark: Color) : super(regular.rgb, regular.alpha != 255) {
|
||||||
|
this.regular = regular
|
||||||
|
this.dark = dark
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val BorderColor = DynamicColor("Component.borderColor")
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(key: String) : super(0) {
|
||||||
|
this.regular = null
|
||||||
|
this.dark = null
|
||||||
|
this.colorKey = key
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRed(): Int {
|
||||||
|
return color.red
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getGreen(): Int {
|
||||||
|
return color.green
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBlue(): Int {
|
||||||
|
return color.blue
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAlpha(): Int {
|
||||||
|
return color.alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRGB(): Int {
|
||||||
|
return color.rgb
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun brighter(): Color {
|
||||||
|
return color.brighter()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun darker(): Color {
|
||||||
|
return color.darker()
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main/kotlin/app/termora/DynamicIcon.kt
Normal file
10
src/main/kotlin/app/termora/DynamicIcon.kt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
|
|
||||||
|
open class DynamicIcon(name: String, private val darkName: String) : FlatSVGIcon(name) {
|
||||||
|
constructor(name: String) : this(name, name)
|
||||||
|
|
||||||
|
val dark by lazy { DynamicIcon(darkName, name) }
|
||||||
|
|
||||||
|
}
|
||||||
54
src/main/kotlin/app/termora/EditHostOptionsPane.kt
Normal file
54
src/main/kotlin/app/termora/EditHostOptionsPane.kt
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.keymgr.OhKeyPair
|
||||||
|
|
||||||
|
class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
||||||
|
init {
|
||||||
|
generalOption.portTextField.value = host.port
|
||||||
|
generalOption.nameTextField.text = host.name
|
||||||
|
generalOption.protocolTypeComboBox.selectedItem = host.protocol
|
||||||
|
generalOption.usernameTextField.text = host.username
|
||||||
|
generalOption.hostTextField.text = host.host
|
||||||
|
generalOption.passwordTextField.text = host.authentication.password
|
||||||
|
generalOption.remarkTextArea.text = host.remark
|
||||||
|
generalOption.authenticationTypeComboBox.selectedItem = host.authentication.type
|
||||||
|
if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||||
|
val ohKeyPair = KeyManager.instance.getOhKeyPair(host.authentication.password)
|
||||||
|
if (ohKeyPair != null) {
|
||||||
|
generalOption.publicKeyTextField.text = ohKeyPair.name
|
||||||
|
generalOption.publicKeyTextField.putClientProperty(OhKeyPair::class, ohKeyPair)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||||
|
proxyOption.proxyHostTextField.text = host.proxy.host
|
||||||
|
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
||||||
|
proxyOption.proxyUsernameTextField.text = host.proxy.username
|
||||||
|
proxyOption.proxyPortTextField.value = host.proxy.port
|
||||||
|
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
|
||||||
|
|
||||||
|
terminalOption.charsetComboBox.selectedItem = host.options.encoding
|
||||||
|
terminalOption.environmentTextArea.text = host.options.env
|
||||||
|
terminalOption.startupCommandTextField.text = host.options.startupCommand
|
||||||
|
|
||||||
|
tunnelingOption.tunnelings.addAll(host.tunnelings)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHost(): Host {
|
||||||
|
val newHost = super.getHost()
|
||||||
|
return host.copy(
|
||||||
|
name = newHost.name,
|
||||||
|
protocol = newHost.protocol,
|
||||||
|
host = newHost.host,
|
||||||
|
port = newHost.port,
|
||||||
|
username = newHost.username,
|
||||||
|
authentication = newHost.authentication,
|
||||||
|
proxy = newHost.proxy,
|
||||||
|
remark = newHost.remark,
|
||||||
|
updateDate = System.currentTimeMillis(),
|
||||||
|
options = newHost.options,
|
||||||
|
tunnelings = newHost.tunnelings,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
236
src/main/kotlin/app/termora/Host.kt
Normal file
236
src/main/kotlin/app/termora/Host.kt
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
fun UUID.toSimpleString(): String {
|
||||||
|
return toString().replace("-", StringUtils.EMPTY)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Protocol {
|
||||||
|
Folder,
|
||||||
|
SSH,
|
||||||
|
Local,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum class AuthenticationType {
|
||||||
|
No,
|
||||||
|
Password,
|
||||||
|
PublicKey,
|
||||||
|
KeyboardInteractive,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ProxyType {
|
||||||
|
No,
|
||||||
|
HTTP,
|
||||||
|
SOCKS5,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Authentication(
|
||||||
|
val type: AuthenticationType,
|
||||||
|
val password: String,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val No = Authentication(AuthenticationType.No, String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Options(
|
||||||
|
/**
|
||||||
|
* 跳板机
|
||||||
|
*/
|
||||||
|
val jumpHosts: List<String> = mutableListOf(),
|
||||||
|
/**
|
||||||
|
* 编码
|
||||||
|
*/
|
||||||
|
val encoding: String = "UTF-8",
|
||||||
|
/**
|
||||||
|
* 环境变量
|
||||||
|
*/
|
||||||
|
val env: String = StringUtils.EMPTY,
|
||||||
|
/**
|
||||||
|
* 连接成功后立即发送命令
|
||||||
|
*/
|
||||||
|
val startupCommand: String = StringUtils.EMPTY,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val Default = Options()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun envs(): Map<String, String> {
|
||||||
|
if (env.isBlank()) return emptyMap()
|
||||||
|
val envs = mutableMapOf<String, String>()
|
||||||
|
for (line in env.lines()) {
|
||||||
|
if (line.isBlank()) continue
|
||||||
|
val vars = line.split("=", limit = 2)
|
||||||
|
if (vars.size != 2) continue
|
||||||
|
envs[vars[0]] = vars[1]
|
||||||
|
}
|
||||||
|
return envs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Proxy(
|
||||||
|
val type: ProxyType,
|
||||||
|
val host: String,
|
||||||
|
val port: Int,
|
||||||
|
val authenticationType: AuthenticationType = AuthenticationType.No,
|
||||||
|
val username: String,
|
||||||
|
val password: String,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val No = Proxy(
|
||||||
|
ProxyType.No,
|
||||||
|
host = StringUtils.EMPTY,
|
||||||
|
port = 7890,
|
||||||
|
username = StringUtils.EMPTY,
|
||||||
|
password = StringUtils.EMPTY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class TunnelingType {
|
||||||
|
Local,
|
||||||
|
Remote,
|
||||||
|
Dynamic
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Tunneling(
|
||||||
|
val name: String = StringUtils.EMPTY,
|
||||||
|
val type: TunnelingType = TunnelingType.Local,
|
||||||
|
val sourceHost: String = StringUtils.EMPTY,
|
||||||
|
val sourcePort: Int = 0,
|
||||||
|
val destinationHost: String = StringUtils.EMPTY,
|
||||||
|
val destinationPort: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class EncryptedHost(
|
||||||
|
var id: String = StringUtils.EMPTY,
|
||||||
|
var name: String = StringUtils.EMPTY,
|
||||||
|
var protocol: String = StringUtils.EMPTY,
|
||||||
|
var host: String = StringUtils.EMPTY,
|
||||||
|
var port: String = StringUtils.EMPTY,
|
||||||
|
var username: String = StringUtils.EMPTY,
|
||||||
|
var remark: String = StringUtils.EMPTY,
|
||||||
|
var authentication: String = StringUtils.EMPTY,
|
||||||
|
var proxy: String = StringUtils.EMPTY,
|
||||||
|
var options: String = StringUtils.EMPTY,
|
||||||
|
var tunnelings: String = StringUtils.EMPTY,
|
||||||
|
var sort: Long = 0L,
|
||||||
|
var deleted: Boolean = false,
|
||||||
|
var parentId: String = StringUtils.EMPTY,
|
||||||
|
var ownerId: String = StringUtils.EMPTY,
|
||||||
|
var creatorId: String = StringUtils.EMPTY,
|
||||||
|
var createDate: Long = 0L,
|
||||||
|
var updateDate: Long = 0L,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Host(
|
||||||
|
/**
|
||||||
|
* 唯一ID
|
||||||
|
*/
|
||||||
|
val id: String = UUID.randomUUID().toSimpleString(),
|
||||||
|
/**
|
||||||
|
* 名称
|
||||||
|
*/
|
||||||
|
val name: String,
|
||||||
|
/**
|
||||||
|
* 协议
|
||||||
|
*/
|
||||||
|
val protocol: Protocol,
|
||||||
|
/**
|
||||||
|
* 主机
|
||||||
|
*/
|
||||||
|
val host: String = StringUtils.EMPTY,
|
||||||
|
/**
|
||||||
|
* 端口
|
||||||
|
*/
|
||||||
|
val port: Int = 0,
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
*/
|
||||||
|
val username: String = StringUtils.EMPTY,
|
||||||
|
/**
|
||||||
|
* 备注
|
||||||
|
*/
|
||||||
|
val remark: String = StringUtils.EMPTY,
|
||||||
|
/**
|
||||||
|
* 认证信息
|
||||||
|
*/
|
||||||
|
val authentication: Authentication = Authentication.No,
|
||||||
|
/**
|
||||||
|
* 代理
|
||||||
|
*/
|
||||||
|
val proxy: Proxy = Proxy.No,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选项,备用字段
|
||||||
|
*/
|
||||||
|
val options: Options = Options.Default,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隧道
|
||||||
|
*/
|
||||||
|
val tunnelings: List<Tunneling> = emptyList(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序
|
||||||
|
*/
|
||||||
|
val sort: Long = 0,
|
||||||
|
/**
|
||||||
|
* 父ID
|
||||||
|
*/
|
||||||
|
val parentId: String = "0",
|
||||||
|
/**
|
||||||
|
* 所属者
|
||||||
|
*/
|
||||||
|
val ownerId: String = "0",
|
||||||
|
/**
|
||||||
|
* 创建者
|
||||||
|
*/
|
||||||
|
val creatorId: String = "0",
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
val createDate: Long = System.currentTimeMillis(),
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
val updateDate: Long = System.currentTimeMillis(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已经删除
|
||||||
|
*/
|
||||||
|
val deleted: Boolean = false
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Host
|
||||||
|
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (ownerId != other.ownerId) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id.hashCode()
|
||||||
|
result = 31 * result + ownerId.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/main/kotlin/app/termora/HostDialog.kt
Normal file
46
src/main/kotlin/app/termora/HostDialog.kt
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import javax.swing.BorderFactory
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.JPanel
|
||||||
|
import javax.swing.UIManager
|
||||||
|
|
||||||
|
class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
||||||
|
private val pane = if (host != null) EditHostOptionsPane(host) else HostOptionsPane()
|
||||||
|
var host: Host? = host
|
||||||
|
private set
|
||||||
|
|
||||||
|
init {
|
||||||
|
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||||
|
isModal = true
|
||||||
|
title = I18n.getString("termora.new-host.title")
|
||||||
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
pane.background = UIManager.getColor("window")
|
||||||
|
|
||||||
|
val panel = JPanel(BorderLayout())
|
||||||
|
panel.add(pane, BorderLayout.CENTER)
|
||||||
|
panel.background = UIManager.getColor("window")
|
||||||
|
panel.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun doOKAction() {
|
||||||
|
if (!pane.validateFields()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
host = pane.getHost()
|
||||||
|
super.doOKAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
54
src/main/kotlin/app/termora/HostManager.kt
Normal file
54
src/main/kotlin/app/termora/HostManager.kt
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.db.Database
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
interface HostListener : EventListener {
|
||||||
|
fun hostAdded(host: Host) {}
|
||||||
|
fun hostRemoved(id: String) {}
|
||||||
|
fun hostsChanged() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class HostManager private constructor() {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { HostManager() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val database get() = Database.instance
|
||||||
|
private val listeners = mutableListOf<HostListener>()
|
||||||
|
|
||||||
|
fun addHost(host: Host, notify: Boolean = true) {
|
||||||
|
assertEventDispatchThread()
|
||||||
|
database.addHost(host)
|
||||||
|
if (notify) listeners.forEach { it.hostAdded(host) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeHost(id: String) {
|
||||||
|
assertEventDispatchThread()
|
||||||
|
database.removeHost(id)
|
||||||
|
listeners.forEach { it.hostRemoved(id) }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hosts(): List<Host> {
|
||||||
|
return database.getHosts()
|
||||||
|
.sortedWith(compareBy<Host> { if (it.protocol == Protocol.Folder) 0 else 1 }.thenBy { it.sort })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAll() {
|
||||||
|
assertEventDispatchThread()
|
||||||
|
database.removeAllHost()
|
||||||
|
listeners.forEach { it.hostsChanged() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addHostListener(listener: HostListener) {
|
||||||
|
listeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeHostListener(listener: HostListener) {
|
||||||
|
listeners.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
842
src/main/kotlin/app/termora/HostOptionsPane.kt
Normal file
842
src/main/kotlin/app/termora/HostOptionsPane.kt
Normal file
@@ -0,0 +1,842 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.keymgr.KeyManagerDialog
|
||||||
|
import app.termora.keymgr.OhKeyPair
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.*
|
||||||
|
import java.awt.event.*
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import javax.swing.*
|
||||||
|
import javax.swing.table.DefaultTableModel
|
||||||
|
|
||||||
|
|
||||||
|
open class HostOptionsPane : OptionsPane() {
|
||||||
|
protected val tunnelingOption = TunnelingOption()
|
||||||
|
protected val generalOption = GeneralOption()
|
||||||
|
protected val proxyOption = ProxyOption()
|
||||||
|
protected val terminalOption = TerminalOption()
|
||||||
|
protected val owner: Window? get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
|
init {
|
||||||
|
addOption(generalOption)
|
||||||
|
addOption(proxyOption)
|
||||||
|
addOption(tunnelingOption)
|
||||||
|
addOption(terminalOption)
|
||||||
|
|
||||||
|
setContentBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
open fun getHost(): Host {
|
||||||
|
val name = generalOption.nameTextField.text
|
||||||
|
val protocol = generalOption.protocolTypeComboBox.selectedItem as Protocol
|
||||||
|
val host = generalOption.hostTextField.text
|
||||||
|
val port = (generalOption.portTextField.value ?: 22) as Int
|
||||||
|
var authentication = Authentication.No
|
||||||
|
var proxy = Proxy.No
|
||||||
|
|
||||||
|
if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.Password) {
|
||||||
|
authentication = authentication.copy(
|
||||||
|
type = AuthenticationType.Password,
|
||||||
|
password = String(generalOption.passwordTextField.password)
|
||||||
|
)
|
||||||
|
} else if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
|
||||||
|
val keyPair = generalOption.publicKeyTextField.getClientProperty(OhKeyPair::class) as OhKeyPair?
|
||||||
|
authentication = authentication.copy(
|
||||||
|
type = AuthenticationType.PublicKey,
|
||||||
|
password = keyPair?.id ?: StringUtils.EMPTY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||||
|
proxy = proxy.copy(
|
||||||
|
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
|
||||||
|
host = proxyOption.proxyHostTextField.text,
|
||||||
|
username = proxyOption.proxyUsernameTextField.text,
|
||||||
|
password = String(proxyOption.proxyPasswordTextField.password),
|
||||||
|
port = proxyOption.proxyPortTextField.value as Int,
|
||||||
|
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val options = Options.Default.copy(
|
||||||
|
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
||||||
|
env = terminalOption.environmentTextArea.text,
|
||||||
|
startupCommand = terminalOption.startupCommandTextField.text
|
||||||
|
)
|
||||||
|
|
||||||
|
return Host(
|
||||||
|
name = name,
|
||||||
|
protocol = protocol,
|
||||||
|
host = host,
|
||||||
|
port = port,
|
||||||
|
username = generalOption.usernameTextField.text,
|
||||||
|
authentication = authentication,
|
||||||
|
proxy = proxy,
|
||||||
|
sort = System.currentTimeMillis(),
|
||||||
|
remark = generalOption.remarkTextArea.text,
|
||||||
|
options = options,
|
||||||
|
tunnelings = tunnelingOption.tunnelings
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateFields(): Boolean {
|
||||||
|
val host = getHost()
|
||||||
|
|
||||||
|
// general
|
||||||
|
if (validateField(generalOption.nameTextField)
|
||||||
|
|| validateField(generalOption.hostTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.protocol == Protocol.SSH) {
|
||||||
|
if (validateField(generalOption.usernameTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
|
if (validateField(generalOption.passwordTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||||
|
if (validateField(generalOption.publicKeyTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxy
|
||||||
|
if (host.proxy.type != ProxyType.No) {
|
||||||
|
if (validateField(proxyOption.proxyHostTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.proxy.authenticationType != AuthenticationType.No) {
|
||||||
|
if (validateField(proxyOption.proxyUsernameTextField)
|
||||||
|
|| validateField(proxyOption.proxyPasswordTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示有错误
|
||||||
|
*/
|
||||||
|
private fun validateField(textField: JTextField): Boolean {
|
||||||
|
if (textField.isEnabled && textField.text.isBlank()) {
|
||||||
|
selectOptionJComponent(textField)
|
||||||
|
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
|
textField.requestFocusInWindow()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
protected inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||||
|
val portTextField = PortSpinner()
|
||||||
|
val nameTextField = OutlineTextField(128)
|
||||||
|
val protocolTypeComboBox = FlatComboBox<Protocol>()
|
||||||
|
val usernameTextField = OutlineTextField(128)
|
||||||
|
val hostTextField = OutlineTextField(255)
|
||||||
|
private val passwordPanel = JPanel(BorderLayout())
|
||||||
|
private val chooseKeyBtn = JButton(Icons.greyKey)
|
||||||
|
val passwordTextField = OutlinePasswordField(255)
|
||||||
|
val publicKeyTextField = OutlineTextField()
|
||||||
|
val remarkTextArea = FixedLengthTextArea(512)
|
||||||
|
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
|
||||||
|
publicKeyTextField.isEditable = false
|
||||||
|
chooseKeyBtn.isFocusable = false
|
||||||
|
|
||||||
|
protocolTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
value.toString().uppercase(),
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = value?.toString() ?: ""
|
||||||
|
when (value) {
|
||||||
|
AuthenticationType.Password -> {
|
||||||
|
text = "Password"
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationType.PublicKey -> {
|
||||||
|
text = "Public Key"
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationType.KeyboardInteractive -> {
|
||||||
|
text = "Keyboard Interactive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
text,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protocolTypeComboBox.addItem(Protocol.SSH)
|
||||||
|
protocolTypeComboBox.addItem(Protocol.Local)
|
||||||
|
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.PublicKey)
|
||||||
|
|
||||||
|
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
|
||||||
|
|
||||||
|
refreshStates()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
protocolTypeComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
refreshStates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationTypeComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
refreshStates()
|
||||||
|
switchPasswordComponent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chooseKeyBtn.addActionListener {
|
||||||
|
chooseKeyPair()
|
||||||
|
}
|
||||||
|
|
||||||
|
addComponentListener(object : ComponentAdapter() {
|
||||||
|
override fun componentResized(e: ComponentEvent) {
|
||||||
|
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
|
||||||
|
removeComponentListener(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun chooseKeyPair() {
|
||||||
|
val dialog = KeyManagerDialog(
|
||||||
|
SwingUtilities.getWindowAncestor(this),
|
||||||
|
selectMode = true,
|
||||||
|
)
|
||||||
|
dialog.pack()
|
||||||
|
dialog.setLocationRelativeTo(null)
|
||||||
|
dialog.isVisible = true
|
||||||
|
if (dialog.ok) {
|
||||||
|
val lastKeyPair = dialog.getLasOhKeyPair()
|
||||||
|
if (lastKeyPair != null) {
|
||||||
|
publicKeyTextField.putClientProperty(OhKeyPair::class, lastKeyPair)
|
||||||
|
publicKeyTextField.text = lastKeyPair.name
|
||||||
|
publicKeyTextField.outline = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshStates() {
|
||||||
|
hostTextField.isEnabled = true
|
||||||
|
portTextField.isEnabled = true
|
||||||
|
usernameTextField.isEnabled = true
|
||||||
|
authenticationTypeComboBox.isEnabled = true
|
||||||
|
passwordTextField.isEnabled = true
|
||||||
|
chooseKeyBtn.isEnabled = true
|
||||||
|
|
||||||
|
if (protocolTypeComboBox.selectedItem == Protocol.Local) {
|
||||||
|
hostTextField.isEnabled = false
|
||||||
|
portTextField.isEnabled = false
|
||||||
|
usernameTextField.isEnabled = false
|
||||||
|
authenticationTypeComboBox.isEnabled = false
|
||||||
|
passwordTextField.isEnabled = false
|
||||||
|
chooseKeyBtn.isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.settings
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.new-host.general")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $formMargin, default:grow, $formMargin, pref, $formMargin, default, default:grow",
|
||||||
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
|
||||||
|
remarkTextArea.rows = 8
|
||||||
|
remarkTextArea.lineWrap = true
|
||||||
|
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
|
|
||||||
|
switchPasswordComponent()
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
|
||||||
|
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.protocol")}:").xy(1, rows)
|
||||||
|
.add(protocolTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
||||||
|
.add(hostTextField).xy(3, rows)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||||
|
.add(portTextField).xy(7, rows).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
||||||
|
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
|
||||||
|
.add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
||||||
|
.add(passwordPanel).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
||||||
|
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
||||||
|
.xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun switchPasswordComponent() {
|
||||||
|
passwordPanel.removeAll()
|
||||||
|
|
||||||
|
if (authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
|
||||||
|
passwordPanel.add(
|
||||||
|
FormBuilder.create()
|
||||||
|
.layout(FormLayout("default:grow, 4dlu, left:pref", "pref"))
|
||||||
|
.add(publicKeyTextField).xy(1, 1)
|
||||||
|
.add(chooseKeyBtn).xy(3, 1)
|
||||||
|
.build(), BorderLayout.CENTER
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
passwordPanel.add(passwordTextField, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
passwordPanel.revalidate()
|
||||||
|
passwordPanel.repaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected inner class ProxyOption : JPanel(BorderLayout()), Option {
|
||||||
|
val proxyTypeComboBox = FlatComboBox<ProxyType>()
|
||||||
|
val proxyHostTextField = OutlineTextField()
|
||||||
|
val proxyPasswordTextField = OutlinePasswordField()
|
||||||
|
val proxyUsernameTextField = OutlineTextField()
|
||||||
|
val proxyPortTextField = PortSpinner(1080)
|
||||||
|
val proxyAuthenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
proxyAuthenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = value?.toString() ?: ""
|
||||||
|
when (value) {
|
||||||
|
AuthenticationType.Password -> {
|
||||||
|
text = "Password"
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationType.PublicKey -> {
|
||||||
|
text = "Public Key"
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationType.KeyboardInteractive -> {
|
||||||
|
text = "Keyboard Interactive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
text,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyTypeComboBox.addItem(ProxyType.No)
|
||||||
|
proxyTypeComboBox.addItem(ProxyType.HTTP)
|
||||||
|
proxyTypeComboBox.addItem(ProxyType.SOCKS5)
|
||||||
|
|
||||||
|
proxyAuthenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||||
|
proxyAuthenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||||
|
|
||||||
|
proxyUsernameTextField.text = "root"
|
||||||
|
|
||||||
|
refreshStates()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
proxyTypeComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
refreshStates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxyAuthenticationTypeComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
refreshStates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshStates() {
|
||||||
|
proxyHostTextField.isEnabled = proxyTypeComboBox.selectedItem != ProxyType.No
|
||||||
|
proxyPortTextField.isEnabled = proxyHostTextField.isEnabled
|
||||||
|
|
||||||
|
proxyAuthenticationTypeComboBox.isEnabled = proxyHostTextField.isEnabled
|
||||||
|
proxyUsernameTextField.isEnabled = proxyAuthenticationTypeComboBox.selectedItem != AuthenticationType.No
|
||||||
|
proxyPasswordTextField.isEnabled = proxyUsernameTextField.isEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.network
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.new-host.proxy")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $formMargin, default:grow, $formMargin, pref, $formMargin, default, default:grow",
|
||||||
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.protocol")}:").xy(1, rows)
|
||||||
|
.add(proxyTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
||||||
|
.add(proxyHostTextField).xy(3, rows)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||||
|
.add(proxyPortTextField).xy(7, rows).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
|
||||||
|
.add(proxyAuthenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
||||||
|
.add(proxyUsernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
||||||
|
.add(proxyPasswordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected inner class TerminalOption : JPanel(BorderLayout()), Option {
|
||||||
|
val charsetComboBox = JComboBox<String>()
|
||||||
|
val startupCommandTextField = OutlineTextField()
|
||||||
|
val environmentTextArea = FixedLengthTextArea(2048)
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
|
||||||
|
|
||||||
|
environmentTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
environmentTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
|
||||||
|
environmentTextArea.rows = 8
|
||||||
|
environmentTextArea.lineWrap = true
|
||||||
|
environmentTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
|
|
||||||
|
for (e in Charset.availableCharsets()) {
|
||||||
|
charsetComboBox.addItem(e.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
charsetComboBox.selectedItem = "UTF-8"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.terminal
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.new-host.terminal")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $formMargin, default:grow, $formMargin, default:grow",
|
||||||
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
|
||||||
|
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows)
|
||||||
|
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
|
||||||
|
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
|
||||||
|
.apply { rows += step }
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected inner class TunnelingOption : JPanel(BorderLayout()), Option {
|
||||||
|
val tunnelings = mutableListOf<Tunneling>()
|
||||||
|
|
||||||
|
private val model = object : DefaultTableModel() {
|
||||||
|
override fun getRowCount(): Int {
|
||||||
|
return tunnelings.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isCellEditable(row: Int, column: Int): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addRow(tunneling: Tunneling) {
|
||||||
|
val rowCount = super.getRowCount()
|
||||||
|
tunnelings.add(tunneling)
|
||||||
|
super.fireTableRowsInserted(rowCount, rowCount + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getValueAt(row: Int, column: Int): Any {
|
||||||
|
val tunneling = tunnelings[row]
|
||||||
|
return when (column) {
|
||||||
|
0 -> tunneling.name
|
||||||
|
1 -> tunneling.type
|
||||||
|
2 -> "${tunneling.sourceHost}:${tunneling.sourcePort}"
|
||||||
|
3 -> "${tunneling.destinationHost}:${tunneling.destinationPort}"
|
||||||
|
else -> super.getValueAt(row, column)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val table = JTable(model)
|
||||||
|
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
|
||||||
|
private val editBtn = JButton(I18n.getString("termora.new-host.tunneling.edit"))
|
||||||
|
private val deleteBtn = JButton(I18n.getString("termora.new-host.tunneling.delete"))
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
val scrollPane = JScrollPane(table)
|
||||||
|
|
||||||
|
model.addColumn(I18n.getString("termora.new-host.tunneling.table.name"))
|
||||||
|
model.addColumn(I18n.getString("termora.new-host.tunneling.table.type"))
|
||||||
|
model.addColumn(I18n.getString("termora.new-host.tunneling.table.source"))
|
||||||
|
model.addColumn(I18n.getString("termora.new-host.tunneling.table.destination"))
|
||||||
|
|
||||||
|
|
||||||
|
table.autoResizeMode = JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS
|
||||||
|
table.border = BorderFactory.createEmptyBorder()
|
||||||
|
table.fillsViewportHeight = true
|
||||||
|
scrollPane.border = BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createEmptyBorder(4, 0, 4, 0),
|
||||||
|
BorderFactory.createMatteBorder(1, 1, 1, 1, DynamicColor.BorderColor)
|
||||||
|
)
|
||||||
|
|
||||||
|
deleteBtn.isFocusable = false
|
||||||
|
addBtn.isFocusable = false
|
||||||
|
editBtn.isFocusable = false
|
||||||
|
|
||||||
|
editBtn.isEnabled = false
|
||||||
|
deleteBtn.isEnabled = false
|
||||||
|
|
||||||
|
val box = Box.createHorizontalBox()
|
||||||
|
box.add(addBtn)
|
||||||
|
box.add(Box.createHorizontalStrut(4))
|
||||||
|
box.add(editBtn)
|
||||||
|
box.add(Box.createHorizontalStrut(4))
|
||||||
|
box.add(deleteBtn)
|
||||||
|
|
||||||
|
add(JLabel("TCP/IP Forwarding:"), BorderLayout.NORTH)
|
||||||
|
add(scrollPane, BorderLayout.CENTER)
|
||||||
|
add(box, BorderLayout.SOUTH)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
addBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent?) {
|
||||||
|
val dialog = PortForwardingDialog(SwingUtilities.getWindowAncestor(this@HostOptionsPane))
|
||||||
|
dialog.isVisible = true
|
||||||
|
val tunneling = dialog.tunneling ?: return
|
||||||
|
model.addRow(tunneling)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
editBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent?) {
|
||||||
|
val row = table.selectedRow
|
||||||
|
if (row < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val dialog = PortForwardingDialog(
|
||||||
|
SwingUtilities.getWindowAncestor(this@HostOptionsPane),
|
||||||
|
tunnelings[row]
|
||||||
|
)
|
||||||
|
dialog.isVisible = true
|
||||||
|
tunnelings[row] = dialog.tunneling ?: return
|
||||||
|
model.fireTableRowsUpdated(row, row)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
deleteBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val rows = table.selectedRows
|
||||||
|
if (rows.isEmpty()) return
|
||||||
|
rows.sortDescending()
|
||||||
|
for (row in rows) {
|
||||||
|
tunnelings.removeAt(row)
|
||||||
|
model.fireTableRowsDeleted(row, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
table.selectionModel.addListSelectionListener {
|
||||||
|
editBtn.isEnabled = table.selectedRowCount > 0
|
||||||
|
deleteBtn.isEnabled = editBtn.isEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
table.addMouseListener(object : MouseAdapter() {
|
||||||
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
|
if (e.clickCount % 2 == 0 && SwingUtilities.isLeftMouseButton(e)) {
|
||||||
|
editBtn.actionListeners.forEach {
|
||||||
|
it.actionPerformed(
|
||||||
|
ActionEvent(
|
||||||
|
editBtn,
|
||||||
|
ActionEvent.ACTION_PERFORMED,
|
||||||
|
StringUtils.EMPTY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.showWriteAccess
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.new-host.tunneling")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class PortForwardingDialog(
|
||||||
|
owner: Window,
|
||||||
|
var tunneling: Tunneling? = null
|
||||||
|
) : DialogWrapper(owner) {
|
||||||
|
private val formMargin = "4dlu"
|
||||||
|
private val typeComboBox = FlatComboBox<TunnelingType>()
|
||||||
|
private val nameTextField = OutlineTextField(32)
|
||||||
|
private val localHostTextField = OutlineTextField()
|
||||||
|
private val localPortSpinner = PortSpinner()
|
||||||
|
private val remoteHostTextField = OutlineTextField()
|
||||||
|
private val remotePortSpinner = PortSpinner()
|
||||||
|
|
||||||
|
init {
|
||||||
|
isModal = true
|
||||||
|
title = I18n.getString("termora.new-host.tunneling")
|
||||||
|
controlsVisible = false
|
||||||
|
|
||||||
|
typeComboBox.addItem(TunnelingType.Local)
|
||||||
|
typeComboBox.addItem(TunnelingType.Remote)
|
||||||
|
typeComboBox.addItem(TunnelingType.Dynamic)
|
||||||
|
|
||||||
|
localHostTextField.text = "127.0.0.1"
|
||||||
|
localPortSpinner.value = 1080
|
||||||
|
|
||||||
|
remoteHostTextField.text = "127.0.0.1"
|
||||||
|
|
||||||
|
typeComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
remoteHostTextField.isEnabled = typeComboBox.selectedItem != TunnelingType.Dynamic
|
||||||
|
remotePortSpinner.isEnabled = remoteHostTextField.isEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tunneling?.let {
|
||||||
|
localHostTextField.text = it.sourceHost
|
||||||
|
localPortSpinner.value = it.sourcePort
|
||||||
|
remoteHostTextField.text = it.destinationHost
|
||||||
|
remotePortSpinner.value = it.destinationPort
|
||||||
|
nameTextField.text = it.name
|
||||||
|
typeComboBox.selectedItem = it.type
|
||||||
|
}
|
||||||
|
|
||||||
|
init()
|
||||||
|
pack()
|
||||||
|
size = Dimension(UIManager.getInt("Dialog.width") - 300, size.height)
|
||||||
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doOKAction() {
|
||||||
|
if (nameTextField.text.isBlank()) {
|
||||||
|
nameTextField.outline = "error"
|
||||||
|
nameTextField.requestFocusInWindow()
|
||||||
|
return
|
||||||
|
} else if (localHostTextField.text.isBlank()) {
|
||||||
|
localHostTextField.outline = "error"
|
||||||
|
localHostTextField.requestFocusInWindow()
|
||||||
|
return
|
||||||
|
} else if (remoteHostTextField.text.isBlank()) {
|
||||||
|
remoteHostTextField.outline = "error"
|
||||||
|
remoteHostTextField.requestFocusInWindow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tunneling = Tunneling(
|
||||||
|
name = nameTextField.text,
|
||||||
|
type = typeComboBox.selectedItem as TunnelingType,
|
||||||
|
sourceHost = localHostTextField.text,
|
||||||
|
sourcePort = localPortSpinner.value as Int,
|
||||||
|
destinationHost = remoteHostTextField.text,
|
||||||
|
destinationPort = remotePortSpinner.value as Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
super.doOKAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doCancelAction() {
|
||||||
|
tunneling = null
|
||||||
|
super.doCancelAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $formMargin, default:grow, $formMargin, pref",
|
||||||
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
return FormBuilder.create().layout(layout).padding("0dlu, $formMargin, $formMargin, $formMargin")
|
||||||
|
.add("${I18n.getString("termora.new-host.tunneling.table.name")}:").xy(1, rows)
|
||||||
|
.add(nameTextField).xyw(3, rows, 3).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.tunneling.table.type")}:").xy(1, rows)
|
||||||
|
.add(typeComboBox).xyw(3, rows, 3).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.tunneling.table.source")}:").xy(1, rows)
|
||||||
|
.add(localHostTextField).xy(3, rows)
|
||||||
|
.add(localPortSpinner).xy(5, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.tunneling.table.destination")}:").xy(1, rows)
|
||||||
|
.add(remoteHostTextField).xy(3, rows)
|
||||||
|
.add(remotePortSpinner).xy(5, rows).apply { rows += step }
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
63
src/main/kotlin/app/termora/HostTerminalTab.kt
Normal file
63
src/main/kotlin/app/termora/HostTerminalTab.kt
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.terminal.*
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import java.beans.PropertyChangeEvent
|
||||||
|
import javax.swing.Icon
|
||||||
|
|
||||||
|
abstract class HostTerminalTab(val host: Host) : PropertyTerminalTab() {
|
||||||
|
protected val coroutineScope by lazy { CoroutineScope(Dispatchers.Swing) }
|
||||||
|
protected val terminal = TerminalFactory.instance.createTerminal()
|
||||||
|
protected val terminalModel get() = terminal.getTerminalModel()
|
||||||
|
protected var unread = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
firePropertyChange(PropertyChangeEvent(this, "icon", null, null))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* visualTerminal */
|
||||||
|
protected fun Terminal.clearScreen() {
|
||||||
|
this.write("${ControlCharacters.ESC}[3J")
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
terminal.getTerminalModel().addDataListener(object : DataListener {
|
||||||
|
override fun onChanged(key: DataKey<*>, data: Any) {
|
||||||
|
if (key == VisualTerminal.Written) {
|
||||||
|
if (hasFocus || unread) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
unread = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun start() {}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return host.name
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(): Icon {
|
||||||
|
if (host.protocol == Protocol.Local || host.protocol == Protocol.SSH) {
|
||||||
|
return if (unread) Icons.terminalUnread else Icons.terminal
|
||||||
|
}
|
||||||
|
return Icons.terminal
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
coroutineScope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGrabFocus() {
|
||||||
|
super.onGrabFocus()
|
||||||
|
if (!unread) return
|
||||||
|
unread = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
583
src/main/kotlin/app/termora/HostTree.kt
Normal file
583
src/main/kotlin/app/termora/HostTree.kt
Normal file
@@ -0,0 +1,583 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.db.Database
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
|
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
||||||
|
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
||||||
|
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.instance
|
||||||
|
private val editor = OutlineTextField(64)
|
||||||
|
|
||||||
|
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() {
|
||||||
|
override fun getTreeCellRendererComponent(
|
||||||
|
tree: JTree,
|
||||||
|
value: Any,
|
||||||
|
sel: Boolean,
|
||||||
|
expanded: Boolean,
|
||||||
|
leaf: Boolean,
|
||||||
|
row: Int,
|
||||||
|
hasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
val host = value as Host
|
||||||
|
val c = super.getTreeCellRendererComponent(tree, host, sel, expanded, leaf, row, hasFocus)
|
||||||
|
if (host.protocol == Protocol.Folder) {
|
||||||
|
icon = if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||||
|
} else if (host.protocol == Protocol.SSH || host.protocol == Protocol.Local) {
|
||||||
|
icon = 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.instance.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 (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||||
|
val host = lastSelectedPathComponent
|
||||||
|
if (host is Host && host.protocol != Protocol.Folder) {
|
||||||
|
ActionManager.getInstance().getAction(Actions.OPEN_HOST)
|
||||||
|
?.actionPerformed(OpenHostActionEvent(this, host))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
val lastHost = lastSelectedPathComponent
|
||||||
|
if (lastHost !is Host) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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.open"))
|
||||||
|
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 property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
|
||||||
|
|
||||||
|
open.addActionListener {
|
||||||
|
getSelectionNodes()
|
||||||
|
.filter { it.protocol != Protocol.Folder }
|
||||||
|
.forEach {
|
||||||
|
ActionManager.getInstance()
|
||||||
|
.getAction(Actions.OPEN_HOST)
|
||||||
|
?.actionPerformed(OpenHostActionEvent(this, it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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) {
|
||||||
|
showAddHostDialog()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
property.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val dialog = HostDialog(SwingUtilities.getWindowAncestor(this@HostTree), lastHost)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showAddHostDialog() {
|
||||||
|
var lastHost = lastSelectedPathComponent
|
||||||
|
if (lastHost !is Host) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastHost.protocol != Protocol.Folder) {
|
||||||
|
val p = model.getParent(lastHost) ?: return
|
||||||
|
lastHost = p
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = HostDialog(SwingUtilities.getWindowAncestor(this))
|
||||||
|
dialog.isVisible = true
|
||||||
|
val host = (dialog.host ?: return).copy(parentId = lastHost.id)
|
||||||
|
|
||||||
|
runCatchingHost(host)
|
||||||
|
|
||||||
|
expandNode(lastHost)
|
||||||
|
selectionPath = TreePath(model.getPathToRoot(host))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private 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)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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 model.getChildCount(p)) {
|
||||||
|
val child = model.getChild(p, i) as Host
|
||||||
|
nodes.add(child)
|
||||||
|
parents.add(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
Database.instance.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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
159
src/main/kotlin/app/termora/HostTreeModel.kt
Normal file
159
src/main/kotlin/app/termora/HostTreeModel.kt
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
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.instance
|
||||||
|
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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/main/kotlin/app/termora/Hyperlink.kt
Normal file
22
src/main/kotlin/app/termora/Hyperlink.kt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
|
import com.formdev.flatlaf.extras.FlatSVGIcon.ColorFilter
|
||||||
|
import org.jdesktop.swingx.JXHyperlink
|
||||||
|
import java.awt.Color
|
||||||
|
import javax.swing.SwingConstants
|
||||||
|
import javax.swing.UIManager
|
||||||
|
|
||||||
|
class Hyperlink(action: AnAction, focusable: Boolean = true) : JXHyperlink(action) {
|
||||||
|
init {
|
||||||
|
val myIcon = FlatSVGIcon(Icons.externalLink.name)
|
||||||
|
myIcon.colorFilter = object : ColorFilter() {
|
||||||
|
override fun filter(color: Color?): Color {
|
||||||
|
return UIManager.getColor("Hyperlink.linkColor")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isFocusable = focusable
|
||||||
|
icon = myIcon
|
||||||
|
horizontalTextPosition = SwingConstants.LEFT
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/main/kotlin/app/termora/I18n.kt
Normal file
57
src/main/kotlin/app/termora/I18n.kt
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.LocaleUtils
|
||||||
|
import org.apache.commons.text.StringSubstitutor
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.text.MessageFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object I18n {
|
||||||
|
private val log = LoggerFactory.getLogger(I18n::class.java)
|
||||||
|
private val bundle by lazy {
|
||||||
|
val bundle = ResourceBundle.getBundle("i18n/messages", Locale.getDefault())
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("I18n: {}", bundle.baseBundleName ?: "null")
|
||||||
|
}
|
||||||
|
return@lazy bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
private val substitutor by lazy { StringSubstitutor { key -> getString(key) } }
|
||||||
|
private val supportedLanguages = sortedMapOf(
|
||||||
|
"en_US" to "English",
|
||||||
|
"zh_CN" to "简体中文",
|
||||||
|
"zh_TW" to "繁體中文",
|
||||||
|
)
|
||||||
|
|
||||||
|
fun containsLanguage(locale: Locale): String? {
|
||||||
|
for (key in supportedLanguages.keys) {
|
||||||
|
val e = LocaleUtils.toLocale(key)
|
||||||
|
if (LocaleUtils.toLocale(key) == locale ||
|
||||||
|
(e.language.equals(locale.language, true) && e.country.equals(locale.country, true))
|
||||||
|
) {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLanguages(): Map<String, String> {
|
||||||
|
return supportedLanguages
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getString(key: String, vararg args: Any): String {
|
||||||
|
try {
|
||||||
|
val text = substitutor.replace(bundle.getString(key))
|
||||||
|
if (args.isNotEmpty()) {
|
||||||
|
return MessageFormat.format(text, *args)
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
} catch (e: MissingResourceException) {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn(e.message, e)
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
83
src/main/kotlin/app/termora/Icons.kt
Normal file
83
src/main/kotlin/app/termora/Icons.kt
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
object Icons {
|
||||||
|
val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") }
|
||||||
|
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
|
||||||
|
val close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") }
|
||||||
|
val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
|
||||||
|
val matchCase by lazy { DynamicIcon("icons/matchCase.svg", "icons/matchCase_dark.svg") }
|
||||||
|
val regex by lazy { DynamicIcon("icons/regex.svg", "icons/regex_dark.svg") }
|
||||||
|
val vcs by lazy { DynamicIcon("icons/vcs.svg", "icons/vcs_dark.svg") }
|
||||||
|
val dumpThreads by lazy { DynamicIcon("icons/dumpThreads.svg", "icons/dumpThreads_dark.svg") }
|
||||||
|
val supertypes by lazy { DynamicIcon("icons/supertypes.svg", "icons/supertypes_dark.svg") }
|
||||||
|
val settings by lazy { DynamicIcon("icons/settings.svg", "icons/settings_dark.svg") }
|
||||||
|
val pin by lazy { DynamicIcon("icons/pin.svg", "icons/pin_dark.svg") }
|
||||||
|
val empty by lazy { DynamicIcon("icons/empty.svg") }
|
||||||
|
val add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") }
|
||||||
|
val networkPolicy by lazy { DynamicIcon("icons/networkPolicy.svg", "icons/networkPolicy_dark.svg") }
|
||||||
|
val clusterRole by lazy { DynamicIcon("icons/clusterRole.svg", "icons/clusterRole_dark.svg") }
|
||||||
|
val daemonSets by lazy { DynamicIcon("icons/daemonSets.svg", "icons/daemonSets_dark.svg") }
|
||||||
|
val role by lazy { DynamicIcon("icons/role.svg", "icons/role_dark.svg") }
|
||||||
|
val locked by lazy { DynamicIcon("icons/locked.svg", "icons/locked_dark.svg") }
|
||||||
|
val warning by lazy { DynamicIcon("icons/warning.svg", "icons/warning_dark.svg") }
|
||||||
|
val warningDialog by lazy { DynamicIcon("icons/warningDialog.svg", "icons/warningDialog_dark.svg") }
|
||||||
|
val unlocked by lazy { DynamicIcon("icons/unlocked.svg", "icons/unlocked_dark.svg") }
|
||||||
|
val i18n by lazy { DynamicIcon("icons/i18n.svg", "icons/i18n_dark.svg") }
|
||||||
|
val rec by lazy { DynamicIcon("icons/rec.svg", "icons/rec_dark.svg") }
|
||||||
|
val stop by lazy { DynamicIcon("icons/stop.svg", "icons/stop_dark.svg") }
|
||||||
|
val find by lazy { DynamicIcon("icons/find.svg", "icons/find_dark.svg") }
|
||||||
|
val keyboard by lazy { DynamicIcon("icons/keyboard.svg", "icons/keyboard_dark.svg") }
|
||||||
|
val moreVertical by lazy { DynamicIcon("icons/moreVertical.svg", "icons/moreVertical_dark.svg") }
|
||||||
|
val colors by lazy { DynamicIcon("icons/colors.svg", "icons/colors_dark.svg") }
|
||||||
|
val chevronDown by lazy { DynamicIcon("icons/chevronDownLarge.svg", "icons/chevronDownLarge_dark.svg") }
|
||||||
|
val homeFolder by lazy { DynamicIcon("icons/homeFolder.svg", "icons/homeFolder_dark.svg") }
|
||||||
|
val openNewTab by lazy { DynamicIcon("icons/openNewTab.svg", "icons/openNewTab_dark.svg") }
|
||||||
|
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") }
|
||||||
|
val export by lazy { DynamicIcon("icons/export.svg", "icons/export_dark.svg") }
|
||||||
|
val terminal by lazy { DynamicIcon("icons/terminal.svg", "icons/terminal_dark.svg") }
|
||||||
|
val azure by lazy { DynamicIcon("icons/azure.svg", "icons/azure_dark.svg") }
|
||||||
|
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
|
||||||
|
val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") }
|
||||||
|
val microsoft by lazy { DynamicIcon("icons/microsoft.svg", "icons/microsoft_dark.svg") }
|
||||||
|
val tencent by lazy { DynamicIcon("icons/tencent.svg") }
|
||||||
|
val google by lazy { DynamicIcon("icons/google-small.svg") }
|
||||||
|
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }
|
||||||
|
val yandexCloud by lazy { DynamicIcon("icons/yandexCloud.svg") }
|
||||||
|
val aws by lazy { DynamicIcon("icons/aws.svg","icons/aws_dark.svg") }
|
||||||
|
val huawei by lazy { DynamicIcon("icons/huawei.svg") }
|
||||||
|
val baidu by lazy { DynamicIcon("icons/baiduyun.svg") }
|
||||||
|
val tianyi by lazy { DynamicIcon("icons/tianyiyun.svg") }
|
||||||
|
val digitalocean by lazy { DynamicIcon("icons/digitalocean.svg","icons/digitalocean_dark.svg") }
|
||||||
|
val terminalUnread by lazy { DynamicIcon("icons/terminalUnread.svg", "icons/terminalUnread_dark.svg") }
|
||||||
|
val dbPrimitive by lazy { DynamicIcon("icons/dbPrimitive.svg", "icons/dbPrimitive_dark.svg") }
|
||||||
|
val linux by lazy { DynamicIcon("icons/linux.svg", "icons/linux_dark.svg") }
|
||||||
|
val success by lazy { DynamicIcon("icons/success.svg", "icons/success_dark.svg") }
|
||||||
|
val network by lazy { DynamicIcon("icons/network.svg", "icons/network_dark.svg") }
|
||||||
|
val server by lazy { DynamicIcon("icons/server.svg", "icons/server_dark.svg") }
|
||||||
|
val runAnything by lazy { DynamicIcon("icons/runAnything.svg", "icons/runAnything_dark.svg") }
|
||||||
|
val uiForm by lazy { DynamicIcon("icons/uiForm.svg", "icons/uiForm_dark.svg") }
|
||||||
|
val cloud by lazy { DynamicIcon("icons/cloud.svg", "icons/cloud_dark.svg") }
|
||||||
|
val externalLink by lazy { DynamicIcon("icons/externalLink.svg", "icons/externalLink_dark.svg") }
|
||||||
|
val user by lazy { DynamicIcon("icons/user.svg", "icons/user_dark.svg") }
|
||||||
|
val infoOutline by lazy { DynamicIcon("icons/infoOutline.svg", "icons/infoOutline_dark.svg") }
|
||||||
|
val lightning by lazy { DynamicIcon("icons/lightning.svg", "icons/lightning_dark.svg") }
|
||||||
|
val split by lazy { DynamicIcon("icons/split.svg", "icons/split_dark.svg") }
|
||||||
|
val setKey by lazy { DynamicIcon("icons/setKey.svg", "icons/setKey_dark.svg") }
|
||||||
|
val greyKey by lazy { DynamicIcon("icons/greyKey.svg", "icons/greyKey_dark.svg") }
|
||||||
|
val sortedSet by lazy { DynamicIcon("icons/sortedSet.svg", "icons/sortedSet_dark.svg") }
|
||||||
|
val colorPicker by lazy { DynamicIcon("icons/colorPicker.svg", "icons/colorPicker_dark.svg") }
|
||||||
|
val folder by lazy { DynamicIcon("icons/folder.svg", "icons/folder_dark.svg") }
|
||||||
|
val help by lazy { DynamicIcon("icons/help.svg", "icons/help_dark.svg") }
|
||||||
|
val expand by lazy { DynamicIcon("icons/expand.svg", "icons/expand_dark.svg") }
|
||||||
|
val collapse by lazy { DynamicIcon("icons/collapse.svg", "icons/collapse_dark.svg") }
|
||||||
|
val expandAll by lazy { DynamicIcon("icons/expandAll.svg", "icons/expandAll_dark.svg") }
|
||||||
|
val collapseAll by lazy { DynamicIcon("icons/collapseAll.svg", "icons/collapseAll_dark.svg") }
|
||||||
|
val web by lazy { DynamicIcon("icons/web.svg", "icons/web_dark.svg") }
|
||||||
|
val download by lazy { DynamicIcon("icons/download.svg", "icons/download_dark.svg") }
|
||||||
|
val upload by lazy { DynamicIcon("icons/upload.svg", "icons/upload_dark.svg") }
|
||||||
|
val ideUpdate by lazy { DynamicIcon("icons/ideUpdate.svg", "icons/ideUpdate_dark.svg") }
|
||||||
|
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") }
|
||||||
|
|
||||||
|
}
|
||||||
74
src/main/kotlin/app/termora/InputDialog.kt
Normal file
74
src/main/kotlin/app/termora/InputDialog.kt
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatTextField
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.event.KeyAdapter
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
|
import javax.swing.BorderFactory
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.UIManager
|
||||||
|
|
||||||
|
class InputDialog(
|
||||||
|
owner: Window,
|
||||||
|
title: String,
|
||||||
|
text: String = StringUtils.EMPTY,
|
||||||
|
placeholderText: String = StringUtils.EMPTY
|
||||||
|
) : DialogWrapper(owner) {
|
||||||
|
private val textField = FlatTextField()
|
||||||
|
private var text: String? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
setSize(340, 60)
|
||||||
|
setLocationRelativeTo(owner)
|
||||||
|
|
||||||
|
super.setTitle(title)
|
||||||
|
|
||||||
|
isResizable = false
|
||||||
|
isModal = true
|
||||||
|
controlsVisible = false
|
||||||
|
titleBarHeight = UIManager.getInt("TabbedPane.tabHeight") * 0.8f
|
||||||
|
|
||||||
|
|
||||||
|
textField.placeholderText = placeholderText
|
||||||
|
textField.text = text
|
||||||
|
textField.addKeyListener(object : KeyAdapter() {
|
||||||
|
override fun keyPressed(e: KeyEvent) {
|
||||||
|
if (e.keyCode == KeyEvent.VK_ENTER) {
|
||||||
|
if (textField.text.isBlank()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
doOKAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
textField.background = UIManager.getColor("window")
|
||||||
|
textField.border = BorderFactory.createEmptyBorder(0, 13, 0, 13)
|
||||||
|
|
||||||
|
return textField
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getText(): String? {
|
||||||
|
isVisible = true
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doCancelAction() {
|
||||||
|
text = null
|
||||||
|
super.doCancelAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doOKAction() {
|
||||||
|
text = textField.text
|
||||||
|
super.doOKAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createSouthPanel(): JComponent? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
951
src/main/kotlin/app/termora/Laf.kt
Normal file
951
src/main/kotlin/app/termora/Laf.kt
Normal file
@@ -0,0 +1,951 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.terminal.ColorTheme
|
||||||
|
import app.termora.terminal.TerminalColor
|
||||||
|
import com.formdev.flatlaf.FlatDarkLaf
|
||||||
|
import com.formdev.flatlaf.FlatLightLaf
|
||||||
|
import com.formdev.flatlaf.FlatPropertiesLaf
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
class LightLaf : FlatLightLaf(), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0
|
||||||
|
TerminalColor.Normal.RED -> 13501701
|
||||||
|
TerminalColor.Normal.GREEN -> 425239
|
||||||
|
TerminalColor.Normal.YELLOW -> 11701248
|
||||||
|
TerminalColor.Normal.BLUE -> 409563
|
||||||
|
TerminalColor.Normal.MAGENTA -> 11733427
|
||||||
|
TerminalColor.Normal.CYAN -> 167566
|
||||||
|
TerminalColor.Normal.WHITE -> 9605778
|
||||||
|
|
||||||
|
TerminalColor.Bright.BLACK -> 0x4c4c4c
|
||||||
|
TerminalColor.Bright.RED -> 0xff0000
|
||||||
|
TerminalColor.Bright.GREEN -> 0x00ff00
|
||||||
|
TerminalColor.Bright.YELLOW -> if (SystemInfo.isWindows) 0xC18301 else 0xffff00
|
||||||
|
TerminalColor.Bright.BLUE -> 0x4682b4
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xff00ff
|
||||||
|
TerminalColor.Bright.CYAN -> 0x00ffff
|
||||||
|
TerminalColor.Bright.WHITE -> 0xffffff
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DarkLaf : FlatDarkLaf(), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0
|
||||||
|
TerminalColor.Normal.RED -> 15749711
|
||||||
|
TerminalColor.Normal.GREEN -> 6067756
|
||||||
|
TerminalColor.Normal.YELLOW -> 10914317
|
||||||
|
TerminalColor.Normal.BLUE -> 3773396
|
||||||
|
TerminalColor.Normal.MAGENTA -> 10973631
|
||||||
|
TerminalColor.Normal.CYAN -> 41891
|
||||||
|
TerminalColor.Normal.WHITE -> 8421504
|
||||||
|
|
||||||
|
|
||||||
|
TerminalColor.Bright.BLACK -> 0x676767
|
||||||
|
TerminalColor.Bright.RED -> 0xef766d
|
||||||
|
TerminalColor.Bright.GREEN -> 0x8cf67a
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xfefb7e
|
||||||
|
TerminalColor.Bright.BLUE -> 0x6a71f6
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xf07ef8
|
||||||
|
TerminalColor.Bright.CYAN -> 0x8ef9fd
|
||||||
|
TerminalColor.Bright.WHITE -> 0xfeffff
|
||||||
|
|
||||||
|
// TerminalColor.Basic.BACKGROUND -> 1974050
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class iTerm2DarkLaf : FlatDarkLaf(), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
|
||||||
|
TerminalColor.Basic.BACKGROUND -> 0
|
||||||
|
TerminalColor.Basic.FOREGROUND -> 0xc7c7c7
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND -> 0xc6dcfc
|
||||||
|
TerminalColor.Basic.SELECTION_FOREGROUND -> 0x000000
|
||||||
|
TerminalColor.Basic.HYPERLINK -> 0x255ab4
|
||||||
|
TerminalColor.Find.BACKGROUND -> 0xffff00
|
||||||
|
TerminalColor.Find.FOREGROUND -> 0
|
||||||
|
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0xc7c7c7
|
||||||
|
|
||||||
|
TerminalColor.Normal.BLACK -> 0
|
||||||
|
TerminalColor.Normal.RED -> 0xb83019
|
||||||
|
TerminalColor.Normal.GREEN -> 0x51bf37
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xc6c43d
|
||||||
|
TerminalColor.Normal.BLUE -> 0x0c24bf
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xb93ec1
|
||||||
|
TerminalColor.Normal.CYAN -> 0x53c2c5
|
||||||
|
TerminalColor.Normal.WHITE -> 0xc7c7c7
|
||||||
|
|
||||||
|
|
||||||
|
TerminalColor.Bright.BLACK -> 0x676767
|
||||||
|
TerminalColor.Bright.RED -> 0xef766d
|
||||||
|
TerminalColor.Bright.GREEN -> 0x8cf67a
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xfefb7e
|
||||||
|
TerminalColor.Bright.BLUE -> 0x6a71f6
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xf07ef8
|
||||||
|
TerminalColor.Bright.CYAN -> 0x8ef9fd
|
||||||
|
TerminalColor.Bright.WHITE -> 0xfeffff
|
||||||
|
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TermiusLightLaf : FlatPropertiesLaf("Termius Light", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "light",
|
||||||
|
"@background" to "#d5dde0",
|
||||||
|
"@windowText" to "#32364a",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0x32364a
|
||||||
|
|
||||||
|
TerminalColor.Basic.FOREGROUND -> 0x32364a
|
||||||
|
|
||||||
|
TerminalColor.Normal.BLACK -> 0x141729
|
||||||
|
TerminalColor.Normal.RED -> 0xf24e50
|
||||||
|
TerminalColor.Normal.GREEN -> 0x198c51
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xf8aa4b
|
||||||
|
TerminalColor.Normal.BLUE -> 0x004878
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0x8f3c91
|
||||||
|
TerminalColor.Normal.CYAN -> 0x2091f6
|
||||||
|
TerminalColor.Normal.WHITE -> 0xeeeeee
|
||||||
|
|
||||||
|
TerminalColor.Bright.BLACK -> 0x3e4257
|
||||||
|
TerminalColor.Bright.RED -> 0xff7375
|
||||||
|
TerminalColor.Bright.GREEN -> 0x21b568
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xfdc47d
|
||||||
|
TerminalColor.Bright.BLUE -> 0x1d6da2
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xff7dc5
|
||||||
|
TerminalColor.Bright.CYAN -> 0x44a7ff
|
||||||
|
TerminalColor.Bright.WHITE -> 0xffffff
|
||||||
|
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TermiusDarkLaf : FlatPropertiesLaf("Termius Dark", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "dark",
|
||||||
|
"@background" to "#141729",
|
||||||
|
"@windowText" to "#21b568",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0x21b568
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_FOREGROUND ->0
|
||||||
|
|
||||||
|
TerminalColor.Basic.FOREGROUND -> 0x21b568
|
||||||
|
|
||||||
|
TerminalColor.Normal.BLACK -> 0x343851
|
||||||
|
TerminalColor.Normal.RED -> 0xf24e50
|
||||||
|
TerminalColor.Normal.GREEN -> 0x008463
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xeca855
|
||||||
|
TerminalColor.Normal.BLUE -> 0x08639f
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xc13282
|
||||||
|
TerminalColor.Normal.CYAN -> 0x2091f6
|
||||||
|
TerminalColor.Normal.WHITE -> 0xe2e3e8
|
||||||
|
|
||||||
|
TerminalColor.Bright.BLACK -> 0x8d91a5
|
||||||
|
TerminalColor.Bright.RED -> 0xff7375
|
||||||
|
TerminalColor.Bright.GREEN -> 0x3ed7be
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xfdc47d
|
||||||
|
TerminalColor.Bright.BLUE -> 0x6ba0c3
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xff7dc5
|
||||||
|
TerminalColor.Bright.CYAN -> 0x44a7ff
|
||||||
|
TerminalColor.Bright.WHITE -> 0xffffff
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NovelLaf : FlatPropertiesLaf("Novel", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "light",
|
||||||
|
"@background" to "#dfdbc3",
|
||||||
|
"@windowText" to "#3b2322",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
|
TerminalColor.Normal.RED -> 0xd30f0f
|
||||||
|
TerminalColor.Normal.GREEN -> 0x00933b
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xd38b40
|
||||||
|
TerminalColor.Normal.BLUE -> 0x00528e
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xcc32cf
|
||||||
|
TerminalColor.Normal.CYAN -> 0x26c3e6
|
||||||
|
TerminalColor.Normal.WHITE -> 0xa6a6a6
|
||||||
|
|
||||||
|
TerminalColor.Bright.BLACK -> 0x5c5c5c
|
||||||
|
TerminalColor.Bright.RED -> 0xe0692f
|
||||||
|
TerminalColor.Bright.GREEN -> 0x00b400
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xfff284
|
||||||
|
TerminalColor.Bright.BLUE -> 0x3ba6f3
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xec88c2
|
||||||
|
TerminalColor.Bright.CYAN -> 0x38daff
|
||||||
|
TerminalColor.Bright.WHITE -> 0xf2f2f2
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0x73635a
|
||||||
|
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AtomOneDarkLaf : FlatPropertiesLaf("Atom One Dark", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "dark",
|
||||||
|
"@background" to "#1e2127",
|
||||||
|
"@windowText" to "#abb2bf",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
|
TerminalColor.Normal.RED -> 0xca6169
|
||||||
|
TerminalColor.Normal.GREEN -> 0x82a568
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xbf8c5d
|
||||||
|
TerminalColor.Normal.BLUE -> 0x56a2e1
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xb76ccd
|
||||||
|
TerminalColor.Normal.CYAN -> 0x4e9aa3
|
||||||
|
TerminalColor.Normal.WHITE -> 0xc5cbd6
|
||||||
|
|
||||||
|
TerminalColor.Bright.BLACK -> 0x5c6370
|
||||||
|
TerminalColor.Bright.RED -> 0xe77c84
|
||||||
|
TerminalColor.Bright.GREEN -> 0xb4e294
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xe9b17b
|
||||||
|
TerminalColor.Bright.BLUE -> 0x7ec5ff
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xdb8df2
|
||||||
|
TerminalColor.Bright.CYAN -> 0x64cfdd
|
||||||
|
TerminalColor.Bright.WHITE -> 0xffffff
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0xabb2bf
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AtomOneLightLaf : FlatPropertiesLaf("Atom One Light", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "light",
|
||||||
|
"@background" to "#f9f9f9",
|
||||||
|
"@windowText" to "#383a42",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
|
TerminalColor.Normal.RED -> 0xe45649
|
||||||
|
TerminalColor.Normal.GREEN -> 0x4c9b4b
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xc99525
|
||||||
|
TerminalColor.Normal.BLUE -> 0x4078f2
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xa626a4
|
||||||
|
TerminalColor.Normal.CYAN -> 0x0184bc
|
||||||
|
TerminalColor.Normal.WHITE -> 0xb8b9bf
|
||||||
|
|
||||||
|
TerminalColor.Bright.BLACK -> 0x474747
|
||||||
|
TerminalColor.Bright.RED -> 0xff7468
|
||||||
|
TerminalColor.Bright.GREEN -> 0x74ca72
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xdba633
|
||||||
|
TerminalColor.Bright.BLUE -> 0x6a99ff
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xc142bf
|
||||||
|
TerminalColor.Bright.CYAN -> 0x00b1fd
|
||||||
|
TerminalColor.Bright.WHITE -> 0xffffff
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0x383a42
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EverforestDarkLaf : FlatPropertiesLaf("Everforest Dark", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "dark",
|
||||||
|
"@background" to "#282e32",
|
||||||
|
"@windowText" to "#d3c6aa",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x42494e
|
||||||
|
TerminalColor.Normal.RED -> 0xa1484a
|
||||||
|
TerminalColor.Normal.GREEN -> 0x778e54
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xba9e68
|
||||||
|
TerminalColor.Normal.BLUE -> 0x388084
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0x906378
|
||||||
|
TerminalColor.Normal.CYAN -> 0x6ca37a
|
||||||
|
TerminalColor.Normal.WHITE -> 0xc0dac6
|
||||||
|
TerminalColor.Bright.BLACK -> 0x575656
|
||||||
|
TerminalColor.Bright.RED -> 0xe67e80
|
||||||
|
TerminalColor.Bright.GREEN -> 0xa7c080
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xdbbc7f
|
||||||
|
TerminalColor.Bright.BLUE -> 0x7fbbb3
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xd699b6
|
||||||
|
TerminalColor.Bright.CYAN -> 0x83c092
|
||||||
|
TerminalColor.Bright.WHITE -> 0xe8f4eb
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0xd3c6aa
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EverforestLightLaf : FlatPropertiesLaf("Everforest Light", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "light",
|
||||||
|
"@background" to "#fefbf1",
|
||||||
|
"@windowText" to "#5c6a72",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x42494e
|
||||||
|
TerminalColor.Normal.RED -> 0xd2413e
|
||||||
|
TerminalColor.Normal.GREEN -> 0x919d45
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xd89902
|
||||||
|
TerminalColor.Normal.BLUE -> 0x2b7ba7
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xbc72a5
|
||||||
|
TerminalColor.Normal.CYAN -> 0x50b08c
|
||||||
|
TerminalColor.Normal.WHITE -> 0xc8d0c9
|
||||||
|
TerminalColor.Bright.BLACK -> 0x575656
|
||||||
|
TerminalColor.Bright.RED -> 0xe67e80
|
||||||
|
TerminalColor.Bright.GREEN -> 0xa7c080
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xdbbc7f
|
||||||
|
TerminalColor.Bright.BLUE -> 0x7fbbb3
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xd699b6
|
||||||
|
TerminalColor.Bright.CYAN -> 0x83c092
|
||||||
|
TerminalColor.Bright.WHITE -> 0xd7e2d8
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0x5c6a72
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class NightOwlLaf : FlatPropertiesLaf("Night Owl", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "dark",
|
||||||
|
"@background" to "#011627",
|
||||||
|
"@windowText" to "#d6deeb",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x072945
|
||||||
|
TerminalColor.Normal.RED -> 0xef5350
|
||||||
|
TerminalColor.Normal.GREEN -> 0x22da6e
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xc5e478
|
||||||
|
TerminalColor.Normal.BLUE -> 0x82aaff
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xc792ea
|
||||||
|
TerminalColor.Normal.CYAN -> 0x21c7a8
|
||||||
|
TerminalColor.Normal.WHITE -> 0xe1f1ff
|
||||||
|
TerminalColor.Bright.BLACK -> 0x575656
|
||||||
|
TerminalColor.Bright.RED -> 0xff7472
|
||||||
|
TerminalColor.Bright.GREEN -> 0x40fa8d
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xffeb95
|
||||||
|
TerminalColor.Bright.BLUE -> 0xa0beff
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xdaa4ff
|
||||||
|
TerminalColor.Bright.CYAN -> 0x7fdbca
|
||||||
|
TerminalColor.Bright.WHITE -> 0xffffff
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0x80a4c2
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LightOwlLaf : FlatPropertiesLaf("Light Owl", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "light",
|
||||||
|
"@background" to "#fbfbfb",
|
||||||
|
"@windowText" to "#403f53",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x403f53
|
||||||
|
TerminalColor.Normal.RED -> 0xde3d3b
|
||||||
|
TerminalColor.Normal.GREEN -> 0x08916a
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xe0af02
|
||||||
|
TerminalColor.Normal.BLUE -> 0x288ed7
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xd6438a
|
||||||
|
TerminalColor.Normal.CYAN -> 0x2aa298
|
||||||
|
TerminalColor.Normal.WHITE -> 0xe8e5e5
|
||||||
|
TerminalColor.Bright.BLACK -> 0x57566d
|
||||||
|
TerminalColor.Bright.RED -> 0xfa5d5b
|
||||||
|
TerminalColor.Bright.GREEN -> 0x1abf90
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xf4c315
|
||||||
|
TerminalColor.Bright.BLUE -> 0x3ca3ec
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xf559a4
|
||||||
|
TerminalColor.Bright.CYAN -> 0x39c6ba
|
||||||
|
TerminalColor.Bright.WHITE -> 0xf6f6f6
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0x90a7b2
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AuraLaf : FlatPropertiesLaf("Aura", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "dark",
|
||||||
|
"@background" to "#21202e",
|
||||||
|
"@windowText" to "#edecee",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x1c1b22
|
||||||
|
TerminalColor.Normal.RED -> 0xff6767
|
||||||
|
TerminalColor.Normal.GREEN -> 0x4deeb8
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xf4be77
|
||||||
|
TerminalColor.Normal.BLUE -> 0x5b72ee
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xa277ff
|
||||||
|
TerminalColor.Normal.CYAN -> 0x51fafa
|
||||||
|
TerminalColor.Normal.WHITE -> 0xdddbfa
|
||||||
|
TerminalColor.Bright.BLACK -> 0x4d4d4d
|
||||||
|
TerminalColor.Bright.RED -> 0xffa285
|
||||||
|
TerminalColor.Bright.GREEN -> 0x99ffdd
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xffd49d
|
||||||
|
TerminalColor.Bright.BLUE -> 0x8296ff
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xb592ff
|
||||||
|
TerminalColor.Bright.CYAN -> 0x8cffff
|
||||||
|
TerminalColor.Bright.WHITE -> 0xffffff
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0xedecee
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Cobalt2Laf : FlatPropertiesLaf("Cobalt2", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "dark",
|
||||||
|
"@background" to "#132738",
|
||||||
|
"@windowText" to "#ffffff",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
|
TerminalColor.Normal.RED -> 0xff0000
|
||||||
|
TerminalColor.Normal.GREEN -> 0x38de21
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xffe50a
|
||||||
|
TerminalColor.Normal.BLUE -> 0x1460d2
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xff4387
|
||||||
|
TerminalColor.Normal.CYAN -> 0x00bbbb
|
||||||
|
TerminalColor.Normal.WHITE -> 0xcfcfcf
|
||||||
|
TerminalColor.Bright.BLACK -> 0x555555
|
||||||
|
TerminalColor.Bright.RED -> 0xff757a
|
||||||
|
TerminalColor.Bright.GREEN -> 0x69fb79
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xfff285
|
||||||
|
TerminalColor.Bright.BLUE -> 0x77adff
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xff92cc
|
||||||
|
TerminalColor.Bright.CYAN -> 0x6bffff
|
||||||
|
TerminalColor.Bright.WHITE -> 0xffffff
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0xf0cc09
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class OctocatDarkLaf : FlatPropertiesLaf("Octocat Dark", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "dark",
|
||||||
|
"@background" to "#101216",
|
||||||
|
"@windowText" to "#8b949e",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
|
TerminalColor.Normal.RED -> 0xf78166
|
||||||
|
TerminalColor.Normal.GREEN -> 0x56d364
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xe3b341
|
||||||
|
TerminalColor.Normal.BLUE -> 0x6ca4f8
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xdb61a2
|
||||||
|
TerminalColor.Normal.CYAN -> 0x2b7489
|
||||||
|
TerminalColor.Normal.WHITE -> 0xDADADA
|
||||||
|
TerminalColor.Bright.BLACK -> 0x4d4d4d
|
||||||
|
TerminalColor.Bright.RED -> 0xffb5a5
|
||||||
|
TerminalColor.Bright.GREEN -> 0x69fb79
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xffcf5f
|
||||||
|
TerminalColor.Bright.BLUE -> 0xb0d0ff
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xff92cc
|
||||||
|
TerminalColor.Bright.CYAN -> 0x54d8ff
|
||||||
|
TerminalColor.Bright.WHITE -> 0xffffff
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0xc9d1d9
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class OctocatLightLaf : FlatPropertiesLaf("Octocat Light", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "light",
|
||||||
|
"@background" to "#f4f4f4",
|
||||||
|
"@windowText" to "#3e3e3e",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
|
TerminalColor.Normal.RED -> 0xff0000
|
||||||
|
TerminalColor.Normal.GREEN -> 0x38de21
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xffe50a
|
||||||
|
TerminalColor.Normal.BLUE -> 0x1460d2
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xff4387
|
||||||
|
TerminalColor.Normal.CYAN -> 0x00bbbb
|
||||||
|
TerminalColor.Normal.WHITE -> 0xcfcfcf
|
||||||
|
TerminalColor.Bright.BLACK -> 0x555555
|
||||||
|
TerminalColor.Bright.RED -> 0xff757a
|
||||||
|
TerminalColor.Bright.GREEN -> 0x69fb79
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xfff285
|
||||||
|
TerminalColor.Bright.BLUE -> 0x77adff
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xff92cc
|
||||||
|
TerminalColor.Bright.CYAN -> 0x6bffff
|
||||||
|
TerminalColor.Bright.WHITE -> 0xffffff
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0x3f3f3f
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AyuDarkLaf : FlatPropertiesLaf("Ayu Dark", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "dark",
|
||||||
|
"@background" to "#0f1419",
|
||||||
|
"@windowText" to "#e6e1cf",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
|
TerminalColor.Normal.RED -> 0xff3333
|
||||||
|
TerminalColor.Normal.GREEN -> 0xb8cc52
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xdbb012
|
||||||
|
TerminalColor.Normal.BLUE -> 0x36a3d9
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xdf7a80
|
||||||
|
TerminalColor.Normal.CYAN -> 0x6ceedf
|
||||||
|
TerminalColor.Normal.WHITE -> 0xababab
|
||||||
|
TerminalColor.Bright.BLACK -> 0x323232
|
||||||
|
TerminalColor.Bright.RED -> 0xff8181
|
||||||
|
TerminalColor.Bright.GREEN -> 0xeafe84
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xffe174
|
||||||
|
TerminalColor.Bright.BLUE -> 0x68d5ff
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xffa3aa
|
||||||
|
TerminalColor.Bright.CYAN -> 0x94fff1
|
||||||
|
TerminalColor.Bright.WHITE -> 0xffffff
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0xf29718
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AyuLightLaf : FlatPropertiesLaf("Ayu Light", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "light",
|
||||||
|
"@background" to "#fafafa",
|
||||||
|
"@windowText" to "#5c6773",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
|
TerminalColor.Normal.RED -> 0xff3333
|
||||||
|
TerminalColor.Normal.GREEN -> 0x319900
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xf29718
|
||||||
|
TerminalColor.Normal.BLUE -> 0x41a6d9
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xe07ead
|
||||||
|
TerminalColor.Normal.CYAN -> 0x1dd1b0
|
||||||
|
TerminalColor.Normal.WHITE -> 0xdfdddd
|
||||||
|
TerminalColor.Bright.BLACK -> 0x323232
|
||||||
|
TerminalColor.Bright.RED -> 0xff5959
|
||||||
|
TerminalColor.Bright.GREEN -> 0xb8e532
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xffc94a
|
||||||
|
TerminalColor.Bright.BLUE -> 0x73d8ff
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xffa3aa
|
||||||
|
TerminalColor.Bright.CYAN -> 0x7ff1cb
|
||||||
|
TerminalColor.Bright.WHITE -> 0xffffff
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0xff6a00
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class HomebrewLaf : FlatPropertiesLaf("Homebrew", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "dark",
|
||||||
|
"@background" to "#000000",
|
||||||
|
"@windowText" to "#00ff00",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x2e2e2e
|
||||||
|
TerminalColor.Normal.RED -> 0xc93434
|
||||||
|
TerminalColor.Normal.GREEN -> 0x348e48
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xe09e00
|
||||||
|
TerminalColor.Normal.BLUE -> 0x0031e0
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xe235ff
|
||||||
|
TerminalColor.Normal.CYAN -> 0x3fc1dd
|
||||||
|
TerminalColor.Normal.WHITE -> 0xd0cfcf
|
||||||
|
TerminalColor.Bright.BLACK -> 0x5b5b5b
|
||||||
|
TerminalColor.Bright.RED -> 0xff6767
|
||||||
|
TerminalColor.Bright.GREEN -> 0x31ff31
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xffdca8
|
||||||
|
TerminalColor.Bright.BLUE -> 0x4465da
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xff5fc8
|
||||||
|
TerminalColor.Bright.CYAN -> 0x8debff
|
||||||
|
TerminalColor.Bright.WHITE -> 0xe6e6e6
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0x23ff18
|
||||||
|
|
||||||
|
TerminalColor.Basic.FOREGROUND -> 0x00ff00
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ProLaf : FlatPropertiesLaf("Pro", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "dark",
|
||||||
|
"@background" to "#000000",
|
||||||
|
"@windowText" to "#f2f2f2",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x2e2e2e
|
||||||
|
TerminalColor.Normal.RED -> 0xc93434
|
||||||
|
TerminalColor.Normal.GREEN -> 0x348e48
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xe09e00
|
||||||
|
TerminalColor.Normal.BLUE -> 0x002bc7
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xe235ff
|
||||||
|
TerminalColor.Normal.CYAN -> 0x3fc1dd
|
||||||
|
TerminalColor.Normal.WHITE -> 0xd0cfcf
|
||||||
|
TerminalColor.Bright.BLACK -> 0x5b5b5b
|
||||||
|
TerminalColor.Bright.RED -> 0xff6767
|
||||||
|
TerminalColor.Bright.GREEN -> 0x31ff31
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xffdca8
|
||||||
|
TerminalColor.Bright.BLUE -> 0x4465da
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xff5fc8
|
||||||
|
TerminalColor.Bright.CYAN -> 0x8debff
|
||||||
|
TerminalColor.Bright.WHITE -> 0xe6e6e6
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0x4d4d4d
|
||||||
|
|
||||||
|
TerminalColor.Basic.FOREGROUND -> 0xf2f2f2
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class NordLightLaf : FlatPropertiesLaf("Nord Light", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "light",
|
||||||
|
"@background" to "#e5e9f0",
|
||||||
|
"@windowText" to "#414858",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x2c3344
|
||||||
|
TerminalColor.Normal.RED -> 0xae545d
|
||||||
|
TerminalColor.Normal.GREEN -> 0x8ca377
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xdabe84
|
||||||
|
TerminalColor.Normal.BLUE -> 0x718fae
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0x95728e
|
||||||
|
TerminalColor.Normal.CYAN -> 0x78acbb
|
||||||
|
TerminalColor.Normal.WHITE -> 0xd8dee9
|
||||||
|
TerminalColor.Bright.BLACK -> 0x4c556a
|
||||||
|
TerminalColor.Bright.RED -> 0xd97982
|
||||||
|
TerminalColor.Bright.GREEN -> 0xa3be8b
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xeacb8a
|
||||||
|
TerminalColor.Bright.BLUE -> 0xa4c7e9
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xb48dac
|
||||||
|
TerminalColor.Bright.CYAN -> 0x8fbcbb
|
||||||
|
TerminalColor.Bright.WHITE -> 0xeceff4
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0x88c0d0
|
||||||
|
|
||||||
|
TerminalColor.Basic.FOREGROUND -> 0x414858
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class NordDarkLaf : FlatPropertiesLaf("Nord Dark", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "dark",
|
||||||
|
"@background" to "#2e3440",
|
||||||
|
"@windowText" to "#d8dee9",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x3b4252
|
||||||
|
TerminalColor.Normal.RED -> 0xae545d
|
||||||
|
TerminalColor.Normal.GREEN -> 0x8ca377
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xdabe84
|
||||||
|
TerminalColor.Normal.BLUE -> 0x718fae
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0x95728e
|
||||||
|
TerminalColor.Normal.CYAN -> 0x78acbb
|
||||||
|
TerminalColor.Normal.WHITE -> 0xd8dee9
|
||||||
|
TerminalColor.Bright.BLACK -> 0x4c556a
|
||||||
|
TerminalColor.Bright.RED -> 0xd97982
|
||||||
|
TerminalColor.Bright.GREEN -> 0xa3be8b
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xeacb8a
|
||||||
|
TerminalColor.Bright.BLUE -> 0xa4c7e9
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xb48dac
|
||||||
|
TerminalColor.Bright.CYAN -> 0x8fbcbb
|
||||||
|
TerminalColor.Bright.WHITE -> 0xeceff4
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0xeceff4
|
||||||
|
|
||||||
|
TerminalColor.Basic.FOREGROUND -> 0xd8dee9
|
||||||
|
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class GitHubLightLaf : FlatPropertiesLaf("GitHub Light", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "light",
|
||||||
|
"@background" to "#f4f4f4",
|
||||||
|
"@windowText" to "#3e3e3e",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x3e3e3e
|
||||||
|
TerminalColor.Normal.RED -> 0x970b16
|
||||||
|
TerminalColor.Normal.GREEN -> 0x07962a
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xf8eec7
|
||||||
|
TerminalColor.Normal.BLUE -> 0x003e8a
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xe94691
|
||||||
|
TerminalColor.Normal.CYAN -> 0x89d1ec
|
||||||
|
TerminalColor.Normal.WHITE -> 0x3e3e3e
|
||||||
|
TerminalColor.Bright.BLACK -> 0x666666
|
||||||
|
TerminalColor.Bright.RED -> 0xde0000
|
||||||
|
TerminalColor.Bright.GREEN -> 0x87d5a2
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xf1d007
|
||||||
|
TerminalColor.Bright.BLUE -> 0x2e6cba
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xffa29f
|
||||||
|
TerminalColor.Bright.CYAN -> 0x1cfafe
|
||||||
|
TerminalColor.Bright.WHITE -> 0xffffff
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0x3f3f3f
|
||||||
|
|
||||||
|
TerminalColor.Basic.FOREGROUND -> 0x3e3e3e
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class GitHubDarkLaf : FlatPropertiesLaf("GitHub Dark", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "dark",
|
||||||
|
"@background" to "#101216",
|
||||||
|
"@windowText" to "#8b949e",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
|
TerminalColor.Normal.RED -> 0xf78166
|
||||||
|
TerminalColor.Normal.GREEN -> 0x56d364
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xe3b341
|
||||||
|
TerminalColor.Normal.BLUE -> 0x6ca4f8
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xdb61a2
|
||||||
|
TerminalColor.Normal.CYAN -> 0x2b7489
|
||||||
|
TerminalColor.Normal.WHITE -> 0x8b949e
|
||||||
|
TerminalColor.Bright.BLACK -> 0x4d4d4d
|
||||||
|
TerminalColor.Bright.RED -> 0xf78166
|
||||||
|
TerminalColor.Bright.GREEN -> 0x56d364
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xe3b341
|
||||||
|
TerminalColor.Bright.BLUE -> 0x6ca4f8
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xdb61a2
|
||||||
|
TerminalColor.Bright.CYAN -> 0x2b7489
|
||||||
|
TerminalColor.Bright.WHITE -> 0xffffff
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0xc9d1d9
|
||||||
|
|
||||||
|
TerminalColor.Basic.FOREGROUND -> 0x8b949e
|
||||||
|
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ChalkLaf : FlatPropertiesLaf("Chalk", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "dark",
|
||||||
|
"@background" to "#2b2d2e",
|
||||||
|
"@windowText" to "#d2d8d9",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Normal.BLACK -> 0x7d8b8f
|
||||||
|
TerminalColor.Normal.RED -> 0xb23a52
|
||||||
|
TerminalColor.Normal.GREEN -> 0x789b6a
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xb9ac4a
|
||||||
|
TerminalColor.Normal.BLUE -> 0x2a7fac
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xbd4f5a
|
||||||
|
TerminalColor.Normal.CYAN -> 0x44a799
|
||||||
|
TerminalColor.Normal.WHITE -> 0xd2d8d9
|
||||||
|
TerminalColor.Bright.BLACK -> 0x888888
|
||||||
|
TerminalColor.Bright.RED -> 0xf24840
|
||||||
|
TerminalColor.Bright.GREEN -> 0x80c470
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xffeb62
|
||||||
|
TerminalColor.Bright.BLUE -> 0x4196ff
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xfc5275
|
||||||
|
TerminalColor.Bright.CYAN -> 0x53cdbd
|
||||||
|
TerminalColor.Bright.WHITE -> 0xd2d8d9
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0x708284
|
||||||
|
|
||||||
|
TerminalColor.Basic.FOREGROUND -> 0xd2d8d9
|
||||||
|
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main/kotlin/app/termora/LocalTerminalTab.kt
Normal file
20
src/main/kotlin/app/termora/LocalTerminalTab.kt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.terminal.PtyConnector
|
||||||
|
import org.apache.commons.io.Charsets
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
|
class LocalTerminalTab(host: Host) : PtyHostTerminalTab(host) {
|
||||||
|
|
||||||
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
|
val winSize = terminalPanel.winSize()
|
||||||
|
val ptyConnector = PtyConnectorFactory.instance.createPtyConnector(
|
||||||
|
winSize.rows, winSize.cols,
|
||||||
|
host.options.envs(),
|
||||||
|
Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8),
|
||||||
|
)
|
||||||
|
|
||||||
|
return ptyConnector
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
109
src/main/kotlin/app/termora/LogicCustomTitleBar.kt
Normal file
109
src/main/kotlin/app/termora/LogicCustomTitleBar.kt
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.jetbrains.JBR
|
||||||
|
import com.jetbrains.WindowDecorations.CustomTitleBar
|
||||||
|
import java.awt.Rectangle
|
||||||
|
import java.awt.Window
|
||||||
|
import javax.swing.RootPaneContainer
|
||||||
|
|
||||||
|
class LogicCustomTitleBar(private val titleBar: CustomTitleBar) : CustomTitleBar {
|
||||||
|
companion object {
|
||||||
|
fun createCustomTitleBar(rootPaneContainer: RootPaneContainer): CustomTitleBar {
|
||||||
|
if (!JBR.isWindowDecorationsSupported()) {
|
||||||
|
return LogicCustomTitleBar(object : CustomTitleBar {
|
||||||
|
override fun getHeight(): Float {
|
||||||
|
val bounds = rootPaneContainer.rootPane
|
||||||
|
.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
||||||
|
if (bounds is Rectangle) {
|
||||||
|
return bounds.height.toFloat()
|
||||||
|
}
|
||||||
|
return 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setHeight(height: Float) {
|
||||||
|
rootPaneContainer.rootPane.putClientProperty(
|
||||||
|
FlatClientProperties.TITLE_BAR_HEIGHT,
|
||||||
|
height.toInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProperties(): MutableMap<String, Any> {
|
||||||
|
return mutableMapOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putProperties(m: MutableMap<String, *>?) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putProperty(key: String?, value: Any?) {
|
||||||
|
if (key == "controls.visible" && value is Boolean) {
|
||||||
|
rootPaneContainer.rootPane.putClientProperty(
|
||||||
|
FlatClientProperties.TITLE_BAR_SHOW_CLOSE,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLeftInset(): Float {
|
||||||
|
return 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRightInset(): Float {
|
||||||
|
val bounds = rootPaneContainer.rootPane
|
||||||
|
.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
||||||
|
if (bounds is Rectangle) {
|
||||||
|
return bounds.width.toFloat()
|
||||||
|
}
|
||||||
|
return 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun forceHitTest(client: Boolean) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getContainingWindow(): Window {
|
||||||
|
return rootPaneContainer as Window
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return JBR.getWindowDecorations().createCustomTitleBar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHeight(): Float {
|
||||||
|
return titleBar.height
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setHeight(height: Float) {
|
||||||
|
titleBar.height = height
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProperties(): MutableMap<String, Any> {
|
||||||
|
return titleBar.properties
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putProperties(m: MutableMap<String, *>?) {
|
||||||
|
titleBar.putProperties(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putProperty(key: String?, value: Any?) {
|
||||||
|
titleBar.putProperty(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLeftInset(): Float {
|
||||||
|
return titleBar.leftInset
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRightInset(): Float {
|
||||||
|
return titleBar.rightInset
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun forceHitTest(client: Boolean) {
|
||||||
|
titleBar.forceHitTest(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getContainingWindow(): Window {
|
||||||
|
return titleBar.containingWindow
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/main/kotlin/app/termora/Main.kt
Normal file
6
src/main/kotlin/app/termora/Main.kt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
ApplicationRunner().run()
|
||||||
|
}
|
||||||
|
|
||||||
45
src/main/kotlin/app/termora/MultiplePtyConnector.kt
Normal file
45
src/main/kotlin/app/termora/MultiplePtyConnector.kt
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.terminal.PtyConnector
|
||||||
|
import app.termora.terminal.PtyConnectorDelegate
|
||||||
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当开启转发时,会获取到所有的 [PtyConnector] 然后跳过中间层,直接找到最近的一个 [MultiplePtyConnector],如果找不到那就以最后一个匹配不到的为准 [getMultiplePtyConnector]。
|
||||||
|
*/
|
||||||
|
class MultiplePtyConnector(private val myConnector: PtyConnector) : PtyConnectorDelegate(myConnector) {
|
||||||
|
|
||||||
|
private val isMultiple get() = ActionManager.getInstance().isSelected(Actions.MULTIPLE)
|
||||||
|
private val ptyConnectors get() = PtyConnectorFactory.instance.getPtyConnectors()
|
||||||
|
|
||||||
|
override fun write(buffer: ByteArray, offset: Int, len: Int) {
|
||||||
|
if (isMultiple) {
|
||||||
|
for (connector in ptyConnectors) {
|
||||||
|
getMultiplePtyConnector(connector).write(buffer, offset, len)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
myConnector.write(buffer, offset, len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun getMultiplePtyConnector(connector: PtyConnector): PtyConnector {
|
||||||
|
if (connector is MultiplePtyConnector) {
|
||||||
|
val c = connector.myConnector
|
||||||
|
if (c is MultiplePtyConnector) {
|
||||||
|
return getMultiplePtyConnector(c)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connector is PtyConnectorDelegate) {
|
||||||
|
val c = connector.ptyConnector
|
||||||
|
if (c != null) {
|
||||||
|
return getMultiplePtyConnector(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return connector
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
44
src/main/kotlin/app/termora/MultipleTerminalListener.kt
Normal file
44
src/main/kotlin/app/termora/MultipleTerminalListener.kt
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.terminal.Terminal
|
||||||
|
import app.termora.terminal.TerminalColor
|
||||||
|
import app.termora.terminal.TextStyle
|
||||||
|
import app.termora.terminal.panel.TerminalDisplay
|
||||||
|
import app.termora.terminal.panel.TerminalPaintListener
|
||||||
|
import app.termora.terminal.panel.TerminalPanel
|
||||||
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
|
import java.awt.Color
|
||||||
|
import java.awt.Graphics
|
||||||
|
|
||||||
|
class MultipleTerminalListener : TerminalPaintListener {
|
||||||
|
override fun after(
|
||||||
|
offset: Int,
|
||||||
|
count: Int,
|
||||||
|
g: Graphics,
|
||||||
|
terminalPanel: TerminalPanel,
|
||||||
|
terminalDisplay: TerminalDisplay,
|
||||||
|
terminal: Terminal
|
||||||
|
) {
|
||||||
|
if (!ActionManager.getInstance().isSelected(Actions.MULTIPLE)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val oldFont = g.font
|
||||||
|
val colorPalette = terminal.getTerminalModel().getColorPalette()
|
||||||
|
val text = I18n.getString("termora.tools.multiple")
|
||||||
|
val font = terminalDisplay.getDisplayFont(text, TextStyle.Default)
|
||||||
|
val width = g.getFontMetrics(font).stringWidth(text)
|
||||||
|
// 正在搜索那么需要下移
|
||||||
|
val finding = terminal.getTerminalModel().getData(TerminalPanel.Finding, false)
|
||||||
|
|
||||||
|
g.font = font
|
||||||
|
g.color = Color(colorPalette.getColor(TerminalColor.Normal.RED))
|
||||||
|
g.drawString(
|
||||||
|
text,
|
||||||
|
terminalDisplay.width - width - terminalPanel.getAverageCharWidth() / 2,
|
||||||
|
g.fontMetrics.ascent + if (finding)
|
||||||
|
g.fontMetrics.height + g.fontMetrics.ascent / 2 else 0
|
||||||
|
)
|
||||||
|
g.font = oldFont
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/main/kotlin/app/termora/MyTabbedPane.kt
Normal file
11
src/main/kotlin/app/termora/MyTabbedPane.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
|
|
||||||
|
class MyTabbedPane : FlatTabbedPane() {
|
||||||
|
override fun setSelectedIndex(index: Int) {
|
||||||
|
val oldIndex = selectedIndex
|
||||||
|
super.setSelectedIndex(index)
|
||||||
|
firePropertyChange("selectedIndex", oldIndex,index)
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/main/kotlin/app/termora/OpenHostActionEvent.kt
Normal file
5
src/main/kotlin/app/termora/OpenHostActionEvent.kt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import java.awt.event.ActionEvent
|
||||||
|
|
||||||
|
class OpenHostActionEvent(source: Any, val host: Host) : ActionEvent(source, ACTION_PERFORMED, String())
|
||||||
165
src/main/kotlin/app/termora/OptionPane.kt
Normal file
165
src/main/kotlin/app/termora/OptionPane.kt
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatTextPane
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import com.jetbrains.JBR
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import org.jdesktop.swingx.JXLabel
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.Desktop
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.event.WindowAdapter
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
|
import java.io.File
|
||||||
|
import javax.swing.*
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
object OptionPane {
|
||||||
|
fun showConfirmDialog(
|
||||||
|
parentComponent: Component?,
|
||||||
|
message: Any,
|
||||||
|
title: String = UIManager.getString("OptionPane.messageDialogTitle"),
|
||||||
|
optionType: Int = JOptionPane.YES_NO_OPTION,
|
||||||
|
messageType: Int = JOptionPane.QUESTION_MESSAGE,
|
||||||
|
icon: Icon? = null,
|
||||||
|
options: Array<Any>? = null,
|
||||||
|
initialValue: Any? = null,
|
||||||
|
): Int {
|
||||||
|
|
||||||
|
val panel = if (message is JComponent) {
|
||||||
|
message
|
||||||
|
} else {
|
||||||
|
val label = FlatTextPane()
|
||||||
|
label.contentType = "text/html"
|
||||||
|
label.text = "<html>$message</html>"
|
||||||
|
label.isEditable = false
|
||||||
|
label.background = null
|
||||||
|
label.border = BorderFactory.createEmptyBorder()
|
||||||
|
label
|
||||||
|
}
|
||||||
|
|
||||||
|
val pane = object : JOptionPane(panel, messageType, optionType, icon, options, initialValue) {
|
||||||
|
override fun selectInitialValue() {
|
||||||
|
super.selectInitialValue()
|
||||||
|
if (message is JComponent) {
|
||||||
|
message.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val dialog = initDialog(pane.createDialog(parentComponent, title))
|
||||||
|
dialog.addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowOpened(e: WindowEvent) {
|
||||||
|
pane.selectInitialValue()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
dialog.isVisible = true
|
||||||
|
dialog.dispose()
|
||||||
|
val selectedValue = pane.value
|
||||||
|
|
||||||
|
|
||||||
|
if (selectedValue == null) {
|
||||||
|
return -1
|
||||||
|
} else if (pane.options == null) {
|
||||||
|
return if (selectedValue is Int) selectedValue else -1
|
||||||
|
} else {
|
||||||
|
var counter = 0
|
||||||
|
|
||||||
|
val maxCounter: Int = pane.options.size
|
||||||
|
while (counter < maxCounter) {
|
||||||
|
if (pane.options[counter] == selectedValue) {
|
||||||
|
return counter
|
||||||
|
}
|
||||||
|
++counter
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showMessageDialog(
|
||||||
|
parentComponent: Component?,
|
||||||
|
message: String,
|
||||||
|
title: String = UIManager.getString("OptionPane.messageDialogTitle"),
|
||||||
|
messageType: Int = JOptionPane.INFORMATION_MESSAGE,
|
||||||
|
duration: Duration = 0.milliseconds,
|
||||||
|
) {
|
||||||
|
val label = JTextPane()
|
||||||
|
label.contentType = "text/html"
|
||||||
|
label.text = "<html>$message</html>"
|
||||||
|
label.isEditable = false
|
||||||
|
label.background = null
|
||||||
|
label.border = BorderFactory.createEmptyBorder()
|
||||||
|
val pane = JOptionPane(label, messageType, JOptionPane.DEFAULT_OPTION)
|
||||||
|
val dialog = initDialog(pane.createDialog(parentComponent, title))
|
||||||
|
if (duration.inWholeMilliseconds > 0) {
|
||||||
|
dialog.addWindowListener(object : WindowAdapter() {
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
override fun windowOpened(e: WindowEvent) {
|
||||||
|
GlobalScope.launch(Dispatchers.Swing) {
|
||||||
|
delay(duration.inWholeMilliseconds)
|
||||||
|
if (dialog.isVisible) {
|
||||||
|
dialog.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pane.selectInitialValue()
|
||||||
|
dialog.isVisible = true
|
||||||
|
dialog.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openFileInFolder(
|
||||||
|
parentComponent: Component,
|
||||||
|
file: File,
|
||||||
|
yMessage: String,
|
||||||
|
nMessage: String? = null,
|
||||||
|
) {
|
||||||
|
if (Desktop.isDesktopSupported() && Desktop.getDesktop()
|
||||||
|
.isSupported(Desktop.Action.BROWSE_FILE_DIR)
|
||||||
|
) {
|
||||||
|
if (JOptionPane.YES_OPTION == showConfirmDialog(
|
||||||
|
parentComponent,
|
||||||
|
yMessage,
|
||||||
|
optionType = JOptionPane.YES_NO_OPTION
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Desktop.getDesktop().browseFileDirectory(file)
|
||||||
|
}
|
||||||
|
} else if (nMessage != null) {
|
||||||
|
showMessageDialog(
|
||||||
|
parentComponent,
|
||||||
|
nMessage,
|
||||||
|
messageType = JOptionPane.INFORMATION_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initDialog(dialog: JDialog): JDialog {
|
||||||
|
|
||||||
|
if (JBR.isWindowDecorationsSupported()) {
|
||||||
|
|
||||||
|
val windowDecorations = JBR.getWindowDecorations()
|
||||||
|
val titleBar = windowDecorations.createCustomTitleBar()
|
||||||
|
titleBar.putProperty("controls.visible", false)
|
||||||
|
titleBar.height = UIManager.getInt("TabbedPane.tabHeight") - if (SystemInfo.isMacOS) 10f else 6f
|
||||||
|
windowDecorations.setCustomTitleBar(dialog, titleBar)
|
||||||
|
|
||||||
|
val label = JLabel(dialog.title)
|
||||||
|
label.putClientProperty(FlatClientProperties.STYLE, "font: bold")
|
||||||
|
val box = Box.createHorizontalBox()
|
||||||
|
box.add(Box.createHorizontalGlue())
|
||||||
|
box.add(label)
|
||||||
|
box.add(Box.createHorizontalGlue())
|
||||||
|
box.preferredSize = Dimension(-1, titleBar.height.toInt())
|
||||||
|
|
||||||
|
dialog.contentPane.add(box, BorderLayout.NORTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
}
|
||||||
136
src/main/kotlin/app/termora/OptionsPane.kt
Normal file
136
src/main/kotlin/app/termora/OptionsPane.kt
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.FlatLaf
|
||||||
|
import java.awt.*
|
||||||
|
import javax.swing.*
|
||||||
|
import javax.swing.border.Border
|
||||||
|
|
||||||
|
|
||||||
|
open class OptionsPane : JPanel(BorderLayout()) {
|
||||||
|
protected val formMargin = "7dlu"
|
||||||
|
|
||||||
|
protected val tabListModel = DefaultListModel<Option>()
|
||||||
|
protected val tabList = object : JList<Option>(tabListModel) {
|
||||||
|
override fun getBackground(): Color {
|
||||||
|
return this@OptionsPane.background
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val cardLayout = CardLayout()
|
||||||
|
private val contentPanel = JPanel(cardLayout)
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
|
||||||
|
tabList.fixedCellHeight = (UIManager.getInt("Tree.rowHeight") * 1.2).toInt()
|
||||||
|
tabList.fixedCellWidth = 170
|
||||||
|
tabList.selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||||
|
tabList.border = BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor),
|
||||||
|
BorderFactory.createEmptyBorder(6, 6, 0, 6)
|
||||||
|
)
|
||||||
|
tabList.cellRenderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
val option = value as Option
|
||||||
|
val c = super.getListCellRendererComponent(list, option.getTitle(), index, isSelected, cellHasFocus)
|
||||||
|
|
||||||
|
icon = option.getIcon(isSelected)
|
||||||
|
if (isSelected && tabList.hasFocus()) {
|
||||||
|
if (!FlatLaf.isLafDark()) {
|
||||||
|
if (icon is DynamicIcon) {
|
||||||
|
icon = (icon as DynamicIcon).dark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
add(tabList, BorderLayout.WEST)
|
||||||
|
add(contentPanel, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectOption(option: Option) {
|
||||||
|
val index = tabListModel.indexOf(option)
|
||||||
|
if (index < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSelectedIndex(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSelectedOption(): Option? {
|
||||||
|
val index = tabList.selectedIndex
|
||||||
|
if (index < 0) return null
|
||||||
|
return tabListModel.getElementAt(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSelectedIndex(): Int {
|
||||||
|
return tabList.selectedIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSelectedIndex(index: Int) {
|
||||||
|
tabList.selectedIndex = index
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectOptionJComponent(c: JComponent) {
|
||||||
|
for (element in tabListModel.elements()) {
|
||||||
|
var p = c as Container?
|
||||||
|
while (p != null) {
|
||||||
|
if (p == element) {
|
||||||
|
selectOption(element)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p = p.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun addOption(option: Option) {
|
||||||
|
for (element in tabListModel.elements()) {
|
||||||
|
if (element.getTitle() == option.getTitle()) {
|
||||||
|
throw UnsupportedOperationException("Title already exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentPanel.add(option.getJComponent(), option.getTitle())
|
||||||
|
tabListModel.addElement(option)
|
||||||
|
|
||||||
|
if (tabList.selectedIndex < 0) {
|
||||||
|
tabList.selectedIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeOption(option: Option) {
|
||||||
|
contentPanel.remove(option.getJComponent())
|
||||||
|
tabListModel.removeElement(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setContentBorder(border: Border) {
|
||||||
|
contentPanel.border = border
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
tabList.addListSelectionListener {
|
||||||
|
if (tabList.selectedIndex >= 0) {
|
||||||
|
cardLayout.show(contentPanel, tabListModel.get(tabList.selectedIndex).getTitle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
fun getIcon(isSelected: Boolean): Icon
|
||||||
|
fun getTitle(): String
|
||||||
|
fun getJComponent(): JComponent
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/main/kotlin/app/termora/PropertyTerminalTab.kt
Normal file
32
src/main/kotlin/app/termora/PropertyTerminalTab.kt
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import java.beans.PropertyChangeEvent
|
||||||
|
import java.beans.PropertyChangeListener
|
||||||
|
|
||||||
|
abstract class PropertyTerminalTab : TerminalTab {
|
||||||
|
protected val listeners = mutableListOf<PropertyChangeListener>()
|
||||||
|
var hasFocus = false
|
||||||
|
protected set
|
||||||
|
|
||||||
|
override fun addPropertyChangeListener(listener: PropertyChangeListener) {
|
||||||
|
listeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removePropertyChangeListener(listener: PropertyChangeListener) {
|
||||||
|
listeners.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun firePropertyChange(event: PropertyChangeEvent) {
|
||||||
|
listeners.forEach { l -> l.propertyChange(event) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGrabFocus() {
|
||||||
|
hasFocus = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLostFocus() {
|
||||||
|
hasFocus = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
70
src/main/kotlin/app/termora/PtyConnectorFactory.kt
Normal file
70
src/main/kotlin/app/termora/PtyConnectorFactory.kt
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.db.Database
|
||||||
|
import app.termora.macro.MacroPtyConnector
|
||||||
|
import app.termora.terminal.PtyConnector
|
||||||
|
import app.termora.terminal.PtyConnectorDelegate
|
||||||
|
import app.termora.terminal.PtyProcessConnector
|
||||||
|
import com.pty4j.PtyProcessBuilder
|
||||||
|
import org.apache.commons.lang3.SystemUtils
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class PtyConnectorFactory {
|
||||||
|
private val ptyConnectors = Collections.synchronizedList(mutableListOf<PtyConnector>())
|
||||||
|
private val database get() = Database.instance
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { PtyConnectorFactory() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createPtyConnector(
|
||||||
|
rows: Int = 24, cols: Int = 80,
|
||||||
|
env: Map<String, String> = emptyMap(),
|
||||||
|
charset: Charset = StandardCharsets.UTF_8
|
||||||
|
): PtyConnector {
|
||||||
|
val envs = mutableMapOf<String, String>()
|
||||||
|
envs.putAll(System.getenv())
|
||||||
|
envs["TERM"] = "xterm-256color"
|
||||||
|
envs.putAll(env)
|
||||||
|
|
||||||
|
val command = database.terminal.localShell
|
||||||
|
val ptyProcess = PtyProcessBuilder(arrayOf(command))
|
||||||
|
.setEnvironment(envs)
|
||||||
|
.setInitialRows(rows)
|
||||||
|
.setInitialColumns(cols)
|
||||||
|
.setConsole(false)
|
||||||
|
.setDirectory(SystemUtils.USER_HOME)
|
||||||
|
.setCygwin(false)
|
||||||
|
.setUseWinConPty(SystemUtils.IS_OS_WINDOWS)
|
||||||
|
.setRedirectErrorStream(false)
|
||||||
|
.setWindowsAnsiColorEnabled(false)
|
||||||
|
.setUnixOpenTtyToPreserveOutputAfterTermination(false)
|
||||||
|
.setSpawnProcessUsingJdkOnMacIntel(true).start()
|
||||||
|
|
||||||
|
return decorate(PtyProcessConnector(ptyProcess, charset))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decorate(ptyConnector: PtyConnector): PtyConnector {
|
||||||
|
// 集成转发,如果PtyConnector支持转发那么应该在当前注释行前面代理
|
||||||
|
val multiplePtyConnector = MultiplePtyConnector(ptyConnector)
|
||||||
|
// 宏应该在转发前面执行,不然会导致重复录制
|
||||||
|
val macroPtyConnector = MacroPtyConnector(multiplePtyConnector)
|
||||||
|
// 集成自动删除
|
||||||
|
val autoRemovePtyConnector = AutoRemovePtyConnector(macroPtyConnector)
|
||||||
|
ptyConnectors.add(autoRemovePtyConnector)
|
||||||
|
return autoRemovePtyConnector
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPtyConnectors(): List<PtyConnector> {
|
||||||
|
return ptyConnectors
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class AutoRemovePtyConnector(connector: PtyConnector) : PtyConnectorDelegate(connector) {
|
||||||
|
override fun close() {
|
||||||
|
ptyConnectors.remove(this)
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/main/kotlin/app/termora/PtyConnectorReader.kt
Normal file
27
src/main/kotlin/app/termora/PtyConnectorReader.kt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.terminal.PtyConnector
|
||||||
|
import app.termora.terminal.Terminal
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
class PtyConnectorReader(
|
||||||
|
private val ptyConnector: PtyConnector,
|
||||||
|
private val terminal: Terminal,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun start() {
|
||||||
|
var i: Int
|
||||||
|
val buffer = CharArray(1024 * 8)
|
||||||
|
while ((ptyConnector.read(buffer).also { i = it }) != -1) {
|
||||||
|
if (i == 0) {
|
||||||
|
delay(10.milliseconds)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val text = String(buffer, 0, i)
|
||||||
|
SwingUtilities.invokeLater { terminal.write(text) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
121
src/main/kotlin/app/termora/PtyHostTerminalTab.kt
Normal file
121
src/main/kotlin/app/termora/PtyHostTerminalTab.kt
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.terminal.ControlCharacters
|
||||||
|
import app.termora.terminal.PtyConnector
|
||||||
|
import app.termora.terminal.PtyConnectorDelegate
|
||||||
|
import app.termora.terminal.TerminalKeyEvent
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
abstract class PtyHostTerminalTab(host: Host) : HostTerminalTab(host) {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private var readerJob: Job? = null
|
||||||
|
private val ptyConnectorDelegate = PtyConnectorDelegate()
|
||||||
|
|
||||||
|
protected val terminalPanel = TerminalPanelFactory.instance.createTerminalPanel(terminal, ptyConnectorDelegate)
|
||||||
|
protected val ptyConnectorFactory get() = PtyConnectorFactory.instance
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
// clear terminal
|
||||||
|
terminal.clearScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开启 PTY
|
||||||
|
val ptyConnector = openPtyConnector()
|
||||||
|
ptyConnectorDelegate.ptyConnector = ptyConnector
|
||||||
|
|
||||||
|
// 开启 reader
|
||||||
|
startPtyConnectorReader()
|
||||||
|
|
||||||
|
// 启动命令
|
||||||
|
if (host.options.startupCommand.isNotBlank()) {
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
delay(250.milliseconds)
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
ptyConnector.write(host.options.startupCommand)
|
||||||
|
ptyConnector.write(terminal.getKeyEncoder().encode(TerminalKeyEvent(KeyEvent.VK_ENTER)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("Host: {} started", host.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
terminal.write("\r\n${ControlCharacters.ESC}[31m")
|
||||||
|
terminal.write(ExceptionUtils.getRootCauseMessage(e))
|
||||||
|
terminal.write("${ControlCharacters.ESC}[0m")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canReconnect(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reconnect() {
|
||||||
|
stop()
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return terminalPanel
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun startPtyConnectorReader() {
|
||||||
|
readerJob?.cancel()
|
||||||
|
readerJob = coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
PtyConnectorReader(ptyConnectorDelegate, terminal).start()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun stop() {
|
||||||
|
readerJob?.cancel()
|
||||||
|
ptyConnectorDelegate.close()
|
||||||
|
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("Host: {} stopped", host.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
stop()
|
||||||
|
super.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("Host: {} disposed", host.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getPtyConnector(): PtyConnector {
|
||||||
|
return ptyConnectorDelegate
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract suspend fun openPtyConnector(): PtyConnector
|
||||||
|
}
|
||||||
15
src/main/kotlin/app/termora/ResponseException.kt
Normal file
15
src/main/kotlin/app/termora/ResponseException.kt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class ResponseException : RuntimeException {
|
||||||
|
val code: Int
|
||||||
|
val response: Response
|
||||||
|
|
||||||
|
constructor(code: Int, response: Response) : this(code, "Response code: $code", response)
|
||||||
|
constructor(code: Int, message: String, response: Response) : super(message) {
|
||||||
|
this.code = code
|
||||||
|
this.response = response
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
229
src/main/kotlin/app/termora/SSHTerminalTab.kt
Normal file
229
src/main/kotlin/app/termora/SSHTerminalTab.kt
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
||||||
|
import app.termora.terminal.ControlCharacters
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
|
import app.termora.terminal.PtyConnector
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.apache.commons.io.Charsets
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.sshd.client.SshClient
|
||||||
|
import org.apache.sshd.client.channel.ChannelShell
|
||||||
|
import org.apache.sshd.client.session.ClientSession
|
||||||
|
import org.apache.sshd.common.SshConstants
|
||||||
|
import org.apache.sshd.common.channel.Channel
|
||||||
|
import org.apache.sshd.common.channel.ChannelListener
|
||||||
|
import org.apache.sshd.common.session.Session
|
||||||
|
import org.apache.sshd.common.session.SessionListener
|
||||||
|
import org.apache.sshd.common.session.SessionListener.Event
|
||||||
|
import org.apache.sshd.common.util.net.SshdSocketAddress
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import javax.swing.JComponent
|
||||||
|
|
||||||
|
|
||||||
|
class SSHTerminalTab(host: Host) : PtyHostTerminalTab(host) {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
private var sshClient: SshClient? = null
|
||||||
|
private var sshSession: ClientSession? = null
|
||||||
|
private var sshChannelShell: ChannelShell? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
terminalPanel.dropFiles = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return terminalPanel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun canReconnect(): Boolean {
|
||||||
|
return !mutex.isLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
|
if (mutex.tryLock()) {
|
||||||
|
try {
|
||||||
|
return doOpenPtyConnector()
|
||||||
|
} finally {
|
||||||
|
mutex.unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw IllegalStateException("Opening PtyConnector")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private suspend fun doOpenPtyConnector(): PtyConnector {
|
||||||
|
|
||||||
|
// 连接提示
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
// clear screen
|
||||||
|
terminal.clearScreen()
|
||||||
|
// hide cursor
|
||||||
|
terminalModel.setData(DataKey.ShowCursor, false)
|
||||||
|
// print
|
||||||
|
terminal.write("SSH client is opening...\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
val client = SshClients.openClient(host).also { sshClient = it }
|
||||||
|
val sessionListener = MySessionListener()
|
||||||
|
val channelListener = MyChannelListener()
|
||||||
|
|
||||||
|
withContext(Dispatchers.Swing) { terminal.write("SSH client opened successfully.\r\n\r\n") }
|
||||||
|
|
||||||
|
client.addSessionListener(sessionListener)
|
||||||
|
client.addChannelListener(channelListener)
|
||||||
|
|
||||||
|
val (session, channel) = try {
|
||||||
|
val session = SshClients.openSession(host, client).also { sshSession = it }
|
||||||
|
val channel = SshClients.openShell(
|
||||||
|
host,
|
||||||
|
terminalPanel.winSize(),
|
||||||
|
session
|
||||||
|
).also { sshChannelShell = it }
|
||||||
|
Pair(session, channel)
|
||||||
|
} finally {
|
||||||
|
client.removeSessionListener(sessionListener)
|
||||||
|
client.removeChannelListener(channelListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newline
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
terminal.write("\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
channel.addChannelListener(object : ChannelListener {
|
||||||
|
override fun channelClosed(channel: Channel, reason: Throwable?) {
|
||||||
|
coroutineScope.launch(Dispatchers.Swing) {
|
||||||
|
terminal.write("\r\n${ControlCharacters.ESC}[31m")
|
||||||
|
terminal.write("Channel has been disconnected.\r\n")
|
||||||
|
terminal.write("${ControlCharacters.ESC}[0m")
|
||||||
|
terminalModel.setData(DataKey.ShowCursor, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 打开隧道
|
||||||
|
openTunnelings(session, host)
|
||||||
|
|
||||||
|
// 隐藏提示
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
// clear screen
|
||||||
|
terminal.clearScreen()
|
||||||
|
// show cursor
|
||||||
|
terminalModel.setData(DataKey.ShowCursor, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ptyConnectorFactory.decorate(
|
||||||
|
ZModemPtyConnectorAdaptor(
|
||||||
|
terminal,
|
||||||
|
terminalPanel,
|
||||||
|
ChannelShellPtyConnector(
|
||||||
|
channel,
|
||||||
|
charset = Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun openTunnelings(session: ClientSession, host: Host) {
|
||||||
|
if (host.tunnelings.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (tunneling in host.tunnelings) {
|
||||||
|
if (tunneling.type == TunnelingType.Local) {
|
||||||
|
session.startLocalPortForwarding(
|
||||||
|
SshdSocketAddress(tunneling.sourceHost, tunneling.sourcePort),
|
||||||
|
SshdSocketAddress(tunneling.destinationHost, tunneling.destinationPort)
|
||||||
|
)
|
||||||
|
} else if (tunneling.type == TunnelingType.Remote) {
|
||||||
|
session.startRemotePortForwarding(
|
||||||
|
SshdSocketAddress(tunneling.sourceHost, tunneling.sourcePort),
|
||||||
|
SshdSocketAddress(tunneling.destinationHost, tunneling.destinationPort),
|
||||||
|
)
|
||||||
|
} else if (tunneling.type == TunnelingType.Dynamic) {
|
||||||
|
session.startDynamicPortForwarding(
|
||||||
|
SshdSocketAddress(
|
||||||
|
tunneling.sourceHost,
|
||||||
|
tunneling.sourcePort
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("SSH [{}] started {} port forwarding.", host.name, tunneling.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
terminal.write("Start [${tunneling.name}] port forwarding successfully.\r\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
if (mutex.tryLock()) {
|
||||||
|
try {
|
||||||
|
super.stop()
|
||||||
|
|
||||||
|
sshChannelShell?.close(true)
|
||||||
|
sshSession?.disableSessionHeartbeat()
|
||||||
|
sshSession?.disconnect(SshConstants.SSH2_DISCONNECT_BY_APPLICATION, StringUtils.EMPTY)
|
||||||
|
sshSession?.close(true)
|
||||||
|
sshClient?.close(true)
|
||||||
|
|
||||||
|
sshChannelShell = null
|
||||||
|
sshSession = null
|
||||||
|
sshClient = null
|
||||||
|
} finally {
|
||||||
|
mutex.unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class MySessionListener : SessionListener, Disposable {
|
||||||
|
override fun sessionEvent(session: Session, event: Event) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
when (event) {
|
||||||
|
Event.KeyEstablished -> terminal.write("Session Key exchange successful.\r\n")
|
||||||
|
Event.Authenticated -> terminal.write("Session authentication successful.\r\n\r\n")
|
||||||
|
Event.KexCompleted -> terminal.write("Session KEX negotiation successful.\r\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sessionEstablished(session: Session) {
|
||||||
|
coroutineScope.launch { terminal.write("Session established.\r\n") }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sessionCreated(session: Session?) {
|
||||||
|
coroutineScope.launch { terminal.write("Session created.\r\n") }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class MyChannelListener : ChannelListener, Disposable {
|
||||||
|
override fun channelOpenSuccess(channel: Channel) {
|
||||||
|
coroutineScope.launch { terminal.write("Channel shell opened successfully.\r\n") }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun channelInitialized(channel: Channel) {
|
||||||
|
coroutineScope.launch { terminal.write("Channel shell initialization successful.\r\n") }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/main/kotlin/app/termora/SearchableHostTreeModel.kt
Normal file
66
src/main/kotlin/app/termora/SearchableHostTreeModel.kt
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
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) : 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 ->
|
||||||
|
e.name.contains(text, true) || TreeUtils.children(model, e, true).filterIsInstance<Host>().any {
|
||||||
|
it.name.contains(text, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(text: String) {
|
||||||
|
this.text = text
|
||||||
|
model.listeners.forEach {
|
||||||
|
it.treeStructureChanged(
|
||||||
|
TreeModelEvent(
|
||||||
|
this, TreePath(root),
|
||||||
|
null, null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
61
src/main/kotlin/app/termora/SettingsDialog.kt
Normal file
61
src/main/kotlin/app/termora/SettingsDialog.kt
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.db.Database
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.event.WindowAdapter
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
|
import javax.swing.BorderFactory
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.JPanel
|
||||||
|
import javax.swing.UIManager
|
||||||
|
|
||||||
|
class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
||||||
|
private val optionsPane = SettingsOptionsPane()
|
||||||
|
private val properties get() = Database.instance.properties
|
||||||
|
|
||||||
|
init {
|
||||||
|
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||||
|
isModal = true
|
||||||
|
title = I18n.getString("termora.setting")
|
||||||
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
|
init()
|
||||||
|
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
Disposer.register(disposable, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
properties.putString("Settings-SelectedOption", optionsPane.getSelectedIndex().toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowActivated(e: WindowEvent) {
|
||||||
|
removeWindowListener(this)
|
||||||
|
val index = properties.getString("Settings-SelectedOption")?.toIntOrNull() ?: return
|
||||||
|
optionsPane.setSelectedIndex(index)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
optionsPane.background = UIManager.getColor("window")
|
||||||
|
|
||||||
|
val panel = JPanel(BorderLayout())
|
||||||
|
panel.add(optionsPane, BorderLayout.CENTER)
|
||||||
|
panel.background = UIManager.getColor("window")
|
||||||
|
panel.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createSouthPanel(): JComponent? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user