Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a71493e52c | ||
|
|
cb327f218c | ||
|
|
6881b6376f | ||
|
|
5027fd9dfb | ||
|
|
49cef39b8b | ||
|
|
5c4acf85e8 | ||
|
|
07bee64b7f | ||
|
|
923afb7e99 | ||
|
|
68df52bfc0 | ||
|
|
c2ee6fc8ac | ||
|
|
9d4562e7e3 | ||
|
|
5733b5f485 | ||
|
|
9dbdb5fd7a | ||
|
|
a1d1821553 | ||
|
|
4a8faea8c5 | ||
|
|
cfb841db00 | ||
|
|
a87d4ddf82 | ||
|
|
6071b251a4 | ||
|
|
950ff517bb | ||
|
|
70008978d8 | ||
|
|
7c445bdadb | ||
|
|
f24151f6d8 | ||
|
|
7d65a88d63 | ||
|
|
ed57c3e5b4 | ||
|
|
00f11c9ed5 | ||
|
|
5ebea06a95 | ||
|
|
3e5df2161b | ||
|
|
ffcb4d028e | ||
|
|
022ae402cc |
14
THIRDPARTY
@@ -46,6 +46,10 @@ flatlaf 3.5.4
|
||||
Apache License 2.0
|
||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||
|
||||
flatlaf 3.5.4-no-natives
|
||||
Apache License 2.0
|
||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||
|
||||
flatlaf-extras 3.5.4
|
||||
Apache License 2.0
|
||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||
@@ -228,4 +232,12 @@ https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||
|
||||
jediterm
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
|
||||
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
|
||||
|
||||
mixpanel-java 1.5.3
|
||||
Apache License 2.0
|
||||
https://github.com/mixpanel/mixpanel-java/blob/master/LICENSE
|
||||
|
||||
json-20231013
|
||||
Public Domain.
|
||||
https://github.com/stleary/JSON-java/blob/master/LICENSE
|
||||
191
build.gradle.kts
@@ -1,9 +1,9 @@
|
||||
import org.gradle.internal.jvm.Jvm
|
||||
import org.gradle.kotlin.dsl.support.uppercaseFirstChar
|
||||
import org.gradle.nativeplatform.platform.internal.ArchitectureInternal
|
||||
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||
import org.gradle.nativeplatform.platform.internal.DefaultOperatingSystem
|
||||
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
|
||||
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
||||
import java.nio.file.Files
|
||||
|
||||
plugins {
|
||||
java
|
||||
@@ -14,11 +14,20 @@ plugins {
|
||||
|
||||
|
||||
group = "app.termora"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
|
||||
val os: DefaultOperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||
var arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||
val arch: Architecture = DefaultNativePlatform.getCurrentArchitecture()
|
||||
|
||||
// macOS 签名信息
|
||||
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
||||
val macOSSign = os.isMacOsX && macOSSignUsername.isNotBlank()
|
||||
&& System.getenv("TERMORA_MAC_SIGN").toBoolean()
|
||||
|
||||
// macOS 公证信息
|
||||
val macOSNotaryKeychainProfile = System.getenv("TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE") ?: StringUtils.EMPTY
|
||||
val macOSNotary = macOSSign && macOSNotaryKeychainProfile.isNotBlank()
|
||||
&& System.getenv("TERMORA_MAC_NOTARY").toBoolean()
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@@ -27,6 +36,9 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// 由于签名和公证,macOS 不携带 natives
|
||||
val useNoNativesFlatLaf = os.isMacOsX && macOSNotary && System.getenv("ENABLE_BUILD").toBoolean()
|
||||
|
||||
testImplementation(kotlin("test"))
|
||||
testImplementation(libs.hutool)
|
||||
testImplementation(libs.sshj)
|
||||
@@ -50,9 +62,25 @@ dependencies {
|
||||
implementation(libs.commons.compress)
|
||||
implementation(libs.kotlinx.coroutines.swing)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.flatlaf)
|
||||
implementation(libs.flatlaf.extras)
|
||||
implementation(libs.flatlaf.swingx)
|
||||
|
||||
implementation(libs.flatlaf) {
|
||||
artifact {
|
||||
if (useNoNativesFlatLaf) {
|
||||
classifier = "no-natives"
|
||||
}
|
||||
}
|
||||
}
|
||||
implementation(libs.flatlaf.extras) {
|
||||
if (useNoNativesFlatLaf) {
|
||||
exclude(group = "com.formdev", module = "flatlaf")
|
||||
}
|
||||
}
|
||||
implementation(libs.flatlaf.swingx) {
|
||||
if (useNoNativesFlatLaf) {
|
||||
exclude(group = "com.formdev", module = "flatlaf")
|
||||
}
|
||||
}
|
||||
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.swingx)
|
||||
implementation(libs.jgoodies.forms)
|
||||
@@ -75,6 +103,7 @@ dependencies {
|
||||
implementation(libs.xodus.environment)
|
||||
implementation(libs.bip39)
|
||||
implementation(libs.colorpicker)
|
||||
implementation(libs.mixpanel)
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -103,8 +132,53 @@ tasks.test {
|
||||
}
|
||||
|
||||
tasks.register<Copy>("copy-dependencies") {
|
||||
from(configurations.runtimeClasspath)
|
||||
.into("${layout.buildDirectory.get()}/libs")
|
||||
val dir = layout.buildDirectory.dir("libs")
|
||||
from(configurations.runtimeClasspath).into(dir)
|
||||
|
||||
// 对 JNA 和 PTY4J 的本地库提取
|
||||
// 提取出来是为了单独签名,不然无法通过公证
|
||||
if (os.isMacOsX && macOSSign) {
|
||||
doLast {
|
||||
val jna = libs.jna.asProvider().get()
|
||||
val dylib = dir.get().dir("dylib").asFile
|
||||
val pty4j = libs.pty4j.get()
|
||||
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
|
||||
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
||||
val targetDir = File(dylib, jna.name)
|
||||
FileUtils.forceMkdir(targetDir)
|
||||
// @formatter:off
|
||||
exec { commandLine("unzip","-j","-o", file.absolutePath, "com/sun/jna/darwin-${arch.name}/*", "-d", targetDir.absolutePath) }
|
||||
// @formatter:on
|
||||
// 删除所有二进制类库
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/darwin-*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/sunos-*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/openbsd-*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/linux-*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/freebsd-*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/dragonflybsd-*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
|
||||
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
|
||||
val targetDir = FileUtils.getFile(dylib, pty4j.name, "darwin")
|
||||
FileUtils.forceMkdir(targetDir)
|
||||
// @formatter:off
|
||||
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/com/pty4j/native/darwin*", "-d", targetDir.absolutePath) }
|
||||
// @formatter:on
|
||||
// 删除所有二进制类库
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
|
||||
}
|
||||
}
|
||||
|
||||
// 对二进制签名
|
||||
Files.walk(dylib.toPath()).use { paths ->
|
||||
for (path in paths) {
|
||||
if (Files.isRegularFile(path)) {
|
||||
signMacOSLocalFile(path.toFile())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register<Exec>("jlink") {
|
||||
@@ -136,6 +210,7 @@ tasks.register<Exec>("jlink") {
|
||||
}
|
||||
|
||||
tasks.register<Exec>("jpackage") {
|
||||
|
||||
val buildDir = layout.buildDirectory.get()
|
||||
val options = mutableListOf(
|
||||
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
||||
@@ -164,6 +239,9 @@ tasks.register<Exec>("jpackage") {
|
||||
arguments.addAll(listOf("--temp", "$buildDir/jpackage"))
|
||||
arguments.addAll(listOf("--dest", "$buildDir/distributions"))
|
||||
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE)))
|
||||
arguments.addAll(listOf("--vendor", "TermoraDev"))
|
||||
arguments.addAll(listOf("--copyright", "TermoraDev"))
|
||||
arguments.addAll(listOf("--description", "A terminal emulator and SSH client."))
|
||||
|
||||
|
||||
if (os.isMacOsX) {
|
||||
@@ -193,6 +271,12 @@ tasks.register<Exec>("jpackage") {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
if (os.isMacOsX && macOSSign) {
|
||||
arguments.add("--mac-sign")
|
||||
arguments.add("--mac-signing-key-user-name")
|
||||
arguments.add(macOSSignUsername)
|
||||
}
|
||||
|
||||
commandLine(arguments)
|
||||
|
||||
}
|
||||
@@ -207,12 +291,18 @@ tasks.register("dist") {
|
||||
|
||||
val distributionDir = layout.buildDirectory.dir("distributions").get()
|
||||
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
|
||||
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
|
||||
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
|
||||
val macOSFinalFilePath = distributionDir.file("${finalFilenameWithoutExtension}.dmg").asFile.absolutePath
|
||||
|
||||
// 清空目录
|
||||
exec { commandLine(gradlew, "clean") }
|
||||
|
||||
// 打包并复制依赖
|
||||
exec { commandLine(gradlew, "jar", "copy-dependencies") }
|
||||
exec {
|
||||
commandLine(gradlew, "jar", "copy-dependencies")
|
||||
environment("ENABLE_BUILD" to true)
|
||||
}
|
||||
|
||||
// 检查依赖的开源协议
|
||||
exec { commandLine(gradlew, "check-license") }
|
||||
@@ -224,29 +314,64 @@ tasks.register("dist") {
|
||||
exec { commandLine(gradlew, "jpackage") }
|
||||
|
||||
// pack
|
||||
exec {
|
||||
if (os.isWindows) { // zip
|
||||
if (os.isWindows) { // zip and msi
|
||||
// zip
|
||||
exec {
|
||||
commandLine(
|
||||
"tar", "-vacf",
|
||||
distributionDir.file("${project.name}-${project.version}-windows-${arch.name}.zip").asFile.absolutePath,
|
||||
distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile.absolutePath,
|
||||
project.name.uppercaseFirstChar()
|
||||
)
|
||||
workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
|
||||
} else if (os.isLinux) { // tar.gz
|
||||
}
|
||||
|
||||
// msi
|
||||
exec {
|
||||
commandLine(
|
||||
"cmd", "/c", "move",
|
||||
"${project.name.uppercaseFirstChar()}-${project.version}.msi",
|
||||
"${finalFilenameWithoutExtension}.msi"
|
||||
)
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
} else if (os.isLinux) { // tar.gz
|
||||
exec {
|
||||
commandLine(
|
||||
"tar", "-czvf",
|
||||
distributionDir.file("${project.name}-${project.version}-linux-${arch.name}.tar.gz").asFile.absolutePath,
|
||||
distributionDir.file("${finalFilenameWithoutExtension}.tar.gz").asFile.absolutePath,
|
||||
project.name.uppercaseFirstChar()
|
||||
)
|
||||
workingDir = distributionDir.asFile
|
||||
} else if (os.isMacOsX) { // rename
|
||||
}
|
||||
} else if (os.isMacOsX) { // rename
|
||||
exec {
|
||||
commandLine(
|
||||
"mv",
|
||||
distributionDir.file("${project.name.uppercaseFirstChar()}-${project.version}.dmg").asFile.absolutePath,
|
||||
distributionDir.file("${project.name}-${project.version}-osx-${arch.name}.dmg").asFile.absolutePath,
|
||||
macOSFinalFilePath,
|
||||
)
|
||||
} else {
|
||||
throw GradleException("${os.name} is not supported")
|
||||
}
|
||||
} else {
|
||||
throw GradleException("${os.name} is not supported")
|
||||
}
|
||||
|
||||
|
||||
// sign dmg
|
||||
if (os.isMacOsX && macOSSign) {
|
||||
|
||||
// sign
|
||||
signMacOSLocalFile(File(macOSFinalFilePath))
|
||||
|
||||
// notary
|
||||
if (macOSNotary) {
|
||||
exec {
|
||||
commandLine(
|
||||
"/usr/bin/xcrun", "notarytool",
|
||||
"submit", macOSFinalFilePath,
|
||||
"--keychain-profile", macOSNotaryKeychainProfile,
|
||||
"--wait",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,21 +396,29 @@ tasks.register("check-license") {
|
||||
thirdParty[nameWithVersion.replace(StringUtils.SPACE, "-")] = license
|
||||
thirdPartyNames.add(nameWithVersion.split(StringUtils.SPACE).first())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (file in configurations.runtimeClasspath.get()) {
|
||||
val name = file.nameWithoutExtension
|
||||
if (!thirdParty.containsKey(name)) {
|
||||
if (logger.isWarnEnabled) {
|
||||
logger.warn("$name does not exist in third-party")
|
||||
}
|
||||
if (!thirdPartyNames.contains(name)) {
|
||||
throw GradleException("$name No license found")
|
||||
}
|
||||
/**
|
||||
* macOS 对本地文件进行签名
|
||||
*/
|
||||
fun signMacOSLocalFile(file: File) {
|
||||
if (os.isMacOsX && macOSSign) {
|
||||
if (file.exists() && file.isFile) {
|
||||
exec {
|
||||
commandLine(
|
||||
"/usr/bin/codesign",
|
||||
"-s", macOSSignUsername,
|
||||
"--timestamp", "--force",
|
||||
"-vvvv", "--options", "runtime",
|
||||
file.absolutePath,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
kotlin {
|
||||
jvmToolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
|
||||
@@ -40,6 +40,7 @@ colorpicker = "2.0.1"
|
||||
rhino = "1.7.15"
|
||||
delight-rhino-sandbox = "0.0.17"
|
||||
testcontainers = "1.20.4"
|
||||
mixpanel = "1.5.3"
|
||||
|
||||
[libraries]
|
||||
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||
@@ -95,6 +96,7 @@ bip39 = { module = "cash.z.ecc.android:kotlin-bip39-jvm", version.ref = "bip39"
|
||||
rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
||||
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
|
||||
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
|
||||
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
|
||||
|
||||
[plugins]
|
||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
|
||||
@@ -20,7 +20,7 @@ object Actions {
|
||||
/**
|
||||
* 关键词高亮
|
||||
*/
|
||||
const val KEYWORD_HIGHLIGHT_EVERYWHERE = "KeywordHighlightAction"
|
||||
const val KEYWORD_HIGHLIGHT = "KeywordHighlightAction"
|
||||
|
||||
/**
|
||||
* Key manager
|
||||
@@ -47,4 +47,14 @@ object Actions {
|
||||
* 打开一个主机
|
||||
*/
|
||||
const val OPEN_HOST = "OpenHostAction"
|
||||
|
||||
/**
|
||||
* 终端日志记录
|
||||
*/
|
||||
const val TERMINAL_LOGGER = "TerminalLogAction"
|
||||
|
||||
/**
|
||||
* 打开 SFTP Tab Action
|
||||
*/
|
||||
const val SFTP = "SFTPAction"
|
||||
}
|
||||
@@ -6,20 +6,26 @@ import com.formdev.flatlaf.FlatSystemProperties
|
||||
import com.formdev.flatlaf.extras.FlatInspector
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jthemedetecor.OsThemeDetector
|
||||
import com.mixpanel.mixpanelapi.ClientDelivery
|
||||
import com.mixpanel.mixpanelapi.MessageBuilder
|
||||
import com.mixpanel.mixpanelapi.MixpanelAPI
|
||||
import com.sun.jna.platform.WindowUtils
|
||||
import com.sun.jna.platform.win32.User32
|
||||
import com.sun.jna.ptr.IntByReference
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.lang3.LocaleUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
import org.apache.commons.lang3.math.NumberUtils
|
||||
import org.json.JSONObject
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.tinylog.configuration.Configuration
|
||||
import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.channels.FileLock
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.util.*
|
||||
import javax.swing.*
|
||||
import javax.swing.WindowConstants.DISPOSE_ON_CLOSE
|
||||
@@ -51,6 +57,9 @@ class ApplicationRunner {
|
||||
// 加载设置
|
||||
loadSettings()
|
||||
|
||||
// 统计
|
||||
enableAnalytics()
|
||||
|
||||
// 设置 LAF
|
||||
setupLaf()
|
||||
|
||||
@@ -251,4 +260,48 @@ class ApplicationRunner {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计 https://mixpanel.com
|
||||
*/
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun enableAnalytics() {
|
||||
if (Application.isUnknownVersion()) {
|
||||
return
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val properties = JSONObject()
|
||||
properties.put("os", SystemUtils.OS_NAME)
|
||||
if (SystemInfo.isLinux) {
|
||||
properties.put("platform", "Linux")
|
||||
} else if (SystemInfo.isWindows) {
|
||||
properties.put("platform", "Windows")
|
||||
} else if (SystemInfo.isMacOS) {
|
||||
properties.put("platform", "macOS")
|
||||
}
|
||||
properties.put("version", Application.getVersion())
|
||||
properties.put("language", Locale.getDefault().toString())
|
||||
val message = MessageBuilder("0871335f59ee6d0eb246b008a20f9d1c")
|
||||
.event(getAnalyticsUserID(), "launch", properties)
|
||||
val delivery = ClientDelivery()
|
||||
delivery.addMessage(message)
|
||||
MixpanelAPI().deliver(delivery, true)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAnalyticsUserID(): String {
|
||||
var id = Database.instance.properties.getString("AnalyticsUserID")
|
||||
if (id.isNullOrBlank()) {
|
||||
id = UUID.randomUUID().toSimpleString()
|
||||
Database.instance.properties.putString("AnalyticsUserID", id)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
}
|
||||
373
src/main/kotlin/app/termora/CustomizeToolBarDialog.kt
Normal file
@@ -0,0 +1,373 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.db.Database
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import kotlinx.serialization.encodeToString
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import javax.swing.*
|
||||
import javax.swing.event.ListDataEvent
|
||||
import javax.swing.event.ListDataListener
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class CustomizeToolBarDialog(
|
||||
owner: Window,
|
||||
private val toolbar: TermoraToolBar
|
||||
) : DialogWrapper(owner) {
|
||||
|
||||
private val moveTopBtn = JButton(Icons.moveUp)
|
||||
private val moveBottomBtn = JButton(Icons.moveDown)
|
||||
private val upBtn = JButton(Icons.up)
|
||||
private val downBtn = JButton(Icons.down)
|
||||
|
||||
private val leftBtn = JButton(Icons.left)
|
||||
private val rightBtn = JButton(Icons.right)
|
||||
private val resetBtn = JButton(Icons.refresh)
|
||||
private val allToLeftBtn = JButton(Icons.applyNotConflictsRight)
|
||||
private val allToRightBtn = JButton(Icons.applyNotConflictsLeft)
|
||||
|
||||
private val leftList = ToolBarActionList()
|
||||
private val rightList = ToolBarActionList()
|
||||
private val actionManager get() = ActionManager.getInstance()
|
||||
|
||||
private var isOk = false
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 150, UIManager.getInt("Dialog.height") - 100)
|
||||
isModal = true
|
||||
controlsVisible = false
|
||||
isResizable = false
|
||||
title = I18n.getString("termora.toolbar.customize-toolbar")
|
||||
setLocationRelativeTo(null)
|
||||
|
||||
moveTopBtn.isEnabled = false
|
||||
moveBottomBtn.isEnabled = false
|
||||
downBtn.isEnabled = false
|
||||
upBtn.isEnabled = false
|
||||
|
||||
leftBtn.isEnabled = false
|
||||
rightBtn.isEnabled = false
|
||||
|
||||
initEvents()
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
|
||||
allToLeftBtn.isEnabled = !rightList.model.isEmpty
|
||||
allToRightBtn.isEnabled = !leftList.model.isEmpty
|
||||
|
||||
val box = JToolBar(JToolBar.VERTICAL)
|
||||
box.add(Box.createVerticalStrut(leftList.fixedCellHeight))
|
||||
box.add(rightBtn)
|
||||
box.add(leftBtn)
|
||||
box.add(Box.createVerticalGlue())
|
||||
box.add(resetBtn)
|
||||
box.add(Box.createVerticalGlue())
|
||||
box.add(allToRightBtn)
|
||||
box.add(allToLeftBtn)
|
||||
box.add(Box.createVerticalStrut(leftList.fixedCellHeight))
|
||||
|
||||
val box2 = JToolBar(JToolBar.VERTICAL)
|
||||
box2.add(Box.createVerticalStrut(leftList.fixedCellHeight))
|
||||
box2.add(moveTopBtn)
|
||||
box2.add(upBtn)
|
||||
box2.add(Box.createVerticalGlue())
|
||||
box2.add(downBtn)
|
||||
box2.add(moveBottomBtn)
|
||||
box2.add(Box.createVerticalStrut(leftList.fixedCellHeight))
|
||||
|
||||
|
||||
return FormBuilder.create().debug(false)
|
||||
.border(BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor))
|
||||
.layout(FormLayout("default:grow, pref, default:grow, pref", "fill:p:grow"))
|
||||
.add(JScrollPane(leftList).apply {
|
||||
border = BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor)
|
||||
}).xy(1, 1)
|
||||
.add(box).xy(2, 1)
|
||||
.add(JScrollPane(rightList).apply {
|
||||
border = BorderFactory.createMatteBorder(0, 1, 0, 1, DynamicColor.BorderColor)
|
||||
}).xy(3, 1)
|
||||
.add(box2).xy(4, 1)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
rightList.addListSelectionListener { resetMoveButtons() }
|
||||
|
||||
leftList.addListSelectionListener {
|
||||
val indices = leftList.selectedIndices
|
||||
rightBtn.isEnabled = indices.isNotEmpty()
|
||||
}
|
||||
|
||||
leftList.model.addListDataListener(object : ListDataListener {
|
||||
override fun intervalAdded(e: ListDataEvent) {
|
||||
contentsChanged(e)
|
||||
}
|
||||
|
||||
override fun intervalRemoved(e: ListDataEvent) {
|
||||
contentsChanged(e)
|
||||
}
|
||||
|
||||
override fun contentsChanged(e: ListDataEvent) {
|
||||
allToLeftBtn.isEnabled = !rightList.model.isEmpty
|
||||
allToRightBtn.isEnabled = !leftList.model.isEmpty
|
||||
resetMoveButtons()
|
||||
}
|
||||
})
|
||||
|
||||
rightList.model.addListDataListener(object : ListDataListener {
|
||||
override fun intervalAdded(e: ListDataEvent) {
|
||||
contentsChanged(e)
|
||||
}
|
||||
|
||||
override fun intervalRemoved(e: ListDataEvent) {
|
||||
contentsChanged(e)
|
||||
}
|
||||
|
||||
override fun contentsChanged(e: ListDataEvent) {
|
||||
allToLeftBtn.isEnabled = !rightList.model.isEmpty
|
||||
allToRightBtn.isEnabled = !leftList.model.isEmpty
|
||||
resetMoveButtons()
|
||||
}
|
||||
})
|
||||
|
||||
resetBtn.addActionListener {
|
||||
leftList.model.removeAllElements()
|
||||
rightList.model.removeAllElements()
|
||||
for (action in toolbar.getAllActions()) {
|
||||
actionManager.getAction(action.id)?.let {
|
||||
rightList.model.addElement(ActionHolder(action.id, it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// move first
|
||||
moveTopBtn.addActionListener {
|
||||
val indices = rightList.selectedIndices.sortedDescending()
|
||||
rightList.clearSelection()
|
||||
for (index in indices.indices) {
|
||||
val ele = rightList.model.getElementAt(indices[index])
|
||||
rightList.model.removeElementAt(indices[index])
|
||||
rightList.model.add(index, ele)
|
||||
rightList.selectionModel.addSelectionInterval(index, max(index - 1, 0))
|
||||
}
|
||||
}
|
||||
|
||||
// move up
|
||||
upBtn.addActionListener {
|
||||
val indices = rightList.selectedIndices.sortedDescending()
|
||||
rightList.clearSelection()
|
||||
for (index in indices) {
|
||||
val ele = rightList.model.getElementAt(index)
|
||||
rightList.model.removeElementAt(index)
|
||||
rightList.model.add(index - 1, ele)
|
||||
rightList.selectionModel.addSelectionInterval(max(index - 1, 0), max(index - 1, 0))
|
||||
}
|
||||
}
|
||||
|
||||
// move down
|
||||
downBtn.addActionListener {
|
||||
val indices = rightList.selectedIndices.sortedDescending()
|
||||
rightList.clearSelection()
|
||||
for (index in indices) {
|
||||
val ele = rightList.model.getElementAt(index)
|
||||
rightList.model.removeElementAt(index)
|
||||
rightList.model.add(index + 1, ele)
|
||||
rightList.selectionModel.addSelectionInterval(index + 1, index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// move last
|
||||
moveBottomBtn.addActionListener {
|
||||
val indices = rightList.selectedIndices.sortedDescending()
|
||||
val size = rightList.model.size
|
||||
rightList.clearSelection()
|
||||
for (index in indices.indices) {
|
||||
val ele = rightList.model.getElementAt(indices[index])
|
||||
rightList.model.removeElementAt(indices[index])
|
||||
rightList.model.add(size - index - 1, ele)
|
||||
rightList.selectionModel.addSelectionInterval(size - index - 1, size - index - 1)
|
||||
}
|
||||
}
|
||||
|
||||
allToLeftBtn.addActionListener {
|
||||
while (!rightList.model.isEmpty) {
|
||||
val ele = rightList.model.getElementAt(0)
|
||||
rightList.model.removeElementAt(0)
|
||||
leftList.model.addElement(ele)
|
||||
}
|
||||
}
|
||||
|
||||
allToRightBtn.addActionListener {
|
||||
while (!leftList.model.isEmpty) {
|
||||
val ele = leftList.model.getElementAt(0)
|
||||
leftList.model.removeElementAt(0)
|
||||
rightList.model.addElement(ele)
|
||||
}
|
||||
}
|
||||
|
||||
leftBtn.addActionListener {
|
||||
val indices = rightList.selectedIndices.sortedDescending()
|
||||
for (index in indices) {
|
||||
val ele = rightList.model.getElementAt(index)
|
||||
rightList.model.removeElementAt(index)
|
||||
leftList.model.addElement(ele)
|
||||
}
|
||||
rightList.clearSelection()
|
||||
val index = min(indices.max(), rightList.model.size - 1)
|
||||
if (!rightList.model.isEmpty) {
|
||||
rightList.addSelectionInterval(index, index)
|
||||
}
|
||||
}
|
||||
|
||||
rightBtn.addActionListener {
|
||||
val indices = leftList.selectedIndices.sortedDescending()
|
||||
val rightSelectedIndex = if (rightList.selectedIndices.isEmpty()) rightList.model.size else
|
||||
rightList.selectionModel.maxSelectionIndex + 1
|
||||
|
||||
if (indices.isNotEmpty()) {
|
||||
for (index in indices.indices) {
|
||||
val ele = leftList.model.getElementAt(indices[index])
|
||||
leftList.model.removeElementAt(indices[index])
|
||||
rightList.model.add(rightSelectedIndex + index, ele)
|
||||
}
|
||||
|
||||
leftList.clearSelection()
|
||||
val index = min(indices.max(), leftList.model.size - 1)
|
||||
if (!leftList.model.isEmpty) {
|
||||
leftList.addSelectionInterval(index, index)
|
||||
}
|
||||
|
||||
rightList.clearSelection()
|
||||
rightList.addSelectionInterval(rightSelectedIndex, rightSelectedIndex)
|
||||
}
|
||||
}
|
||||
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowOpened(e: WindowEvent) {
|
||||
removeWindowListener(this)
|
||||
|
||||
|
||||
for (action in toolbar.getActions()) {
|
||||
if (action.visible) {
|
||||
actionManager.getAction(action.id)
|
||||
?.let { rightList.model.addElement(ActionHolder(action.id, it)) }
|
||||
} else {
|
||||
actionManager.getAction(action.id)
|
||||
?.let { leftList.model.addElement(ActionHolder(action.id, it)) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun resetMoveButtons() {
|
||||
val indices = rightList.selectedIndices
|
||||
if (indices.isEmpty()) {
|
||||
moveTopBtn.isEnabled = false
|
||||
moveBottomBtn.isEnabled = false
|
||||
downBtn.isEnabled = false
|
||||
upBtn.isEnabled = false
|
||||
} else {
|
||||
moveTopBtn.isEnabled = !indices.contains(0)
|
||||
upBtn.isEnabled = moveTopBtn.isEnabled
|
||||
moveBottomBtn.isEnabled = !indices.contains(rightList.model.size - 1)
|
||||
downBtn.isEnabled = moveBottomBtn.isEnabled
|
||||
}
|
||||
leftBtn.isEnabled = indices.isNotEmpty()
|
||||
}
|
||||
|
||||
private class ToolBarActionList : JList<ActionHolder>() {
|
||||
private val model = DefaultListModel<ActionHolder>()
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
setModel(model)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
background = UIManager.getColor("window")
|
||||
fixedCellHeight = UIManager.getInt("Tree.rowHeight")
|
||||
cellRenderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = value?.toString() ?: StringUtils.EMPTY
|
||||
if (value is ActionHolder) {
|
||||
val action = value.action
|
||||
text = action.getValue(Action.NAME)?.toString() ?: text
|
||||
}
|
||||
|
||||
val c = super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
||||
if (value is ActionHolder) {
|
||||
val action = value.action
|
||||
val icon = action.getValue(Action.SMALL_ICON) as Icon?
|
||||
if (icon != null) {
|
||||
this.icon = icon
|
||||
if (icon is DynamicIcon) {
|
||||
if (isSelected && cellHasFocus) {
|
||||
this.icon = icon.dark
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
}
|
||||
|
||||
override fun getModel(): DefaultListModel<ActionHolder> {
|
||||
return model
|
||||
}
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
isOk = true
|
||||
|
||||
val actions = mutableListOf<ToolBarAction>()
|
||||
for (i in 0 until rightList.model.size()) {
|
||||
actions.add(ToolBarAction(rightList.model.getElementAt(i).id, true))
|
||||
}
|
||||
|
||||
for (i in 0 until leftList.model.size()) {
|
||||
actions.add(ToolBarAction(leftList.model.getElementAt(i).id, false))
|
||||
}
|
||||
|
||||
Database.instance.properties.putString("Termora.ToolBar.Actions", ohMyJson.encodeToString(actions))
|
||||
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
fun open(): Boolean {
|
||||
isModal = true
|
||||
isVisible = true
|
||||
return isOk
|
||||
}
|
||||
|
||||
private class ActionHolder(val id: String, val action: Action)
|
||||
}
|
||||
@@ -8,9 +8,15 @@ import kotlinx.coroutines.swing.Swing
|
||||
import java.beans.PropertyChangeEvent
|
||||
import javax.swing.Icon
|
||||
|
||||
abstract class HostTerminalTab(val host: Host) : PropertyTerminalTab() {
|
||||
abstract class HostTerminalTab(
|
||||
val host: Host,
|
||||
protected val terminal: Terminal = TerminalFactory.instance.createTerminal()
|
||||
) : PropertyTerminalTab() {
|
||||
companion object {
|
||||
val Host = DataKey(app.termora.Host::class)
|
||||
}
|
||||
|
||||
protected val coroutineScope by lazy { CoroutineScope(Dispatchers.Swing) }
|
||||
protected val terminal = TerminalFactory.instance.createTerminal()
|
||||
protected val terminalModel get() = terminal.getTerminalModel()
|
||||
protected var unread = false
|
||||
set(value) {
|
||||
@@ -25,6 +31,7 @@ abstract class HostTerminalTab(val host: Host) : PropertyTerminalTab() {
|
||||
}
|
||||
|
||||
init {
|
||||
terminal.getTerminalModel().setData(Host, host)
|
||||
terminal.getTerminalModel().addDataListener(object : DataListener {
|
||||
override fun onChanged(key: DataKey<*>, data: Any) {
|
||||
if (key == VisualTerminal.Written) {
|
||||
@@ -51,6 +58,7 @@ abstract class HostTerminalTab(val host: Host) : PropertyTerminalTab() {
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
terminal.close()
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ package app.termora
|
||||
object Icons {
|
||||
val bulletList by lazy { DynamicIcon("icons/bulletList.svg", "icons/bulletList_dark.svg") }
|
||||
val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") }
|
||||
val moveUp by lazy { DynamicIcon("icons/moveUp.svg", "icons/moveUp_dark.svg") }
|
||||
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
|
||||
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
|
||||
val close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") }
|
||||
val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
|
||||
val matchCase by lazy { DynamicIcon("icons/matchCase.svg", "icons/matchCase_dark.svg") }
|
||||
@@ -14,6 +16,7 @@ object Icons {
|
||||
val settings by lazy { DynamicIcon("icons/settings.svg", "icons/settings_dark.svg") }
|
||||
val pin by lazy { DynamicIcon("icons/pin.svg", "icons/pin_dark.svg") }
|
||||
val empty by lazy { DynamicIcon("icons/empty.svg") }
|
||||
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") }
|
||||
val add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") }
|
||||
val errorIntroduction by lazy { DynamicIcon("icons/errorIntroduction.svg", "icons/errorIntroduction_dark.svg") }
|
||||
val networkPolicy by lazy { DynamicIcon("icons/networkPolicy.svg", "icons/networkPolicy_dark.svg") }
|
||||
@@ -47,11 +50,11 @@ object Icons {
|
||||
val google by lazy { DynamicIcon("icons/google-small.svg") }
|
||||
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }
|
||||
val yandexCloud by lazy { DynamicIcon("icons/yandexCloud.svg") }
|
||||
val aws by lazy { DynamicIcon("icons/aws.svg","icons/aws_dark.svg") }
|
||||
val aws by lazy { DynamicIcon("icons/aws.svg", "icons/aws_dark.svg") }
|
||||
val huawei by lazy { DynamicIcon("icons/huawei.svg") }
|
||||
val baidu by lazy { DynamicIcon("icons/baiduyun.svg") }
|
||||
val tianyi by lazy { DynamicIcon("icons/tianyiyun.svg") }
|
||||
val digitalocean by lazy { DynamicIcon("icons/digitalocean.svg","icons/digitalocean_dark.svg") }
|
||||
val digitalocean by lazy { DynamicIcon("icons/digitalocean.svg", "icons/digitalocean_dark.svg") }
|
||||
val terminalUnread by lazy { DynamicIcon("icons/terminalUnread.svg", "icons/terminalUnread_dark.svg") }
|
||||
val dbPrimitive by lazy { DynamicIcon("icons/dbPrimitive.svg", "icons/dbPrimitive_dark.svg") }
|
||||
val linux by lazy { DynamicIcon("icons/linux.svg", "icons/linux_dark.svg") }
|
||||
@@ -73,8 +76,23 @@ object Icons {
|
||||
val colorPicker by lazy { DynamicIcon("icons/colorPicker.svg", "icons/colorPicker_dark.svg") }
|
||||
val folder by lazy { DynamicIcon("icons/folder.svg", "icons/folder_dark.svg") }
|
||||
val listFiles by lazy { DynamicIcon("icons/listFiles.svg", "icons/listFiles_dark.svg") }
|
||||
val left by lazy { DynamicIcon("icons/left.svg", "icons/left_dark.svg") }
|
||||
val right by lazy { DynamicIcon("icons/right.svg", "icons/right_dark.svg") }
|
||||
val dotListFiles by lazy { DynamicIcon("icons/dotListFiles.svg", "icons/dotListFiles_dark.svg") }
|
||||
val fileTransfer by lazy { DynamicIcon("icons/fileTransfer.svg", "icons/fileTransfer_dark.svg") }
|
||||
val help by lazy { DynamicIcon("icons/help.svg", "icons/help_dark.svg") }
|
||||
val applyNotConflictsLeft by lazy {
|
||||
DynamicIcon(
|
||||
"icons/applyNotConflictsLeft.svg",
|
||||
"icons/applyNotConflictsLeft_dark.svg"
|
||||
)
|
||||
}
|
||||
val applyNotConflictsRight by lazy {
|
||||
DynamicIcon(
|
||||
"icons/applyNotConflictsRight.svg",
|
||||
"icons/applyNotConflictsRight_dark.svg"
|
||||
)
|
||||
}
|
||||
val expand by lazy { DynamicIcon("icons/expand.svg", "icons/expand_dark.svg") }
|
||||
val collapse by lazy { DynamicIcon("icons/collapse.svg", "icons/collapse_dark.svg") }
|
||||
val expandAll by lazy { DynamicIcon("icons/expandAll.svg", "icons/expandAll_dark.svg") }
|
||||
|
||||
@@ -8,6 +8,51 @@ import com.formdev.flatlaf.FlatPropertiesLaf
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import java.util.*
|
||||
|
||||
class DraculaLaf : FlatPropertiesLaf("Dracula", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "dark",
|
||||
"@background" to "#282935",
|
||||
"@windowText" to "#eaeaea",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Basic.BACKGROUND -> 0x282935
|
||||
TerminalColor.Basic.FOREGROUND -> 0xeaeaea
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND -> 0x56596b
|
||||
TerminalColor.Basic.SELECTION_FOREGROUND -> 0xfeffff
|
||||
TerminalColor.Basic.HYPERLINK -> 0x255ab4
|
||||
|
||||
TerminalColor.Cursor.BACKGROUND -> 0xc7c7c7
|
||||
|
||||
TerminalColor.Find.BACKGROUND -> 0xffff00
|
||||
TerminalColor.Find.FOREGROUND -> 0x282935
|
||||
|
||||
TerminalColor.Normal.BLACK -> 0
|
||||
TerminalColor.Normal.RED -> 0xef766d
|
||||
TerminalColor.Normal.GREEN -> 0x88f397
|
||||
TerminalColor.Normal.YELLOW -> 0xf4f8a7
|
||||
TerminalColor.Normal.BLUE -> 0xc4a9f4
|
||||
TerminalColor.Normal.MAGENTA -> 0xf297cd
|
||||
TerminalColor.Normal.CYAN -> 0xaceafb
|
||||
TerminalColor.Normal.WHITE -> 0xc7c7c7
|
||||
|
||||
TerminalColor.Bright.BLACK -> 0x676767
|
||||
TerminalColor.Bright.RED -> 0xef766d
|
||||
TerminalColor.Bright.GREEN -> 0x88f397
|
||||
TerminalColor.Bright.YELLOW -> 0xf4f8a7
|
||||
TerminalColor.Bright.BLUE -> 0xc4a9f4
|
||||
TerminalColor.Bright.MAGENTA -> 0xf297cd
|
||||
TerminalColor.Bright.CYAN -> 0xaceafb
|
||||
TerminalColor.Bright.WHITE -> 0xfeffff
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class LightLaf : FlatLightLaf(), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
@@ -163,7 +208,7 @@ class TermiusDarkLaf : FlatPropertiesLaf("Termius Dark", Properties().apply {
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0x21b568
|
||||
|
||||
TerminalColor.Basic.SELECTION_FOREGROUND ->0
|
||||
TerminalColor.Basic.SELECTION_FOREGROUND -> 0
|
||||
|
||||
TerminalColor.Basic.FOREGROUND -> 0x21b568
|
||||
|
||||
|
||||
@@ -1,6 +1,44 @@
|
||||
package app.termora
|
||||
|
||||
import com.pty4j.util.PtyUtil
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
import java.io.File
|
||||
|
||||
fun main() {
|
||||
// 由于 macOS 签名和公证问题,依赖二进制依赖会单独在一个文件夹
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
setupNativeLibraries()
|
||||
}
|
||||
|
||||
ApplicationRunner().run()
|
||||
}
|
||||
|
||||
|
||||
private fun setupNativeLibraries() {
|
||||
if (!SystemUtils.IS_OS_MAC_OSX) {
|
||||
return
|
||||
}
|
||||
|
||||
val appPath = Application.getAppPath()
|
||||
if (StringUtils.isBlank(appPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
val contents = File(appPath).parentFile?.parentFile ?: return
|
||||
val dylib = FileUtils.getFile(contents, "app", "dylib")
|
||||
if (!dylib.exists()) {
|
||||
return
|
||||
}
|
||||
|
||||
val jna = FileUtils.getFile(dylib, "jna")
|
||||
if (jna.exists()) {
|
||||
System.setProperty("jna.boot.library.path", jna.absolutePath)
|
||||
}
|
||||
|
||||
val pty4j = FileUtils.getFile(dylib, "pty4j")
|
||||
if (pty4j.exists()) {
|
||||
System.setProperty(PtyUtil.PREFERRED_NATIVE_FOLDER_KEY, pty4j.absolutePath)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jetbrains.JBR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.JXLabel
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
@@ -122,7 +123,7 @@ object OptionPane {
|
||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop()
|
||||
.isSupported(Desktop.Action.BROWSE_FILE_DIR)
|
||||
) {
|
||||
if (JOptionPane.YES_OPTION == showConfirmDialog(
|
||||
if (yMessage.isEmpty() || JOptionPane.YES_OPTION == showConfirmDialog(
|
||||
parentComponent,
|
||||
yMessage,
|
||||
optionType = JOptionPane.YES_NO_OPTION
|
||||
|
||||
@@ -30,7 +30,12 @@ class PtyConnectorFactory {
|
||||
envs.putAll(env)
|
||||
|
||||
val command = database.terminal.localShell
|
||||
val ptyProcess = PtyProcessBuilder(arrayOf(command))
|
||||
val commands = mutableListOf(command)
|
||||
if (SystemUtils.IS_OS_UNIX) {
|
||||
commands.add("-l")
|
||||
}
|
||||
|
||||
val ptyProcess = PtyProcessBuilder(commands.toTypedArray())
|
||||
.setEnvironment(envs)
|
||||
.setInitialRows(rows)
|
||||
.setInitialColumns(cols)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.terminal.ControlCharacters
|
||||
import app.termora.terminal.PtyConnector
|
||||
import app.termora.terminal.PtyConnectorDelegate
|
||||
import app.termora.terminal.TerminalKeyEvent
|
||||
import app.termora.terminal.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
@@ -12,7 +9,11 @@ import java.awt.event.KeyEvent
|
||||
import javax.swing.JComponent
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
abstract class PtyHostTerminalTab(host: Host) : HostTerminalTab(host) {
|
||||
abstract class PtyHostTerminalTab(
|
||||
host: Host,
|
||||
terminal: Terminal = TerminalFactory.instance.createTerminal()
|
||||
) : HostTerminalTab(host, terminal) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
|
||||
}
|
||||
@@ -60,6 +61,10 @@ abstract class PtyHostTerminalTab(host: Host) : HostTerminalTab(host) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
|
||||
// 失败关闭
|
||||
stop()
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
terminal.write("\r\n${ControlCharacters.ESC}[31m")
|
||||
terminal.write(ExceptionUtils.getRootCauseMessage(e))
|
||||
|
||||
@@ -20,7 +20,7 @@ class SFTPTerminalTab : Disposable, TerminalTab {
|
||||
}
|
||||
|
||||
override fun getIcon(): Icon {
|
||||
return Icons.fileTransfer
|
||||
return Icons.folder
|
||||
}
|
||||
|
||||
override fun addPropertyChangeListener(listener: PropertyChangeListener) {
|
||||
@@ -34,6 +34,9 @@ class SFTPTerminalTab : Disposable, TerminalTab {
|
||||
return transportPanel
|
||||
}
|
||||
|
||||
override fun canClone(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun canClose(): Boolean {
|
||||
assertEventDispatchThread()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||
import app.termora.terminal.ControlCharacters
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.PtyConnector
|
||||
@@ -24,6 +25,7 @@ import org.apache.sshd.common.util.net.SshdSocketAddress
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.charset.StandardCharsets
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
|
||||
class SSHTerminalTab(host: Host) : PtyHostTerminalTab(host) {
|
||||
@@ -76,6 +78,9 @@ class SSHTerminalTab(host: Host) : PtyHostTerminalTab(host) {
|
||||
}
|
||||
|
||||
val client = SshClients.openClient(host).also { sshClient = it }
|
||||
// keyboard interactive
|
||||
client.userInteraction = TerminalUserInteraction(SwingUtilities.getWindowAncestor(terminalPanel))
|
||||
|
||||
val sessionListener = MySessionListener()
|
||||
val channelListener = MyChannelListener()
|
||||
|
||||
|
||||
@@ -654,6 +654,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
gistTextField.isEnabled = false
|
||||
tokenTextField.isEnabled = false
|
||||
keysCheckBox.isEnabled = false
|
||||
macrosCheckBox.isEnabled = false
|
||||
keywordHighlightsCheckBox.isEnabled = false
|
||||
hostsCheckBox.isEnabled = false
|
||||
domainTextField.isEnabled = false
|
||||
@@ -685,6 +686,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
keysCheckBox.isEnabled = true
|
||||
hostsCheckBox.isEnabled = true
|
||||
typeComboBox.isEnabled = true
|
||||
macrosCheckBox.isEnabled = true
|
||||
gistTextField.isEnabled = true
|
||||
tokenTextField.isEnabled = true
|
||||
domainTextField.isEnabled = true
|
||||
@@ -872,6 +874,8 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
var rows = 1
|
||||
val step = 2
|
||||
|
||||
val branch = if (Application.isUnknownVersion()) "main" else Application.getVersion()
|
||||
|
||||
return FormBuilder.create().padding("$formMargin, $formMargin, $formMargin, $formMargin")
|
||||
.layout(layout).debug(true)
|
||||
.add(I18n.getString("termora.settings.about.termora", Application.getVersion()))
|
||||
@@ -881,7 +885,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
.add("${I18n.getString("termora.settings.about.source")}:").xy(1, rows)
|
||||
.add(
|
||||
createHyperlink(
|
||||
"https://github.com/TermoraDev/termora/tree/${Application.getVersion()}",
|
||||
"https://github.com/TermoraDev/termora/tree/${branch}",
|
||||
"https://github.com/TermoraDev/termora",
|
||||
)
|
||||
).xy(3, rows).apply { rows += step }
|
||||
@@ -890,7 +894,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
.add("${I18n.getString("termora.settings.about.third-party")}:").xy(1, rows)
|
||||
.add(
|
||||
createHyperlink(
|
||||
"https://github.com/TermoraDev/termora/blob/${Application.getVersion()}/THIRDPARTY",
|
||||
"https://github.com/TermoraDev/termora/blob/${branch}/THIRDPARTY",
|
||||
"Open-source software"
|
||||
)
|
||||
).xy(3, rows).apply { rows += step }
|
||||
|
||||
@@ -64,9 +64,12 @@ object SshClients {
|
||||
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||
session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password)
|
||||
}
|
||||
if (!session.auth().verify(timeout).await(timeout)) {
|
||||
|
||||
val verifyTimeout = Duration.ofSeconds(timeout.seconds * 5)
|
||||
if (!session.auth().verify(verifyTimeout).await(verifyTimeout)) {
|
||||
throw SshException("Authentication failed")
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package app.termora
|
||||
import app.termora.db.Database
|
||||
import app.termora.terminal.*
|
||||
import app.termora.terminal.panel.TerminalPanel
|
||||
import app.termora.tlog.TerminalLoggerDataListener
|
||||
import java.awt.Color
|
||||
import javax.swing.UIManager
|
||||
|
||||
@@ -15,6 +16,10 @@ class TerminalFactory {
|
||||
|
||||
fun createTerminal(): Terminal {
|
||||
val terminal = MyVisualTerminal()
|
||||
|
||||
// terminal logger listener
|
||||
terminal.getTerminalModel().addDataListener(TerminalLoggerDataListener(terminal))
|
||||
|
||||
terminals.add(terminal)
|
||||
return terminal
|
||||
}
|
||||
@@ -23,7 +28,7 @@ class TerminalFactory {
|
||||
return terminals
|
||||
}
|
||||
|
||||
private inner class MyVisualTerminal : VisualTerminal() {
|
||||
open class MyVisualTerminal : VisualTerminal() {
|
||||
private val terminalModel by lazy { MyTerminalModel(this) }
|
||||
|
||||
override fun getTerminalModel(): TerminalModel {
|
||||
@@ -31,13 +36,13 @@ class TerminalFactory {
|
||||
}
|
||||
}
|
||||
|
||||
private inner class MyTerminalModel(terminal: Terminal) : TerminalModelImpl(terminal) {
|
||||
open class MyTerminalModel(terminal: Terminal) : TerminalModelImpl(terminal) {
|
||||
private val colorPalette by lazy { MyColorPalette(terminal) }
|
||||
private val config get() = Database.instance.terminal
|
||||
|
||||
init {
|
||||
setData(DataKey.CursorStyle, config.cursor)
|
||||
setData(TerminalPanel.Debug, config.debug)
|
||||
this.setData(DataKey.CursorStyle, config.cursor)
|
||||
this.setData(TerminalPanel.Debug, config.debug)
|
||||
}
|
||||
|
||||
override fun getColorPalette(): ColorPalette {
|
||||
@@ -97,7 +102,7 @@ class TerminalFactory {
|
||||
|
||||
}
|
||||
|
||||
private inner class MyColorPalette(terminal: Terminal) : ColorPaletteImpl(terminal) {
|
||||
class MyColorPalette(terminal: Terminal) : ColorPaletteImpl(terminal) {
|
||||
private val colorTheme by lazy { FlatLafColorTheme() }
|
||||
override fun getTheme(): ColorTheme {
|
||||
return colorTheme
|
||||
|
||||
@@ -42,5 +42,10 @@ interface TerminalTab : Disposable {
|
||||
*/
|
||||
fun canClose(): Boolean = true
|
||||
|
||||
/**
|
||||
* 是否可以克隆
|
||||
*/
|
||||
fun canClone(): Boolean = true
|
||||
|
||||
|
||||
}
|
||||
@@ -4,30 +4,25 @@ import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
|
||||
import app.termora.findeverywhere.FindEverywhere
|
||||
import app.termora.findeverywhere.FindEverywhereProvider
|
||||
import app.termora.findeverywhere.FindEverywhereResult
|
||||
import app.termora.transport.TransportPanel
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.action.ActionContainerFactory
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.KeyEvent
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.beans.PropertyChangeEvent
|
||||
import java.awt.*
|
||||
import java.awt.event.*
|
||||
import java.beans.PropertyChangeListener
|
||||
import javax.swing.*
|
||||
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
||||
import kotlin.math.min
|
||||
|
||||
class TerminalTabbed(
|
||||
private val toolbar: JToolBar,
|
||||
private val termoraToolBar: TermoraToolBar,
|
||||
private val tabbedPane: FlatTabbedPane,
|
||||
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager {
|
||||
private val tabs = mutableListOf<TerminalTab>()
|
||||
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
|
||||
private val toolbar = termoraToolBar.getJToolBar()
|
||||
|
||||
private val iconListener = PropertyChangeListener { e ->
|
||||
val source = e.source
|
||||
@@ -53,33 +48,6 @@ class TerminalTabbed(
|
||||
tabbedPane.styleMap = mapOf(
|
||||
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground")
|
||||
)
|
||||
|
||||
val actionManager = ActionManager.getInstance()
|
||||
val actionContainerFactory = ActionContainerFactory(actionManager)
|
||||
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
|
||||
updateBtn.isVisible = updateBtn.isEnabled
|
||||
updateBtn.addChangeListener { updateBtn.isVisible = updateBtn.isEnabled }
|
||||
|
||||
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
|
||||
override fun actionPerformed(e: ActionEvent?) {
|
||||
actionManager.getAction(Actions.FIND_EVERYWHERE)?.actionPerformed(e)
|
||||
}
|
||||
|
||||
override fun isEnabled(): Boolean {
|
||||
return actionManager.getAction(Actions.FIND_EVERYWHERE)?.isEnabled ?: false
|
||||
}
|
||||
}))
|
||||
toolbar.add(Box.createHorizontalStrut(UIManager.getInt("TabbedPane.tabHeight")))
|
||||
toolbar.add(Box.createHorizontalGlue())
|
||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.MACRO)))
|
||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.KEYWORD_HIGHLIGHT_EVERYWHERE)))
|
||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.KEY_MANAGER)))
|
||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.MULTIPLE)))
|
||||
toolbar.add(updateBtn)
|
||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.FIND_EVERYWHERE)))
|
||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.SETTING)))
|
||||
|
||||
|
||||
tabbedPane.trailingComponent = toolbar
|
||||
|
||||
add(tabbedPane, BorderLayout.CENTER)
|
||||
@@ -92,18 +60,16 @@ class TerminalTabbed(
|
||||
tabbedPane.setTabCloseCallback { _, i -> removeTabAt(i, true) }
|
||||
|
||||
// 选中变动
|
||||
tabbedPane.addPropertyChangeListener("selectedIndex", object : PropertyChangeListener {
|
||||
override fun propertyChange(evt: PropertyChangeEvent) {
|
||||
val oldIndex = evt.oldValue as Int
|
||||
val newIndex = evt.newValue as Int
|
||||
if (oldIndex >= 0 && tabs.size > newIndex) {
|
||||
tabs[oldIndex].onLostFocus()
|
||||
}
|
||||
if (newIndex >= 0 && tabs.size > newIndex) {
|
||||
tabs[newIndex].onGrabFocus()
|
||||
}
|
||||
tabbedPane.addPropertyChangeListener("selectedIndex") { evt ->
|
||||
val oldIndex = evt.oldValue as Int
|
||||
val newIndex = evt.newValue as Int
|
||||
if (oldIndex >= 0 && tabs.size > newIndex) {
|
||||
tabs[oldIndex].onLostFocus()
|
||||
}
|
||||
})
|
||||
if (newIndex >= 0 && tabs.size > newIndex) {
|
||||
tabs[newIndex].onGrabFocus()
|
||||
}
|
||||
}
|
||||
|
||||
// 选择变动
|
||||
tabbedPane.addChangeListener {
|
||||
@@ -174,7 +140,8 @@ class TerminalTabbed(
|
||||
override fun find(pattern: String): List<FindEverywhereResult> {
|
||||
val results = mutableListOf<FindEverywhereResult>()
|
||||
for (i in 0 until tabbedPane.tabCount) {
|
||||
if (tabbedPane.getComponentAt(i) is WelcomePanel) {
|
||||
val c = tabbedPane.getComponentAt(i)
|
||||
if (c is WelcomePanel || c is TransportPanel) {
|
||||
continue
|
||||
}
|
||||
results.add(
|
||||
@@ -208,6 +175,9 @@ class TerminalTabbed(
|
||||
}
|
||||
})
|
||||
|
||||
// 监听全局事件
|
||||
toolkit.addAWTEventListener(customizeToolBarAWTEventListener, AWTEvent.MOUSE_EVENT_MASK)
|
||||
|
||||
}
|
||||
|
||||
private fun removeTabAt(index: Int, disposable: Boolean = true) {
|
||||
@@ -248,6 +218,7 @@ class TerminalTabbed(
|
||||
|
||||
private fun showContextMenu(tabIndex: Int, e: MouseEvent) {
|
||||
val c = tabbedPane.getComponentAt(tabIndex) as JComponent
|
||||
val tab = tabs[tabIndex]
|
||||
|
||||
val popupMenu = FlatPopupMenu()
|
||||
|
||||
@@ -272,14 +243,10 @@ class TerminalTabbed(
|
||||
// 克隆
|
||||
val clone = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.clone"))
|
||||
clone.addActionListener {
|
||||
val index = tabbedPane.selectedIndex
|
||||
if (index > 0) {
|
||||
val tab = tabs[index]
|
||||
if (tab is HostTerminalTab) {
|
||||
ActionManager.getInstance()
|
||||
.getAction(Actions.OPEN_HOST)
|
||||
.actionPerformed(OpenHostActionEvent(this, tab.host))
|
||||
}
|
||||
if (tab is HostTerminalTab) {
|
||||
ActionManager.getInstance()
|
||||
.getAction(Actions.OPEN_HOST)
|
||||
.actionPerformed(OpenHostActionEvent(this, tab.host))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,13 +255,14 @@ class TerminalTabbed(
|
||||
openInNewWindow.addActionListener {
|
||||
val index = tabbedPane.selectedIndex
|
||||
if (index > 0) {
|
||||
val tab = tabs[index]
|
||||
val title = tabbedPane.getTitleAt(index)
|
||||
removeTabAt(index, false)
|
||||
val dialog = TerminalTabDialog(
|
||||
owner = SwingUtilities.getWindowAncestor(this),
|
||||
terminalTab = tab,
|
||||
size = Dimension(min(size.width, 1280), min(size.height, 800))
|
||||
)
|
||||
dialog.title = title
|
||||
Disposer.register(dialog, tab)
|
||||
Disposer.register(this, dialog)
|
||||
dialog.isVisible = true
|
||||
@@ -332,12 +300,11 @@ class TerminalTabbed(
|
||||
clone.isEnabled = close.isEnabled
|
||||
openInNewWindow.isEnabled = close.isEnabled
|
||||
|
||||
// SFTP不允许克隆
|
||||
if (clone.isEnabled && getSelectedTerminalTab() is SFTPTerminalTab) {
|
||||
// 如果不允许克隆
|
||||
if (clone.isEnabled && !tab.canClone()) {
|
||||
clone.isEnabled = false
|
||||
}
|
||||
|
||||
|
||||
if (close.isEnabled) {
|
||||
popupMenu.addSeparator()
|
||||
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
|
||||
@@ -370,6 +337,64 @@ class TerminalTabbed(
|
||||
Disposer.register(this, tab)
|
||||
}
|
||||
|
||||
/**
|
||||
* 对着 ToolBar 右键
|
||||
*/
|
||||
private inner class CustomizeToolBarAWTEventListener : AWTEventListener, Disposable {
|
||||
init {
|
||||
Disposer.register(this@TerminalTabbed, this)
|
||||
}
|
||||
|
||||
override fun eventDispatched(event: AWTEvent) {
|
||||
if (event !is MouseEvent || event.id != MouseEvent.MOUSE_CLICKED || !SwingUtilities.isRightMouseButton(event)) return
|
||||
// 如果 ToolBar 没有显示
|
||||
if (!toolbar.isShowing) return
|
||||
// 如果不是作用于在 ToolBar 上面
|
||||
if (!Rectangle(toolbar.locationOnScreen, toolbar.size).contains(event.locationOnScreen)) return
|
||||
|
||||
// 显示右键菜单
|
||||
showContextMenu(event)
|
||||
}
|
||||
|
||||
private fun showContextMenu(event: MouseEvent) {
|
||||
val popupMenu = FlatPopupMenu()
|
||||
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
|
||||
val dialog = CustomizeToolBarDialog(
|
||||
SwingUtilities.getWindowAncestor(this@TerminalTabbed),
|
||||
termoraToolBar
|
||||
)
|
||||
if (dialog.open()) {
|
||||
termoraToolBar.rebuild()
|
||||
}
|
||||
}
|
||||
popupMenu.show(event.component, event.x, event.y)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
toolkit.removeAWTEventListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
/*private inner class CustomizeToolBarDialog(owner: Window) : DialogWrapper(owner) {
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||
isModal = true
|
||||
title = I18n.getString("termora.setting")
|
||||
setLocationRelativeTo(null)
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val model = DefaultListModel<String>()
|
||||
val checkBoxList = CheckBoxList(model)
|
||||
checkBoxList.fixedCellHeight = UIManager.getInt("Tree.rowHeight")
|
||||
model.addElement("Test")
|
||||
return checkBoxList
|
||||
}
|
||||
|
||||
}*/
|
||||
|
||||
private inner class SwitchFindEverywhereResult(
|
||||
private val title: String,
|
||||
private val icon: Icon?,
|
||||
|
||||
@@ -4,6 +4,8 @@ import app.termora.findeverywhere.FindEverywhere
|
||||
import app.termora.highlight.KeywordHighlightDialog
|
||||
import app.termora.keymgr.KeyManagerDialog
|
||||
import app.termora.macro.MacroAction
|
||||
import app.termora.tlog.TerminalLoggerAction
|
||||
import app.termora.transport.SFTPAction
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import com.formdev.flatlaf.extras.FlatDesktop
|
||||
@@ -43,12 +45,12 @@ class TermoraFrame : JFrame() {
|
||||
private val log = LoggerFactory.getLogger(TermoraFrame::class.java)
|
||||
}
|
||||
|
||||
private val toolbar = JToolBar()
|
||||
private val titleBar = LogicCustomTitleBar.createCustomTitleBar(this)
|
||||
private val tabbedPane = MyTabbedPane()
|
||||
private val toolbar = TermoraToolBar(titleBar, tabbedPane)
|
||||
private lateinit var terminalTabbed: TerminalTabbed
|
||||
private val disposable = Disposer.newDisposable()
|
||||
private val isWindowDecorationsSupported by lazy { JBR.isWindowDecorationsSupported() }
|
||||
private val titleBar = LogicCustomTitleBar.createCustomTitleBar(this)
|
||||
private val updaterManager get() = UpdaterManager.instance
|
||||
|
||||
private val preferencesHandler = object : Runnable {
|
||||
@@ -66,6 +68,7 @@ class TermoraFrame : JFrame() {
|
||||
FlatDesktop.setPreferencesHandler(that)
|
||||
}
|
||||
})
|
||||
dialog.setLocationRelativeTo(owner)
|
||||
dialog.isVisible = true
|
||||
}
|
||||
}
|
||||
@@ -80,38 +83,6 @@ class TermoraFrame : JFrame() {
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
// 监听窗口大小变动,然后修改边距避开控制按钮
|
||||
addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentResized(e: ComponentEvent) {
|
||||
if (SystemInfo.isMacOS) {
|
||||
val left = titleBar.leftInset.toInt()
|
||||
if (tabbedPane.tabAreaInsets.left != left) {
|
||||
tabbedPane.tabAreaInsets = Insets(0, left, 0, 0)
|
||||
}
|
||||
} else if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
|
||||
val right = titleBar.rightInset.toInt()
|
||||
|
||||
for (i in 0 until toolbar.componentCount) {
|
||||
val c = toolbar.getComponent(i)
|
||||
if (c.name == "spacing") {
|
||||
if (c.width == right) {
|
||||
return
|
||||
}
|
||||
toolbar.remove(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (right > 0) {
|
||||
val spacing = Box.createHorizontalStrut(right)
|
||||
spacing.name = "spacing"
|
||||
toolbar.add(spacing)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
forceHitTest()
|
||||
|
||||
// macos 需要判断是否全部删除
|
||||
@@ -209,7 +180,7 @@ class TermoraFrame : JFrame() {
|
||||
|
||||
|
||||
// Keyword Highlight
|
||||
ActionManager.getInstance().addAction(Actions.KEYWORD_HIGHLIGHT_EVERYWHERE, object : AnAction(
|
||||
ActionManager.getInstance().addAction(Actions.KEYWORD_HIGHLIGHT, object : AnAction(
|
||||
I18n.getString("termora.highlight"),
|
||||
Icons.edit
|
||||
) {
|
||||
@@ -233,6 +204,12 @@ class TermoraFrame : JFrame() {
|
||||
}
|
||||
})
|
||||
|
||||
// 终端日志记录
|
||||
ActionManager.getInstance().addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
|
||||
|
||||
// SFTP
|
||||
ActionManager.getInstance().addAction(Actions.SFTP, SFTPAction())
|
||||
|
||||
// macro
|
||||
ActionManager.getInstance().addAction(Actions.MACRO, MacroAction())
|
||||
|
||||
@@ -246,7 +223,9 @@ class TermoraFrame : JFrame() {
|
||||
val focusWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
|
||||
val frame = this@TermoraFrame
|
||||
if (focusWindow == frame) {
|
||||
FindEverywhere(frame).isVisible = true
|
||||
val dialog = FindEverywhere(frame)
|
||||
dialog.setLocationRelativeTo(frame)
|
||||
dialog.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -405,7 +384,7 @@ class TermoraFrame : JFrame() {
|
||||
}
|
||||
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (e.source == toolbar) {
|
||||
if (e.source == toolbar.getJToolBar()) {
|
||||
if (!isWindowDecorationsSupported && SwingUtilities.isLeftMouseButton(e)) {
|
||||
if (JBR.isWindowMoveSupported()) {
|
||||
JBR.getWindowMove().startMovingTogetherWithMouse(this@TermoraFrame, e.button)
|
||||
@@ -440,8 +419,8 @@ class TermoraFrame : JFrame() {
|
||||
tabbedPane.addMouseListener(mouseAdapter)
|
||||
tabbedPane.addMouseMotionListener(mouseAdapter)
|
||||
|
||||
toolbar.addMouseListener(mouseAdapter)
|
||||
toolbar.addMouseMotionListener(mouseAdapter)
|
||||
toolbar.getJToolBar().addMouseListener(mouseAdapter)
|
||||
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
||||
}
|
||||
|
||||
private fun initDesktopHandler() {
|
||||
|
||||
175
src/main/kotlin/app/termora/TermoraToolBar.kt
Normal file
@@ -0,0 +1,175 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.db.Database
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jetbrains.WindowDecorations
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.action.ActionContainerFactory
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import java.awt.Insets
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import javax.swing.Box
|
||||
import javax.swing.JToolBar
|
||||
|
||||
|
||||
@Serializable
|
||||
data class ToolBarAction(
|
||||
val id: String,
|
||||
val visible: Boolean,
|
||||
)
|
||||
|
||||
class TermoraToolBar(
|
||||
private val titleBar: WindowDecorations.CustomTitleBar,
|
||||
private val tabbedPane: FlatTabbedPane
|
||||
) {
|
||||
private val properties by lazy { Database.instance.properties }
|
||||
private val toolbar by lazy { MyToolBar().apply { rebuild(this) } }
|
||||
|
||||
|
||||
fun getJToolBar(): JToolBar {
|
||||
return toolbar
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取到所有的 Action
|
||||
*/
|
||||
fun getAllActions(): List<ToolBarAction> {
|
||||
return listOf(
|
||||
ToolBarAction(Actions.SFTP, true),
|
||||
ToolBarAction(Actions.TERMINAL_LOGGER, true),
|
||||
ToolBarAction(Actions.MACRO, true),
|
||||
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
|
||||
ToolBarAction(Actions.KEY_MANAGER, true),
|
||||
ToolBarAction(Actions.MULTIPLE, true),
|
||||
ToolBarAction(Actions.FIND_EVERYWHERE, true),
|
||||
ToolBarAction(Actions.SETTING, true),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取到所有 Action,会根据用户个性化排序/显示
|
||||
*/
|
||||
fun getActions(): List<ToolBarAction> {
|
||||
val text = properties.getString(
|
||||
"Termora.ToolBar.Actions",
|
||||
StringUtils.EMPTY
|
||||
)
|
||||
|
||||
val actions = getAllActions()
|
||||
|
||||
if (text.isBlank()) {
|
||||
return actions
|
||||
}
|
||||
|
||||
// 存储的 action
|
||||
val storageActions = (ohMyJson.runCatching {
|
||||
ohMyJson.decodeFromString<List<ToolBarAction>>(text)
|
||||
}.getOrNull() ?: return actions).toMutableList()
|
||||
|
||||
for (action in actions) {
|
||||
// 如果存储的 action 不包含这个,那么这个可能是新增的,新增的默认显示出来
|
||||
if (storageActions.none { it.id == action.id }) {
|
||||
storageActions.addFirst(ToolBarAction(action.id, true))
|
||||
}
|
||||
}
|
||||
|
||||
// 如果存储的 Action 在所有 Action 里没有,那么移除
|
||||
storageActions.removeIf { e -> actions.none { e.id == it.id } }
|
||||
|
||||
return storageActions
|
||||
}
|
||||
|
||||
fun rebuild() {
|
||||
rebuild(this.toolbar)
|
||||
}
|
||||
|
||||
private fun rebuild(toolbar: JToolBar) {
|
||||
val actionManager = ActionManager.getInstance()
|
||||
val actionContainerFactory = ActionContainerFactory(actionManager)
|
||||
|
||||
toolbar.removeAll()
|
||||
|
||||
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
|
||||
override fun actionPerformed(e: ActionEvent?) {
|
||||
actionManager.getAction(Actions.FIND_EVERYWHERE)?.actionPerformed(e)
|
||||
}
|
||||
|
||||
override fun isEnabled(): Boolean {
|
||||
return actionManager.getAction(Actions.FIND_EVERYWHERE)?.isEnabled ?: false
|
||||
}
|
||||
}))
|
||||
|
||||
toolbar.add(Box.createHorizontalGlue())
|
||||
|
||||
|
||||
// update btn
|
||||
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
|
||||
updateBtn.isVisible = updateBtn.isEnabled
|
||||
updateBtn.addChangeListener { updateBtn.isVisible = updateBtn.isEnabled }
|
||||
toolbar.add(updateBtn)
|
||||
|
||||
|
||||
// 获取显示的Action,如果不是 false 那么就是显示出来
|
||||
for (action in getActions()) {
|
||||
if (action.visible) {
|
||||
actionManager.getAction(action.id)?.let {
|
||||
toolbar.add(actionContainerFactory.createButton(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (toolbar is MyToolBar) {
|
||||
toolbar.adjust()
|
||||
}
|
||||
|
||||
toolbar.revalidate()
|
||||
toolbar.repaint()
|
||||
}
|
||||
|
||||
private inner class MyToolBar : JToolBar() {
|
||||
init {
|
||||
// 监听窗口大小变动,然后修改边距避开控制按钮
|
||||
addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentResized(e: ComponentEvent) {
|
||||
adjust()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun adjust() {
|
||||
if (SystemInfo.isMacOS) {
|
||||
val left = titleBar.leftInset.toInt()
|
||||
if (tabbedPane.tabAreaInsets.left != left) {
|
||||
tabbedPane.tabAreaInsets = Insets(0, left, 0, 0)
|
||||
}
|
||||
} else if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
|
||||
val right = titleBar.rightInset.toInt()
|
||||
val toolbar = this@MyToolBar
|
||||
for (i in 0 until toolbar.componentCount) {
|
||||
val c = toolbar.getComponent(i)
|
||||
if (c.name == "spacing") {
|
||||
if (c.width == right) {
|
||||
return
|
||||
}
|
||||
toolbar.remove(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (right > 0) {
|
||||
val spacing = Box.createHorizontalStrut(right)
|
||||
spacing.name = "spacing"
|
||||
toolbar.add(spacing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ class ThemeManager private constructor() {
|
||||
val themes = mapOf(
|
||||
"Light" to LightLaf::class.java.name,
|
||||
"Dark" to DarkLaf::class.java.name,
|
||||
"Dracula" to DraculaLaf::class.java.name,
|
||||
"iTerm2 Dark" to iTerm2DarkLaf::class.java.name,
|
||||
"Termius Dark" to TermiusDarkLaf::class.java.name,
|
||||
"Termius Light" to TermiusLightLaf::class.java.name,
|
||||
|
||||
@@ -5,7 +5,12 @@ import app.termora.I18n
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
|
||||
class QuickActionsFindEverywhereProvider : FindEverywhereProvider {
|
||||
private val actions = listOf(Actions.KEY_MANAGER, Actions.KEYWORD_HIGHLIGHT_EVERYWHERE, Actions.MULTIPLE)
|
||||
private val actions = listOf(
|
||||
Actions.KEY_MANAGER,
|
||||
Actions.KEYWORD_HIGHLIGHT,
|
||||
Actions.MULTIPLE,
|
||||
)
|
||||
|
||||
override fun find(pattern: String): List<FindEverywhereResult> {
|
||||
val actionManager = ActionManager.getInstance()
|
||||
return actions
|
||||
|
||||
@@ -7,11 +7,11 @@ import java.awt.event.ActionEvent
|
||||
import javax.swing.Icon
|
||||
|
||||
class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
|
||||
|
||||
private val actionManager get() = ActionManager.getInstance()
|
||||
|
||||
override fun find(pattern: String): List<FindEverywhereResult> {
|
||||
val list = mutableListOf<FindEverywhereResult>()
|
||||
ActionManager.getInstance().getAction(Actions.ADD_HOST)?.let {
|
||||
actionManager?.let {
|
||||
list.add(CreateHostFindEverywhereResult())
|
||||
}
|
||||
|
||||
@@ -21,34 +21,21 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
|
||||
Icons.terminal
|
||||
) {
|
||||
override fun actionPerformed(evt: ActionEvent) {
|
||||
ActionManager.getInstance().getAction(Actions.OPEN_HOST)
|
||||
?.actionPerformed(
|
||||
OpenHostActionEvent(
|
||||
this, Host(
|
||||
name = name,
|
||||
protocol = Protocol.Local
|
||||
)
|
||||
actionManager.getAction(Actions.OPEN_HOST)?.actionPerformed(
|
||||
OpenHostActionEvent(
|
||||
this, Host(
|
||||
name = name,
|
||||
protocol = Protocol.Local
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}))
|
||||
|
||||
// SFTP
|
||||
list.add(ActionFindEverywhereResult(object : AnAction("SFTP", Icons.fileTransfer) {
|
||||
override fun actionPerformed(evt: ActionEvent) {
|
||||
val terminalTabbedManager = Application.getService(TerminalTabbedManager::class)
|
||||
val tabs = terminalTabbedManager.getTerminalTabs()
|
||||
for (i in tabs.indices) {
|
||||
val tab = tabs[i]
|
||||
if (tab is SFTPTerminalTab) {
|
||||
terminalTabbedManager.setSelectedTerminalTab(tab)
|
||||
return
|
||||
}
|
||||
}
|
||||
// 创建一个新的
|
||||
terminalTabbedManager.addTerminalTab(SFTPTerminalTab())
|
||||
}
|
||||
}))
|
||||
actionManager.getAction(Actions.SFTP)?.let {
|
||||
list.add(ActionFindEverywhereResult(it))
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package app.termora.keyboardinteractive
|
||||
|
||||
import app.termora.DialogWrapper
|
||||
import app.termora.I18n
|
||||
import app.termora.OutlinePasswordField
|
||||
import app.termora.OutlineTextField
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.text.JTextComponent
|
||||
|
||||
class KeyboardInteractiveDialog(
|
||||
owner: Window,
|
||||
private val prompt: String,
|
||||
echo: Boolean
|
||||
) : DialogWrapper(owner) {
|
||||
|
||||
private val textField = (if (echo) OutlineTextField() else OutlinePasswordField()) as JTextComponent
|
||||
|
||||
|
||||
init {
|
||||
isModal = true
|
||||
isResizable = true
|
||||
controlsVisible = false
|
||||
title = I18n.getString("termora.new-host.title")
|
||||
|
||||
init()
|
||||
pack()
|
||||
size = Dimension(300, size.height)
|
||||
setLocationRelativeTo(null)
|
||||
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val formMargin = "4dlu"
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin"
|
||||
)
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
return FormBuilder.create().layout(layout).padding("$formMargin, $formMargin, 0, $formMargin")
|
||||
.add(prompt).xy(1, rows)
|
||||
.add(textField).xy(3, rows).apply { rows += step }
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun doCancelAction() {
|
||||
textField.text = StringUtils.EMPTY
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
if (textField.text.isBlank()) {
|
||||
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||
textField.requestFocusInWindow()
|
||||
return
|
||||
}
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
fun getText(): String {
|
||||
isModal = true
|
||||
isVisible = true
|
||||
return textField.text
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package app.termora.keyboardinteractive
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.sshd.client.auth.keyboard.UserInteraction
|
||||
import org.apache.sshd.client.session.ClientSession
|
||||
import java.awt.Window
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
class TerminalUserInteraction(
|
||||
private val owner: Window
|
||||
) : UserInteraction {
|
||||
|
||||
|
||||
override fun interactive(
|
||||
session: ClientSession?,
|
||||
name: String?,
|
||||
instruction: String?,
|
||||
lang: String?,
|
||||
prompt: Array<out String>,
|
||||
echo: BooleanArray
|
||||
): Array<String> {
|
||||
val passwords = Array(prompt.size) { StringUtils.EMPTY }
|
||||
|
||||
SwingUtilities.invokeAndWait {
|
||||
for (i in prompt.indices) {
|
||||
val dialog = KeyboardInteractiveDialog(
|
||||
owner,
|
||||
prompt[i],
|
||||
true
|
||||
)
|
||||
dialog.title = instruction ?: name ?: StringUtils.EMPTY
|
||||
passwords[i] = dialog.getText()
|
||||
if (passwords[i].isBlank()) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (passwords.last().isBlank()) {
|
||||
throw IllegalStateException("User interaction was cancelled.")
|
||||
}
|
||||
|
||||
if (passwords.all { it.isEmpty() }) {
|
||||
return emptyArray()
|
||||
}
|
||||
|
||||
return passwords
|
||||
}
|
||||
|
||||
override fun getUpdatedPassword(session: ClientSession?, prompt: String?, lang: String?): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ package app.termora.native
|
||||
import app.termora.native.osx.DispatchNative
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import de.jangassen.jfa.foundation.Foundation
|
||||
import de.jangassen.jfa.foundation.Foundation.NSArray
|
||||
import jnafilechooser.api.JnaFileChooser
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.Window
|
||||
import java.io.File
|
||||
import java.util.concurrent.CompletableFuture
|
||||
@@ -17,6 +17,12 @@ class FileChooser {
|
||||
var allowsOtherFileTypes = true
|
||||
var canCreateDirectories = true
|
||||
var win32Filters = mutableListOf<Pair<String, List<String>>>()
|
||||
var osxAllowedFileTypes = emptyList<String>()
|
||||
|
||||
/**
|
||||
* 默认的打开目录
|
||||
*/
|
||||
var defaultDirectory = StringUtils.EMPTY
|
||||
|
||||
fun showOpenDialog(owner: Window? = null): CompletableFuture<List<File>> {
|
||||
val future = CompletableFuture<List<File>>()
|
||||
@@ -26,6 +32,17 @@ class FileChooser {
|
||||
val fileChooser = JnaFileChooser()
|
||||
fileChooser.isMultiSelectionEnabled = allowsMultiSelection
|
||||
fileChooser.setTitle(title)
|
||||
|
||||
if (defaultDirectory.isNotBlank()) {
|
||||
fileChooser.setCurrentDirectory(defaultDirectory)
|
||||
}
|
||||
|
||||
if (win32Filters.isNotEmpty()) {
|
||||
for ((name, filters) in win32Filters) {
|
||||
fileChooser.addFilter(name, *filters.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
if (fileChooser.showOpenDialog(owner)) {
|
||||
future.complete(fileChooser.selectedFiles.toList())
|
||||
} else {
|
||||
@@ -91,6 +108,27 @@ class FileChooser {
|
||||
// 是否允许多选
|
||||
Foundation.invoke(openPanelInstance, "setAllowsMultipleSelection:", allowsMultiSelection)
|
||||
|
||||
// 限制文件类型
|
||||
if (osxAllowedFileTypes.isNotEmpty()) {
|
||||
Foundation.invoke(
|
||||
openPanelInstance,
|
||||
"setAllowedFileTypes:",
|
||||
Foundation.fillArray(osxAllowedFileTypes.toTypedArray())
|
||||
)
|
||||
}
|
||||
|
||||
if (defaultDirectory.isNotBlank()) {
|
||||
Foundation.invoke(
|
||||
openPanelInstance,
|
||||
"setDirectoryURL:",
|
||||
Foundation.invoke(
|
||||
"NSURL",
|
||||
"fileURLWithPath:",
|
||||
Foundation.nsString(defaultDirectory)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// 标题
|
||||
if (title.isNotBlank()) {
|
||||
Foundation.invoke(openPanelInstance, "setTitle:", Foundation.nsString(title))
|
||||
@@ -103,7 +141,7 @@ class FileChooser {
|
||||
}
|
||||
|
||||
val files = mutableListOf<File>()
|
||||
val urls = NSArray(Foundation.invoke(openPanelInstance, "URLs"))
|
||||
val urls = Foundation.NSArray(Foundation.invoke(openPanelInstance, "URLs"))
|
||||
for (i in 0 until urls.count()) {
|
||||
val url = Foundation.invoke(urls.at(i), "path")
|
||||
if (url != null) {
|
||||
|
||||
@@ -769,6 +769,11 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
||||
args.append("0")
|
||||
} else if (args.startsWithMoreMark()) {
|
||||
return
|
||||
} else if (args.startsWithQuestionMark()) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn("ignore SGR: {}", args)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val iterator = args.controlSequences().iterator()
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package app.termora.terminal
|
||||
|
||||
import org.apache.commons.codec.binary.Base64
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Toolkit
|
||||
import java.awt.datatransfer.StringSelection
|
||||
|
||||
class OperatingSystemCommandProcessor(terminal: Terminal, reader: TerminalReader) :
|
||||
AbstractProcessor(terminal, reader) {
|
||||
@@ -95,6 +98,25 @@ class OperatingSystemCommandProcessor(terminal: Terminal, reader: TerminalReader
|
||||
replyColor(mode, terminalColor)
|
||||
}
|
||||
|
||||
// Ps = 5 2 ⇒ Manipulate Selection Data. These controls may be disabled using the allowWindowOps resource. The parameter Pt is parsed as
|
||||
52 -> {
|
||||
val pair = suffix.split(";", limit = 2).let {
|
||||
Pair(it.first(), it.last())
|
||||
}
|
||||
|
||||
// base64
|
||||
if (pair.first == "c") {
|
||||
val text = String(Base64.decodeBase64(pair.second))
|
||||
Toolkit.getDefaultToolkit().systemClipboard.setContents(StringSelection(text), null)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Copy {} to clipboard", text)
|
||||
}
|
||||
} else if (log.isWarnEnabled) {
|
||||
log.warn("Manipulate Selection Data. Unknown: {}", pair)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn("Unknown OSC: $prefix")
|
||||
|
||||
@@ -86,7 +86,11 @@ class TerminalLine {
|
||||
if (chars.size() > i) {
|
||||
if (chars.get(i).isNull) {
|
||||
chars.set(i, Char.Space)
|
||||
styles.set(i, TextStyle.Default)
|
||||
// 如果等于默认,那么替换成当前的样式
|
||||
// 如果不是默认,那么不需要替换样式
|
||||
if (styles.getTextStyle(i) == TextStyle.Default) {
|
||||
styles.set(i, buffer.style)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break
|
||||
|
||||
@@ -7,12 +7,11 @@ import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import java.awt.datatransfer.Transferable
|
||||
import java.awt.datatransfer.UnsupportedFlavorException
|
||||
import java.awt.event.InputEvent
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.KeyStroke
|
||||
|
||||
class TerminalCopyAction(private val terminalPanel: TerminalPanel) : TerminalAction(
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_C, terminalPanel.toolkit.menuShortcutKeyMaskEx)
|
||||
) {
|
||||
class TerminalCopyAction(private val terminalPanel: TerminalPanel) : TerminalPredicateAction {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(TerminalCopyAction::class.java)
|
||||
}
|
||||
@@ -36,10 +35,16 @@ class TerminalCopyAction(private val terminalPanel: TerminalPanel) : TerminalAct
|
||||
}
|
||||
|
||||
override fun test(keyStroke: KeyStroke, e: KeyEvent): Boolean {
|
||||
if (!SystemInfo.isMacOS) {
|
||||
return false
|
||||
if (SystemInfo.isMacOS) {
|
||||
return KeyStroke.getKeyStroke(KeyEvent.VK_C, terminalPanel.toolkit.menuShortcutKeyMaskEx) == keyStroke
|
||||
}
|
||||
return super.test(keyStroke, e)
|
||||
|
||||
// Ctrl + Insert
|
||||
val keyStroke1 = KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, InputEvent.CTRL_DOWN_MASK)
|
||||
// Ctrl + Shift + C
|
||||
val keyStroke2 = KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK or InputEvent.SHIFT_DOWN_MASK)
|
||||
|
||||
return keyStroke == keyStroke1 || keyStroke == keyStroke2
|
||||
}
|
||||
|
||||
private class EmptyTransferable : Transferable {
|
||||
|
||||
@@ -298,7 +298,7 @@ class TerminalDisplay(
|
||||
g.drawLine(xOffset, ly, xOffset + charWidth, ly)
|
||||
}
|
||||
|
||||
// 删除线
|
||||
// 双下划线
|
||||
if (textStyle.doublyUnderline) {
|
||||
if (textStyle.underline) {
|
||||
g.drawLine(xOffset, i * lineHeight - 3, xOffset + charWidth, i * lineHeight - 3)
|
||||
|
||||
@@ -2,6 +2,8 @@ package app.termora.terminal.panel
|
||||
|
||||
import app.termora.terminal.PtyConnector
|
||||
import app.termora.terminal.Terminal
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import java.awt.event.InputEvent
|
||||
import java.awt.event.KeyAdapter
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.KeyStroke
|
||||
@@ -45,6 +47,11 @@ class TerminalPanelKeyAdapter(
|
||||
ptyConnector.write(encode)
|
||||
}
|
||||
|
||||
// https://github.com/TermoraDev/termora/issues/52
|
||||
if (SystemInfo.isWindows && e.keyCode == KeyEvent.VK_TAB && isCtrlPressedOnly(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Character.isISOControl(e.keyChar)) {
|
||||
terminal.getSelectionModel().clearSelection()
|
||||
// 如果不为空表示已经发送过了,所以这里为空的时候再发送
|
||||
@@ -55,4 +62,12 @@ class TerminalPanelKeyAdapter(
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun isCtrlPressedOnly(e: KeyEvent): Boolean {
|
||||
val modifiersEx = e.modifiersEx
|
||||
return (modifiersEx and InputEvent.ALT_DOWN_MASK) == 0
|
||||
&& (modifiersEx and InputEvent.ALT_GRAPH_DOWN_MASK) == 0
|
||||
&& (modifiersEx and InputEvent.CTRL_DOWN_MASK) != 0
|
||||
&& (modifiersEx and InputEvent.SHIFT_DOWN_MASK) == 0
|
||||
}
|
||||
}
|
||||
@@ -65,10 +65,11 @@ class TerminalPanelMouseTrackingAdapter(
|
||||
}
|
||||
|
||||
override fun mouseWheelMoved(e: MouseWheelEvent) {
|
||||
if (shouldSendMouseData) {
|
||||
if (this.shouldSendMouseData || terminalModel.isAlternateScreenBuffer()) {
|
||||
val unitsToScroll = e.unitsToScroll
|
||||
val encode = terminal.getKeyEncoder()
|
||||
.encode(TerminalKeyEvent(if (e.wheelRotation < 0) KeyEvent.VK_UP else KeyEvent.VK_DOWN))
|
||||
if (encode.isBlank()) return
|
||||
|
||||
for (i in 0 until abs(unitsToScroll)) {
|
||||
ptyConnector.write(encode)
|
||||
|
||||
@@ -3,12 +3,11 @@ package app.termora.terminal.panel
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.event.InputEvent
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.KeyStroke
|
||||
|
||||
class TerminalPasteAction(private val terminalPanel: TerminalPanel) : TerminalAction(
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_V, terminalPanel.toolkit.menuShortcutKeyMaskEx)
|
||||
) {
|
||||
class TerminalPasteAction(private val terminalPanel: TerminalPanel) : TerminalPredicateAction {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(TerminalPasteAction::class.java)
|
||||
}
|
||||
@@ -28,10 +27,16 @@ class TerminalPasteAction(private val terminalPanel: TerminalPanel) : TerminalAc
|
||||
}
|
||||
|
||||
override fun test(keyStroke: KeyStroke, e: KeyEvent): Boolean {
|
||||
if (!SystemInfo.isMacOS) {
|
||||
return false
|
||||
if (SystemInfo.isMacOS) {
|
||||
return KeyStroke.getKeyStroke(KeyEvent.VK_V, terminalPanel.toolkit.menuShortcutKeyMaskEx) == keyStroke
|
||||
}
|
||||
return super.test(keyStroke, e)
|
||||
|
||||
// Shift + Insert
|
||||
val keyStroke1 = KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, InputEvent.SHIFT_DOWN_MASK)
|
||||
// Ctrl + Shift + V
|
||||
val keyStroke2 = KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_DOWN_MASK or InputEvent.SHIFT_DOWN_MASK)
|
||||
|
||||
return keyStroke == keyStroke1 || keyStroke == keyStroke2
|
||||
}
|
||||
|
||||
}
|
||||
57
src/main/kotlin/app/termora/tlog/LogViewerTerminal.kt
Normal file
@@ -0,0 +1,57 @@
|
||||
package app.termora.tlog
|
||||
|
||||
import app.termora.TerminalFactory
|
||||
import app.termora.terminal.*
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
class LogViewerTerminal : TerminalFactory.MyVisualTerminal() {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(LogViewerTerminal::class.java)
|
||||
}
|
||||
|
||||
private val document by lazy { MyDocument(this) }
|
||||
private val terminalModel by lazy { LogViewerTerminalModel(this) }
|
||||
|
||||
override fun getDocument(): Document {
|
||||
return document
|
||||
}
|
||||
|
||||
override fun getTerminalModel(): TerminalModel {
|
||||
return terminalModel
|
||||
}
|
||||
|
||||
private class MyDocument(terminal: Terminal) : DocumentImpl(terminal) {
|
||||
override fun eraseInDisplay(n: Int) {
|
||||
// 预览日志的时候,不处理清屏操作,不然会导致日志看不到。
|
||||
// 例如,用户输入了 cat xxx.txt ,然后执行了 clear 那么就看不到了
|
||||
if (n == 3) {
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("ignore $n eraseInDisplay")
|
||||
}
|
||||
return
|
||||
}
|
||||
super.eraseInDisplay(n)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private class LogViewerTerminalModel(terminal: Terminal) : TerminalFactory.MyTerminalModel(terminal) {
|
||||
override fun getMaxRows(): Int {
|
||||
return Int.MAX_VALUE
|
||||
}
|
||||
|
||||
override fun <T : Any> getData(key: DataKey<T>): T {
|
||||
if (key == DataKey.ShowCursor) {
|
||||
return false as T
|
||||
}
|
||||
return super.getData(key)
|
||||
}
|
||||
|
||||
override fun <T : Any> getData(key: DataKey<T>, defaultValue: T): T {
|
||||
if (key == DataKey.ShowCursor) {
|
||||
return false as T
|
||||
}
|
||||
return super.getData(key, defaultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/main/kotlin/app/termora/tlog/LogViewerTerminalTab.kt
Normal file
@@ -0,0 +1,73 @@
|
||||
package app.termora.tlog
|
||||
|
||||
import app.termora.Host
|
||||
import app.termora.Icons
|
||||
import app.termora.Protocol
|
||||
import app.termora.PtyHostTerminalTab
|
||||
import app.termora.terminal.PtyConnector
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.nio.file.Files
|
||||
import javax.swing.Icon
|
||||
|
||||
class LogViewerTerminalTab(private val file: File) : PtyHostTerminalTab(
|
||||
Host(
|
||||
name = file.name,
|
||||
protocol = Protocol.Local
|
||||
),
|
||||
LogViewerTerminal()
|
||||
) {
|
||||
|
||||
init {
|
||||
// 不记录日志
|
||||
terminal.getTerminalModel().setData(TerminalLoggerDataListener.IgnoreTerminalLogger, true)
|
||||
}
|
||||
|
||||
override suspend fun openPtyConnector(): PtyConnector {
|
||||
if (!file.exists()) {
|
||||
throw FileNotFoundException(file.absolutePath)
|
||||
}
|
||||
|
||||
val input = withContext(Dispatchers.IO) {
|
||||
Files.newBufferedReader(file.toPath())
|
||||
}
|
||||
|
||||
return object : PtyConnector {
|
||||
|
||||
override fun read(buffer: CharArray): Int {
|
||||
return input.read(buffer)
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteArray, offset: Int, len: Int) {
|
||||
|
||||
}
|
||||
|
||||
override fun resize(rows: Int, cols: Int) {
|
||||
|
||||
}
|
||||
|
||||
override fun waitFor(): Int {
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
input.close()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun getIcon(): Icon {
|
||||
return Icons.listFiles
|
||||
}
|
||||
|
||||
override fun canReconnect(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun canClone(): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
110
src/main/kotlin/app/termora/tlog/TerminalLoggerAction.kt
Normal file
@@ -0,0 +1,110 @@
|
||||
package app.termora.tlog
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.db.Database
|
||||
import app.termora.native.FileChooser
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.awt.Window
|
||||
import java.awt.event.ActionEvent
|
||||
import java.io.File
|
||||
import java.time.LocalDate
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JFileChooser
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
class TerminalLoggerAction : AnAction(I18n.getString("termora.terminal-logger"), Icons.listFiles) {
|
||||
private val properties by lazy { Database.instance.properties }
|
||||
|
||||
/**
|
||||
* 是否开启了记录
|
||||
*/
|
||||
var isRecording = properties.getString("terminal.logger.isRecording")?.toBoolean() ?: false
|
||||
private set(value) {
|
||||
field = value
|
||||
// firePropertyChange
|
||||
putValue("Recording", value)
|
||||
properties.putString("terminal.logger.isRecording", value.toString())
|
||||
}
|
||||
|
||||
init {
|
||||
smallIcon = if (isRecording) Icons.dotListFiles else Icons.listFiles
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: ActionEvent) {
|
||||
val source = evt.source
|
||||
if (source !is JComponent) return
|
||||
|
||||
val popupMenu = FlatPopupMenu()
|
||||
if (isRecording) {
|
||||
// stop
|
||||
popupMenu.add(I18n.getString("termora.terminal-logger.stop-recording")).addActionListener {
|
||||
isRecording = false
|
||||
smallIcon = Icons.listFiles
|
||||
}
|
||||
} else {
|
||||
// start
|
||||
popupMenu.add(I18n.getString("termora.terminal-logger.start-recording")).addActionListener {
|
||||
isRecording = true
|
||||
smallIcon = Icons.dotListFiles
|
||||
}
|
||||
}
|
||||
|
||||
popupMenu.addSeparator()
|
||||
|
||||
// 打开日志浏览
|
||||
popupMenu.add(I18n.getString("termora.terminal-logger.open-log-viewer")).addActionListener {
|
||||
openLogViewer(SwingUtilities.getWindowAncestor(source))
|
||||
}
|
||||
|
||||
// 打开日志文件夹
|
||||
popupMenu.add(
|
||||
I18n.getString(
|
||||
"termora.terminal-logger.open-in-folder",
|
||||
if (SystemInfo.isMacOS) I18n.getString("termora.finder")
|
||||
else if (SystemInfo.isWindows) I18n.getString("termora.explorer")
|
||||
else I18n.getString("termora.folder")
|
||||
)
|
||||
).addActionListener {
|
||||
val dir = getLogDir()
|
||||
Application.browse(dir.toURI())
|
||||
}
|
||||
|
||||
val width = popupMenu.preferredSize.width
|
||||
popupMenu.show(source, -(width / 2) + source.width / 2, source.height)
|
||||
}
|
||||
|
||||
private fun openLogViewer(owner: Window) {
|
||||
val fc = FileChooser()
|
||||
fc.allowsMultiSelection = true
|
||||
fc.title = I18n.getString("termora.terminal-logger.open-log-viewer")
|
||||
fc.fileSelectionMode = JFileChooser.FILES_ONLY
|
||||
|
||||
if (SystemInfo.isMacOS) {
|
||||
fc.osxAllowedFileTypes = listOf("log")
|
||||
} else if (SystemInfo.isWindows) {
|
||||
fc.win32Filters.add(Pair("Log files", listOf("log")))
|
||||
}
|
||||
|
||||
fc.defaultDirectory = getLogDir().absolutePath
|
||||
fc.showOpenDialog(owner).thenAccept { files ->
|
||||
if (files.isNotEmpty()) {
|
||||
SwingUtilities.invokeLater {
|
||||
val manager = Application.getService(TerminalTabbedManager::class)
|
||||
for (file in files) {
|
||||
val tab = LogViewerTerminalTab(file)
|
||||
tab.start()
|
||||
manager.addTerminalTab(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getLogDir(): File {
|
||||
val dir = FileUtils.getFile(Application.getBaseDataDir(), "terminal", "logs", LocalDate.now().toString())
|
||||
FileUtils.forceMkdir(dir)
|
||||
return dir
|
||||
}
|
||||
}
|
||||
194
src/main/kotlin/app/termora/tlog/TerminalLoggerDataListener.kt
Normal file
@@ -0,0 +1,194 @@
|
||||
package app.termora.tlog
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.terminal.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.onFailure
|
||||
import kotlinx.coroutines.channels.onSuccess
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.time.DateFormatUtils
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.beans.PropertyChangeListener
|
||||
import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class TerminalLoggerDataListener(private val terminal: Terminal) : DataListener {
|
||||
companion object {
|
||||
/**
|
||||
* 忽略日志的标记
|
||||
*/
|
||||
val IgnoreTerminalLogger = DataKey(Boolean::class)
|
||||
|
||||
private val log = LoggerFactory.getLogger(TerminalLoggerDataListener::class.java)
|
||||
}
|
||||
|
||||
private var coroutineScope: CoroutineScope? = null
|
||||
private var channel: Channel<String>? = null
|
||||
private var file: File? = null
|
||||
private var writer: BufferedWriter? = null
|
||||
|
||||
private val isRecording = AtomicBoolean(false)
|
||||
private val isClosed = AtomicBoolean(false)
|
||||
|
||||
// 监听 Recording 变化,如果已经停止录制,那么立即关闭文件
|
||||
private val terminalLoggerActionPropertyChangeListener = PropertyChangeListener { evt ->
|
||||
if (evt.propertyName == "Recording") {
|
||||
if (evt.newValue == false) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val host: Host?
|
||||
get() {
|
||||
if (terminal.getTerminalModel().hasData(HostTerminalTab.Host)) {
|
||||
return terminal.getTerminalModel().getData(HostTerminalTab.Host)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
terminal.addTerminalListener(object : TerminalListener {
|
||||
override fun onClose(terminal: Terminal) {
|
||||
if (isClosed.compareAndSet(false, true)) {
|
||||
// 设置为已经关闭
|
||||
isClosed.set(true)
|
||||
|
||||
// 移除变动监听
|
||||
terminal.getTerminalModel().removeDataListener(this@TerminalLoggerDataListener)
|
||||
|
||||
// 关闭流
|
||||
close()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onChanged(key: DataKey<*>, data: Any) {
|
||||
if (key != VisualTerminal.Written || isClosed.get()) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果忽略了,那么跳过
|
||||
if (terminal.getTerminalModel().getData(IgnoreTerminalLogger, false)) {
|
||||
return
|
||||
}
|
||||
|
||||
val host = this.host ?: return
|
||||
val action = ActionManager.getInstance().getAction(Actions.TERMINAL_LOGGER)
|
||||
if (action !is TerminalLoggerAction || !action.isRecording) {
|
||||
return
|
||||
}
|
||||
|
||||
try {// 尝试记录
|
||||
tryRecord(data as String, host, action)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryRecord(text: String, host: Host, action: TerminalLoggerAction) {
|
||||
if (isRecording.compareAndSet(false, true)) {
|
||||
|
||||
val file = createFile(host, action.getLogDir()).apply { file = this }
|
||||
val writer = BufferedWriter(FileWriter(file, false)).apply { writer = this }
|
||||
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("Terminal logger file: ${file.absolutePath}")
|
||||
}
|
||||
|
||||
action.removePropertyChangeListener(terminalLoggerActionPropertyChangeListener)
|
||||
action.addPropertyChangeListener(terminalLoggerActionPropertyChangeListener)
|
||||
|
||||
val coroutineScope = this.coroutineScope ?: CoroutineScope(Dispatchers.IO).apply { coroutineScope = this }
|
||||
val channel = this.channel ?: Channel<String>(Channel.UNLIMITED).apply { channel = this }
|
||||
|
||||
coroutineScope.launch {
|
||||
while (coroutineScope.isActive) {
|
||||
channel.receiveCatching().onSuccess {
|
||||
writer.write(it)
|
||||
}.onFailure { e ->
|
||||
if (log.isErrorEnabled && e is Throwable) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val date = DateFormatUtils.format(Date(), I18n.getString("termora.date-format"))
|
||||
channel.trySend("[BEGIN] ---- $date ----").isSuccess
|
||||
channel.trySend("${ControlCharacters.LF}${ControlCharacters.CR}").isSuccess
|
||||
}
|
||||
|
||||
if (isRecording.get()) {
|
||||
channel?.trySend(text)?.isSuccess
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFile(host: Host, dir: File): File {
|
||||
val now = DateFormatUtils.format(Date(), "HH_mm_ss_SSS")
|
||||
val filename = "${dir.absolutePath}${File.separator}${host.name}.${now}.log"
|
||||
return try {
|
||||
// 如果名称中包含 :\\n 等符号会获取失败,那么采用 ID 代替
|
||||
Paths.get(filename).toFile()
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
try {
|
||||
Paths.get(dir.absolutePath, "${host.id}.${now}.log").toFile()
|
||||
} catch (e: Exception) {
|
||||
Paths.get(dir.absolutePath, "${UUID.randomUUID().toSimpleString()}.${now}.log").toFile()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun close() {
|
||||
if (!isRecording.compareAndSet(true, false)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 移除监听
|
||||
ActionManager.getInstance().getAction(Actions.TERMINAL_LOGGER)
|
||||
?.removePropertyChangeListener(terminalLoggerActionPropertyChangeListener)
|
||||
|
||||
|
||||
this.channel?.close()
|
||||
this.coroutineScope?.cancel()
|
||||
|
||||
this.channel = null
|
||||
this.coroutineScope = null
|
||||
|
||||
// write end
|
||||
runCatching {
|
||||
val date = DateFormatUtils.format(Date(), I18n.getString("termora.date-format"))
|
||||
this.writer?.write("${ControlCharacters.LF}${ControlCharacters.CR}")
|
||||
this.writer?.write("[END] ---- $date ----")
|
||||
}.onFailure {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(it.message, it)
|
||||
}
|
||||
}
|
||||
|
||||
IOUtils.closeQuietly(this.writer)
|
||||
|
||||
val file = this.file
|
||||
if (log.isInfoEnabled && file != null) {
|
||||
log.info("Terminal logger file: {} saved", file.absolutePath)
|
||||
}
|
||||
|
||||
this.writer = null
|
||||
this.file = null
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -239,7 +239,7 @@ class FileSystemPanel(
|
||||
}
|
||||
} else {
|
||||
transportPanel.transport(
|
||||
sourceWorkdir = localFileSystemPanel.workdir,
|
||||
sourceWorkdir = path.path.parent,
|
||||
targetWorkdir = workdir,
|
||||
isSourceDirectory = false,
|
||||
sourcePath = path.path,
|
||||
|
||||
@@ -74,11 +74,14 @@ class FileTransportTableModel(transportManager: TransportManager) : DefaultTable
|
||||
val speed = if (isTransporting) transport.speed else 0
|
||||
val estimatedTime = if (isTransporting && speed > 0)
|
||||
(transport.size - transport.transferredSize) / speed else 0
|
||||
val progress = transport.progress * 100.0
|
||||
|
||||
return when (column) {
|
||||
COLUMN_NAME -> " ${transport.name}"
|
||||
COLUMN_STATUS -> formatStatus(transport.state)
|
||||
COLUMN_PROGRESS -> String.format("%.0f%%", transport.progress * 100.0)
|
||||
|
||||
// 如果进度已经完成但是状态还是传输中,那么进度显示:99%
|
||||
COLUMN_PROGRESS -> String.format("%.0f%%", if (progress >= 100.0 && isTransporting) 99.0 else progress)
|
||||
|
||||
// 大小
|
||||
COLUMN_SIZE -> if (transport.size < 0) "-"
|
||||
|
||||
20
src/main/kotlin/app/termora/transport/SFTPAction.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
package app.termora.transport
|
||||
|
||||
import app.termora.*
|
||||
import java.awt.event.ActionEvent
|
||||
|
||||
class SFTPAction : AnAction("SFTP", Icons.folder) {
|
||||
override fun actionPerformed(evt: ActionEvent) {
|
||||
val terminalTabbedManager = Application.getService(TerminalTabbedManager::class)
|
||||
val tabs = terminalTabbedManager.getTerminalTabs()
|
||||
for (tab in tabs) {
|
||||
if (tab is SFTPTerminalTab) {
|
||||
terminalTabbedManager.setSelectedTerminalTab(tab)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 创建一个新的
|
||||
terminalTabbedManager.addTerminalTab(SFTPTerminalTab())
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.termora.transport
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||
import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon
|
||||
import com.formdev.flatlaf.icons.FlatOptionPaneInformationIcon
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
@@ -113,6 +114,10 @@ class SftpFileSystemPanel(
|
||||
|
||||
try {
|
||||
val client = SshClients.openClient(host).apply { client = this }
|
||||
withContext(Dispatchers.Swing) {
|
||||
client.userInteraction =
|
||||
TerminalUserInteraction(SwingUtilities.getWindowAncestor(this@SftpFileSystemPanel))
|
||||
}
|
||||
val session = SshClients.openSession(host, client).apply { session = this }
|
||||
fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
|
||||
session.addCloseFutureListener { onClose() }
|
||||
|
||||
@@ -102,6 +102,7 @@ class TransportManager : Disposable {
|
||||
}
|
||||
|
||||
if (transport == null) {
|
||||
needDelay = true
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -170,6 +170,13 @@ termora.tabbed.contextmenu.close-other-tabs=Close Other Tabs
|
||||
termora.tabbed.contextmenu.close-all-tabs=Close All Tabs
|
||||
termora.tabbed.contextmenu.reconnect=Reconnect
|
||||
|
||||
# Terminal logger
|
||||
termora.terminal-logger=Terminal Logger
|
||||
termora.terminal-logger.start-recording=Start Recording
|
||||
termora.terminal-logger.stop-recording=Stop Recording
|
||||
termora.terminal-logger.open-log-viewer=Open Log Viewer
|
||||
termora.terminal-logger.open-in-folder=Open in {0}
|
||||
|
||||
|
||||
# Highlight
|
||||
termora.highlight=Highlight Sets
|
||||
@@ -261,7 +268,8 @@ termora.transport.jobs.table.estimated-time=Estimated time
|
||||
|
||||
termora.transport.jobs.contextmenu.delete=${termora.remove}
|
||||
termora.transport.jobs.contextmenu.delete-all=Delete All
|
||||
|
||||
# ToolBar
|
||||
termora.toolbar.customize-toolbar=Customize Toolbar...
|
||||
|
||||
# Terminal
|
||||
termora.terminal.size=Size: {0} x {1}
|
||||
|
||||
@@ -166,6 +166,15 @@ termora.tabbed.contextmenu.close-other-tabs=关闭其他标签页
|
||||
termora.tabbed.contextmenu.close-all-tabs=关闭所有标签页
|
||||
termora.tabbed.contextmenu.reconnect=重新连接
|
||||
|
||||
|
||||
|
||||
# Terminal logger
|
||||
termora.terminal-logger=终端日志
|
||||
termora.terminal-logger.start-recording=开始记录
|
||||
termora.terminal-logger.stop-recording=停止记录
|
||||
termora.terminal-logger.open-log-viewer=打开日志浏览器
|
||||
termora.terminal-logger.open-in-folder=在 {0} 中打开
|
||||
|
||||
# Highlight
|
||||
termora.highlight=关键词高亮
|
||||
termora.highlight.text-color=文本颜色
|
||||
@@ -251,6 +260,8 @@ termora.transport.jobs.table.speed=速度
|
||||
termora.transport.jobs.table.estimated-time=剩余时间
|
||||
|
||||
termora.transport.jobs.contextmenu.delete-all=删除所有
|
||||
# ToolBar
|
||||
termora.toolbar.customize-toolbar=自定义工具栏...
|
||||
|
||||
termora.terminal.size=大小: {0} x {1}
|
||||
termora.terminal.copied=已复制
|
||||
|
||||
@@ -161,6 +161,15 @@ termora.tabbed.contextmenu.close-all-tabs=關閉所有標籤
|
||||
termora.tabbed.contextmenu.reconnect=重新連接
|
||||
|
||||
|
||||
|
||||
# Terminal logger
|
||||
termora.terminal-logger=終端日誌
|
||||
termora.terminal-logger.start-recording=開始記錄
|
||||
termora.terminal-logger.stop-recording=停止記錄
|
||||
termora.terminal-logger.open-log-viewer=開啟日誌瀏覽器
|
||||
termora.terminal-logger.open-in-folder=在 {0} 中打開
|
||||
|
||||
|
||||
# Highlight
|
||||
termora.highlight=關鍵字高亮
|
||||
termora.highlight.text-color=文字顏色
|
||||
@@ -231,6 +240,8 @@ termora.transport.jobs.table.speed=速度
|
||||
termora.transport.jobs.table.estimated-time=剩餘時間
|
||||
|
||||
termora.transport.jobs.contextmenu.delete-all=刪除所有
|
||||
# ToolBar
|
||||
termora.toolbar.customize-toolbar=自訂工具列...
|
||||
|
||||
termora.terminal.size=大小: {0} x {1}
|
||||
termora.terminal.copied=已複製
|
||||
|
||||
4
src/main/resources/icons/applyNotConflictsLeft.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="M8.5 12.5L13 8L8.5 3.5M3.5 12.5L8 8L3.5 3.5" stroke="#6C707E" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 330 B |
4
src/main/resources/icons/applyNotConflictsLeft_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="M8.5 12.5L13 8L8.5 3.5M3.5 12.5L8 8L3.5 3.5" stroke="#CED0D6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 330 B |
4
src/main/resources/icons/applyNotConflictsRight.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="M7.5 3.5L3 8L7.5 12.5M12.5 3.5L8 8L12.5 12.5" stroke="#6C707E" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 331 B |
4
src/main/resources/icons/applyNotConflictsRight_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="M7.5 3.5L3 8L7.5 12.5M12.5 3.5L8 8L12.5 12.5" stroke="#CED0D6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 331 B |
7
src/main/resources/icons/changelog.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!-- Copyright 2000-2024 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">
|
||||
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#6C707E"/>
|
||||
<rect x="5" y="5" width="6" height="1" rx="0.5" fill="#6C707E"/>
|
||||
<rect x="5" y="7.5" width="6" height="1" rx="0.5" fill="#6C707E"/>
|
||||
<rect x="5" y="10" width="6" height="1" rx="0.5" fill="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 501 B |
7
src/main/resources/icons/changelog_dark.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!-- Copyright 2000-2024 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">
|
||||
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#CED0D6"/>
|
||||
<rect x="5" y="5" width="6" height="1" rx="0.5" fill="#CED0D6"/>
|
||||
<rect x="5" y="7.5" width="6" height="1" rx="0.5" fill="#CED0D6"/>
|
||||
<rect x="5" y="10" width="6" height="1" rx="0.5" fill="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 501 B |
9
src/main/resources/icons/dotListFiles.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<!-- 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">
|
||||
<rect x="2.5" y="1.5" width="11" height="13" rx="1.5" stroke="#6C707E"/>
|
||||
<rect x="5" y="5" width="6" height="1" rx="0.5" fill="#6C707E"/>
|
||||
<rect x="5" y="7.5" width="6" height="1" rx="0.5" fill="#6C707E"/>
|
||||
<rect x="5" y="10" width="6" height="1" rx="0.5" fill="#6C707E"/>
|
||||
<path d="M16 13.5C16 14.8807 14.8807 16 13.5 16C12.1193 16 11 14.8807 11 13.5C11 12.1193 12.1193 11 13.5 11C14.8807 11 16 12.1193 16 13.5Z" fill="#6C707E"/>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 663 B |
9
src/main/resources/icons/dotListFiles_dark.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<!-- 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">
|
||||
<rect x="2.5" y="1.5" width="11" height="13" rx="1.5" stroke="#CED0D6"/>
|
||||
<rect x="5" y="5" width="6" height="1" rx="0.5" fill="#CED0D6"/>
|
||||
<rect x="5" y="7.5" width="6" height="1" rx="0.5" fill="#CED0D6"/>
|
||||
<rect x="5" y="10" width="6" height="1" rx="0.5" fill="#CED0D6"/>
|
||||
<path d="M16 13.5C16 14.8807 14.8807 16 13.5 16C12.1193 16 11 14.8807 11 13.5C11 12.1193 12.1193 11 13.5 11C14.8807 11 16 12.1193 16 13.5Z" fill="#CED0D6"/>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 663 B |
@@ -1,4 +1,5 @@
|
||||
<!-- 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="M8.10584 4.34613L8.25344 4.5H8.46667H13C13.8284 4.5 14.5 5.17157 14.5 6V12.1333C14.5 12.9529 13.932 13.5 13.3667 13.5H2.63333C2.06804 13.5 1.5 12.9529 1.5 12.1333V3.86667C1.5 3.04707 2.06804 2.5 2.63333 2.5H6.1217C6.25792 2.5 6.38824 2.55557 6.48253 2.65387L8.10584 4.34613Z" fill="#EBECF0" stroke="#6C707E"/>
|
||||
<path d="M8.10584 4.34613L8.25344 4.5H8.46667H13C13.8284 4.5 14.5 5.17157 14.5 6V12.1333C14.5 12.9529 13.932 13.5 13.3667 13.5H2.63333C2.06804 13.5 1.5 12.9529 1.5 12.1333V3.86667C1.5 3.04707 2.06804 2.5 2.63333 2.5H6.1217C6.25792 2.5 6.38824 2.55557 6.48253 2.65387L8.10584 4.34613Z"
|
||||
stroke="#6C707E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 549 B After Width: | Height: | Size: 548 B |
@@ -1,4 +1,5 @@
|
||||
<!-- 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="M8.10584 4.34613L8.25344 4.5H8.46667H13C13.8284 4.5 14.5 5.17157 14.5 6V12.1333C14.5 12.9529 13.932 13.5 13.3667 13.5H2.63333C2.06804 13.5 1.5 12.9529 1.5 12.1333V3.86667C1.5 3.04707 2.06804 2.5 2.63333 2.5H6.1217C6.25792 2.5 6.38824 2.55557 6.48253 2.65387L8.10584 4.34613Z" fill="#43454A" stroke="#CED0D6"/>
|
||||
<path d="M8.10584 4.34613L8.25344 4.5H8.46667H13C13.8284 4.5 14.5 5.17157 14.5 6V12.1333C14.5 12.9529 13.932 13.5 13.3667 13.5H2.63333C2.06804 13.5 1.5 12.9529 1.5 12.1333V3.86667C1.5 3.04707 2.06804 2.5 2.63333 2.5H6.1217C6.25792 2.5 6.38824 2.55557 6.48253 2.65387L8.10584 4.34613Z"
|
||||
stroke="#CED0D6"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 549 B After Width: | Height: | Size: 548 B |
6
src/main/resources/icons/left.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M6.3464 3.64645C6.54166 3.45118 6.85824 3.45118 7.0535 3.64645C7.24877 3.84171 7.24877 4.15829 7.0535 4.35355L3.90352 7.50354L13.5035 7.50354C13.7797 7.50354 14.0035 7.7274 14.0035 8.00354C14.0035 8.27968 13.7797 8.50354 13.5035 8.50354L3.91059 8.50354L7.05351 11.6464C7.24877 11.8417 7.24877 12.1583 7.05351 12.3536C6.85824 12.5488 6.54166 12.5488 6.3464 12.3536L2.3464 8.35355L1.99284 8L2.3464 7.64645L6.3464 3.64645Z"
|
||||
fill="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 741 B |
6
src/main/resources/icons/left_dark.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M6.3464 3.64645C6.54166 3.45118 6.85824 3.45118 7.0535 3.64645C7.24877 3.84171 7.24877 4.15829 7.0535 4.35355L3.90352 7.50354L13.5035 7.50354C13.7797 7.50354 14.0035 7.7274 14.0035 8.00354C14.0035 8.27968 13.7797 8.50354 13.5035 8.50354L3.91059 8.50354L7.05351 11.6464C7.24877 11.8417 7.24877 12.1583 7.05351 12.3536C6.85824 12.5488 6.54166 12.5488 6.3464 12.3536L2.3464 8.35355L1.99284 8L2.3464 7.64645L6.3464 3.64645Z"
|
||||
fill="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 741 B |
8
src/main/resources/icons/moveDown.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M11.8536 10.1535C12.0488 9.95828 12.0488 9.6417 11.8536 9.44644C11.6583 9.25117 11.3417 9.25117 11.1464 9.44644L8.5 12.0929L8.5 4.5C8.5 4.22386 8.27615 4 8 4C7.72386 4 7.5 4.22386 7.5 4.5L7.5 12.0929L4.85355 9.44644C4.65829 9.25118 4.34171 9.25118 4.14645 9.44644C3.95118 9.6417 3.95118 9.95828 4.14645 10.1535L7.64645 13.6535L8 14.0071L8.35355 13.6535L11.8536 10.1535Z"
|
||||
fill="#6C707E"/>
|
||||
|
||||
<rect x="2" y="15" width="12" height="1" rx="0.5" fill="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 763 B |
8
src/main/resources/icons/moveDown_dark.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M11.8536 10.1535C12.0488 9.95828 12.0488 9.6417 11.8536 9.44644C11.6583 9.25117 11.3417 9.25117 11.1464 9.44644L8.5 12.0929L8.5 4.5C8.5 4.22386 8.27615 4 8 4C7.72386 4 7.5 4.22386 7.5 4.5L7.5 12.0929L4.85355 9.44644C4.65829 9.25118 4.34171 9.25118 4.14645 9.44644C3.95118 9.6417 3.95118 9.95828 4.14645 10.1535L7.64645 13.6535L8 14.0071L8.35355 13.6535L11.8536 10.1535Z"
|
||||
fill="#CED0D6"/>
|
||||
|
||||
<rect x="2" y="15" width="12" height="1" rx="0.5" fill="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 763 B |
7
src/main/resources/icons/moveUp.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!-- 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">
|
||||
<rect x="2" y="0" width="12" height="1" rx="0.5" fill="#6C707E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M11.8536 5.85354C12.0488 6.0488 12.0488 6.36538 11.8536 6.56064C11.6583 6.75591 11.3417 6.75591 11.1464 6.56064L8.5 3.9142L8.5 11.5071C8.5 11.7832 8.27615 12.0071 8 12.0071C7.72386 12.0071 7.5 11.7832 7.5 11.5071L7.5 3.91419L4.85355 6.56064C4.65829 6.7559 4.34171 6.7559 4.14645 6.56064C3.95118 6.36538 3.95118 6.0488 4.14645 5.85353L7.64645 2.35354L8 1.99998L8.35355 2.35354L11.8536 5.85354Z"
|
||||
fill="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 783 B |
7
src/main/resources/icons/moveUp_dark.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!-- 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">
|
||||
<rect x="2" y="0" width="12" height="1" rx="0.5" fill="#CED0D6"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M11.8536 5.85354C12.0488 6.0488 12.0488 6.36538 11.8536 6.56064C11.6583 6.75591 11.3417 6.75591 11.1464 6.56064L8.5 3.9142L8.5 11.5071C8.5 11.7832 8.27615 12.0071 8 12.0071C7.72386 12.0071 7.5 11.7832 7.5 11.5071L7.5 3.91419L4.85355 6.56064C4.65829 6.7559 4.34171 6.7559 4.14645 6.56064C3.95118 6.36538 3.95118 6.0488 4.14645 5.85353L7.64645 2.35354L8 1.99998L8.35355 2.35354L11.8536 5.85354Z"
|
||||
fill="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 783 B |
6
src/main/resources/icons/right.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M9.65355 12.3536C9.45829 12.5488 9.14171 12.5488 8.94645 12.3536C8.75118 12.1583 8.75118 11.8417 8.94645 11.6464L12.0894 8.50354L2.49646 8.50354C2.22032 8.50354 1.99646 8.27968 1.99646 8.00354C1.99646 7.7274 2.22032 7.50354 2.49646 7.50354L12.0964 7.50354L8.94645 4.35355C8.75118 4.15829 8.75118 3.84171 8.94645 3.64645C9.14171 3.45118 9.45829 3.45118 9.65355 3.64645L13.6536 7.64645L14.0071 8L13.6536 8.35355L9.65355 12.3536Z"
|
||||
fill="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 748 B |
6
src/main/resources/icons/right_dark.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M9.65355 12.3536C9.45829 12.5488 9.14171 12.5488 8.94645 12.3536C8.75118 12.1583 8.75118 11.8417 8.94645 11.6464L12.0894 8.50354L2.49646 8.50354C2.22032 8.50354 1.99646 8.27968 1.99646 8.00354C1.99646 7.7274 2.22032 7.50354 2.49646 7.50354L12.0964 7.50354L8.94645 4.35355C8.75118 4.15829 8.75118 3.84171 8.94645 3.64645C9.14171 3.45118 9.45829 3.45118 9.65355 3.64645L13.6536 7.64645L14.0071 8L13.6536 8.35355L9.65355 12.3536Z"
|
||||
fill="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 748 B |
9
src/main/resources/icons/showLogs.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<!-- Copyright 2000-2024 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="M12 14C12.5523 14 13 13.5523 13 13C13 12.4477 12.5523 12 12 12C11.4477 12 11 12.4477 11 13C11 13.5523 11.4477 14 12 14Z" fill="#3574F0"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 16C9 16 8 13 8 13C8 13 9 10 12 10C15 10 16 13 16 13C16 13 15 16 12 16ZM14.9024 12.9785L14.9131 13L14.9024 13.0215C14.787 13.2525 14.6081 13.5582 14.3568 13.8598C13.8602 14.4557 13.1189 15 12 15C10.8811 15 10.1398 14.4557 9.64322 13.8598C9.39186 13.5582 9.21302 13.2525 9.09755 13.0215L9.08686 13L9.09755 12.9785C9.21302 12.7475 9.39186 12.4418 9.64322 12.1402C10.1398 11.5443 10.8811 11 12 11C13.1189 11 13.8602 11.5443 14.3568 12.1402C14.6081 12.4418 14.787 12.7475 14.9024 12.9785Z" fill="#3574F0"/>
|
||||
<path d="M12 1C13.1046 1 14 1.89543 14 3V9.40904C13.6947 9.2748 13.3618 9.16675 13 9.09548V3C13 2.44772 12.5523 2 12 2H4C3.44772 2 3 2.44772 3 3V13C3 13.5523 3.44772 14 4 14H7.35149C7.49887 14.2842 7.7089 14.637 7.99347 15H4C2.89543 15 2 14.1046 2 13V3C2 2.15611 2.52265 1.4343 3.26192 1.1406C3.49028 1.04987 3.73932 1 4 1H12Z" fill="#6C707E"/>
|
||||
<path d="M9.00093 10C8.64732 10.2692 8.35059 10.5672 8.10677 10.8598C8.06775 10.9066 8.03 10.9534 7.99347 11H5.5C5.22386 11 5 10.7761 5 10.5C5 10.2239 5.22386 10 5.5 10H9.00093Z" fill="#6C707E"/>
|
||||
<path d="M10.5 6C10.7761 6 11 5.77614 11 5.5C11 5.22386 10.7761 5 10.5 5H5.5C5.22386 5 5 5.22386 5 5.5C5 5.77614 5.22386 6 5.5 6H10.5Z" fill="#6C707E"/>
|
||||
<path d="M11 8C11 7.72386 10.7761 7.5 10.5 7.5H5.5C5.22386 7.5 5 7.72386 5 8C5 8.27614 5.22386 8.5 5.5 8.5H10.5C10.7761 8.5 11 8.27614 11 8Z" fill="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
9
src/main/resources/icons/showLogs_dark.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<!-- Copyright 2000-2024 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="M12 14C12.5523 14 13 13.5523 13 13C13 12.4477 12.5523 12 12 12C11.4477 12 11 12.4477 11 13C11 13.5523 11.4477 14 12 14Z" fill="#548AF7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 16C9 16 8 13 8 13C8 13 9 10 12 10C15 10 16 13 16 13C16 13 15 16 12 16ZM14.9024 12.9785L14.9131 13L14.9024 13.0215C14.787 13.2525 14.6081 13.5582 14.3568 13.8598C13.8602 14.4557 13.1189 15 12 15C10.8811 15 10.1398 14.4557 9.64322 13.8598C9.39186 13.5582 9.21302 13.2525 9.09755 13.0215L9.08686 13L9.09755 12.9785C9.21302 12.7475 9.39186 12.4418 9.64322 12.1402C10.1398 11.5443 10.8811 11 12 11C13.1189 11 13.8602 11.5443 14.3568 12.1402C14.6081 12.4418 14.787 12.7475 14.9024 12.9785Z" fill="#548AF7"/>
|
||||
<path d="M12 1C13.1046 1 14 1.89543 14 3V9.40904C13.6947 9.2748 13.3618 9.16675 13 9.09548V3C13 2.44772 12.5523 2 12 2H4C3.44772 2 3 2.44772 3 3V13C3 13.5523 3.44772 14 4 14H7.35149C7.49887 14.2842 7.7089 14.637 7.99347 15H4C2.89543 15 2 14.1046 2 13V3C2 2.15611 2.52265 1.4343 3.26192 1.1406C3.49028 1.04987 3.73932 1 4 1H12Z" fill="#CED0D6"/>
|
||||
<path d="M9.00093 10C8.64732 10.2692 8.35059 10.5672 8.10677 10.8598C8.06775 10.9066 8.03 10.9534 7.99347 11H5.5C5.22386 11 5 10.7761 5 10.5C5 10.2239 5.22386 10 5.5 10H9.00093Z" fill="#CED0D6"/>
|
||||
<path d="M10.5 6C10.7761 6 11 5.77614 11 5.5C11 5.22386 10.7761 5 10.5 5H5.5C5.22386 5 5 5.22386 5 5.5C5 5.77614 5.22386 6 5.5 6H10.5Z" fill="#CED0D6"/>
|
||||
<path d="M11 8C11 7.72386 10.7761 7.5 10.5 7.5H5.5C5.22386 7.5 5 7.72386 5 8C5 8.27614 5.22386 8.5 5.5 8.5H10.5C10.7761 8.5 11 8.27614 11 8Z" fill="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
14
src/test/kotlin/app/termora/StringFormatTest.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package app.termora
|
||||
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import java.util.*
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class StringFormatTest {
|
||||
@Test
|
||||
fun test() {
|
||||
assertFailsWith(IllegalFormatConversionException::class) { String.format("%.0f%%", 99) }
|
||||
assertDoesNotThrow { String.format("%.0f%%", 99.0) }
|
||||
}
|
||||
}
|
||||