Init Commit

This commit is contained in:
hstyi
2025-01-02 10:51:54 +08:00
commit 470b95cc42
418 changed files with 29687 additions and 0 deletions

45
.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1,45 @@
# Termora
**Termora** 是一个终端模拟器和 SSH 客户端,支持 WindowsmacOS 和 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
docs/readme.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

3
gradle.properties Normal file
View File

@@ -0,0 +1,3 @@
org.gradle.caching=true
org.gradle.parallel=true
kotlin.code.style=official

98
gradle/libs.versions.toml Normal file
View 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

Binary file not shown.

View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0"
}
rootProject.name = "termora"

View 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();
}
}

View 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();
}
}

View 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;
}
}

View 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;
}
}
}

View 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
)

View 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);
}
}

View 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;
}
}

View 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);
}
}

View File

@@ -0,0 +1,4 @@
/**
* https://github.com/scraymer/Zmodem-in-Java
*/
package zmodem;

View 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();
}
}

View 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
}
}

View 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();
}

View File

@@ -0,0 +1,7 @@
package zmodem.xfer.io;
import java.io.IOException;
public abstract class ObjectInputStream<T> {
public abstract T read() throws IOException;
}

View 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;
}

View 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;
}
}

View 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);
}
}

View 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();
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -0,0 +1,6 @@
package zmodem.xfer.util;
public class InvalidChecksumException extends RuntimeException {
private static final long serialVersionUID = 3864874377147160043L;
}

View File

@@ -0,0 +1,7 @@
package zmodem.xfer.util;
/**
* Created by asirotinkin on 12.11.2014.
*/
class TimeoutException extends Exception {
}

View 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);
}

View 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";
}
}

View 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";
}
}

View 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";
}
}

View 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);
}
}

View 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] + "}";
}
}

View File

@@ -0,0 +1,9 @@
package zmodem.xfer.zm.packet;
import java.io.IOException;
public class InvalidPacketException extends IOException {
private static final long serialVersionUID = 6436104259898858243L;
}

View File

@@ -0,0 +1,5 @@
package zmodem.xfer.zm.proto;
public enum Action {
ESCAPE, DATA, HEADER, CANCEL, FINISH;
}

View 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;
}
}

View 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 {
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -0,0 +1,9 @@
package zmodem.xfer.zm.util;
import zmodem.xfer.util.Buffer;
public abstract class ZMPacket {
public abstract Buffer marshall();
}

View 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());
}
}

View 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;
}
}

View 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);
}
}
}

View 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));
}
}

View 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;
}
}

View 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();
}
}

View 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"
}

View 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)
}

View 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()
}
}
}

View File

@@ -0,0 +1,10 @@
package app.termora
/**
* 将在 JVM 进程退出时释放
*/
class ApplicationDisposable : Disposable {
companion object {
val instance by lazy { ApplicationDisposable() }
}
}

View 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)
}
}
}

View 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])
}
}
}
}

View 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)
}
}

View 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()
}
}

View 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()
}
}
}

View 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) {
}
}

View 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))
}
}

View 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
}
}

View 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()
}
}

View 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) }
}

View 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,
)
}
}

View 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
}
}

View 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()
}
}

View 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)
}
}

View 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()
}
}
}
}

View 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
}
}

View 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
}
}
}

View 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 })
}
}

View 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
}
}

View 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
}
}
}

View 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") }
}

View 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
}
}

View 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
}
}
}

View 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
}
}

View 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
}
}

View File

@@ -0,0 +1,6 @@
package app.termora
fun main() {
ApplicationRunner().run()
}

View 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
}
}

View 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
}
}

View 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)
}
}

View 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())

View 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
}
}

View 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
}
}

View 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
}
}

View 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()
}
}
}

View 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) }
}
}
}

View 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
}

View 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
}
}

View 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") }
}
}
}

View 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
)
)
}
}
}

View 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