mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 18:32:58 +08:00
Compare commits
27 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 |
29
.github/workflows/osx-aarch64.yml
vendored
29
.github/workflows/osx-aarch64.yml
vendored
@@ -10,6 +10,28 @@ 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.6-osx-aarch64-b825.69.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
|
||||||
@@ -23,9 +45,12 @@ jobs:
|
|||||||
java-version: '21.0.6'
|
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
|
||||||
|
|||||||
29
.github/workflows/osx-x86-64.yml
vendored
29
.github/workflows/osx-x86-64.yml
vendored
@@ -10,6 +10,29 @@ 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.6-osx-x64-b825.69.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
|
||||||
|
|
||||||
@@ -24,7 +47,11 @@ jobs:
|
|||||||
|
|
||||||
|
|
||||||
# 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,6 +15,7 @@
|
|||||||
## 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
|
||||||
@@ -33,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,6 +11,7 @@
|
|||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
- 支持 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 协议
|
||||||
@@ -29,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`
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ plugins {
|
|||||||
|
|
||||||
|
|
||||||
group = "app.termora"
|
group = "app.termora"
|
||||||
version = "1.0.6"
|
version = "1.0.7"
|
||||||
|
|
||||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||||
@@ -38,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)
|
||||||
@@ -251,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}"))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -68,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()
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -137,8 +138,15 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果是取消,那么不需要移动到其它窗口
|
// 如果是取消,那么不需要移动到其它窗口
|
||||||
val c = if (cancelled) null else getTopMostWindowUnderMouse()
|
val c = if (cancelled) owner else getTopMostWindowUnderMouse()
|
||||||
if (c != owner && c is TermoraFrame) {
|
|
||||||
|
// 如果等于 null 表示在空地方释放,那么单独一个窗口
|
||||||
|
if (c == null) {
|
||||||
|
val window = TermoraFrameManager.getInstance().createWindow()
|
||||||
|
dragToAnotherWindow(window)
|
||||||
|
window.location = releasedPoint
|
||||||
|
window.isVisible = true
|
||||||
|
} else if (c != owner && c is TermoraFrame) { // 如果在某个窗口内释放,那么就移动到某个窗口内
|
||||||
dragToAnotherWindow(c)
|
dragToAnotherWindow(c)
|
||||||
} else {
|
} else {
|
||||||
val tab = this.terminalTab
|
val tab = this.terminalTab
|
||||||
@@ -161,6 +169,7 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun mouseReleased(e: MouseEvent) {
|
override fun mouseReleased(e: MouseEvent) {
|
||||||
|
releasedPoint = e.point
|
||||||
stopDrag()
|
stopDrag()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -120,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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import org.apache.commons.io.Charsets
|
|||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
|
|
||||||
class SerialTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
|
class SerialTerminalTab(windowScope: WindowScope, host: Host) :
|
||||||
|
PtyHostTerminalTab(windowScope, host) {
|
||||||
override suspend fun openPtyConnector(): PtyConnector {
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
val serialPort = Serials.openPort(host)
|
val serialPort = Serials.openPort(host)
|
||||||
return SerialPortPtyConnector(
|
return SerialPortPtyConnector(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -691,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)
|
||||||
@@ -924,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,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)
|
||||||
@@ -397,22 +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)) {
|
||||||
val bytes = ptyConnector.getCharset()
|
ptyConnector.write(
|
||||||
.encode("${ControlCharacters.ESC}[200~${content}${ControlCharacters.ESC}[201~")
|
"${ControlCharacters.ESC}[200~${content}${ControlCharacters.ESC}[201~".toByteArray(
|
||||||
.array()
|
ptyConnector.getCharset()
|
||||||
ptyConnector.write(bytes)
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
val bytes = ptyConnector.getCharset()
|
ptyConnector.write(content.toByteArray(ptyConnector.getCharset()))
|
||||||
.encode(content)
|
|
||||||
.array()
|
|
||||||
ptyConnector.write(bytes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
terminal.getScrollingModel().scrollToRow(
|
terminal.getScrollingModel().scrollToRow(
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -180,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
|
||||||
@@ -311,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=作者
|
||||||
@@ -167,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=将命令发送到所有会话
|
||||||
|
|
||||||
@@ -283,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=作者
|
||||||
@@ -164,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=將指令傳送到所有會話
|
||||||
|
|
||||||
@@ -265,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/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