mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 18:32:58 +08:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e690bafed | ||
|
|
28b511e179 | ||
|
|
f010a13abd | ||
|
|
4d80ffafdd | ||
|
|
9aecd4d54b | ||
|
|
65091823eb | ||
|
|
d17218bfbd | ||
|
|
724c5d2632 | ||
|
|
6806c26028 | ||
|
|
dcd89174c9 | ||
|
|
9a8707b8cb | ||
|
|
28f1d05f06 | ||
|
|
54b044584e | ||
|
|
ed39449a20 | ||
|
|
2ff3f3a352 | ||
|
|
91e2e964a5 | ||
|
|
ca6cc68fed | ||
|
|
0962de7735 | ||
|
|
062b957fdb | ||
|
|
4efe4e5663 | ||
|
|
25eb6966c4 | ||
|
|
7843460020 | ||
|
|
1cbc6ba4a9 | ||
|
|
a43407bee8 | ||
|
|
05c4ec9af2 | ||
|
|
9236064293 | ||
|
|
e1955a371e | ||
|
|
58b56c4221 | ||
|
|
1e461e529f | ||
|
|
38ada1207c | ||
|
|
8bd1b34f46 | ||
|
|
4a513360e6 | ||
|
|
22da5c1c37 | ||
|
|
483582a8d1 | ||
|
|
f037cbfac0 |
4
.github/workflows/linux-x86-64.yml
vendored
4
.github/workflows/linux-x86-64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# download jdk
|
# download jdk
|
||||||
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.5-linux-x64-b509.30.tar.gz
|
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-x64-b825.69.tar.gz
|
||||||
|
|
||||||
# install jdk
|
# install jdk
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'jdkfile'
|
distribution: 'jdkfile'
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
java-version: '21.0.5'
|
java-version: '21.0.6'
|
||||||
architecture: x64
|
architecture: x64
|
||||||
|
|
||||||
# dist
|
# dist
|
||||||
|
|||||||
33
.github/workflows/osx-aarch64.yml
vendored
33
.github/workflows/osx-aarch64.yml
vendored
@@ -10,9 +10,31 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install the Apple certificate
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
env:
|
||||||
|
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||||
|
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
|
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# create variables
|
||||||
|
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
|
||||||
|
# import certificate from secrets
|
||||||
|
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
|
||||||
|
|
||||||
|
# create temporary keychain
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
# import certificate to keychain
|
||||||
|
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||||
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
|
||||||
# download jdk
|
# download jdk
|
||||||
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.5-osx-aarch64-b509.30.tar.gz
|
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b825.69.tar.gz
|
||||||
|
|
||||||
# install jdk
|
# install jdk
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
@@ -20,12 +42,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'jdkfile'
|
distribution: 'jdkfile'
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
java-version: '21.0.5'
|
java-version: '21.0.6'
|
||||||
architecture: aarch64
|
architecture: aarch64
|
||||||
|
|
||||||
|
|
||||||
# dist
|
# dist
|
||||||
- run: |
|
- name: Dist
|
||||||
|
env:
|
||||||
|
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' }}
|
||||||
|
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||||
|
run: |
|
||||||
./gradlew dist --no-daemon
|
./gradlew dist --no-daemon
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
|
|||||||
33
.github/workflows/osx-x86-64.yml
vendored
33
.github/workflows/osx-x86-64.yml
vendored
@@ -10,8 +10,31 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install the Apple certificate
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
env:
|
||||||
|
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||||
|
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
|
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# create variables
|
||||||
|
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
|
||||||
|
# import certificate from secrets
|
||||||
|
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
|
||||||
|
|
||||||
|
# create temporary keychain
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
# import certificate to keychain
|
||||||
|
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||||
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
|
||||||
# download jdk
|
# download jdk
|
||||||
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.5-osx-x64-b509.30.tar.gz
|
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b825.69.tar.gz
|
||||||
|
|
||||||
# install jdk
|
# install jdk
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
@@ -19,12 +42,16 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'jdkfile'
|
distribution: 'jdkfile'
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
java-version: '21.0.5'
|
java-version: '21.0.6'
|
||||||
architecture: x64
|
architecture: x64
|
||||||
|
|
||||||
|
|
||||||
# dist
|
# dist
|
||||||
- run: |
|
- name: Dist
|
||||||
|
env:
|
||||||
|
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' }}
|
||||||
|
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||||
|
run: |
|
||||||
./gradlew dist --no-daemon
|
./gradlew dist --no-daemon
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
|
|||||||
13
.github/workflows/winget.yml
vendored
Normal file
13
.github/workflows/winget.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
name: Publish to WinGet
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [ released ]
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: vedantmgoyal9/winget-releaser@main
|
||||||
|
with:
|
||||||
|
identifier: TermoraDev.Termora
|
||||||
|
installers-regex: 'x86-64\.msi$' # Only x86-64.msi files
|
||||||
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
@@ -15,10 +15,12 @@
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- SSH and local terminal support
|
- SSH and local terminal support
|
||||||
|
- Serial port protocol support
|
||||||
- [SFTP](./docs/sftp.png?raw=1) file transfer support
|
- [SFTP](./docs/sftp.png?raw=1) file transfer support
|
||||||
- Compatible with Windows, macOS, and Linux
|
- Compatible with Windows, macOS, and Linux
|
||||||
- Zmodem protocol support
|
- Zmodem protocol support
|
||||||
- SSH port forwarding
|
- SSH port forwarding & Jump hosts
|
||||||
|
- Terminal log
|
||||||
- Configuration synchronization via [Gist](https://gist.github.com)
|
- Configuration synchronization via [Gist](https://gist.github.com)
|
||||||
- Macro support (record and replay scripts)
|
- Macro support (record and replay scripts)
|
||||||
- Keyword highlighting
|
- Keyword highlighting
|
||||||
@@ -32,6 +34,7 @@
|
|||||||
|
|
||||||
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||||
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
||||||
|
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,12 @@
|
|||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
- 支持 SSH 和本地终端
|
- 支持 SSH 和本地终端
|
||||||
|
- 支持串口协议
|
||||||
- 支持 [SFTP](./docs/sftp-zh_CN.png?raw=1) 文件传输
|
- 支持 [SFTP](./docs/sftp-zh_CN.png?raw=1) 文件传输
|
||||||
- 支持 Windows、macOS、Linux 平台
|
- 支持 Windows、macOS、Linux 平台
|
||||||
- 支持 Zmodem 协议
|
- 支持 Zmodem 协议
|
||||||
- 支持 SSH 端口转发
|
- 支持 SSH 端口转发和跳板机
|
||||||
|
- 终端日志记录
|
||||||
- 支持配置同步到 [Gist](https://gist.github.com)
|
- 支持配置同步到 [Gist](https://gist.github.com)
|
||||||
- 支持宏(录制脚本并回放)
|
- 支持宏(录制脚本并回放)
|
||||||
- 支持关键词高亮
|
- 支持关键词高亮
|
||||||
@@ -28,6 +30,7 @@
|
|||||||
|
|
||||||
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||||
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
||||||
|
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
|
|||||||
@@ -241,3 +241,7 @@ https://github.com/mixpanel/mixpanel-java/blob/master/LICENSE
|
|||||||
json-20231013
|
json-20231013
|
||||||
Public Domain.
|
Public Domain.
|
||||||
https://github.com/stleary/JSON-java/blob/master/LICENSE
|
https://github.com/stleary/JSON-java/blob/master/LICENSE
|
||||||
|
|
||||||
|
jSerialComm 2.11.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/Fazecast/jSerialComm/blob/master/LICENSE-APACHE-2.0
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import org.gradle.internal.jvm.Jvm
|
import org.gradle.internal.jvm.Jvm
|
||||||
import org.gradle.kotlin.dsl.support.uppercaseFirstChar
|
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.DefaultNativePlatform
|
||||||
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
|
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
|
||||||
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
||||||
@@ -14,10 +15,10 @@ plugins {
|
|||||||
|
|
||||||
|
|
||||||
group = "app.termora"
|
group = "app.termora"
|
||||||
version = "1.0.5"
|
version = "1.0.7"
|
||||||
|
|
||||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||||
val arch: Architecture = DefaultNativePlatform.getCurrentArchitecture()
|
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||||
|
|
||||||
// macOS 签名信息
|
// macOS 签名信息
|
||||||
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
||||||
@@ -37,7 +38,7 @@ repositories {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// 由于签名和公证,macOS 不携带 natives
|
// 由于签名和公证,macOS 不携带 natives
|
||||||
val useNoNativesFlatLaf = os.isMacOsX && macOSNotary && System.getenv("ENABLE_BUILD").toBoolean()
|
val useNoNativesFlatLaf = os.isMacOsX && System.getenv("ENABLE_BUILD").toBoolean()
|
||||||
|
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
testImplementation(libs.hutool)
|
testImplementation(libs.hutool)
|
||||||
@@ -104,6 +105,7 @@ dependencies {
|
|||||||
implementation(libs.bip39)
|
implementation(libs.bip39)
|
||||||
implementation(libs.colorpicker)
|
implementation(libs.colorpicker)
|
||||||
implementation(libs.mixpanel)
|
implementation(libs.mixpanel)
|
||||||
|
implementation(libs.jSerialComm)
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
@@ -148,6 +150,8 @@ tasks.register<Copy>("copy-dependencies") {
|
|||||||
val jna = libs.jna.asProvider().get()
|
val jna = libs.jna.asProvider().get()
|
||||||
val dylib = dir.get().dir("dylib").asFile
|
val dylib = dir.get().dir("dylib").asFile
|
||||||
val pty4j = libs.pty4j.get()
|
val pty4j = libs.pty4j.get()
|
||||||
|
val jSerialComm = libs.jSerialComm.get()
|
||||||
|
|
||||||
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
|
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
|
||||||
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
||||||
val targetDir = File(dylib, jna.name)
|
val targetDir = File(dylib, jna.name)
|
||||||
@@ -172,6 +176,21 @@ tasks.register<Copy>("copy-dependencies") {
|
|||||||
// @formatter:on
|
// @formatter:on
|
||||||
// 删除所有二进制类库
|
// 删除所有二进制类库
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
|
||||||
|
} else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) {
|
||||||
|
val archName = if (arch.isArm) "aarch64" else "x86_64"
|
||||||
|
val targetDir = FileUtils.getFile(dylib, jSerialComm.name, "OSX", archName)
|
||||||
|
FileUtils.forceMkdir(targetDir)
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "OSX/${archName}/*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
// 删除所有二进制类库
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "Android/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "FreeBSD/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "Linux/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "OpenBSD/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,15 +251,18 @@ tasks.register<Exec>("jpackage") {
|
|||||||
"-Dapp-version=${project.version}",
|
"-Dapp-version=${project.version}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
options.add("-Dsun.java2d.metal=true")
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
options.add("-Dsun.java2d.metal=true")
|
|
||||||
options.add("-Dapple.awt.application.appearance=system")
|
options.add("-Dapple.awt.application.appearance=system")
|
||||||
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if (os.isLinux) {
|
||||||
options.add("-Dsun.java2d.opengl=true")
|
options.add("-Dsun.java2d.opengl=true")
|
||||||
}
|
}
|
||||||
|
|
||||||
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage", "--verbose")
|
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage")
|
||||||
arguments.addAll(listOf("--runtime-image", "${buildDir}/jlink"))
|
arguments.addAll(listOf("--runtime-image", "${buildDir}/jlink"))
|
||||||
arguments.addAll(listOf("--name", project.name.uppercaseFirstChar()))
|
arguments.addAll(listOf("--name", project.name.uppercaseFirstChar()))
|
||||||
arguments.addAll(listOf("--app-version", "${project.version}"))
|
arguments.addAll(listOf("--app-version", "${project.version}"))
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ rhino = "1.7.15"
|
|||||||
delight-rhino-sandbox = "0.0.17"
|
delight-rhino-sandbox = "0.0.17"
|
||||||
testcontainers = "1.20.4"
|
testcontainers = "1.20.4"
|
||||||
mixpanel = "1.5.3"
|
mixpanel = "1.5.3"
|
||||||
|
jSerialComm="2.11.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||||
@@ -97,6 +98,7 @@ rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
|||||||
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
|
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
|
||||||
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
|
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
|
||||||
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
|
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
|
||||||
|
jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "jSerialComm" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||||
|
|||||||
@@ -111,11 +111,18 @@ object Application {
|
|||||||
return "Termora"
|
return "Termora"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("OPT_IN_USAGE")
|
||||||
fun browse(uri: URI, async: Boolean = true) {
|
fun browse(uri: URI, async: Boolean = true) {
|
||||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
// https://github.com/TermoraDev/termora/issues/178
|
||||||
|
if (SystemInfo.isWindows && uri.scheme == "file") {
|
||||||
|
if (async) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
||||||
|
} else {
|
||||||
|
tryBrowse(uri)
|
||||||
|
}
|
||||||
|
} else if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||||
Desktop.getDesktop().browse(uri)
|
Desktop.getDesktop().browse(uri)
|
||||||
} else if (async) {
|
} else if (async) {
|
||||||
@Suppress("OPT_IN_USAGE")
|
|
||||||
GlobalScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
GlobalScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
||||||
} else {
|
} else {
|
||||||
tryBrowse(uri)
|
tryBrowse(uri)
|
||||||
|
|||||||
@@ -22,10 +22,6 @@ class ChannelShellPtyConnector(
|
|||||||
output.flush()
|
output.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun write(buffer: String) {
|
|
||||||
write(buffer.toByteArray(charset))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun resize(rows: Int, cols: Int) {
|
override fun resize(rows: Int, cols: Int) {
|
||||||
channel.sendWindowChange(cols, rows)
|
channel.sendWindowChange(cols, rows)
|
||||||
}
|
}
|
||||||
@@ -38,4 +34,8 @@ class ChannelShellPtyConnector(
|
|||||||
override fun close() {
|
override fun close() {
|
||||||
channel.close(true)
|
channel.close(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getCharset(): Charset {
|
||||||
|
return charset
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -454,6 +454,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
*/
|
*/
|
||||||
var debug by BooleanPropertyDelegate(false)
|
var debug by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 蜂鸣声
|
||||||
|
*/
|
||||||
|
var beep by BooleanPropertyDelegate(true)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 选中复制
|
* 选中复制
|
||||||
*/
|
*/
|
||||||
@@ -463,6 +468,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
* 光标样式
|
* 光标样式
|
||||||
*/
|
*/
|
||||||
var cursor by CursorStylePropertyDelegate(CursorStyle.Block)
|
var cursor by CursorStylePropertyDelegate(CursorStyle.Block)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 终端断开连接时自动关闭Tab
|
||||||
|
*/
|
||||||
|
var autoCloseTabWhenDisconnected by BooleanPropertyDelegate(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jumpHostsOption.filter = { it.id != host.id }
|
jumpHostsOption.filter = { it.id != host.id }
|
||||||
|
|
||||||
|
val serialComm = host.options.serialComm
|
||||||
|
if (serialComm.port.isNotBlank()) {
|
||||||
|
serialCommOption.serialPortComboBox.selectedItem = serialComm.port
|
||||||
|
}
|
||||||
|
serialCommOption.baudRateComboBox.selectedItem = serialComm.baudRate
|
||||||
|
serialCommOption.dataBitsComboBox.selectedItem = serialComm.dataBits
|
||||||
|
serialCommOption.parityComboBox.selectedItem = serialComm.parity
|
||||||
|
serialCommOption.stopBitsComboBox.selectedItem = serialComm.stopBits
|
||||||
|
serialCommOption.flowControlComboBox.selectedItem = serialComm.flowControl
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getHost(): Host {
|
override fun getHost(): Host {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ enum class Protocol {
|
|||||||
Folder,
|
Folder,
|
||||||
SSH,
|
SSH,
|
||||||
Local,
|
Local,
|
||||||
|
Serial
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -39,6 +40,53 @@ data class Authentication(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class SerialCommParity {
|
||||||
|
None,
|
||||||
|
Even,
|
||||||
|
Odd,
|
||||||
|
Mark,
|
||||||
|
Space
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SerialCommFlowControl {
|
||||||
|
None,
|
||||||
|
RTS_CTS,
|
||||||
|
XON_XOFF,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SerialComm(
|
||||||
|
/**
|
||||||
|
* 串口
|
||||||
|
*/
|
||||||
|
val port: String = StringUtils.EMPTY,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 波特率
|
||||||
|
*/
|
||||||
|
val baudRate: Int = 9600,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据位:5、6、7、8
|
||||||
|
*/
|
||||||
|
val dataBits: Int = 8,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止位: 1、1.5、2
|
||||||
|
*/
|
||||||
|
val stopBits: String = "1",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验位
|
||||||
|
*/
|
||||||
|
val parity: SerialCommParity = SerialCommParity.None,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流控
|
||||||
|
*/
|
||||||
|
val flowControl: SerialCommFlowControl = SerialCommFlowControl.None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Options(
|
data class Options(
|
||||||
@@ -61,7 +109,12 @@ data class Options(
|
|||||||
/**
|
/**
|
||||||
* SSH 心跳间隔
|
* SSH 心跳间隔
|
||||||
*/
|
*/
|
||||||
val heartbeatInterval: Int = 30
|
val heartbeatInterval: Int = 30,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 串口配置
|
||||||
|
*/
|
||||||
|
val serialComm: SerialComm = SerialComm(),
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val Default = Options()
|
val Default = Options()
|
||||||
|
|||||||
@@ -67,37 +67,53 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
|||||||
|
|
||||||
private suspend fun testConnection(host: Host) {
|
private suspend fun testConnection(host: Host) {
|
||||||
val owner = this
|
val owner = this
|
||||||
if (host.protocol != Protocol.SSH) {
|
if (host.protocol == Protocol.Local) {
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
OptionPane.showMessageDialog(owner, I18n.getString("termora.new-host.test-connection-successful"))
|
OptionPane.showMessageDialog(owner, I18n.getString("termora.new-host.test-connection-successful"))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (host.protocol == Protocol.SSH) {
|
||||||
|
testSSH(host)
|
||||||
|
} else if (host.protocol == Protocol.Serial) {
|
||||||
|
testSerial(host)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, ExceptionUtils.getMessage(e),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
I18n.getString("termora.new-host.test-connection-successful")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testSSH(host: Host) {
|
||||||
var client: SshClient? = null
|
var client: SshClient? = null
|
||||||
var session: ClientSession? = null
|
var session: ClientSession? = null
|
||||||
try {
|
try {
|
||||||
client = SshClients.openClient(host)
|
client = SshClients.openClient(host)
|
||||||
client.userInteraction = TerminalUserInteraction(owner)
|
client.userInteraction = TerminalUserInteraction(owner)
|
||||||
session = SshClients.openSession(host, client)
|
session = SshClients.openSession(host, client)
|
||||||
withContext(Dispatchers.Swing) {
|
|
||||||
OptionPane.showMessageDialog(
|
|
||||||
owner,
|
|
||||||
I18n.getString("termora.new-host.test-connection-successful")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
withContext(Dispatchers.Swing) {
|
|
||||||
OptionPane.showMessageDialog(
|
|
||||||
owner, ExceptionUtils.getRootCauseMessage(e),
|
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
session?.close()
|
session?.close()
|
||||||
client?.close()
|
client?.close()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testSerial(host: Host) {
|
||||||
|
Serials.openPort(host).closePort()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun doOKAction() {
|
override fun doOKAction() {
|
||||||
|
|||||||
@@ -2,11 +2,17 @@ package app.termora
|
|||||||
|
|
||||||
import app.termora.keymgr.KeyManager
|
import app.termora.keymgr.KeyManager
|
||||||
import app.termora.keymgr.KeyManagerDialog
|
import app.termora.keymgr.KeyManagerDialog
|
||||||
|
import com.fazecast.jSerialComm.SerialPort
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
import java.awt.event.*
|
import java.awt.event.*
|
||||||
@@ -22,6 +28,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
protected val proxyOption = ProxyOption()
|
protected val proxyOption = ProxyOption()
|
||||||
protected val terminalOption = TerminalOption()
|
protected val terminalOption = TerminalOption()
|
||||||
protected val jumpHostsOption = JumpHostsOption()
|
protected val jumpHostsOption = JumpHostsOption()
|
||||||
|
protected val serialCommOption = SerialCommOption()
|
||||||
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -30,6 +37,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
addOption(tunnelingOption)
|
addOption(tunnelingOption)
|
||||||
addOption(jumpHostsOption)
|
addOption(jumpHostsOption)
|
||||||
addOption(terminalOption)
|
addOption(terminalOption)
|
||||||
|
addOption(serialCommOption)
|
||||||
|
|
||||||
setContentBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
setContentBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
||||||
}
|
}
|
||||||
@@ -43,6 +51,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
var authentication = Authentication.No
|
var authentication = Authentication.No
|
||||||
var proxy = Proxy.No
|
var proxy = Proxy.No
|
||||||
|
|
||||||
|
|
||||||
if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.Password) {
|
if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.Password) {
|
||||||
authentication = authentication.copy(
|
authentication = authentication.copy(
|
||||||
type = AuthenticationType.Password,
|
type = AuthenticationType.Password,
|
||||||
@@ -66,12 +75,23 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val serialComm = SerialComm(
|
||||||
|
port = serialCommOption.serialPortComboBox.selectedItem?.toString() ?: StringUtils.EMPTY,
|
||||||
|
baudRate = serialCommOption.baudRateComboBox.selectedItem?.toString()?.toIntOrNull() ?: 9600,
|
||||||
|
dataBits = serialCommOption.dataBitsComboBox.selectedItem as Int? ?: 8,
|
||||||
|
stopBits = serialCommOption.stopBitsComboBox.selectedItem as String? ?: "1",
|
||||||
|
parity = serialCommOption.parityComboBox.selectedItem as SerialCommParity,
|
||||||
|
flowControl = serialCommOption.flowControlComboBox.selectedItem as SerialCommFlowControl
|
||||||
|
)
|
||||||
|
|
||||||
val options = Options.Default.copy(
|
val options = Options.Default.copy(
|
||||||
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
||||||
env = terminalOption.environmentTextArea.text,
|
env = terminalOption.environmentTextArea.text,
|
||||||
startupCommand = terminalOption.startupCommandTextField.text,
|
startupCommand = terminalOption.startupCommandTextField.text,
|
||||||
heartbeatInterval = (terminalOption.heartbeatIntervalTextField.value ?: 30) as Int,
|
heartbeatInterval = (terminalOption.heartbeatIntervalTextField.value ?: 30) as Int,
|
||||||
jumpHosts = jumpHostsOption.jumpHosts.map { it.id }
|
jumpHosts = jumpHostsOption.jumpHosts.map { it.id },
|
||||||
|
serialComm = serialComm
|
||||||
)
|
)
|
||||||
|
|
||||||
return Host(
|
return Host(
|
||||||
@@ -103,6 +123,12 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
if (validateField(generalOption.usernameTextField)) {
|
if (validateField(generalOption.usernameTextField)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
} else if (host.protocol == Protocol.Serial) {
|
||||||
|
if (validateField(serialCommOption.serialPortComboBox)
|
||||||
|
|| validateField(serialCommOption.baudRateComboBox)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (host.authentication.type == AuthenticationType.Password) {
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
@@ -152,7 +178,8 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
* 返回 true 表示有错误
|
* 返回 true 表示有错误
|
||||||
*/
|
*/
|
||||||
private fun validateField(comboBox: JComboBox<*>): Boolean {
|
private fun validateField(comboBox: JComboBox<*>): Boolean {
|
||||||
if (comboBox.isEnabled && comboBox.selectedItem == null) {
|
val selectedItem = comboBox.selectedItem
|
||||||
|
if (comboBox.isEnabled && (selectedItem == null || (selectedItem is String && selectedItem.isBlank()))) {
|
||||||
selectOptionJComponent(comboBox)
|
selectOptionJComponent(comboBox)
|
||||||
comboBox.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
comboBox.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
comboBox.requestFocusInWindow()
|
comboBox.requestFocusInWindow()
|
||||||
@@ -259,6 +286,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
protocolTypeComboBox.addItem(Protocol.SSH)
|
protocolTypeComboBox.addItem(Protocol.SSH)
|
||||||
protocolTypeComboBox.addItem(Protocol.Local)
|
protocolTypeComboBox.addItem(Protocol.Local)
|
||||||
|
protocolTypeComboBox.addItem(Protocol.Serial)
|
||||||
|
|
||||||
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||||
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||||
@@ -328,7 +356,9 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
passwordTextField.isEnabled = true
|
passwordTextField.isEnabled = true
|
||||||
chooseKeyBtn.isEnabled = true
|
chooseKeyBtn.isEnabled = true
|
||||||
|
|
||||||
if (protocolTypeComboBox.selectedItem == Protocol.Local) {
|
if (protocolTypeComboBox.selectedItem == Protocol.Local
|
||||||
|
|| protocolTypeComboBox.selectedItem == Protocol.Serial
|
||||||
|
) {
|
||||||
hostTextField.isEnabled = false
|
hostTextField.isEnabled = false
|
||||||
portTextField.isEnabled = false
|
portTextField.isEnabled = false
|
||||||
usernameTextField.isEnabled = false
|
usernameTextField.isEnabled = false
|
||||||
@@ -901,6 +931,127 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected inner class SerialCommOption : JPanel(BorderLayout()), Option {
|
||||||
|
val serialPortComboBox = OutlineComboBox<String>()
|
||||||
|
val baudRateComboBox = OutlineComboBox<Int>()
|
||||||
|
val dataBitsComboBox = OutlineComboBox<Int>()
|
||||||
|
val parityComboBox = OutlineComboBox<SerialCommParity>()
|
||||||
|
val stopBitsComboBox = OutlineComboBox<String>()
|
||||||
|
val flowControlComboBox = OutlineComboBox<SerialCommFlowControl>()
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
|
||||||
|
serialPortComboBox.isEditable = true
|
||||||
|
|
||||||
|
baudRateComboBox.isEditable = true
|
||||||
|
baudRateComboBox.addItem(9600)
|
||||||
|
baudRateComboBox.addItem(19200)
|
||||||
|
baudRateComboBox.addItem(38400)
|
||||||
|
baudRateComboBox.addItem(57600)
|
||||||
|
baudRateComboBox.addItem(115200)
|
||||||
|
|
||||||
|
dataBitsComboBox.addItem(5)
|
||||||
|
dataBitsComboBox.addItem(6)
|
||||||
|
dataBitsComboBox.addItem(7)
|
||||||
|
dataBitsComboBox.addItem(8)
|
||||||
|
dataBitsComboBox.selectedItem = 8
|
||||||
|
|
||||||
|
parityComboBox.addItem(SerialCommParity.None)
|
||||||
|
parityComboBox.addItem(SerialCommParity.Even)
|
||||||
|
parityComboBox.addItem(SerialCommParity.Odd)
|
||||||
|
parityComboBox.addItem(SerialCommParity.Mark)
|
||||||
|
parityComboBox.addItem(SerialCommParity.Space)
|
||||||
|
|
||||||
|
stopBitsComboBox.addItem("1")
|
||||||
|
stopBitsComboBox.addItem("1.5")
|
||||||
|
stopBitsComboBox.addItem("2")
|
||||||
|
stopBitsComboBox.selectedItem = "1"
|
||||||
|
|
||||||
|
flowControlComboBox.addItem(SerialCommFlowControl.None)
|
||||||
|
flowControlComboBox.addItem(SerialCommFlowControl.RTS_CTS)
|
||||||
|
flowControlComboBox.addItem(SerialCommFlowControl.XON_XOFF)
|
||||||
|
|
||||||
|
flowControlComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
val text = value?.toString() ?: StringUtils.EMPTY
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
text.replace('_', '/'),
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
addComponentListener(object : ComponentAdapter() {
|
||||||
|
override fun componentShown(e: ComponentEvent) {
|
||||||
|
removeComponentListener(this)
|
||||||
|
@Suppress("OPT_IN_USAGE")
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
for (commPort in SerialPort.getCommPorts()) {
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
serialPortComboBox.addItem(commPort.systemPortName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.new-host.serial")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $formMargin, default:grow, $formMargin",
|
||||||
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.serial.port")}:").xy(1, rows)
|
||||||
|
.add(serialPortComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.serial.baud-rate")}:").xy(1, rows)
|
||||||
|
.add(baudRateComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.serial.data-bits")}:").xy(1, rows)
|
||||||
|
.add(dataBitsComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.serial.parity")}:").xy(1, rows)
|
||||||
|
.add(parityComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.serial.stop-bits")}:").xy(1, rows)
|
||||||
|
.add(stopBitsComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.serial.flow-control")}:").xy(1, rows)
|
||||||
|
.add(flowControlComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.build()
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected inner class JumpHostsOption : JPanel(BorderLayout()), Option {
|
protected inner class JumpHostsOption : JPanel(BorderLayout()), Option {
|
||||||
val jumpHosts = mutableListOf<Host>()
|
val jumpHosts = mutableListOf<Host>()
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ class HostTree : JTree(), Disposable {
|
|||||||
icon = if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
icon = if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||||
} else if (host.protocol == Protocol.SSH || host.protocol == Protocol.Local) {
|
} else if (host.protocol == Protocol.SSH || host.protocol == Protocol.Local) {
|
||||||
icon = if (sel && this@HostTree.hasFocus()) Icons.terminal.dark else Icons.terminal
|
icon = if (sel && this@HostTree.hasFocus()) Icons.terminal.dark else Icons.terminal
|
||||||
|
} else if (host.protocol == Protocol.Serial) {
|
||||||
|
icon = if (sel && this@HostTree.hasFocus()) Icons.plugin.dark else Icons.plugin
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package app.termora
|
|||||||
object Icons {
|
object Icons {
|
||||||
val bulletList by lazy { DynamicIcon("icons/bulletList.svg", "icons/bulletList_dark.svg") }
|
val bulletList by lazy { DynamicIcon("icons/bulletList.svg", "icons/bulletList_dark.svg") }
|
||||||
val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") }
|
val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") }
|
||||||
|
val plugin by lazy { DynamicIcon("icons/plugin.svg", "icons/plugin_dark.svg") }
|
||||||
val moveUp by lazy { DynamicIcon("icons/moveUp.svg", "icons/moveUp_dark.svg") }
|
val moveUp by lazy { DynamicIcon("icons/moveUp.svg", "icons/moveUp_dark.svg") }
|
||||||
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
|
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
|
||||||
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
|
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
|
||||||
@@ -67,6 +68,7 @@ object Icons {
|
|||||||
val network by lazy { DynamicIcon("icons/network.svg", "icons/network_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 server by lazy { DynamicIcon("icons/server.svg", "icons/server_dark.svg") }
|
||||||
val runAnything by lazy { DynamicIcon("icons/runAnything.svg", "icons/runAnything_dark.svg") }
|
val runAnything by lazy { DynamicIcon("icons/runAnything.svg", "icons/runAnything_dark.svg") }
|
||||||
|
val run by lazy { DynamicIcon("icons/run.svg", "icons/run_dark.svg") }
|
||||||
val uiForm by lazy { DynamicIcon("icons/uiForm.svg", "icons/uiForm_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 cloud by lazy { DynamicIcon("icons/cloud.svg", "icons/cloud_dark.svg") }
|
||||||
val externalLink by lazy { DynamicIcon("icons/externalLink.svg", "icons/externalLink_dark.svg") }
|
val externalLink by lazy { DynamicIcon("icons/externalLink.svg", "icons/externalLink_dark.svg") }
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import app.termora.terminal.PtyConnector
|
|||||||
import org.apache.commons.io.Charsets
|
import org.apache.commons.io.Charsets
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
class LocalTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
|
class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
||||||
|
PtyHostTerminalTab(windowScope, host) {
|
||||||
|
|
||||||
override suspend fun openPtyConnector(): PtyConnector {
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
val winSize = terminalPanel.winSize()
|
val winSize = terminalPanel.winSize()
|
||||||
|
|||||||
@@ -41,4 +41,9 @@ private fun setupNativeLibraries() {
|
|||||||
if (pty4j.exists()) {
|
if (pty4j.exists()) {
|
||||||
System.setProperty(PtyUtil.PREFERRED_NATIVE_FOLDER_KEY, pty4j.absolutePath)
|
System.setProperty(PtyUtil.PREFERRED_NATIVE_FOLDER_KEY, pty4j.absolutePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val jSerialComm = FileUtils.getFile(dylib, "jSerialComm")
|
||||||
|
if (jSerialComm.exists()) {
|
||||||
|
System.setProperty("jSerialComm.library.path", jSerialComm.absolutePath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -75,6 +75,7 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
private var terminalTab: TerminalTab? = null
|
private var terminalTab: TerminalTab? = null
|
||||||
private var isDragging = false
|
private var isDragging = false
|
||||||
private var lastVisitTabIndex = -1
|
private var lastVisitTabIndex = -1
|
||||||
|
private var releasedPoint = Point()
|
||||||
|
|
||||||
override fun mousePressed(e: MouseEvent) {
|
override fun mousePressed(e: MouseEvent) {
|
||||||
val index = indexAtLocation(e.x, e.y)
|
val index = indexAtLocation(e.x, e.y)
|
||||||
@@ -136,19 +137,26 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val tab = this.terminalTab
|
// 如果是取消,那么不需要移动到其它窗口
|
||||||
val terminalTabbedManager = terminalTabbedManager
|
val c = if (cancelled) owner else getTopMostWindowUnderMouse()
|
||||||
|
|
||||||
if (tab != null && terminalTabbedManager != null) {
|
// 如果等于 null 表示在空地方释放,那么单独一个窗口
|
||||||
// 如果是手动取消
|
if (c == null) {
|
||||||
if (cancelled) {
|
val window = TermoraFrameManager.getInstance().createWindow()
|
||||||
terminalTabbedManager.addTerminalTab(tabIndex, tab)
|
dragToAnotherWindow(window)
|
||||||
} else if (lastVisitTabIndex > 0) {
|
window.location = releasedPoint
|
||||||
terminalTabbedManager.addTerminalTab(lastVisitTabIndex, tab)
|
window.isVisible = true
|
||||||
} else if (lastVisitTabIndex == 0) {
|
} else if (c != owner && c is TermoraFrame) { // 如果在某个窗口内释放,那么就移动到某个窗口内
|
||||||
terminalTabbedManager.addTerminalTab(1, tab)
|
dragToAnotherWindow(c)
|
||||||
} else {
|
} else {
|
||||||
terminalTabbedManager.addTerminalTab(tab)
|
val tab = this.terminalTab
|
||||||
|
val terminalTabbedManager = terminalTabbedManager
|
||||||
|
if (tab != null && terminalTabbedManager != null) {
|
||||||
|
moveTab(
|
||||||
|
terminalTabbedManager,
|
||||||
|
tab,
|
||||||
|
lastVisitTabIndex
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +169,7 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun mouseReleased(e: MouseEvent) {
|
override fun mouseReleased(e: MouseEvent) {
|
||||||
|
releasedPoint = e.point
|
||||||
stopDrag()
|
stopDrag()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +193,71 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getTopMostWindowUnderMouse(): Window? {
|
||||||
|
val mouseLocation = MouseInfo.getPointerInfo().location
|
||||||
|
val owner = owner
|
||||||
|
if (owner.isVisible && owner.bounds.contains(mouseLocation)) {
|
||||||
|
return owner
|
||||||
|
}
|
||||||
|
|
||||||
|
val windows = Window.getWindows()
|
||||||
|
// 倒序遍历,最上层的窗口优先匹配
|
||||||
|
for (i in windows.indices.reversed()) {
|
||||||
|
val window = windows[i]
|
||||||
|
if (window !is TermoraFrame) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (window.isVisible && window.bounds.contains(mouseLocation)) {
|
||||||
|
val topComponent = SwingUtilities.getDeepestComponentAt(
|
||||||
|
window,
|
||||||
|
mouseLocation.x - window.x, mouseLocation.y - window.y
|
||||||
|
)
|
||||||
|
if (topComponent != null) {
|
||||||
|
return SwingUtilities.getWindowAncestor(topComponent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun dragToAnotherWindow(frame: TermoraFrame) {
|
||||||
|
val tab = this.terminalTab ?: return
|
||||||
|
val tabbedManager = frame.getData(DataProviders.TerminalTabbed) ?: return
|
||||||
|
val tabbedPane = frame.getData(DataProviders.TabbedPane) ?: return
|
||||||
|
val location = Point(MouseInfo.getPointerInfo().location)
|
||||||
|
SwingUtilities.convertPointFromScreen(location, tabbedPane)
|
||||||
|
val index = tabbedPane.indexAtLocation(location.x, location.y)
|
||||||
|
|
||||||
|
moveTab(
|
||||||
|
tabbedManager,
|
||||||
|
tab,
|
||||||
|
index
|
||||||
|
)
|
||||||
|
|
||||||
|
if (frame.hasFocus()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
frame.requestFocus()
|
||||||
|
tabbedPane.selectedComponent?.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun moveTab(terminalTabbedManager: TerminalTabbedManager, tab: TerminalTab, lastVisitTabIndex: Int) {
|
||||||
|
// 如果是手动取消
|
||||||
|
if (cancelled) {
|
||||||
|
terminalTabbedManager.addTerminalTab(tabIndex, tab)
|
||||||
|
} else if (lastVisitTabIndex > 0) {
|
||||||
|
terminalTabbedManager.addTerminalTab(lastVisitTabIndex, tab)
|
||||||
|
} else if (lastVisitTabIndex == 0) {
|
||||||
|
terminalTabbedManager.addTerminalTab(1, tab)
|
||||||
|
} else {
|
||||||
|
terminalTabbedManager.addTerminalTab(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ abstract class PtyHostTerminalTab(
|
|||||||
private var readerJob: Job? = null
|
private var readerJob: Job? = null
|
||||||
private val ptyConnectorDelegate = PtyConnectorDelegate()
|
private val ptyConnectorDelegate = PtyConnectorDelegate()
|
||||||
|
|
||||||
protected val terminalPanel =
|
private val terminalPanelFactory = TerminalPanelFactory.getInstance(windowScope)
|
||||||
TerminalPanelFactory.getInstance(windowScope).createTerminalPanel(terminal, ptyConnectorDelegate)
|
protected val terminalPanel = terminalPanelFactory.createTerminalPanel(terminal, ptyConnectorDelegate)
|
||||||
|
.apply { Disposer.register(this@PtyHostTerminalTab, this) }
|
||||||
protected val ptyConnectorFactory get() = PtyConnectorFactory.getInstance(windowScope)
|
protected val ptyConnectorFactory get() = PtyConnectorFactory.getInstance(windowScope)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -53,8 +54,12 @@ abstract class PtyHostTerminalTab(
|
|||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
delay(250.milliseconds)
|
delay(250.milliseconds)
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
ptyConnector.write(host.options.startupCommand)
|
val charset = ptyConnector.getCharset()
|
||||||
ptyConnector.write(terminal.getKeyEncoder().encode(TerminalKeyEvent(KeyEvent.VK_ENTER)))
|
ptyConnector.write(host.options.startupCommand.toByteArray(charset))
|
||||||
|
ptyConnector.write(
|
||||||
|
terminal.getKeyEncoder().encode(TerminalKeyEvent(KeyEvent.VK_ENTER))
|
||||||
|
.toByteArray(charset)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,6 +121,7 @@ abstract class PtyHostTerminalTab(
|
|||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
stop()
|
stop()
|
||||||
|
terminalPanel
|
||||||
super.dispose()
|
super.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.actions.TabReconnectAction
|
import app.termora.actions.TabReconnectAction
|
||||||
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
||||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
@@ -27,11 +29,13 @@ import org.apache.sshd.common.session.SessionListener.Event
|
|||||||
import org.apache.sshd.common.util.net.SshdSocketAddress
|
import org.apache.sshd.common.util.net.SshdSocketAddress
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.util.EventObject
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
|
||||||
class SSHTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
|
class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
||||||
|
PtyHostTerminalTab(windowScope, host) {
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
|
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
|
||||||
}
|
}
|
||||||
@@ -41,6 +45,9 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(
|
|||||||
private var sshClient: SshClient? = null
|
private var sshClient: SshClient? = null
|
||||||
private var sshSession: ClientSession? = null
|
private var sshSession: ClientSession? = null
|
||||||
private var sshChannelShell: ChannelShell? = null
|
private var sshChannelShell: ChannelShell? = null
|
||||||
|
private val terminalTabbedManager
|
||||||
|
get() = AnActionEvent(getJComponent(), StringUtils.EMPTY, EventObject(getJComponent()))
|
||||||
|
.getData(DataProviders.TerminalTabbedManager)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
terminalPanel.dropFiles = false
|
terminalPanel.dropFiles = false
|
||||||
@@ -119,13 +126,25 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(
|
|||||||
override fun channelClosed(channel: Channel, reason: Throwable?) {
|
override fun channelClosed(channel: Channel, reason: Throwable?) {
|
||||||
coroutineScope.launch(Dispatchers.Swing) {
|
coroutineScope.launch(Dispatchers.Swing) {
|
||||||
terminal.write("\r\n\r\n${ControlCharacters.ESC}[31m")
|
terminal.write("\r\n\r\n${ControlCharacters.ESC}[31m")
|
||||||
terminal.write("Channel has been disconnected.")
|
terminal.write(I18n.getString("termora.terminal.channel-disconnected"))
|
||||||
if (reconnectShortcut is KeyShortcut) {
|
if (reconnectShortcut is KeyShortcut) {
|
||||||
terminal.write(" Type $reconnectShortcut to reconnect.")
|
terminal.write(
|
||||||
|
I18n.getString(
|
||||||
|
"termora.terminal.channel-reconnect",
|
||||||
|
reconnectShortcut.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
terminal.write("\r\n")
|
terminal.write("\r\n")
|
||||||
terminal.write("${ControlCharacters.ESC}[0m")
|
terminal.write("${ControlCharacters.ESC}[0m")
|
||||||
terminalModel.setData(DataKey.ShowCursor, false)
|
terminalModel.setData(DataKey.ShowCursor, false)
|
||||||
|
if (Database.getDatabase().terminal.autoCloseTabWhenDisconnected) {
|
||||||
|
terminalTabbedManager?.let { manager ->
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
manager.closeTerminalTab(this@SSHTerminalTab, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
61
src/main/kotlin/app/termora/SerialPortPtyConnector.kt
Normal file
61
src/main/kotlin/app/termora/SerialPortPtyConnector.kt
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.terminal.PtyConnector
|
||||||
|
import com.fazecast.jSerialComm.SerialPort
|
||||||
|
import com.fazecast.jSerialComm.SerialPortDataListener
|
||||||
|
import com.fazecast.jSerialComm.SerialPortEvent
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class SerialPortPtyConnector(
|
||||||
|
private val serialPort: SerialPort,
|
||||||
|
private val charset: Charset = Charsets.UTF_8
|
||||||
|
) : PtyConnector, SerialPortDataListener {
|
||||||
|
|
||||||
|
private val queue = LinkedBlockingQueue<Char>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
serialPort.addDataListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(buffer: CharArray): Int {
|
||||||
|
buffer[0] = queue.poll(1, TimeUnit.SECONDS) ?: return 0
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(buffer: ByteArray, offset: Int, len: Int) {
|
||||||
|
serialPort.writeBytes(buffer, len, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resize(rows: Int, cols: Int) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun waitFor(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
queue.clear()
|
||||||
|
serialPort.closePort()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getListeningEvents(): Int {
|
||||||
|
return SerialPort.LISTENING_EVENT_DATA_RECEIVED
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialEvent(event: SerialPortEvent) {
|
||||||
|
if (event.eventType == SerialPort.LISTENING_EVENT_DATA_RECEIVED) {
|
||||||
|
val data = event.receivedData
|
||||||
|
if (data.isEmpty()) return
|
||||||
|
for (c in String(data, charset).toCharArray()) {
|
||||||
|
queue.add(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCharset(): Charset {
|
||||||
|
return charset
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/main/kotlin/app/termora/SerialTerminalTab.kt
Normal file
21
src/main/kotlin/app/termora/SerialTerminalTab.kt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.terminal.PtyConnector
|
||||||
|
import org.apache.commons.io.Charsets
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import javax.swing.Icon
|
||||||
|
|
||||||
|
class SerialTerminalTab(windowScope: WindowScope, host: Host) :
|
||||||
|
PtyHostTerminalTab(windowScope, host) {
|
||||||
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
|
val serialPort = Serials.openPort(host)
|
||||||
|
return SerialPortPtyConnector(
|
||||||
|
serialPort,
|
||||||
|
Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(): Icon {
|
||||||
|
return Icons.plugin
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/main/kotlin/app/termora/Serials.kt
Normal file
38
src/main/kotlin/app/termora/Serials.kt
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.fazecast.jSerialComm.SerialPort
|
||||||
|
|
||||||
|
object Serials {
|
||||||
|
fun openPort(host: Host): SerialPort {
|
||||||
|
val serialComm = host.options.serialComm
|
||||||
|
val serialPort = SerialPort.getCommPort(serialComm.port)
|
||||||
|
serialPort.setBaudRate(serialComm.baudRate)
|
||||||
|
serialPort.setNumDataBits(serialComm.dataBits)
|
||||||
|
|
||||||
|
when (serialComm.parity) {
|
||||||
|
SerialCommParity.None -> serialPort.setParity(SerialPort.NO_PARITY)
|
||||||
|
SerialCommParity.Mark -> serialPort.setParity(SerialPort.MARK_PARITY)
|
||||||
|
SerialCommParity.Even -> serialPort.setParity(SerialPort.EVEN_PARITY)
|
||||||
|
SerialCommParity.Odd -> serialPort.setParity(SerialPort.ODD_PARITY)
|
||||||
|
SerialCommParity.Space -> serialPort.setParity(SerialPort.SPACE_PARITY)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (serialComm.stopBits) {
|
||||||
|
"1" -> serialPort.setNumStopBits(SerialPort.ONE_STOP_BIT)
|
||||||
|
"1.5" -> serialPort.setNumStopBits(SerialPort.ONE_POINT_FIVE_STOP_BITS)
|
||||||
|
"2" -> serialPort.setNumStopBits(SerialPort.TWO_STOP_BITS)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (serialComm.flowControl) {
|
||||||
|
SerialCommFlowControl.None -> serialPort.setFlowControl(SerialPort.FLOW_CONTROL_DISABLED)
|
||||||
|
SerialCommFlowControl.RTS_CTS -> serialPort.setFlowControl(SerialPort.FLOW_CONTROL_RTS_ENABLED or SerialPort.FLOW_CONTROL_CTS_ENABLED)
|
||||||
|
SerialCommFlowControl.XON_XOFF -> serialPort.setFlowControl(SerialPort.FLOW_CONTROL_XONXOFF_IN_ENABLED or SerialPort.FLOW_CONTROL_XONXOFF_OUT_ENABLED)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serialPort.openPort()) {
|
||||||
|
throw IllegalStateException("Open serial port [${serialComm.port}] failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return serialPort
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import app.termora.terminal.CursorStyle
|
|||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.terminal.panel.TerminalPanel
|
import app.termora.terminal.panel.TerminalPanel
|
||||||
import cash.z.ecc.android.bip39.Mnemonics
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
import com.formdev.flatlaf.extras.components.*
|
import com.formdev.flatlaf.extras.components.*
|
||||||
import com.formdev.flatlaf.util.FontUtils
|
import com.formdev.flatlaf.util.FontUtils
|
||||||
@@ -43,6 +44,7 @@ import org.jdesktop.swingx.JXEditorPane
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
|
import java.awt.Toolkit
|
||||||
import java.awt.datatransfer.StringSelection
|
import java.awt.datatransfer.StringSelection
|
||||||
import java.awt.event.ItemEvent
|
import java.awt.event.ItemEvent
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -298,12 +300,14 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private inner class TerminalOption : JPanel(BorderLayout()), Option {
|
private inner class TerminalOption : JPanel(BorderLayout()), Option {
|
||||||
private val cursorStyleComboBox = FlatComboBox<CursorStyle>()
|
private val cursorStyleComboBox = FlatComboBox<CursorStyle>()
|
||||||
private val debugComboBox = YesOrNoComboBox()
|
private val debugComboBox = YesOrNoComboBox()
|
||||||
|
private val beepComboBox = YesOrNoComboBox()
|
||||||
private val fontComboBox = FlatComboBox<String>()
|
private val fontComboBox = FlatComboBox<String>()
|
||||||
private val shellComboBox = FlatComboBox<String>()
|
private val shellComboBox = FlatComboBox<String>()
|
||||||
private val maxRowsTextField = IntSpinner(0, 0)
|
private val maxRowsTextField = IntSpinner(0, 0)
|
||||||
private val fontSizeTextField = IntSpinner(0, 9, 99)
|
private val fontSizeTextField = IntSpinner(0, 9, 99)
|
||||||
private val terminalSetting get() = Database.getDatabase().terminal
|
private val terminalSetting get() = Database.getDatabase().terminal
|
||||||
private val selectCopyComboBox = YesOrNoComboBox()
|
private val selectCopyComboBox = YesOrNoComboBox()
|
||||||
|
private val autoCloseTabComboBox = YesOrNoComboBox()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -319,6 +323,13 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
autoCloseTabComboBox.addItemListener { e ->
|
||||||
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
|
terminalSetting.autoCloseTabWhenDisconnected = autoCloseTabComboBox.selectedItem as Boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
autoCloseTabComboBox.toolTipText = I18n.getString("termora.settings.terminal.auto-close-tab-description")
|
||||||
|
|
||||||
selectCopyComboBox.addItemListener { e ->
|
selectCopyComboBox.addItemListener { e ->
|
||||||
if (e.stateChange == ItemEvent.SELECTED) {
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
terminalSetting.selectCopy = selectCopyComboBox.selectedItem as Boolean
|
terminalSetting.selectCopy = selectCopyComboBox.selectedItem as Boolean
|
||||||
@@ -355,6 +366,13 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
beepComboBox.addItemListener { e ->
|
||||||
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
|
terminalSetting.beep = beepComboBox.selectedItem as Boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
shellComboBox.addItemListener {
|
shellComboBox.addItemListener {
|
||||||
if (it.stateChange == ItemEvent.SELECTED) {
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
terminalSetting.localShell = shellComboBox.selectedItem as String
|
terminalSetting.localShell = shellComboBox.selectedItem as String
|
||||||
@@ -453,8 +471,10 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
fontComboBox.selectedItem = terminalSetting.font
|
fontComboBox.selectedItem = terminalSetting.font
|
||||||
debugComboBox.selectedItem = terminalSetting.debug
|
debugComboBox.selectedItem = terminalSetting.debug
|
||||||
|
beepComboBox.selectedItem = terminalSetting.beep
|
||||||
cursorStyleComboBox.selectedItem = terminalSetting.cursor
|
cursorStyleComboBox.selectedItem = terminalSetting.cursor
|
||||||
selectCopyComboBox.selectedItem = terminalSetting.selectCopy
|
selectCopyComboBox.selectedItem = terminalSetting.selectCopy
|
||||||
|
autoCloseTabComboBox.selectedItem = terminalSetting.autoCloseTabWhenDisconnected
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(isSelected: Boolean): Icon {
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
@@ -472,9 +492,14 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private fun getCenterComponent(): JComponent {
|
private fun getCenterComponent(): JComponent {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow",
|
"left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow",
|
||||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val beepBtn = JButton(Icons.run)
|
||||||
|
beepBtn.isFocusable = false
|
||||||
|
beepBtn.putClientProperty(FlatClientProperties.BUTTON_TYPE, FlatClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON)
|
||||||
|
beepBtn.addActionListener { Toolkit.getDefaultToolkit().beep() }
|
||||||
|
|
||||||
var rows = 1
|
var rows = 1
|
||||||
val step = 2
|
val step = 2
|
||||||
val panel = FormBuilder.create().layout(layout)
|
val panel = FormBuilder.create().layout(layout)
|
||||||
@@ -487,10 +512,15 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
.add(maxRowsTextField).xy(3, rows).apply { rows += step }
|
.add(maxRowsTextField).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.debug")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.debug")}:").xy(1, rows)
|
||||||
.add(debugComboBox).xy(3, rows).apply { rows += step }
|
.add(debugComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.settings.terminal.beep")}:").xy(1, rows)
|
||||||
|
.add(beepComboBox).xy(3, rows)
|
||||||
|
.add(beepBtn).xy(5, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.select-copy")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.select-copy")}:").xy(1, rows)
|
||||||
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
|
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
|
||||||
.add(cursorStyleComboBox).xy(3, rows).apply { rows += step }
|
.add(cursorStyleComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.settings.terminal.auto-close-tab")}:").xy(1, rows)
|
||||||
|
.add(autoCloseTabComboBox).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.local-shell")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.local-shell")}:").xy(1, rows)
|
||||||
.add(shellComboBox).xyw(3, rows, 5)
|
.add(shellComboBox).xyw(3, rows, 5)
|
||||||
.build()
|
.build()
|
||||||
@@ -550,12 +580,6 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeComboBox.selectedItem == SyncType.Gitee) {
|
|
||||||
gistTextField.trailingComponent = null
|
|
||||||
} else {
|
|
||||||
gistTextField.trailingComponent = visitGistBtn
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAll()
|
removeAll()
|
||||||
add(getCenterComponent(), BorderLayout.CENTER)
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
revalidate()
|
revalidate()
|
||||||
@@ -987,6 +1011,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
typeComboBox.addItem(SyncType.GitHub)
|
typeComboBox.addItem(SyncType.GitHub)
|
||||||
typeComboBox.addItem(SyncType.GitLab)
|
typeComboBox.addItem(SyncType.GitLab)
|
||||||
typeComboBox.addItem(SyncType.Gitee)
|
typeComboBox.addItem(SyncType.Gitee)
|
||||||
|
typeComboBox.addItem(SyncType.WebDAV)
|
||||||
|
|
||||||
hostsCheckBox.isFocusable = false
|
hostsCheckBox.isFocusable = false
|
||||||
keysCheckBox.isFocusable = false
|
keysCheckBox.isFocusable = false
|
||||||
@@ -1005,7 +1030,31 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
tokenTextField.text = sync.token
|
tokenTextField.text = sync.token
|
||||||
domainTextField.trailingComponent = JButton(Icons.externalLink).apply {
|
domainTextField.trailingComponent = JButton(Icons.externalLink).apply {
|
||||||
addActionListener {
|
addActionListener {
|
||||||
Application.browse(URI.create("https://docs.gitlab.com/ee/api/snippets.html"))
|
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
||||||
|
Application.browse(URI.create("https://docs.gitlab.com/ee/api/snippets.html"))
|
||||||
|
|
||||||
|
} else if (typeComboBox.selectedItem == SyncType.WebDAV) {
|
||||||
|
val url = domainTextField.text
|
||||||
|
if (url.isNullOrBlank()) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
I18n.getString("termora.settings.sync.webdav.help")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val uri = URI.create(url)
|
||||||
|
val sb = StringBuilder()
|
||||||
|
sb.append(uri.scheme).append("://")
|
||||||
|
if (tokenTextField.password.isNotEmpty() && gistTextField.text.isNotBlank()) {
|
||||||
|
sb.append(String(tokenTextField.password)).append(":").append(gistTextField.text)
|
||||||
|
sb.append('@')
|
||||||
|
}
|
||||||
|
sb.append(uri.authority).append(uri.path)
|
||||||
|
if (!uri.query.isNullOrBlank()) {
|
||||||
|
sb.append('?').append(uri.query)
|
||||||
|
}
|
||||||
|
Application.browse(URI.create(sb.toString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1015,12 +1064,15 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
tokenTextField.trailingComponent = if (tokenTextField.password.isEmpty()) getTokenBtn else null
|
tokenTextField.trailingComponent = if (tokenTextField.password.isEmpty()) getTokenBtn else null
|
||||||
|
|
||||||
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
if (domainTextField.text.isBlank()) {
|
||||||
if (domainTextField.text.isBlank()) {
|
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
||||||
domainTextField.text = StringUtils.defaultIfBlank(sync.domain, "https://gitlab.com/api")
|
domainTextField.text = StringUtils.defaultIfBlank(sync.domain, "https://gitlab.com/api")
|
||||||
|
} else if (typeComboBox.selectedItem == SyncType.WebDAV) {
|
||||||
|
domainTextField.text = sync.domain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val lastSyncTime = sync.lastSyncTime
|
val lastSyncTime = sync.lastSyncTime
|
||||||
lastSyncTimeLabel.text = "${I18n.getString("termora.settings.sync.last-sync-time")}: ${
|
lastSyncTimeLabel.text = "${I18n.getString("termora.settings.sync.last-sync-time")}: ${
|
||||||
if (lastSyncTime > 0) DateFormatUtils.format(
|
if (lastSyncTime > 0) DateFormatUtils.format(
|
||||||
@@ -1069,17 +1121,37 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
val builder = FormBuilder.create().layout(layout).debug(false)
|
val builder = FormBuilder.create().layout(layout).debug(false)
|
||||||
val box = Box.createHorizontalBox()
|
val box = Box.createHorizontalBox()
|
||||||
box.add(typeComboBox)
|
box.add(typeComboBox)
|
||||||
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
if (typeComboBox.selectedItem == SyncType.GitLab || typeComboBox.selectedItem == SyncType.WebDAV) {
|
||||||
box.add(Box.createHorizontalStrut(4))
|
box.add(Box.createHorizontalStrut(4))
|
||||||
box.add(domainTextField)
|
box.add(domainTextField)
|
||||||
}
|
}
|
||||||
builder.add("${I18n.getString("termora.settings.sync.type")}:").xy(1, rows)
|
builder.add("${I18n.getString("termora.settings.sync.type")}:").xy(1, rows)
|
||||||
.add(box).xy(3, rows).apply { rows += step }
|
.add(box).xy(3, rows).apply { rows += step }
|
||||||
|
|
||||||
builder.add("${I18n.getString("termora.settings.sync.token")}:").xy(1, rows)
|
val isWebDAV = typeComboBox.selectedItem == SyncType.WebDAV
|
||||||
.add(tokenTextField).xy(3, rows).apply { rows += step }
|
|
||||||
.add("${I18n.getString("termora.settings.sync.gist")}:").xy(1, rows)
|
val tokenText = if (isWebDAV) {
|
||||||
.add(gistTextField).xy(3, rows).apply { rows += step }
|
I18n.getString("termora.new-host.general.username")
|
||||||
|
} else {
|
||||||
|
I18n.getString("termora.settings.sync.token")
|
||||||
|
}
|
||||||
|
|
||||||
|
val gistText = if (isWebDAV) {
|
||||||
|
I18n.getString("termora.new-host.general.password")
|
||||||
|
} else {
|
||||||
|
I18n.getString("termora.settings.sync.gist")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeComboBox.selectedItem == SyncType.Gitee || isWebDAV) {
|
||||||
|
gistTextField.trailingComponent = null
|
||||||
|
} else {
|
||||||
|
gistTextField.trailingComponent = visitGistBtn
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.add("${tokenText}:").xy(1, rows)
|
||||||
|
.add(if (isWebDAV) gistTextField else tokenTextField).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${gistText}:").xy(1, rows)
|
||||||
|
.add(if (isWebDAV) tokenTextField else gistTextField).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.sync.range")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.sync.range")}:").xy(1, rows)
|
||||||
.add(rangeBox).xy(3, rows).apply { rows += step }
|
.add(rangeBox).xy(3, rows).apply { rows += step }
|
||||||
// Sync buttons
|
// Sync buttons
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.apache.sshd.common.SshException
|
|||||||
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
||||||
import org.apache.sshd.common.global.KeepAliveHandler
|
import org.apache.sshd.common.global.KeepAliveHandler
|
||||||
import org.apache.sshd.common.kex.BuiltinDHFactories
|
import org.apache.sshd.common.kex.BuiltinDHFactories
|
||||||
|
import org.apache.sshd.common.keyprovider.KeyIdentityProvider
|
||||||
import org.apache.sshd.common.util.net.SshdSocketAddress
|
import org.apache.sshd.common.util.net.SshdSocketAddress
|
||||||
import org.apache.sshd.core.CoreModuleProperties
|
import org.apache.sshd.core.CoreModuleProperties
|
||||||
import org.apache.sshd.server.forward.AcceptAllForwardingFilter
|
import org.apache.sshd.server.forward.AcceptAllForwardingFilter
|
||||||
@@ -156,6 +157,11 @@ object SshClients {
|
|||||||
builder.hostConfigEntryResolver(HostConfigEntryResolver.EMPTY)
|
builder.hostConfigEntryResolver(HostConfigEntryResolver.EMPTY)
|
||||||
|
|
||||||
val sshClient = builder.build() as JGitSshClient
|
val sshClient = builder.build() as JGitSshClient
|
||||||
|
|
||||||
|
// https://github.com/TermoraDev/termora/issues/180
|
||||||
|
// JGit 会尝试读取本地的私钥或缓存的私钥
|
||||||
|
sshClient.keyIdentityProvider = KeyIdentityProvider { mutableListOf() }
|
||||||
|
|
||||||
val heartbeatInterval = max(host.options.heartbeatInterval, 3)
|
val heartbeatInterval = max(host.options.heartbeatInterval, 3)
|
||||||
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
|
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
|
||||||
CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true)
|
CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true)
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ class TerminalFactory private constructor() : Disposable {
|
|||||||
|
|
||||||
// terminal logger listener
|
// terminal logger listener
|
||||||
terminal.getTerminalModel().addDataListener(TerminalLoggerDataListener(terminal))
|
terminal.getTerminalModel().addDataListener(TerminalLoggerDataListener(terminal))
|
||||||
|
terminal.addTerminalListener(object : TerminalListener {
|
||||||
|
override fun onClose(terminal: Terminal) {
|
||||||
|
terminals.remove(terminal)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
terminals.add(terminal)
|
terminals.add(terminal)
|
||||||
return terminal
|
return terminal
|
||||||
@@ -51,6 +56,11 @@ class TerminalFactory private constructor() : Disposable {
|
|||||||
return colorPalette
|
return colorPalette
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun bell() {
|
||||||
|
if (config.beep) {
|
||||||
|
super.bell()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : Any> getData(key: DataKey<T>): T {
|
override fun <T : Any> getData(key: DataKey<T>): T {
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ class TerminalPanelFactory {
|
|||||||
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
||||||
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
||||||
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
||||||
|
Disposer.register(terminalPanel, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
terminalPanels.remove(terminalPanel)
|
||||||
|
}
|
||||||
|
})
|
||||||
terminalPanels.add(terminalPanel)
|
terminalPanels.add(terminalPanel)
|
||||||
return terminalPanel
|
return terminalPanel
|
||||||
}
|
}
|
||||||
@@ -47,4 +52,8 @@ class TerminalPanelFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeTerminalPanel(terminalPanel: TerminalPanel) {
|
||||||
|
terminalPanels.remove(terminalPanel)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -116,6 +116,7 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
Disposer.register(windowScope, terminalTabbed)
|
Disposer.register(windowScope, terminalTabbed)
|
||||||
add(terminalTabbed)
|
add(terminalTabbed)
|
||||||
|
|
||||||
|
dataProviderSupport.addData(DataProviders.TabbedPane, tabbedPane)
|
||||||
dataProviderSupport.addData(DataProviders.TermoraFrame, this)
|
dataProviderSupport.addData(DataProviders.TermoraFrame, this)
|
||||||
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
|
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import com.formdev.flatlaf.util.SystemInfo
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.event.WindowAdapter
|
import java.awt.event.WindowAdapter
|
||||||
import java.awt.event.WindowEvent
|
import java.awt.event.WindowEvent
|
||||||
import javax.swing.WindowConstants.DISPOSE_ON_CLOSE
|
import javax.swing.JOptionPane
|
||||||
|
import javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class TermoraFrameManager {
|
class TermoraFrameManager {
|
||||||
@@ -22,7 +23,7 @@ class TermoraFrameManager {
|
|||||||
val frame = TermoraFrame()
|
val frame = TermoraFrame()
|
||||||
registerCloseCallback(frame)
|
registerCloseCallback(frame)
|
||||||
frame.title = if (SystemInfo.isLinux) null else Application.getName()
|
frame.title = if (SystemInfo.isLinux) null else Application.getName()
|
||||||
frame.defaultCloseOperation = DISPOSE_ON_CLOSE
|
frame.defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
||||||
frame.setSize(1280, 800)
|
frame.setSize(1280, 800)
|
||||||
frame.setLocationRelativeTo(null)
|
frame.setLocationRelativeTo(null)
|
||||||
return frame
|
return frame
|
||||||
@@ -43,6 +44,21 @@ class TermoraFrameManager {
|
|||||||
this@TermoraFrameManager.dispose()
|
this@TermoraFrameManager.dispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun windowClosing(e: WindowEvent) {
|
||||||
|
if (ApplicationScope.windowScopes().size == 1) {
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
window,
|
||||||
|
I18n.getString("termora.quit-confirm", Application.getName()),
|
||||||
|
optionType = JOptionPane.YES_NO_OPTION,
|
||||||
|
) == JOptionPane.YES_OPTION
|
||||||
|
) {
|
||||||
|
window.dispose()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ class OutlinePasswordField(
|
|||||||
styleMap = mapOf(
|
styleMap = mapOf(
|
||||||
"showRevealButton" to true
|
"showRevealButton" to true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
putClientProperty("JPasswordField.cutCopyAllowed", true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import app.termora.Application.ohMyJson
|
|||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.time.DateFormatUtils
|
||||||
import org.commonmark.node.BulletList
|
import org.commonmark.node.BulletList
|
||||||
import org.commonmark.node.Heading
|
import org.commonmark.node.Heading
|
||||||
import org.commonmark.node.Paragraph
|
import org.commonmark.node.Paragraph
|
||||||
@@ -97,7 +98,14 @@ class UpdaterManager private constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val parser = Parser.builder().build()
|
val parser = Parser.builder().build()
|
||||||
val document = parser.parse("# ${name.trim()}\n${body.trim()}")
|
val document = parser.parse(
|
||||||
|
"# 🎉 ${name.trim()} (${
|
||||||
|
DateFormatUtils.format(
|
||||||
|
publishedDate,
|
||||||
|
"yyyy-MM-dd"
|
||||||
|
)
|
||||||
|
}) \n${body.trim()}"
|
||||||
|
)
|
||||||
val renderer = HtmlRenderer.builder()
|
val renderer = HtmlRenderer.builder()
|
||||||
.attributeProviderFactory {
|
.attributeProviderFactory {
|
||||||
AttributeProvider { node, _, attributes ->
|
AttributeProvider { node, _, attributes ->
|
||||||
@@ -106,7 +114,7 @@ class UpdaterManager private constructor() {
|
|||||||
attributes["style"] = "margin: 5px 0;"
|
attributes["style"] = "margin: 5px 0;"
|
||||||
} else if (node is BulletList) {
|
} else if (node is BulletList) {
|
||||||
attributes["style"] = "margin: 0 20px;"
|
attributes["style"] = "margin: 0 20px;"
|
||||||
}else if(node is Paragraph){
|
} else if (node is Paragraph) {
|
||||||
attributes["style"] = "margin: 0;"
|
attributes["style"] = "margin: 0;"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ object DataProviders {
|
|||||||
val Terminal = DataKey(app.termora.terminal.Terminal::class)
|
val Terminal = DataKey(app.termora.terminal.Terminal::class)
|
||||||
val PtyConnector = DataKey(app.termora.terminal.PtyConnector::class)
|
val PtyConnector = DataKey(app.termora.terminal.PtyConnector::class)
|
||||||
|
|
||||||
|
val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
|
||||||
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
|
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
|
||||||
val TerminalTab = DataKey(app.termora.TerminalTab::class)
|
val TerminalTab = DataKey(app.termora.TerminalTab::class)
|
||||||
val TerminalTabbedManager = DataKey(app.termora.TerminalTabbedManager::class)
|
val TerminalTabbedManager = DataKey(app.termora.TerminalTabbedManager::class)
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
import app.termora.LocalTerminalTab
|
import app.termora.*
|
||||||
import app.termora.OpenHostActionEvent
|
|
||||||
import app.termora.Protocol
|
|
||||||
import app.termora.SSHTerminalTab
|
|
||||||
|
|
||||||
class OpenHostAction : AnAction() {
|
class OpenHostAction : AnAction() {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -18,9 +15,11 @@ class OpenHostAction : AnAction() {
|
|||||||
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||||
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
|
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
|
||||||
|
|
||||||
val tab = if (evt.host.protocol == Protocol.SSH)
|
val tab = when (evt.host.protocol) {
|
||||||
SSHTerminalTab(windowScope, evt.host)
|
Protocol.SSH -> SSHTerminalTab(windowScope, evt.host)
|
||||||
else LocalTerminalTab(windowScope, evt.host)
|
Protocol.Serial -> SerialTerminalTab(windowScope, evt.host)
|
||||||
|
else -> LocalTerminalTab(windowScope, evt.host)
|
||||||
|
}
|
||||||
|
|
||||||
terminalTabbedManager.addTerminalTab(tab)
|
terminalTabbedManager.addTerminalTab(tab)
|
||||||
tab.start()
|
tab.start()
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class TerminalUserInteraction(
|
|||||||
prompt[i],
|
prompt[i],
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
dialog.setLocationRelativeTo(owner)
|
||||||
dialog.title = instruction ?: name ?: StringUtils.EMPTY
|
dialog.title = instruction ?: name ?: StringUtils.EMPTY
|
||||||
passwords[i] = dialog.getText()
|
passwords[i] = dialog.getText()
|
||||||
if (passwords[i].isBlank()) {
|
if (passwords[i].isBlank()) {
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package app.termora.keymap
|
package app.termora.keymap
|
||||||
|
|
||||||
import app.termora.ApplicationScope
|
import app.termora.*
|
||||||
import app.termora.Database
|
|
||||||
import app.termora.DialogWrapper
|
|
||||||
import app.termora.Disposable
|
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.findeverywhere.FindEverywhereAction
|
import app.termora.findeverywhere.FindEverywhereAction
|
||||||
@@ -17,6 +14,7 @@ import java.awt.event.KeyEvent
|
|||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.JDialog
|
import javax.swing.JDialog
|
||||||
import javax.swing.KeyStroke
|
import javax.swing.KeyStroke
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
class KeymapManager private constructor() : Disposable {
|
class KeymapManager private constructor() : Disposable {
|
||||||
|
|
||||||
@@ -32,8 +30,9 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
private val keymapKeyEventDispatcher = KeymapKeyEventDispatcher()
|
private val keymapKeyEventDispatcher = KeymapKeyEventDispatcher()
|
||||||
private val myKeyEventDispatcher = MyKeyEventDispatcher()
|
private val myKeyEventDispatcher = MyKeyEventDispatcher()
|
||||||
private val database get() = Database.getDatabase()
|
private val database get() = Database.getDatabase()
|
||||||
|
private val properties get() = database.properties
|
||||||
private val keymaps = linkedMapOf<String, Keymap>()
|
private val keymaps = linkedMapOf<String, Keymap>()
|
||||||
private val activeKeymap get() = database.properties.getString("Keymap.Active")
|
private val activeKeymap get() = properties.getString("Keymap.Active")
|
||||||
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
|
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -146,20 +145,39 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated(message = "Deprecated")
|
||||||
private inner class MyKeyEventDispatcher : KeyEventDispatcher {
|
private inner class MyKeyEventDispatcher : KeyEventDispatcher {
|
||||||
// double shift
|
// double shift
|
||||||
private var lastTime = -1L
|
private var lastTime = -1L
|
||||||
|
private val findEverywhereAction
|
||||||
|
get() = ActionManager.getInstance().getAction(FindEverywhereAction.FIND_EVERYWHERE)
|
||||||
|
private val deprecatedKey by lazy { "${Application.getVersion()}.FindEverywhereActionDeprecated" }
|
||||||
|
|
||||||
|
|
||||||
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
|
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
|
||||||
if (e.keyCode == KeyEvent.VK_SHIFT && e.id == KeyEvent.KEY_PRESSED) {
|
if (e.keyCode == KeyEvent.VK_SHIFT && e.id == KeyEvent.KEY_PRESSED) {
|
||||||
val owner = AnActionEvent(e.source, StringUtils.EMPTY, e).getData(DataProviders.TermoraFrame)
|
val evt = AnActionEvent(e.source, StringUtils.EMPTY, e)
|
||||||
?: return false
|
val owner = evt.getData(DataProviders.TermoraFrame) ?: return false
|
||||||
if (keyboardFocusManager.focusedWindow == owner) {
|
if (keyboardFocusManager.focusedWindow == owner) {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
if (now - 250 < lastTime) {
|
if (now - 250 < lastTime) {
|
||||||
app.termora.actions.ActionManager.getInstance()
|
if (!properties.getString(deprecatedKey, "false").toBoolean()) {
|
||||||
.getAction(FindEverywhereAction.FIND_EVERYWHERE)
|
properties.putString(deprecatedKey, "true")
|
||||||
?.actionPerformed(AnActionEvent(e.source, StringUtils.EMPTY, e))
|
val shortcut = getActiveKeymap().getShortcut(FindEverywhereAction.FIND_EVERYWHERE)
|
||||||
|
.firstOrNull()
|
||||||
|
if (shortcut == null) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
I18n.getString("termora.find-everywhere.double-shift-deprecated")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
I18n.getString("termora.find-everywhere.double-shift-deprecated-instead", shortcut)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SwingUtilities.invokeLater { findEverywhereAction?.actionPerformed(evt) }
|
||||||
}
|
}
|
||||||
lastTime = now
|
lastTime = now
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package app.termora.keymgr
|
|||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.AES.decodeBase64
|
import app.termora.AES.decodeBase64
|
||||||
|
import app.termora.actions.AnAction
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.native.FileChooser
|
import app.termora.native.FileChooser
|
||||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
import com.formdev.flatlaf.extras.components.FlatTable
|
import com.formdev.flatlaf.extras.components.FlatTable
|
||||||
@@ -48,6 +51,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
private val exportBtn = JButton(I18n.getString("termora.keymgr.export"))
|
private val exportBtn = JButton(I18n.getString("termora.keymgr.export"))
|
||||||
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
|
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
|
||||||
private val deleteBtn = JButton(I18n.getString("termora.remove"))
|
private val deleteBtn = JButton(I18n.getString("termora.remove"))
|
||||||
|
private val sshCopyIdBtn = JButton("ssh-copy-id")
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -59,6 +63,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
|
|
||||||
exportBtn.isEnabled = false
|
exportBtn.isEnabled = false
|
||||||
editBtn.isEnabled = false
|
editBtn.isEnabled = false
|
||||||
|
sshCopyIdBtn.isEnabled = false
|
||||||
deleteBtn.isEnabled = false
|
deleteBtn.isEnabled = false
|
||||||
|
|
||||||
keyPairTableModel.addColumn(I18n.getString("termora.keymgr.table.name"))
|
keyPairTableModel.addColumn(I18n.getString("termora.keymgr.table.name"))
|
||||||
@@ -75,7 +80,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
val formMargin = "4dlu"
|
val formMargin = "4dlu"
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"default:grow",
|
"default:grow",
|
||||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, 16dlu, pref"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rows = 1
|
var rows = 1
|
||||||
@@ -91,6 +96,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
.add(importBtn).xy(1, rows).apply { rows += step }
|
.add(importBtn).xy(1, rows).apply { rows += step }
|
||||||
.add(exportBtn).xy(1, rows).apply { rows += step }
|
.add(exportBtn).xy(1, rows).apply { rows += step }
|
||||||
.add(deleteBtn).xy(1, rows).apply { rows += step }
|
.add(deleteBtn).xy(1, rows).apply { rows += step }
|
||||||
|
.add(sshCopyIdBtn).xy(1, rows).apply { rows += step }
|
||||||
.build(), BorderLayout.EAST)
|
.build(), BorderLayout.EAST)
|
||||||
border = BorderFactory.createEmptyBorder(if (SystemInfo.isWindows || SystemInfo.isLinux) 6 else 0, 12, 12, 12)
|
border = BorderFactory.createEmptyBorder(if (SystemInfo.isWindows || SystemInfo.isLinux) 6 else 0, 12, 12, 12)
|
||||||
|
|
||||||
@@ -175,13 +181,48 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
sshCopyIdBtn.addActionListener(object : AnAction() {
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
sshCopyId(evt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
keyPairTable.selectionModel.addListSelectionListener {
|
keyPairTable.selectionModel.addListSelectionListener {
|
||||||
exportBtn.isEnabled = keyPairTable.selectedRowCount > 0
|
exportBtn.isEnabled = keyPairTable.selectedRowCount > 0
|
||||||
editBtn.isEnabled = exportBtn.isEnabled
|
editBtn.isEnabled = exportBtn.isEnabled
|
||||||
deleteBtn.isEnabled = exportBtn.isEnabled
|
deleteBtn.isEnabled = exportBtn.isEnabled
|
||||||
|
sshCopyIdBtn.isEnabled = exportBtn.isEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sshCopyId(evt: AnActionEvent) {
|
||||||
|
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
|
||||||
|
val keyPairs = keyPairTable.selectedRows.map { keyPairTableModel.getOhKeyPair(it) }
|
||||||
|
val publicKeys = mutableListOf<Pair<String, String>>()
|
||||||
|
for (keyPair in keyPairs) {
|
||||||
|
val publicKey = OhKeyPairKeyPairProvider.generateKeyPair(keyPair).public
|
||||||
|
val baos = ByteArrayOutputStream()
|
||||||
|
OpenSSHKeyPairResourceWriter.INSTANCE.writePublicKey(publicKey, keyPair.name, baos)
|
||||||
|
publicKeys.add(Pair(keyPair.name, baos.toString(Charsets.UTF_8)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publicKeys.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val owner = SwingUtilities.getWindowAncestor(this) ?: return
|
||||||
|
val hostTreeDialog = HostTreeDialog(owner) {
|
||||||
|
it.protocol == Protocol.SSH
|
||||||
|
}
|
||||||
|
hostTreeDialog.isVisible = true
|
||||||
|
val hosts = hostTreeDialog.hosts
|
||||||
|
if (hosts.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SSHCopyIdDialog(owner, windowScope, hosts, publicKeys).start()
|
||||||
|
}
|
||||||
|
|
||||||
private fun exportKeyPairs(file: File, keyPairs: List<OhKeyPair>) {
|
private fun exportKeyPairs(file: File, keyPairs: List<OhKeyPair>) {
|
||||||
file.outputStream().use { fis ->
|
file.outputStream().use { fis ->
|
||||||
val names = mutableMapOf<String, Int>()
|
val names = mutableMapOf<String, Int>()
|
||||||
|
|||||||
197
src/main/kotlin/app/termora/keymgr/SSHCopyIdDialog.kt
Normal file
197
src/main/kotlin/app/termora/keymgr/SSHCopyIdDialog.kt
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
package app.termora.keymgr
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
|
import app.termora.terminal.ControlCharacters
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
|
import app.termora.terminal.PtyConnectorDelegate
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.sshd.client.SshClient
|
||||||
|
import org.apache.sshd.client.channel.ClientChannelEvent
|
||||||
|
import org.apache.sshd.client.session.ClientSession
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.AbstractAction
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.UIManager
|
||||||
|
|
||||||
|
class SSHCopyIdDialog(
|
||||||
|
owner: Window,
|
||||||
|
private val windowScope: WindowScope,
|
||||||
|
private val hosts: List<Host>,
|
||||||
|
// key: name , value: public key
|
||||||
|
private val publicKeys: List<Pair<String, String>>,
|
||||||
|
) : DialogWrapper(owner) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(SSHCopyIdDialog::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val terminalPanelFactory = TerminalPanelFactory.getInstance(windowScope)
|
||||||
|
private val terminal by lazy {
|
||||||
|
TerminalFactory.getInstance(windowScope).createTerminal().apply {
|
||||||
|
getTerminalModel().setData(DataKey.ShowCursor, false)
|
||||||
|
getTerminalModel().setData(DataKey.AutoNewline, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val terminalPanel by lazy {
|
||||||
|
terminalPanelFactory.createTerminalPanel(terminal, PtyConnectorDelegate())
|
||||||
|
}
|
||||||
|
private val coroutineScope = CoroutineScope(Job() + Dispatchers.IO)
|
||||||
|
|
||||||
|
init {
|
||||||
|
size = Dimension(UIManager.getInt("Dialog.width") - 100, UIManager.getInt("Dialog.height") - 100)
|
||||||
|
isModal = true
|
||||||
|
title = "SSH Copy ID"
|
||||||
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
|
Disposer.register(disposable, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
coroutineScope.cancel()
|
||||||
|
terminal.close()
|
||||||
|
Disposer.dispose(terminalPanel)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
return terminalPanel
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
coroutineScope.launch {
|
||||||
|
try {
|
||||||
|
doStart()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun createActions(): List<AbstractAction> {
|
||||||
|
return listOf(CancelAction())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun magenta(text: Any): String {
|
||||||
|
return "${ControlCharacters.ESC}[35m${text}${ControlCharacters.ESC}[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cyan(text: Any): String {
|
||||||
|
return "${ControlCharacters.ESC}[36m${text}${ControlCharacters.ESC}[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun red(text: Any): String {
|
||||||
|
return "${ControlCharacters.ESC}[31m${text}${ControlCharacters.ESC}[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun green(text: Any): String {
|
||||||
|
return "${ControlCharacters.ESC}[32m${text}${ControlCharacters.ESC}[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun doStart() {
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
terminal.write(
|
||||||
|
I18n.getString(
|
||||||
|
"termora.keymgr.ssh-copy-id.number",
|
||||||
|
magenta(hosts.size),
|
||||||
|
magenta(publicKeys.size)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
terminal.getDocument().newline()
|
||||||
|
terminal.getDocument().newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
var myClient: SshClient? = null
|
||||||
|
var mySession: ClientSession? = null
|
||||||
|
val timeout = Duration.ofMinutes(1)
|
||||||
|
|
||||||
|
// 获取公钥名称最长的
|
||||||
|
val publicKeyNameLength = publicKeys.maxOfOrNull { it.first.length } ?: 0
|
||||||
|
|
||||||
|
for (index in hosts.indices) {
|
||||||
|
if (!coroutineScope.isActive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val host = hosts[index]
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
terminal.write("[${cyan(index + 1)}/${cyan(hosts.size)}] ${host.name}")
|
||||||
|
terminal.getDocument().newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((j, e) in publicKeys.withIndex()) {
|
||||||
|
if (!coroutineScope.isActive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val publicKeyName = e.first.padEnd(publicKeyNameLength, ' ')
|
||||||
|
val publicKey = e.second
|
||||||
|
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
// @formatter:off
|
||||||
|
terminal.write("\t[${cyan(j + 1)}/${cyan(publicKeys.size)}] $publicKeyName ${I18n.getString("termora.transport.sftp.connecting")}")
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val client = SshClients.openClient(host).apply { myClient = this }
|
||||||
|
client.userInteraction = TerminalUserInteraction(owner)
|
||||||
|
val session = SshClients.openSession(host, client).apply { mySession = this }
|
||||||
|
val channel =
|
||||||
|
session.createExecChannel("mkdir -p ~/.ssh && grep -qxF \"$publicKey\" ~/.ssh/authorized_keys || echo \"$publicKey\" >> ~/.ssh/authorized_keys")
|
||||||
|
val baos = ByteArrayOutputStream()
|
||||||
|
channel.out = baos
|
||||||
|
if (channel.open().verify(timeout).await(timeout)) {
|
||||||
|
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), timeout);
|
||||||
|
}
|
||||||
|
if (channel.exitStatus != 0) {
|
||||||
|
throw IllegalStateException("Server response: ${channel.exitStatus}")
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
terminal.getDocument().eraseInLine(2)
|
||||||
|
// @formatter:off
|
||||||
|
terminal.write("\r\t[${cyan(j + 1)}/${cyan(publicKeys.size)}] $publicKeyName ${green(I18n.getString("termora.keymgr.ssh-copy-id.successful"))}")
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
terminal.getDocument().eraseInLine(2)
|
||||||
|
// @formatter:off
|
||||||
|
terminal.write("\r\t[${cyan(j + 1)}/${cyan(publicKeys.size)}] $publicKeyName ${red("${I18n.getString("termora.keymgr.ssh-copy-id.failed")}: ${e.message}")}")
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
IOUtils.closeQuietly(mySession)
|
||||||
|
IOUtils.closeQuietly(myClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
terminal.getDocument().newline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
terminal.getDocument().newline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
terminal.write(I18n.getString("termora.keymgr.ssh-copy-id.end"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,42 +1,19 @@
|
|||||||
package app.termora.sync
|
package app.termora.sync
|
||||||
|
|
||||||
import app.termora.*
|
|
||||||
import app.termora.AES.CBC.aesCBCDecrypt
|
|
||||||
import app.termora.AES.CBC.aesCBCEncrypt
|
|
||||||
import app.termora.AES.decodeBase64
|
|
||||||
import app.termora.AES.encodeBase64String
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.highlight.KeywordHighlight
|
import app.termora.ResponseException
|
||||||
import app.termora.highlight.KeywordHighlightManager
|
|
||||||
import app.termora.keymap.Keymap
|
|
||||||
import app.termora.keymap.KeymapManager
|
|
||||||
import app.termora.keymgr.KeyManager
|
|
||||||
import app.termora.keymgr.OhKeyPair
|
|
||||||
import app.termora.macro.Macro
|
|
||||||
import app.termora.macro.MacroManager
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.apache.commons.lang3.ArrayUtils
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import javax.swing.SwingUtilities
|
|
||||||
|
|
||||||
abstract class GitSyncer : Syncer {
|
abstract class GitSyncer : SafetySyncer() {
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(GitSyncer::class.java)
|
private val log = LoggerFactory.getLogger(GitSyncer::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected val description = "${Application.getName()} config"
|
|
||||||
protected val httpClient get() = Application.httpClient
|
|
||||||
protected val hostManager get() = HostManager.getInstance()
|
|
||||||
protected val keyManager get() = KeyManager.getInstance()
|
|
||||||
protected val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
|
||||||
protected val macroManager get() = MacroManager.getInstance()
|
|
||||||
protected val keymapManager get() = KeymapManager.getInstance()
|
|
||||||
|
|
||||||
override fun pull(config: SyncConfig): GistResponse {
|
override fun pull(config: SyncConfig): GistResponse {
|
||||||
|
|
||||||
if (log.isInfoEnabled) {
|
if (log.isInfoEnabled) {
|
||||||
@@ -92,174 +69,6 @@ abstract class GitSyncer : Syncer {
|
|||||||
return gistResponse
|
return gistResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeHosts(text: String, config: SyncConfig) {
|
|
||||||
// aes key
|
|
||||||
val key = getKey(config)
|
|
||||||
val encryptedHosts = ohMyJson.decodeFromString<List<EncryptedHost>>(text)
|
|
||||||
val hosts = hostManager.hosts().associateBy { it.id }
|
|
||||||
|
|
||||||
for (encryptedHost in encryptedHosts) {
|
|
||||||
val oldHost = hosts[encryptedHost.id]
|
|
||||||
|
|
||||||
// 如果一样,则无需配置
|
|
||||||
if (oldHost != null) {
|
|
||||||
if (oldHost.updateDate == encryptedHost.updateDate) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// aes iv
|
|
||||||
val iv = getIv(encryptedHost.id)
|
|
||||||
val host = Host(
|
|
||||||
id = encryptedHost.id,
|
|
||||||
name = encryptedHost.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
protocol = Protocol.valueOf(
|
|
||||||
encryptedHost.protocol.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
|
||||||
),
|
|
||||||
host = encryptedHost.host.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
port = encryptedHost.port.decodeBase64().aesCBCDecrypt(key, iv)
|
|
||||||
.decodeToString().toIntOrNull() ?: 0,
|
|
||||||
username = encryptedHost.username.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
remark = encryptedHost.remark.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
authentication = ohMyJson.decodeFromString(
|
|
||||||
encryptedHost.authentication.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
|
||||||
),
|
|
||||||
proxy = ohMyJson.decodeFromString(
|
|
||||||
encryptedHost.proxy.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
|
||||||
),
|
|
||||||
options = ohMyJson.decodeFromString(
|
|
||||||
encryptedHost.options.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
|
||||||
),
|
|
||||||
tunnelings = ohMyJson.decodeFromString(
|
|
||||||
encryptedHost.tunnelings.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
|
||||||
),
|
|
||||||
sort = encryptedHost.sort,
|
|
||||||
parentId = encryptedHost.parentId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
ownerId = encryptedHost.ownerId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
creatorId = encryptedHost.creatorId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
createDate = encryptedHost.createDate,
|
|
||||||
updateDate = encryptedHost.updateDate,
|
|
||||||
deleted = encryptedHost.deleted
|
|
||||||
)
|
|
||||||
SwingUtilities.invokeLater { hostManager.addHost(host) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (log.isWarnEnabled) {
|
|
||||||
log.warn("Decode host: ${encryptedHost.id} failed. error: {}", e.message, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Decode hosts: {}", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeKeys(text: String, config: SyncConfig) {
|
|
||||||
// aes key
|
|
||||||
val key = getKey(config)
|
|
||||||
val encryptedKeys = ohMyJson.decodeFromString<List<OhKeyPair>>(text)
|
|
||||||
|
|
||||||
for (encryptedKey in encryptedKeys) {
|
|
||||||
try {
|
|
||||||
// aes iv
|
|
||||||
val iv = getIv(encryptedKey.id)
|
|
||||||
val keyPair = OhKeyPair(
|
|
||||||
id = encryptedKey.id,
|
|
||||||
publicKey = encryptedKey.publicKey.decodeBase64().aesCBCDecrypt(key, iv).encodeBase64String(),
|
|
||||||
privateKey = encryptedKey.privateKey.decodeBase64().aesCBCDecrypt(key, iv).encodeBase64String(),
|
|
||||||
type = encryptedKey.type.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
name = encryptedKey.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
remark = encryptedKey.remark.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
length = encryptedKey.length,
|
|
||||||
sort = encryptedKey.sort
|
|
||||||
)
|
|
||||||
SwingUtilities.invokeLater { keyManager.addOhKeyPair(keyPair) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (log.isWarnEnabled) {
|
|
||||||
log.warn("Decode key: ${encryptedKey.id} failed. error: {}", e.message, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Decode keys: {}", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeKeywordHighlights(text: String, config: SyncConfig) {
|
|
||||||
// aes key
|
|
||||||
val key = getKey(config)
|
|
||||||
val encryptedKeywordHighlights = ohMyJson.decodeFromString<List<KeywordHighlight>>(text)
|
|
||||||
|
|
||||||
for (e in encryptedKeywordHighlights) {
|
|
||||||
try {
|
|
||||||
// aes iv
|
|
||||||
val iv = getIv(e.id)
|
|
||||||
keywordHighlightManager.addKeywordHighlight(
|
|
||||||
e.copy(
|
|
||||||
keyword = e.keyword.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
description = e.description.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
if (log.isWarnEnabled) {
|
|
||||||
log.warn("Decode KeywordHighlight: ${e.id} failed. error: {}", ex.message, ex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Decode KeywordHighlight: {}", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeMacros(text: String, config: SyncConfig) {
|
|
||||||
// aes key
|
|
||||||
val key = getKey(config)
|
|
||||||
val encryptedMacros = ohMyJson.decodeFromString<List<Macro>>(text)
|
|
||||||
|
|
||||||
for (e in encryptedMacros) {
|
|
||||||
try {
|
|
||||||
// aes iv
|
|
||||||
val iv = getIv(e.id)
|
|
||||||
macroManager.addMacro(
|
|
||||||
e.copy(
|
|
||||||
name = e.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
macro = e.macro.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
if (log.isWarnEnabled) {
|
|
||||||
log.warn("Decode Macro: ${e.id} failed. error: {}", ex.message, ex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Decode Macros: {}", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeKeymaps(text: String, config: SyncConfig) {
|
|
||||||
|
|
||||||
for (keymap in ohMyJson.decodeFromString<List<JsonObject>>(text).mapNotNull { Keymap.fromJSON(it) }) {
|
|
||||||
keymapManager.addKeymap(keymap)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Decode Keymaps: {}", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getKey(config: SyncConfig): ByteArray {
|
|
||||||
return ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getIv(id: String): ByteArray {
|
|
||||||
return ArrayUtils.subarray(id.padEnd(16, '0').toByteArray(), 0, 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun push(config: SyncConfig): GistResponse {
|
override fun push(config: SyncConfig): GistResponse {
|
||||||
val gistFiles = mutableListOf<GistFile>()
|
val gistFiles = mutableListOf<GistFile>()
|
||||||
@@ -268,62 +77,16 @@ abstract class GitSyncer : Syncer {
|
|||||||
|
|
||||||
// Hosts
|
// Hosts
|
||||||
if (config.ranges.contains(SyncRange.Hosts)) {
|
if (config.ranges.contains(SyncRange.Hosts)) {
|
||||||
val encryptedHosts = mutableListOf<EncryptedHost>()
|
val hostsContent = encodeHosts(key)
|
||||||
for (host in hostManager.hosts()) {
|
|
||||||
// aes iv
|
|
||||||
val iv = ArrayUtils.subarray(host.id.padEnd(16, '0').toByteArray(), 0, 16)
|
|
||||||
val encryptedHost = EncryptedHost()
|
|
||||||
encryptedHost.id = host.id
|
|
||||||
encryptedHost.name = host.name.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.protocol = host.protocol.name.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.host = host.host.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.port = "${host.port}".aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.username = host.username.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.remark = host.remark.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.authentication = ohMyJson.encodeToString(host.authentication)
|
|
||||||
.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.proxy = ohMyJson.encodeToString(host.proxy).aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.options =
|
|
||||||
ohMyJson.encodeToString(host.options).aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.tunnelings =
|
|
||||||
ohMyJson.encodeToString(host.tunnelings).aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.sort = host.sort
|
|
||||||
encryptedHost.deleted = host.deleted
|
|
||||||
encryptedHost.parentId = host.parentId.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.ownerId = host.ownerId.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.creatorId = host.creatorId.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
encryptedHost.createDate = host.createDate
|
|
||||||
encryptedHost.updateDate = host.updateDate
|
|
||||||
encryptedHosts.add(encryptedHost)
|
|
||||||
}
|
|
||||||
|
|
||||||
val hostsContent = ohMyJson.encodeToString(encryptedHosts)
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Push encryptedHosts: {}", hostsContent)
|
log.debug("Push encryptedHosts: {}", hostsContent)
|
||||||
}
|
}
|
||||||
gistFiles.add(GistFile("Hosts", hostsContent))
|
gistFiles.add(GistFile("Hosts", hostsContent))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyPairs
|
// KeyPairs
|
||||||
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
||||||
val encryptedKeys = mutableListOf<OhKeyPair>()
|
val keysContent = encodeKeys(key)
|
||||||
for (keyPair in keyManager.getOhKeyPairs()) {
|
|
||||||
// aes iv
|
|
||||||
val iv = ArrayUtils.subarray(keyPair.id.padEnd(16, '0').toByteArray(), 0, 16)
|
|
||||||
val encryptedKeyPair = OhKeyPair(
|
|
||||||
id = keyPair.id,
|
|
||||||
publicKey = keyPair.publicKey.decodeBase64().aesCBCEncrypt(key, iv).encodeBase64String(),
|
|
||||||
privateKey = keyPair.privateKey.decodeBase64().aesCBCEncrypt(key, iv).encodeBase64String(),
|
|
||||||
type = keyPair.type.aesCBCEncrypt(key, iv).encodeBase64String(),
|
|
||||||
name = keyPair.name.aesCBCEncrypt(key, iv).encodeBase64String(),
|
|
||||||
remark = keyPair.remark.aesCBCEncrypt(key, iv).encodeBase64String(),
|
|
||||||
length = keyPair.length,
|
|
||||||
sort = keyPair.sort
|
|
||||||
)
|
|
||||||
encryptedKeys.add(encryptedKeyPair)
|
|
||||||
}
|
|
||||||
val keysContent = ohMyJson.encodeToString(encryptedKeys)
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Push encryptedKeys: {}", keysContent)
|
log.debug("Push encryptedKeys: {}", keysContent)
|
||||||
}
|
}
|
||||||
@@ -332,17 +95,7 @@ abstract class GitSyncer : Syncer {
|
|||||||
|
|
||||||
// Highlights
|
// Highlights
|
||||||
if (config.ranges.contains(SyncRange.KeywordHighlights)) {
|
if (config.ranges.contains(SyncRange.KeywordHighlights)) {
|
||||||
val keywordHighlights = mutableListOf<KeywordHighlight>()
|
val keywordHighlightsContent = encodeKeywordHighlights(key)
|
||||||
for (keywordHighlight in keywordHighlightManager.getKeywordHighlights()) {
|
|
||||||
// aes iv
|
|
||||||
val iv = getIv(keywordHighlight.id)
|
|
||||||
val encryptedKeyPair = keywordHighlight.copy(
|
|
||||||
keyword = keywordHighlight.keyword.aesCBCEncrypt(key, iv).encodeBase64String(),
|
|
||||||
description = keywordHighlight.description.aesCBCEncrypt(key, iv).encodeBase64String(),
|
|
||||||
)
|
|
||||||
keywordHighlights.add(encryptedKeyPair)
|
|
||||||
}
|
|
||||||
val keywordHighlightsContent = ohMyJson.encodeToString(keywordHighlights)
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Push keywordHighlights: {}", keywordHighlightsContent)
|
log.debug("Push keywordHighlights: {}", keywordHighlightsContent)
|
||||||
}
|
}
|
||||||
@@ -351,17 +104,7 @@ abstract class GitSyncer : Syncer {
|
|||||||
|
|
||||||
// Macros
|
// Macros
|
||||||
if (config.ranges.contains(SyncRange.Macros)) {
|
if (config.ranges.contains(SyncRange.Macros)) {
|
||||||
val macros = mutableListOf<Macro>()
|
val macrosContent = encodeMacros(key)
|
||||||
for (macro in macroManager.getMacros()) {
|
|
||||||
val iv = getIv(macro.id)
|
|
||||||
macros.add(
|
|
||||||
macro.copy(
|
|
||||||
name = macro.name.aesCBCEncrypt(key, iv).encodeBase64String(),
|
|
||||||
macro = macro.macro.aesCBCEncrypt(key, iv).encodeBase64String()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val macrosContent = ohMyJson.encodeToString(macros)
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Push macros: {}", macrosContent)
|
log.debug("Push macros: {}", macrosContent)
|
||||||
}
|
}
|
||||||
@@ -370,22 +113,11 @@ abstract class GitSyncer : Syncer {
|
|||||||
|
|
||||||
// Keymap
|
// Keymap
|
||||||
if (config.ranges.contains(SyncRange.Keymap)) {
|
if (config.ranges.contains(SyncRange.Keymap)) {
|
||||||
val keymaps = mutableListOf<JsonObject>()
|
val keymapsContent = encodeKeymaps()
|
||||||
for (keymap in keymapManager.getKeymaps()) {
|
if (log.isDebugEnabled) {
|
||||||
// 只读的是内置的
|
log.debug("Push keymaps: {}", keymapsContent)
|
||||||
if (keymap.isReadonly) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
keymaps.add(keymap.toJSONObject())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keymaps.isNotEmpty()) {
|
|
||||||
val keymapsContent = ohMyJson.encodeToString(keymaps)
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Push keymaps: {}", keymapsContent)
|
|
||||||
}
|
|
||||||
gistFiles.add(GistFile("Keymaps", keymapsContent))
|
|
||||||
}
|
}
|
||||||
|
gistFiles.add(GistFile("Keymaps", keymapsContent))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gistFiles.isEmpty()) {
|
if (gistFiles.isEmpty()) {
|
||||||
|
|||||||
299
src/main/kotlin/app/termora/sync/SafetySyncer.kt
Normal file
299
src/main/kotlin/app/termora/sync/SafetySyncer.kt
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
package app.termora.sync
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.AES.CBC.aesCBCDecrypt
|
||||||
|
import app.termora.AES.CBC.aesCBCEncrypt
|
||||||
|
import app.termora.AES.decodeBase64
|
||||||
|
import app.termora.AES.encodeBase64String
|
||||||
|
import app.termora.Application.ohMyJson
|
||||||
|
import app.termora.highlight.KeywordHighlight
|
||||||
|
import app.termora.highlight.KeywordHighlightManager
|
||||||
|
import app.termora.keymap.Keymap
|
||||||
|
import app.termora.keymap.KeymapManager
|
||||||
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.keymgr.OhKeyPair
|
||||||
|
import app.termora.macro.Macro
|
||||||
|
import app.termora.macro.MacroManager
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
abstract class SafetySyncer : Syncer {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(SafetySyncer::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val description = "${Application.getName()} config"
|
||||||
|
protected val httpClient get() = Application.httpClient
|
||||||
|
protected val hostManager get() = HostManager.getInstance()
|
||||||
|
protected val keyManager get() = KeyManager.getInstance()
|
||||||
|
protected val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
||||||
|
protected val macroManager get() = MacroManager.getInstance()
|
||||||
|
protected val keymapManager get() = KeymapManager.getInstance()
|
||||||
|
|
||||||
|
protected fun decodeHosts(text: String, config: SyncConfig) {
|
||||||
|
// aes key
|
||||||
|
val key = getKey(config)
|
||||||
|
val encryptedHosts = ohMyJson.decodeFromString<List<EncryptedHost>>(text)
|
||||||
|
val hosts = hostManager.hosts().associateBy { it.id }
|
||||||
|
|
||||||
|
for (encryptedHost in encryptedHosts) {
|
||||||
|
val oldHost = hosts[encryptedHost.id]
|
||||||
|
|
||||||
|
// 如果一样,则无需配置
|
||||||
|
if (oldHost != null) {
|
||||||
|
if (oldHost.updateDate == encryptedHost.updateDate) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// aes iv
|
||||||
|
val iv = getIv(encryptedHost.id)
|
||||||
|
val host = Host(
|
||||||
|
id = encryptedHost.id,
|
||||||
|
name = encryptedHost.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
protocol = Protocol.valueOf(
|
||||||
|
encryptedHost.protocol.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
||||||
|
),
|
||||||
|
host = encryptedHost.host.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
port = encryptedHost.port.decodeBase64().aesCBCDecrypt(key, iv)
|
||||||
|
.decodeToString().toIntOrNull() ?: 0,
|
||||||
|
username = encryptedHost.username.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
remark = encryptedHost.remark.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
authentication = ohMyJson.decodeFromString(
|
||||||
|
encryptedHost.authentication.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
||||||
|
),
|
||||||
|
proxy = ohMyJson.decodeFromString(
|
||||||
|
encryptedHost.proxy.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
||||||
|
),
|
||||||
|
options = ohMyJson.decodeFromString(
|
||||||
|
encryptedHost.options.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
||||||
|
),
|
||||||
|
tunnelings = ohMyJson.decodeFromString(
|
||||||
|
encryptedHost.tunnelings.decodeBase64().aesCBCDecrypt(key, iv).decodeToString()
|
||||||
|
),
|
||||||
|
sort = encryptedHost.sort,
|
||||||
|
parentId = encryptedHost.parentId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
ownerId = encryptedHost.ownerId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
creatorId = encryptedHost.creatorId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
createDate = encryptedHost.createDate,
|
||||||
|
updateDate = encryptedHost.updateDate,
|
||||||
|
deleted = encryptedHost.deleted
|
||||||
|
)
|
||||||
|
SwingUtilities.invokeLater { hostManager.addHost(host) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn("Decode host: ${encryptedHost.id} failed. error: {}", e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Decode hosts: {}", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun encodeHosts(key: ByteArray): String {
|
||||||
|
val encryptedHosts = mutableListOf<EncryptedHost>()
|
||||||
|
for (host in hostManager.hosts()) {
|
||||||
|
// aes iv
|
||||||
|
val iv = ArrayUtils.subarray(host.id.padEnd(16, '0').toByteArray(), 0, 16)
|
||||||
|
val encryptedHost = EncryptedHost()
|
||||||
|
encryptedHost.id = host.id
|
||||||
|
encryptedHost.name = host.name.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.protocol = host.protocol.name.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.host = host.host.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.port = "${host.port}".aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.username = host.username.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.remark = host.remark.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.authentication = ohMyJson.encodeToString(host.authentication)
|
||||||
|
.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.proxy = ohMyJson.encodeToString(host.proxy).aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.options =
|
||||||
|
ohMyJson.encodeToString(host.options).aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.tunnelings =
|
||||||
|
ohMyJson.encodeToString(host.tunnelings).aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.sort = host.sort
|
||||||
|
encryptedHost.deleted = host.deleted
|
||||||
|
encryptedHost.parentId = host.parentId.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.ownerId = host.ownerId.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.creatorId = host.creatorId.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
encryptedHost.createDate = host.createDate
|
||||||
|
encryptedHost.updateDate = host.updateDate
|
||||||
|
encryptedHosts.add(encryptedHost)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ohMyJson.encodeToString(encryptedHosts)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun decodeKeys(text: String, config: SyncConfig) {
|
||||||
|
// aes key
|
||||||
|
val key = getKey(config)
|
||||||
|
val encryptedKeys = ohMyJson.decodeFromString<List<OhKeyPair>>(text)
|
||||||
|
|
||||||
|
for (encryptedKey in encryptedKeys) {
|
||||||
|
try {
|
||||||
|
// aes iv
|
||||||
|
val iv = getIv(encryptedKey.id)
|
||||||
|
val keyPair = OhKeyPair(
|
||||||
|
id = encryptedKey.id,
|
||||||
|
publicKey = encryptedKey.publicKey.decodeBase64().aesCBCDecrypt(key, iv).encodeBase64String(),
|
||||||
|
privateKey = encryptedKey.privateKey.decodeBase64().aesCBCDecrypt(key, iv).encodeBase64String(),
|
||||||
|
type = encryptedKey.type.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
name = encryptedKey.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
remark = encryptedKey.remark.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
length = encryptedKey.length,
|
||||||
|
sort = encryptedKey.sort
|
||||||
|
)
|
||||||
|
SwingUtilities.invokeLater { keyManager.addOhKeyPair(keyPair) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn("Decode key: ${encryptedKey.id} failed. error: {}", e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Decode keys: {}", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun encodeKeys(key: ByteArray): String {
|
||||||
|
val encryptedKeys = mutableListOf<OhKeyPair>()
|
||||||
|
for (keyPair in keyManager.getOhKeyPairs()) {
|
||||||
|
// aes iv
|
||||||
|
val iv = ArrayUtils.subarray(keyPair.id.padEnd(16, '0').toByteArray(), 0, 16)
|
||||||
|
val encryptedKeyPair = OhKeyPair(
|
||||||
|
id = keyPair.id,
|
||||||
|
publicKey = keyPair.publicKey.decodeBase64().aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||||
|
privateKey = keyPair.privateKey.decodeBase64().aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||||
|
type = keyPair.type.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||||
|
name = keyPair.name.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||||
|
remark = keyPair.remark.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||||
|
length = keyPair.length,
|
||||||
|
sort = keyPair.sort
|
||||||
|
)
|
||||||
|
encryptedKeys.add(encryptedKeyPair)
|
||||||
|
}
|
||||||
|
return ohMyJson.encodeToString(encryptedKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun decodeKeywordHighlights(text: String, config: SyncConfig) {
|
||||||
|
// aes key
|
||||||
|
val key = getKey(config)
|
||||||
|
val encryptedKeywordHighlights = ohMyJson.decodeFromString<List<KeywordHighlight>>(text)
|
||||||
|
|
||||||
|
for (e in encryptedKeywordHighlights) {
|
||||||
|
try {
|
||||||
|
// aes iv
|
||||||
|
val iv = getIv(e.id)
|
||||||
|
keywordHighlightManager.addKeywordHighlight(
|
||||||
|
e.copy(
|
||||||
|
keyword = e.keyword.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
description = e.description.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn("Decode KeywordHighlight: ${e.id} failed. error: {}", ex.message, ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Decode KeywordHighlight: {}", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun encodeKeywordHighlights(key: ByteArray): String {
|
||||||
|
val keywordHighlights = mutableListOf<KeywordHighlight>()
|
||||||
|
for (keywordHighlight in keywordHighlightManager.getKeywordHighlights()) {
|
||||||
|
// aes iv
|
||||||
|
val iv = getIv(keywordHighlight.id)
|
||||||
|
val encryptedKeyPair = keywordHighlight.copy(
|
||||||
|
keyword = keywordHighlight.keyword.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||||
|
description = keywordHighlight.description.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||||
|
)
|
||||||
|
keywordHighlights.add(encryptedKeyPair)
|
||||||
|
}
|
||||||
|
return ohMyJson.encodeToString(keywordHighlights)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun decodeMacros(text: String, config: SyncConfig) {
|
||||||
|
// aes key
|
||||||
|
val key = getKey(config)
|
||||||
|
val encryptedMacros = ohMyJson.decodeFromString<List<Macro>>(text)
|
||||||
|
|
||||||
|
for (e in encryptedMacros) {
|
||||||
|
try {
|
||||||
|
// aes iv
|
||||||
|
val iv = getIv(e.id)
|
||||||
|
macroManager.addMacro(
|
||||||
|
e.copy(
|
||||||
|
name = e.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
macro = e.macro.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn("Decode Macro: ${e.id} failed. error: {}", ex.message, ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Decode Macros: {}", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun encodeMacros(key: ByteArray): String {
|
||||||
|
val macros = mutableListOf<Macro>()
|
||||||
|
for (macro in macroManager.getMacros()) {
|
||||||
|
val iv = getIv(macro.id)
|
||||||
|
macros.add(
|
||||||
|
macro.copy(
|
||||||
|
name = macro.name.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||||
|
macro = macro.macro.aesCBCEncrypt(key, iv).encodeBase64String()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return ohMyJson.encodeToString(macros)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun decodeKeymaps(text: String, config: SyncConfig) {
|
||||||
|
|
||||||
|
for (keymap in ohMyJson.decodeFromString<List<JsonObject>>(text).mapNotNull { Keymap.fromJSON(it) }) {
|
||||||
|
keymapManager.addKeymap(keymap)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Decode Keymaps: {}", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun encodeKeymaps(): String {
|
||||||
|
val keymaps = mutableListOf<JsonObject>()
|
||||||
|
for (keymap in keymapManager.getKeymaps()) {
|
||||||
|
// 只读的是内置的
|
||||||
|
if (keymap.isReadonly) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keymaps.add(keymap.toJSONObject())
|
||||||
|
}
|
||||||
|
|
||||||
|
return ohMyJson.encodeToString(keymaps)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun getKey(config: SyncConfig): ByteArray {
|
||||||
|
return ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun getIv(id: String): ByteArray {
|
||||||
|
return ArrayUtils.subarray(id.padEnd(16, '0').toByteArray(), 0, 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ enum class SyncType {
|
|||||||
GitLab,
|
GitLab,
|
||||||
GitHub,
|
GitHub,
|
||||||
Gitee,
|
Gitee,
|
||||||
|
WebDAV,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class SyncRange {
|
enum class SyncRange {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class SyncerProvider private constructor() {
|
|||||||
SyncType.GitHub -> GitHubSyncer.getInstance()
|
SyncType.GitHub -> GitHubSyncer.getInstance()
|
||||||
SyncType.Gitee -> GiteeSyncer.getInstance()
|
SyncType.Gitee -> GiteeSyncer.getInstance()
|
||||||
SyncType.GitLab -> GitLabSyncer.getInstance()
|
SyncType.GitLab -> GitLabSyncer.getInstance()
|
||||||
|
SyncType.WebDAV -> WebDAVSyncer.getInstance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
152
src/main/kotlin/app/termora/sync/WebDAVSyncer.kt
Normal file
152
src/main/kotlin/app/termora/sync/WebDAVSyncer.kt
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package app.termora.sync
|
||||||
|
|
||||||
|
import app.termora.Application.ohMyJson
|
||||||
|
import app.termora.ApplicationScope
|
||||||
|
import app.termora.PBKDF2
|
||||||
|
import app.termora.ResponseException
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import okhttp3.Credentials
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
class WebDAVSyncer private constructor() : SafetySyncer() {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(WebDAVSyncer::class.java)
|
||||||
|
|
||||||
|
fun getInstance(): WebDAVSyncer {
|
||||||
|
return ApplicationScope.forApplicationScope().getOrCreate(WebDAVSyncer::class) { WebDAVSyncer() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun pull(config: SyncConfig): GistResponse {
|
||||||
|
val response = httpClient.newCall(newRequestBuilder(config).get().build()).execute()
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw ResponseException(response.code, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
val text = response.use { resp -> resp.body?.use { it.string() } }
|
||||||
|
?: throw ResponseException(response.code, response)
|
||||||
|
|
||||||
|
val json = ohMyJson.decodeFromString<JsonObject>(text)
|
||||||
|
|
||||||
|
// decode hosts
|
||||||
|
json["Hosts"]?.jsonPrimitive?.content?.let {
|
||||||
|
decodeHosts(it, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode KeyPairs
|
||||||
|
json["KeyPairs"]?.jsonPrimitive?.content?.let {
|
||||||
|
decodeKeys(it, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode Highlights
|
||||||
|
json["KeywordHighlights"]?.jsonPrimitive?.content?.let {
|
||||||
|
decodeKeywordHighlights(it, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode Macros
|
||||||
|
json["Macros"]?.jsonPrimitive?.content?.let {
|
||||||
|
decodeMacros(it, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode Keymaps
|
||||||
|
json["Keymaps"]?.jsonPrimitive?.content?.let {
|
||||||
|
decodeKeymaps(it, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
return GistResponse(config, emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun push(config: SyncConfig): GistResponse {
|
||||||
|
// aes key
|
||||||
|
val key = getKey(config)
|
||||||
|
val json = buildJsonObject {
|
||||||
|
// Hosts
|
||||||
|
if (config.ranges.contains(SyncRange.Hosts)) {
|
||||||
|
val hostsContent = encodeHosts(key)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Push encryptedHosts: {}", hostsContent)
|
||||||
|
}
|
||||||
|
put("Hosts", hostsContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyPairs
|
||||||
|
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
||||||
|
val keysContent = encodeKeys(key)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Push encryptedKeys: {}", keysContent)
|
||||||
|
}
|
||||||
|
put("KeyPairs", keysContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlights
|
||||||
|
if (config.ranges.contains(SyncRange.KeywordHighlights)) {
|
||||||
|
val keywordHighlightsContent = encodeKeywordHighlights(key)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Push keywordHighlights: {}", keywordHighlightsContent)
|
||||||
|
}
|
||||||
|
put("KeywordHighlights", keywordHighlightsContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Macros
|
||||||
|
if (config.ranges.contains(SyncRange.Macros)) {
|
||||||
|
val macrosContent = encodeMacros(key)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Push macros: {}", macrosContent)
|
||||||
|
}
|
||||||
|
put("Macros", macrosContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keymap
|
||||||
|
if (config.ranges.contains(SyncRange.Keymap)) {
|
||||||
|
val keymapsContent = encodeKeymaps()
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Push keymaps: {}", keymapsContent)
|
||||||
|
}
|
||||||
|
put("Keymaps", keymapsContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = httpClient.newCall(
|
||||||
|
newRequestBuilder(config).put(
|
||||||
|
ohMyJson.encodeToString(json)
|
||||||
|
.toRequestBody("application/json".toMediaType())
|
||||||
|
).build()
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw ResponseException(response.code, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
return GistResponse(
|
||||||
|
config = config,
|
||||||
|
gists = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun getWebDavFileUrl(config: SyncConfig): String {
|
||||||
|
return config.options["domain"] ?: throw IllegalStateException("domain is not defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getKey(config: SyncConfig): ByteArray {
|
||||||
|
return PBKDF2.generateSecret(
|
||||||
|
config.gistId.toCharArray(),
|
||||||
|
config.token.toByteArray(),
|
||||||
|
10000, 128
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newRequestBuilder(config: SyncConfig): Request.Builder {
|
||||||
|
return Request.Builder()
|
||||||
|
.header("Authorization", Credentials.basic(config.gistId, config.token, Charsets.UTF_8))
|
||||||
|
.url(getWebDavFileUrl(config))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -485,9 +485,11 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
|||||||
val m = args.first()
|
val m = args.first()
|
||||||
if (m == '6') {
|
if (m == '6') {
|
||||||
val position = terminal.getCursorModel().getPosition()
|
val position = terminal.getCursorModel().getPosition()
|
||||||
ptyConnector.write("${ControlCharacters.ESC}[${position.y};${position.x}R")
|
val bytes = "${ControlCharacters.ESC}[${position.y};${position.x}R".toByteArray(ptyConnector.getCharset())
|
||||||
|
ptyConnector.write(bytes)
|
||||||
} else if (m == '5') {
|
} else if (m == '5') {
|
||||||
ptyConnector.write("${ControlCharacters.ESC}[0n")
|
val bytes = "${ControlCharacters.ESC}[0n".toByteArray(ptyConnector.getCharset())
|
||||||
|
ptyConnector.write(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -689,6 +691,13 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
|||||||
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
|
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
|
||||||
1049 -> {
|
1049 -> {
|
||||||
|
|
||||||
|
// Save cursor
|
||||||
|
if (enable) {
|
||||||
|
CursorStoreStores.store(terminal)
|
||||||
|
} else {
|
||||||
|
CursorStoreStores.restore(terminal)
|
||||||
|
}
|
||||||
|
|
||||||
// 如果是关闭 清屏
|
// 如果是关闭 清屏
|
||||||
if (!enable) {
|
if (!enable) {
|
||||||
terminal.getDocument().eraseInDisplay(2)
|
terminal.getDocument().eraseInDisplay(2)
|
||||||
@@ -922,7 +931,7 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (log.isWarnEnabled) {
|
if (log.isWarnEnabled) {
|
||||||
log.warn("xterm-256 foreground color, code: $code")
|
log.warn("xterm-256 background color, code: $code")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
66
src/main/kotlin/app/termora/terminal/CursorStoreStores.kt
Normal file
66
src/main/kotlin/app/termora/terminal/CursorStoreStores.kt
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package app.termora.terminal
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
|
||||||
|
object CursorStoreStores {
|
||||||
|
private val log = LoggerFactory.getLogger(CursorStoreStores::class.java)
|
||||||
|
|
||||||
|
fun restore(terminal: Terminal) {
|
||||||
|
val terminalModel = terminal.getTerminalModel()
|
||||||
|
val cursorStore = if (terminalModel.hasData(DataKey.SaveCursor)) {
|
||||||
|
terminalModel.getData(DataKey.SaveCursor)
|
||||||
|
} else {
|
||||||
|
CursorStore(
|
||||||
|
position = Position(1, 1),
|
||||||
|
textStyle = TextStyle.Default,
|
||||||
|
autoWarpMode = false,
|
||||||
|
originMode = false,
|
||||||
|
graphicCharacterSet = GraphicCharacterSet()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
terminalModel.setData(DataKey.OriginMode, cursorStore.originMode)
|
||||||
|
terminalModel.setData(DataKey.TextStyle, cursorStore.textStyle)
|
||||||
|
terminalModel.setData(DataKey.AutoWrapMode, cursorStore.autoWarpMode)
|
||||||
|
terminalModel.setData(DataKey.GraphicCharacterSet, cursorStore.graphicCharacterSet)
|
||||||
|
|
||||||
|
val region = if (terminalModel.isOriginMode()) terminalModel.getScrollingRegion()
|
||||||
|
else ScrollingRegion(top = 1, bottom = terminalModel.getRows())
|
||||||
|
var y = cursorStore.position.y
|
||||||
|
if (y < region.top) {
|
||||||
|
y = 1
|
||||||
|
} else if (y > region.bottom) {
|
||||||
|
y = region.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal.getCursorModel().move(row = y, col = cursorStore.position.x)
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Restore Cursor (DECRC). $cursorStore")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun store(terminal: Terminal) {
|
||||||
|
val terminalModel = terminal.getTerminalModel()
|
||||||
|
|
||||||
|
val graphicCharacterSet = terminalModel.getData(DataKey.GraphicCharacterSet)
|
||||||
|
// 避免引用
|
||||||
|
val characterSets = mutableMapOf<Graphic, CharacterSet>()
|
||||||
|
characterSets.putAll(graphicCharacterSet.characterSets)
|
||||||
|
|
||||||
|
val cursorStore = CursorStore(
|
||||||
|
position = terminal.getCursorModel().getPosition(),
|
||||||
|
textStyle = terminalModel.getData(DataKey.TextStyle),
|
||||||
|
autoWarpMode = terminalModel.getData(DataKey.AutoWrapMode, false),
|
||||||
|
originMode = terminalModel.isOriginMode(),
|
||||||
|
graphicCharacterSet = graphicCharacterSet.copy(characterSets = characterSets),
|
||||||
|
)
|
||||||
|
|
||||||
|
terminalModel.setData(DataKey.SaveCursor, cursorStore)
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Save Cursor (DECSC). $cursorStore")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -333,59 +333,12 @@ class EscapeSequenceProcessor(terminal: Terminal, reader: TerminalReader) : Abst
|
|||||||
|
|
||||||
// ESC 7 Save Cursor (DECSC), VT100.
|
// ESC 7 Save Cursor (DECSC), VT100.
|
||||||
'7' -> {
|
'7' -> {
|
||||||
val graphicCharacterSet = terminalModel.getData(DataKey.GraphicCharacterSet)
|
CursorStoreStores.store(terminal)
|
||||||
// 避免引用
|
|
||||||
val characterSets = mutableMapOf<Graphic, CharacterSet>()
|
|
||||||
characterSets.putAll(graphicCharacterSet.characterSets)
|
|
||||||
|
|
||||||
val cursorStore = CursorStore(
|
|
||||||
position = terminal.getCursorModel().getPosition(),
|
|
||||||
textStyle = terminalModel.getData(DataKey.TextStyle),
|
|
||||||
autoWarpMode = terminalModel.getData(DataKey.AutoWrapMode, false),
|
|
||||||
originMode = terminalModel.isOriginMode(),
|
|
||||||
graphicCharacterSet = graphicCharacterSet.copy(characterSets = characterSets),
|
|
||||||
)
|
|
||||||
|
|
||||||
terminalModel.setData(DataKey.SaveCursor, cursorStore)
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Save Cursor (DECSC). $cursorStore")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore Cursor (DECRC), VT100.
|
// Restore Cursor (DECRC), VT100.
|
||||||
'8' -> {
|
'8' -> {
|
||||||
val cursorStore = if (terminalModel.hasData(DataKey.SaveCursor)) {
|
CursorStoreStores.restore(terminal)
|
||||||
terminalModel.getData(DataKey.SaveCursor)
|
|
||||||
} else {
|
|
||||||
CursorStore(
|
|
||||||
position = Position(1, 1),
|
|
||||||
textStyle = TextStyle.Default,
|
|
||||||
autoWarpMode = false,
|
|
||||||
originMode = false,
|
|
||||||
graphicCharacterSet = GraphicCharacterSet()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
terminalModel.setData(DataKey.OriginMode, cursorStore.originMode)
|
|
||||||
terminalModel.setData(DataKey.TextStyle, cursorStore.textStyle)
|
|
||||||
terminalModel.setData(DataKey.AutoWrapMode, cursorStore.autoWarpMode)
|
|
||||||
terminalModel.setData(DataKey.GraphicCharacterSet, cursorStore.graphicCharacterSet)
|
|
||||||
|
|
||||||
val region = if (terminalModel.isOriginMode()) terminalModel.getScrollingRegion()
|
|
||||||
else ScrollingRegion(top = 1, bottom = terminalModel.getRows())
|
|
||||||
var y = cursorStore.position.y
|
|
||||||
if (y < region.top) {
|
|
||||||
y = 1
|
|
||||||
} else if (y > region.bottom) {
|
|
||||||
y = region.bottom
|
|
||||||
}
|
|
||||||
|
|
||||||
terminal.getCursorModel().move(row = y, col = cursorStore.position.x)
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Restore Cursor (DECRC). $cursorStore")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.termora.terminal
|
package app.termora.terminal
|
||||||
|
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
|
|
||||||
interface PtyConnector {
|
interface PtyConnector {
|
||||||
@@ -15,15 +16,18 @@ interface PtyConnector {
|
|||||||
*/
|
*/
|
||||||
fun write(buffer: ByteArray, offset: Int, len: Int)
|
fun write(buffer: ByteArray, offset: Int, len: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入数组。
|
||||||
|
*
|
||||||
|
* 如果要写入 String 字符串,请通过 [getCharset] 编码。
|
||||||
|
*/
|
||||||
fun write(buffer: ByteArray) {
|
fun write(buffer: ByteArray) {
|
||||||
write(buffer, 0, buffer.size)
|
write(buffer, 0, buffer.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun write(buffer: String) {
|
/**
|
||||||
if (buffer.isEmpty()) return
|
* 写入单个 Int
|
||||||
write(buffer.toByteArray())
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
fun write(buffer: Int) {
|
fun write(buffer: Int) {
|
||||||
write(ByteBuffer.allocate(Integer.BYTES).putInt(buffer).flip().array())
|
write(ByteBuffer.allocate(Integer.BYTES).putInt(buffer).flip().array())
|
||||||
}
|
}
|
||||||
@@ -43,4 +47,8 @@ interface PtyConnector {
|
|||||||
*/
|
*/
|
||||||
fun close()
|
fun close()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编码
|
||||||
|
*/
|
||||||
|
fun getCharset(): Charset = Charsets.UTF_8
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.termora.terminal
|
package app.termora.terminal
|
||||||
|
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
open class PtyConnectorDelegate(
|
open class PtyConnectorDelegate(
|
||||||
@Volatile var ptyConnector: PtyConnector? = null
|
@Volatile var ptyConnector: PtyConnector? = null
|
||||||
) : PtyConnector {
|
) : PtyConnector {
|
||||||
@@ -26,5 +28,7 @@ open class PtyConnectorDelegate(
|
|||||||
ptyConnector = null
|
ptyConnector = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getCharset(): Charset {
|
||||||
|
return ptyConnector?.getCharset() ?: super.getCharset()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -20,9 +20,6 @@ class PtyProcessConnector(private val process: PtyProcess, private val charset:
|
|||||||
output.flush()
|
output.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun write(buffer: String) {
|
|
||||||
write(buffer.toByteArray(charset))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun resize(rows: Int, cols: Int) {
|
override fun resize(rows: Int, cols: Int) {
|
||||||
process.winSize = WinSize(cols, rows)
|
process.winSize = WinSize(cols, rows)
|
||||||
@@ -38,5 +35,7 @@ class PtyProcessConnector(private val process: PtyProcess, private val charset:
|
|||||||
process.destroyForcibly()
|
process.destroyForcibly()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getCharset(): Charset {
|
||||||
|
return charset
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package app.termora.terminal.panel
|
package app.termora.terminal.panel
|
||||||
|
|
||||||
|
import app.termora.Database
|
||||||
import app.termora.DynamicColor
|
import app.termora.DynamicColor
|
||||||
import app.termora.assertEventDispatchThread
|
import app.termora.assertEventDispatchThread
|
||||||
import app.termora.Database
|
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
@@ -49,6 +49,8 @@ class TerminalDisplay(
|
|||||||
init {
|
init {
|
||||||
terminalPanel.addTerminalPaintListener(toaster)
|
terminalPanel.addTerminalPaintListener(toaster)
|
||||||
putClientProperty(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)
|
putClientProperty(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)
|
||||||
|
|
||||||
|
cursor = Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun paint(g: Graphics) {
|
override fun paint(g: Graphics) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.terminal.panel
|
package app.termora.terminal.panel
|
||||||
|
|
||||||
|
import app.termora.Disposable
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.actions.DataProviderSupport
|
import app.termora.actions.DataProviderSupport
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
@@ -30,7 +31,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||||||
|
|
||||||
|
|
||||||
class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnector) :
|
class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnector) :
|
||||||
JPanel(BorderLayout()), DataProvider {
|
JPanel(BorderLayout()), DataProvider, Disposable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val Debug = DataKey(Boolean::class)
|
val Debug = DataKey(Boolean::class)
|
||||||
@@ -298,7 +299,7 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
|||||||
|
|
||||||
// 输入法提交
|
// 输入法提交
|
||||||
if (committedCharacterCount > 0) {
|
if (committedCharacterCount > 0) {
|
||||||
ptyConnector.write(sb.toString())
|
ptyConnector.write(sb.toString().toByteArray(ptyConnector.getCharset()))
|
||||||
} else {
|
} else {
|
||||||
val breakIterator = BreakIterator.getCharacterInstance()
|
val breakIterator = BreakIterator.getCharacterInstance()
|
||||||
val chars = mutableListOf<Char>()
|
val chars = mutableListOf<Char>()
|
||||||
@@ -397,16 +398,20 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
|||||||
* 执行粘贴操作
|
* 执行粘贴操作
|
||||||
*/
|
*/
|
||||||
fun paste(text: String) {
|
fun paste(text: String) {
|
||||||
val content = if (SystemInfo.isWindows) {
|
var content = text
|
||||||
text.replace("${ControlCharacters.CR}${ControlCharacters.LF}", "${ControlCharacters.LF}")
|
if (!SystemInfo.isWindows) {
|
||||||
} else {
|
content = content.replace("\r\n", "\n")
|
||||||
text.replace(ControlCharacters.LF, ControlCharacters.CR)
|
|
||||||
}
|
}
|
||||||
|
content = content.replace('\n', '\r')
|
||||||
|
|
||||||
if (terminal.getTerminalModel().getData(DataKey.BracketedPasteMode, false)) {
|
if (terminal.getTerminalModel().getData(DataKey.BracketedPasteMode, false)) {
|
||||||
ptyConnector.write("${ControlCharacters.ESC}[200~${content}${ControlCharacters.ESC}[201~")
|
ptyConnector.write(
|
||||||
|
"${ControlCharacters.ESC}[200~${content}${ControlCharacters.ESC}[201~".toByteArray(
|
||||||
|
ptyConnector.getCharset()
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
ptyConnector.write(content)
|
ptyConnector.write(content.toByteArray(ptyConnector.getCharset()))
|
||||||
}
|
}
|
||||||
|
|
||||||
terminal.getScrollingModel().scrollToRow(
|
terminal.getScrollingModel().scrollToRow(
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class TerminalPanelKeyAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
terminal.getSelectionModel().clearSelection()
|
terminal.getSelectionModel().clearSelection()
|
||||||
ptyConnector.write("${e.keyChar}")
|
ptyConnector.write("${e.keyChar}".toByteArray(ptyConnector.getCharset()))
|
||||||
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ class TerminalPanelKeyAdapter(
|
|||||||
|
|
||||||
val encode = terminal.getKeyEncoder().encode(AWTTerminalKeyEvent(e))
|
val encode = terminal.getKeyEncoder().encode(AWTTerminalKeyEvent(e))
|
||||||
if (encode.isNotEmpty()) {
|
if (encode.isNotEmpty()) {
|
||||||
ptyConnector.write(encode)
|
ptyConnector.write(encode.toByteArray(ptyConnector.getCharset()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/TermoraDev/termora/issues/52
|
// https://github.com/TermoraDev/termora/issues/52
|
||||||
@@ -64,7 +64,7 @@ class TerminalPanelKeyAdapter(
|
|||||||
terminal.getSelectionModel().clearSelection()
|
terminal.getSelectionModel().clearSelection()
|
||||||
// 如果不为空表示已经发送过了,所以这里为空的时候再发送
|
// 如果不为空表示已经发送过了,所以这里为空的时候再发送
|
||||||
if (encode.isEmpty()) {
|
if (encode.isEmpty()) {
|
||||||
ptyConnector.write("${e.keyChar}")
|
ptyConnector.write("${e.keyChar}".toByteArray(ptyConnector.getCharset()))
|
||||||
}
|
}
|
||||||
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,9 +70,9 @@ class TerminalPanelMouseTrackingAdapter(
|
|||||||
val encode = terminal.getKeyEncoder()
|
val encode = terminal.getKeyEncoder()
|
||||||
.encode(TerminalKeyEvent(if (e.wheelRotation < 0) KeyEvent.VK_UP else KeyEvent.VK_DOWN))
|
.encode(TerminalKeyEvent(if (e.wheelRotation < 0) KeyEvent.VK_UP else KeyEvent.VK_DOWN))
|
||||||
if (encode.isBlank()) return
|
if (encode.isBlank()) return
|
||||||
|
val bytes = encode.toByteArray(ptyConnector.getCharset())
|
||||||
for (i in 0 until abs(unitsToScroll)) {
|
for (i in 0 until abs(unitsToScroll)) {
|
||||||
ptyConnector.write(encode)
|
ptyConnector.write(bytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import java.io.FileNotFoundException
|
|||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
|
|
||||||
class LogViewerTerminalTab(windowScope: WindowScope, private val file: File) : PtyHostTerminalTab(
|
class LogViewerTerminalTab(
|
||||||
|
windowScope: WindowScope,
|
||||||
|
private val file: File,
|
||||||
|
) : PtyHostTerminalTab(
|
||||||
windowScope,
|
windowScope,
|
||||||
Host(
|
Host(
|
||||||
name = file.name,
|
name = file.name,
|
||||||
|
|||||||
@@ -25,9 +25,8 @@ import java.awt.Component
|
|||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
import java.awt.datatransfer.DataFlavor
|
import java.awt.datatransfer.DataFlavor
|
||||||
import java.awt.datatransfer.StringSelection
|
import java.awt.datatransfer.StringSelection
|
||||||
import java.awt.dnd.DnDConstants
|
import java.awt.datatransfer.Transferable
|
||||||
import java.awt.dnd.DropTarget
|
import java.awt.datatransfer.UnsupportedFlavorException
|
||||||
import java.awt.dnd.DropTargetDropEvent
|
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
@@ -80,6 +79,8 @@ class FileSystemPanel(
|
|||||||
bookmarkBtn.isBookmark = bookmarkBtn.getBookmarks().contains(workdir.toString())
|
bookmarkBtn.isBookmark = bookmarkBtn.getBookmarks().contains(workdir.toString())
|
||||||
|
|
||||||
table.setUI(FlatTableUI())
|
table.setUI(FlatTableUI())
|
||||||
|
table.dragEnabled = true
|
||||||
|
table.dropMode = DropMode.INSERT_ROWS
|
||||||
table.rowHeight = UIManager.getInt("Table.rowHeight")
|
table.rowHeight = UIManager.getInt("Table.rowHeight")
|
||||||
table.autoResizeMode = JTable.AUTO_RESIZE_OFF
|
table.autoResizeMode = JTable.AUTO_RESIZE_OFF
|
||||||
table.fillsViewportHeight = true
|
table.fillsViewportHeight = true
|
||||||
@@ -231,17 +232,45 @@ class FileSystemPanel(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 本地文件系统不支持本地拖拽进去
|
|
||||||
if (!tableModel.isLocalFileSystem) {
|
table.transferHandler = object : TransferHandler() {
|
||||||
table.dropTarget = object : DropTarget() {
|
override fun canImport(support: TransferSupport): Boolean {
|
||||||
override fun drop(dtde: DropTargetDropEvent) {
|
if (support.isDataFlavorSupported(FileSystemTableRowTransferable.dataFlavor)) {
|
||||||
dtde.acceptDrop(DnDConstants.ACTION_COPY)
|
val data = support.transferable.getTransferData(FileSystemTableRowTransferable.dataFlavor)
|
||||||
val files = dtde.transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*>
|
return data is FileSystemTableRowTransferable && data.fileSystemPanel != this@FileSystemPanel
|
||||||
if (files.isEmpty()) return
|
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||||
copyLocalFileToFileSystem(files.filterIsInstance<File>())
|
return !tableModel.isLocalFileSystem
|
||||||
}
|
}
|
||||||
}.apply {
|
return false
|
||||||
this.defaultActions = DnDConstants.ACTION_COPY
|
}
|
||||||
|
|
||||||
|
override fun importData(comp: JComponent?, t: Transferable): Boolean {
|
||||||
|
if (t.isDataFlavorSupported(FileSystemTableRowTransferable.dataFlavor)) {
|
||||||
|
val data = t.getTransferData(FileSystemTableRowTransferable.dataFlavor)
|
||||||
|
if (data !is FileSystemTableRowTransferable) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
data.fileSystemPanel.transport(data.paths)
|
||||||
|
return true
|
||||||
|
} else if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||||
|
val files = t.getTransferData(DataFlavor.javaFileListFlavor) as List<*>
|
||||||
|
if (files.isEmpty()) return false
|
||||||
|
copyLocalFileToFileSystem(files.filterIsInstance<File>())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSourceActions(c: JComponent?): Int {
|
||||||
|
return COPY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createTransferable(c: JComponent?): Transferable? {
|
||||||
|
val paths = table.selectedRows.filter { it != 0 }.map { tableModel.getCacheablePath(it) }
|
||||||
|
if (paths.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return FileSystemTableRowTransferable(this@FileSystemPanel, paths)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -838,4 +867,28 @@ class FileSystemPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class FileSystemTableRowTransferable(
|
||||||
|
val fileSystemPanel: FileSystemPanel,
|
||||||
|
val paths: List<FileSystemTableModel.CacheablePath>
|
||||||
|
) : Transferable {
|
||||||
|
companion object {
|
||||||
|
val dataFlavor = DataFlavor(FileSystemTableRowTransferable::class.java, "TableRowTransferable")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTransferDataFlavors(): Array<DataFlavor> {
|
||||||
|
return arrayOf(dataFlavor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isDataFlavorSupported(flavor: DataFlavor?): Boolean {
|
||||||
|
return flavor == dataFlavor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTransferData(flavor: DataFlavor?): Any {
|
||||||
|
if (flavor != dataFlavor) {
|
||||||
|
throw UnsupportedFlavorException(flavor)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,8 @@ termora.date-format=MM/dd/yyyy hh:mm:ss a
|
|||||||
termora.finder=Finder
|
termora.finder=Finder
|
||||||
termora.folder=Folder
|
termora.folder=Folder
|
||||||
termora.explorer=Explorer
|
termora.explorer=Explorer
|
||||||
|
termora.quit-confirm=Quit {0}?
|
||||||
|
|
||||||
|
|
||||||
# update
|
# update
|
||||||
termora.update.title=New version
|
termora.update.title=New version
|
||||||
@@ -61,9 +63,12 @@ termora.settings.terminal.font=Font
|
|||||||
termora.settings.terminal.size=Size
|
termora.settings.terminal.size=Size
|
||||||
termora.settings.terminal.max-rows=Max rows
|
termora.settings.terminal.max-rows=Max rows
|
||||||
termora.settings.terminal.debug=Debug mode
|
termora.settings.terminal.debug=Debug mode
|
||||||
|
termora.settings.terminal.beep=Beep
|
||||||
termora.settings.terminal.select-copy=Select copy
|
termora.settings.terminal.select-copy=Select copy
|
||||||
termora.settings.terminal.cursor-style=Cursor type
|
termora.settings.terminal.cursor-style=Cursor type
|
||||||
termora.settings.terminal.local-shell=Local shell
|
termora.settings.terminal.local-shell=Local shell
|
||||||
|
termora.settings.terminal.auto-close-tab=Auto Close Tab
|
||||||
|
termora.settings.terminal.auto-close-tab-description=Automatically close the tab when the terminal is disconnected normally
|
||||||
|
|
||||||
termora.settings.sync=Sync
|
termora.settings.sync=Sync
|
||||||
termora.settings.sync.push=Push
|
termora.settings.sync.push=Push
|
||||||
@@ -83,6 +88,7 @@ termora.settings.sync.last-sync-time=Last sync time
|
|||||||
termora.settings.sync.gist=Gist
|
termora.settings.sync.gist=Gist
|
||||||
termora.settings.sync.token=Token
|
termora.settings.sync.token=Token
|
||||||
termora.settings.sync.type=Type
|
termora.settings.sync.type=Type
|
||||||
|
termora.settings.sync.webdav.help=WebDAV storage address, e.g. https://yourhost/webdav/termora.json
|
||||||
|
|
||||||
termora.settings.about=About
|
termora.settings.about=About
|
||||||
termora.settings.about.author=Author
|
termora.settings.about.author=Author
|
||||||
@@ -109,6 +115,8 @@ termora.find-everywhere.groups.opened-hosts=Opened hosts
|
|||||||
termora.find-everywhere.groups.tools=Tools
|
termora.find-everywhere.groups.tools=Tools
|
||||||
termora.find-everywhere.groups.settings=${termora.setting}
|
termora.find-everywhere.groups.settings=${termora.setting}
|
||||||
termora.find-everywhere.quick-command.local-terminal=Local Terminal
|
termora.find-everywhere.quick-command.local-terminal=Local Terminal
|
||||||
|
termora.find-everywhere.double-shift-deprecated=The double-click Shift shortcut will be removed in a future version
|
||||||
|
termora.find-everywhere.double-shift-deprecated-instead=${termora.find-everywhere.double-shift-deprecated}, use {0} instead
|
||||||
|
|
||||||
# Welcome
|
# Welcome
|
||||||
termora.welcome.my-hosts=My hosts
|
termora.welcome.my-hosts=My hosts
|
||||||
@@ -145,6 +153,14 @@ termora.new-host.terminal.heartbeat-interval=Heartbeat Interval
|
|||||||
termora.new-host.terminal.startup-commands=Startup Command
|
termora.new-host.terminal.startup-commands=Startup Command
|
||||||
termora.new-host.terminal.env=Environment
|
termora.new-host.terminal.env=Environment
|
||||||
|
|
||||||
|
termora.new-host.serial=Serial
|
||||||
|
termora.new-host.serial.port=Port
|
||||||
|
termora.new-host.serial.baud-rate=Baud rate
|
||||||
|
termora.new-host.serial.data-bits=Data bits
|
||||||
|
termora.new-host.serial.parity=Parity
|
||||||
|
termora.new-host.serial.stop-bits=Stop bits
|
||||||
|
termora.new-host.serial.flow-control=Flow control
|
||||||
|
|
||||||
termora.new-host.tunneling=Tunneling
|
termora.new-host.tunneling=Tunneling
|
||||||
termora.new-host.tunneling.table.name=Name
|
termora.new-host.tunneling.table.name=Name
|
||||||
termora.new-host.tunneling.table.type=Type
|
termora.new-host.tunneling.table.type=Type
|
||||||
@@ -172,6 +188,12 @@ termora.keymgr.table.type=Type
|
|||||||
termora.keymgr.table.length=Length
|
termora.keymgr.table.length=Length
|
||||||
termora.keymgr.table.remark=Description
|
termora.keymgr.table.remark=Description
|
||||||
|
|
||||||
|
termora.keymgr.ssh-copy-id.number=Number of hosts [{0}] Number of public keys [{1}]
|
||||||
|
termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied}
|
||||||
|
termora.keymgr.ssh-copy-id.failed=Copy Failure
|
||||||
|
termora.keymgr.ssh-copy-id.end=End of public key copying
|
||||||
|
|
||||||
|
|
||||||
# Tabbed
|
# Tabbed
|
||||||
termora.tabbed.contextmenu.rename=Rename
|
termora.tabbed.contextmenu.rename=Rename
|
||||||
termora.tabbed.contextmenu.clone=Clone
|
termora.tabbed.contextmenu.clone=Clone
|
||||||
@@ -303,6 +325,8 @@ termora.actions.switch-tab=Switch to specific Tab [1..9]
|
|||||||
# Terminal
|
# Terminal
|
||||||
termora.terminal.size=Size: {0} x {1}
|
termora.terminal.size=Size: {0} x {1}
|
||||||
termora.terminal.copied=Copied
|
termora.terminal.copied=Copied
|
||||||
|
termora.terminal.channel-disconnected=Channel has been disconnected.\u0020
|
||||||
|
termora.terminal.channel-reconnect=Type {0} to reconnect.
|
||||||
|
|
||||||
|
|
||||||
# zmodem
|
# zmodem
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ termora.date-format=yyyy-MM-dd HH:mm:ss
|
|||||||
termora.finder=访达
|
termora.finder=访达
|
||||||
termora.folder=文件夹
|
termora.folder=文件夹
|
||||||
termora.explorer=文件管理器
|
termora.explorer=文件管理器
|
||||||
|
termora.quit-confirm=你要退出 {0} 吗?
|
||||||
|
|
||||||
# update
|
# update
|
||||||
termora.update.title=新版本
|
termora.update.title=新版本
|
||||||
@@ -59,15 +60,20 @@ termora.find-everywhere.groups.opened-hosts=已打开的主机
|
|||||||
termora.find-everywhere.groups.tools=工具
|
termora.find-everywhere.groups.tools=工具
|
||||||
termora.find-everywhere.groups.settings=${termora.setting}
|
termora.find-everywhere.groups.settings=${termora.setting}
|
||||||
termora.find-everywhere.quick-command.local-terminal=本地终端
|
termora.find-everywhere.quick-command.local-terminal=本地终端
|
||||||
|
termora.find-everywhere.double-shift-deprecated=双击 Shift 快捷键将会在未来的版本中移除
|
||||||
|
termora.find-everywhere.double-shift-deprecated-instead=${termora.find-everywhere.double-shift-deprecated},请使用 {0} 代替
|
||||||
|
|
||||||
termora.settings.terminal=终端
|
termora.settings.terminal=终端
|
||||||
termora.settings.terminal.font=字体
|
termora.settings.terminal.font=字体
|
||||||
termora.settings.terminal.size=大小
|
termora.settings.terminal.size=大小
|
||||||
termora.settings.terminal.max-rows=最大行数
|
termora.settings.terminal.max-rows=最大行数
|
||||||
termora.settings.terminal.debug=调试模式
|
termora.settings.terminal.debug=调试模式
|
||||||
|
termora.settings.terminal.beep=蜂鸣声
|
||||||
termora.settings.terminal.select-copy=选中复制
|
termora.settings.terminal.select-copy=选中复制
|
||||||
termora.settings.terminal.cursor-style=光标样式
|
termora.settings.terminal.cursor-style=光标样式
|
||||||
termora.settings.terminal.local-shell=本地终端
|
termora.settings.terminal.local-shell=本地终端
|
||||||
|
termora.settings.terminal.auto-close-tab=自动关闭标签
|
||||||
|
termora.settings.terminal.auto-close-tab-description=当终端正常断开连接时自动关闭标签页
|
||||||
|
|
||||||
|
|
||||||
termora.settings.sync=同步
|
termora.settings.sync=同步
|
||||||
@@ -85,6 +91,7 @@ termora.settings.sync.import.successful=导入数据成功
|
|||||||
termora.settings.sync.gist=片段
|
termora.settings.sync.gist=片段
|
||||||
termora.settings.sync.token=令牌
|
termora.settings.sync.token=令牌
|
||||||
termora.settings.sync.type=类型
|
termora.settings.sync.type=类型
|
||||||
|
termora.settings.sync.webdav.help=WebDAV 的存储地址,例如:https://yourhost/webdav/termora.json
|
||||||
|
|
||||||
termora.settings.about=关于
|
termora.settings.about=关于
|
||||||
termora.settings.about.author=作者
|
termora.settings.about.author=作者
|
||||||
@@ -132,6 +139,14 @@ termora.new-host.terminal.startup-commands=启动命令
|
|||||||
termora.new-host.terminal.env=环境
|
termora.new-host.terminal.env=环境
|
||||||
|
|
||||||
|
|
||||||
|
termora.new-host.serial=串口
|
||||||
|
termora.new-host.serial.port=端口
|
||||||
|
termora.new-host.serial.baud-rate=波特率
|
||||||
|
termora.new-host.serial.data-bits=数据位
|
||||||
|
termora.new-host.serial.parity=校验位
|
||||||
|
termora.new-host.serial.stop-bits=停止位
|
||||||
|
termora.new-host.serial.flow-control=流控
|
||||||
|
|
||||||
|
|
||||||
termora.new-host.test-connection=测试连接
|
termora.new-host.test-connection=测试连接
|
||||||
termora.new-host.test-connection-successful=连接成功
|
termora.new-host.test-connection-successful=连接成功
|
||||||
@@ -159,6 +174,10 @@ termora.keymgr.table.type=类型
|
|||||||
termora.keymgr.table.length=长度
|
termora.keymgr.table.length=长度
|
||||||
termora.keymgr.table.remark=备注
|
termora.keymgr.table.remark=备注
|
||||||
|
|
||||||
|
termora.keymgr.ssh-copy-id.number=主机数量 [{0}] 公钥数量 [{1}]
|
||||||
|
termora.keymgr.ssh-copy-id.failed=复制失败
|
||||||
|
termora.keymgr.ssh-copy-id.end=复制公钥结束
|
||||||
|
|
||||||
# Tools
|
# Tools
|
||||||
termora.tools.multiple=将命令发送到所有会话
|
termora.tools.multiple=将命令发送到所有会话
|
||||||
|
|
||||||
@@ -275,6 +294,8 @@ termora.toolbar.customize-toolbar=自定义工具栏...
|
|||||||
|
|
||||||
termora.terminal.size=大小: {0} x {1}
|
termora.terminal.size=大小: {0} x {1}
|
||||||
termora.terminal.copied=已复制
|
termora.terminal.copied=已复制
|
||||||
|
termora.terminal.channel-disconnected=终端断开连接,
|
||||||
|
termora.terminal.channel-reconnect=按 {0} 进行重连。
|
||||||
|
|
||||||
|
|
||||||
# Actions
|
# Actions
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ termora.date-format=yyyy/MM/dd HH:mm:ss
|
|||||||
termora.finder=訪達
|
termora.finder=訪達
|
||||||
termora.folder=資料夾
|
termora.folder=資料夾
|
||||||
termora.explorer=檔案管理器
|
termora.explorer=檔案管理器
|
||||||
|
termora.quit-confirm=你要退出 {0} 嗎?
|
||||||
|
|
||||||
# update
|
# update
|
||||||
termora.update.title=新版本
|
termora.update.title=新版本
|
||||||
@@ -64,15 +65,20 @@ termora.find-everywhere.groups.opened-hosts=已開啟的主機
|
|||||||
termora.find-everywhere.groups.tools=工具
|
termora.find-everywhere.groups.tools=工具
|
||||||
termora.find-everywhere.groups.settings=${termora.setting}
|
termora.find-everywhere.groups.settings=${termora.setting}
|
||||||
termora.find-everywhere.quick-command.local-terminal=本地端
|
termora.find-everywhere.quick-command.local-terminal=本地端
|
||||||
|
termora.find-everywhere.double-shift-deprecated=雙擊 Shift 快捷鍵將會在未來的版本中移除
|
||||||
|
termora.find-everywhere.double-shift-deprecated-instead=${termora.find-everywhere.double-shift-deprecated},請使用 {0} 代替
|
||||||
|
|
||||||
termora.settings.terminal=終端
|
termora.settings.terminal=終端
|
||||||
termora.settings.terminal.font=字體
|
termora.settings.terminal.font=字體
|
||||||
termora.settings.terminal.size=大小
|
termora.settings.terminal.size=大小
|
||||||
termora.settings.terminal.max-rows=最大行數
|
termora.settings.terminal.max-rows=最大行數
|
||||||
termora.settings.terminal.debug=偵錯模式
|
termora.settings.terminal.debug=偵錯模式
|
||||||
|
termora.settings.terminal.beep=蜂鳴聲
|
||||||
termora.settings.terminal.select-copy=選取複製
|
termora.settings.terminal.select-copy=選取複製
|
||||||
termora.settings.terminal.cursor-style=遊標風格
|
termora.settings.terminal.cursor-style=遊標風格
|
||||||
termora.settings.terminal.local-shell=本地端
|
termora.settings.terminal.local-shell=本地端
|
||||||
|
termora.settings.terminal.auto-close-tab=自動關閉標籤
|
||||||
|
termora.settings.terminal.auto-close-tab-description=當終端正常斷開連線時自動關閉標籤頁
|
||||||
|
|
||||||
termora.settings.sync=同步
|
termora.settings.sync=同步
|
||||||
termora.settings.sync.push=推送
|
termora.settings.sync.push=推送
|
||||||
@@ -89,6 +95,7 @@ termora.settings.sync.import.successful=導入資料成功
|
|||||||
termora.settings.sync.gist=片段
|
termora.settings.sync.gist=片段
|
||||||
termora.settings.sync.token=令牌
|
termora.settings.sync.token=令牌
|
||||||
termora.settings.sync.type=類型
|
termora.settings.sync.type=類型
|
||||||
|
termora.settings.sync.webdav.help=WebDAV 的儲存位址,例如:https://yourhost/webdav/termora.json
|
||||||
|
|
||||||
termora.settings.about=關於
|
termora.settings.about=關於
|
||||||
termora.settings.about.author=作者
|
termora.settings.about.author=作者
|
||||||
@@ -130,6 +137,14 @@ termora.new-host.terminal.startup-commands=啟動命令
|
|||||||
termora.new-host.terminal.heartbeat-interval=心跳間隔
|
termora.new-host.terminal.heartbeat-interval=心跳間隔
|
||||||
termora.new-host.terminal.env=環境
|
termora.new-host.terminal.env=環境
|
||||||
|
|
||||||
|
termora.new-host.serial=串口
|
||||||
|
termora.new-host.serial.port=端口
|
||||||
|
termora.new-host.serial.baud-rate=波特率
|
||||||
|
termora.new-host.serial.data-bits=資料位
|
||||||
|
termora.new-host.serial.parity=校驗位
|
||||||
|
termora.new-host.serial.stop-bits=停止位
|
||||||
|
termora.new-host.serial.flow-control=流控
|
||||||
|
|
||||||
termora.new-host.test-connection=測試連接
|
termora.new-host.test-connection=測試連接
|
||||||
termora.new-host.test-connection-successful=連線成功
|
termora.new-host.test-connection-successful=連線成功
|
||||||
|
|
||||||
@@ -156,6 +171,10 @@ termora.keymgr.table.type=型別
|
|||||||
termora.keymgr.table.length=長度
|
termora.keymgr.table.length=長度
|
||||||
termora.keymgr.table.remark=備註
|
termora.keymgr.table.remark=備註
|
||||||
|
|
||||||
|
termora.keymgr.ssh-copy-id.number=主機數量 [{0}] 公鑰數量 [{1}]
|
||||||
|
termora.keymgr.ssh-copy-id.failed=複製失敗
|
||||||
|
termora.keymgr.ssh-copy-id.end=複製公鑰結束
|
||||||
|
|
||||||
# Tools
|
# Tools
|
||||||
termora.tools.multiple=將指令傳送到所有會話
|
termora.tools.multiple=將指令傳送到所有會話
|
||||||
|
|
||||||
@@ -257,6 +276,8 @@ termora.toolbar.customize-toolbar=自訂工具列...
|
|||||||
|
|
||||||
termora.terminal.size=大小: {0} x {1}
|
termora.terminal.size=大小: {0} x {1}
|
||||||
termora.terminal.copied=已複製
|
termora.terminal.copied=已複製
|
||||||
|
termora.terminal.channel-disconnected=終端機連線中斷,
|
||||||
|
termora.terminal.channel-reconnect=按 {0} 進行重新連線。
|
||||||
|
|
||||||
# Actions
|
# Actions
|
||||||
termora.actions.copy-from-terminal=從終端複製
|
termora.actions.copy-from-terminal=從終端複製
|
||||||
|
|||||||
4
src/main/resources/icons/plugin.svg
Normal file
4
src/main/resources/icons/plugin.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 4H7C5.34315 4 4 5.34315 4 7V9C4 10.6569 5.34315 12 7 12H11V11V10V6V5V4ZM12 5V4C12 3.44772 11.5523 3 11 3H7C5.13616 3 3.57006 4.27477 3.12602 6H1C0.447715 6 0 6.44772 0 7V9C0 9.55228 0.447715 10 1 10H3.12602C3.57006 11.7252 5.13616 13 7 13H11C11.5523 13 12 12.5523 12 12V11H15.5C15.7761 11 16 10.7761 16 10.5C16 10.2239 15.7761 10 15.5 10H12V6H15.5C15.7761 6 16 5.77614 16 5.5C16 5.22386 15.7761 5 15.5 5H12ZM3 7V9H1V7L3 7Z" fill="#6C707E"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 724 B |
4
src/main/resources/icons/plugin_dark.svg
Normal file
4
src/main/resources/icons/plugin_dark.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 4H7C5.34315 4 4 5.34315 4 7V9C4 10.6569 5.34315 12 7 12H11V11V10V6V5V4ZM12 5V4C12 3.44772 11.5523 3 11 3H7C5.13616 3 3.57006 4.27477 3.12602 6H1C0.447715 6 0 6.44772 0 7V9C0 9.55228 0.447715 10 1 10H3.12602C3.57006 11.7252 5.13616 13 7 13H11C11.5523 13 12 12.5523 12 12V11H15.5C15.7761 11 16 10.7761 16 10.5C16 10.2239 15.7761 10 15.5 10H12V6H15.5C15.7761 6 16 5.77614 16 5.5C16 5.22386 15.7761 5 15.5 5H12ZM3 7V9H1V7L3 7Z" fill="#CED0D6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 724 B |
4
src/main/resources/icons/run.svg
Normal file
4
src/main/resources/icons/run.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13.5 7.13397C14.1667 7.51888 14.1667 8.48113 13.5 8.86603L4.5 14.0622C3.83333 14.4471 3 13.966 3 13.1962L3 2.80385C3 2.03405 3.83333 1.55292 4.5 1.93782L13.5 7.13397Z" stroke="#6C707E"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 427 B |
4
src/main/resources/icons/run_dark.svg
Normal file
4
src/main/resources/icons/run_dark.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13.5 7.13397C14.1667 7.51888 14.1667 8.48113 13.5 8.86603L4.5 14.0622C3.83333 14.4471 3 13.966 3 13.1962L3 2.80385C3 2.03405 3.83333 1.55292 4.5 1.93782L13.5 7.13397Z" stroke="#CED0D6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 427 B |
@@ -1,6 +1,6 @@
|
|||||||
FROM linuxserver/openssh-server
|
FROM linuxserver/openssh-server
|
||||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||||
&& apk update && apk add wget gcc g++ git make zsh htop && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
|
&& apk update && apk add wget gcc g++ git make zsh htop inetutils-telnet && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
|
||||||
&& tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \
|
&& tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \
|
||||||
&& ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz
|
&& ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user