mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5145cfa8a5 | ||
|
|
87b1a5e315 | ||
|
|
fa59869f2c | ||
|
|
1ae64fe0db | ||
|
|
f8d363836e | ||
|
|
38dccb1d22 | ||
|
|
3e31a89b92 | ||
|
|
d8f892cc02 | ||
|
|
873deb55aa | ||
|
|
c08712d79b | ||
|
|
61bc905727 | ||
|
|
17859be3c5 | ||
|
|
7a24e34695 | ||
|
|
58638eaad8 | ||
|
|
09d2f2d193 | ||
|
|
9121eff8d8 | ||
|
|
8b090b0526 | ||
|
|
15a0d642ff | ||
|
|
dc4333da21 | ||
|
|
184f6d46dc | ||
|
|
68788905fe | ||
|
|
fc46216a3f | ||
|
|
563143645e | ||
|
|
891ccb901b | ||
|
|
928a866fe7 | ||
|
|
ea25b5b46f | ||
|
|
1de10e6129 | ||
|
|
aaf9c2e8d2 | ||
|
|
b8196b5730 | ||
|
|
0a83e8beb4 |
124
build.gradle.kts
124
build.gradle.kts
@@ -14,13 +14,14 @@ plugins {
|
|||||||
java
|
java
|
||||||
idea
|
idea
|
||||||
application
|
application
|
||||||
|
`maven-publish`
|
||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
alias(libs.plugins.kotlinx.serialization)
|
alias(libs.plugins.kotlinx.serialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
group = "app.termora"
|
group = "app.termora"
|
||||||
version = "1.0.14"
|
version = "1.0.16"
|
||||||
|
|
||||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||||
@@ -56,67 +57,67 @@ dependencies {
|
|||||||
|
|
||||||
// implementation(platform(libs.koin.bom))
|
// implementation(platform(libs.koin.bom))
|
||||||
// implementation(libs.koin.core)
|
// implementation(libs.koin.core)
|
||||||
implementation(libs.slf4j.api)
|
api(libs.slf4j.api)
|
||||||
implementation(libs.pty4j)
|
api(libs.pty4j)
|
||||||
implementation(libs.slf4j.tinylog)
|
api(libs.slf4j.tinylog)
|
||||||
implementation(libs.tinylog.impl)
|
api(libs.tinylog.impl)
|
||||||
implementation(libs.commons.codec)
|
api(libs.commons.codec)
|
||||||
implementation(libs.commons.io)
|
api(libs.commons.io)
|
||||||
implementation(libs.commons.lang3)
|
api(libs.commons.lang3)
|
||||||
implementation(libs.commons.csv)
|
api(libs.commons.csv)
|
||||||
implementation(libs.commons.net)
|
api(libs.commons.net)
|
||||||
implementation(libs.commons.text)
|
api(libs.commons.text)
|
||||||
implementation(libs.commons.compress)
|
api(libs.commons.compress)
|
||||||
implementation(libs.commons.vfs2) { exclude(group = "*", module = "*") }
|
api(libs.commons.vfs2) { exclude(group = "*", module = "*") }
|
||||||
implementation(libs.kotlinx.coroutines.swing)
|
api(libs.kotlinx.coroutines.swing)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
api(libs.kotlinx.coroutines.core)
|
||||||
|
|
||||||
implementation(libs.flatlaf) {
|
api(libs.flatlaf) {
|
||||||
artifact {
|
artifact {
|
||||||
if (useNoNativesFlatLaf) {
|
if (useNoNativesFlatLaf) {
|
||||||
classifier = "no-natives"
|
classifier = "no-natives"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
implementation(libs.flatlaf.extras) {
|
api(libs.flatlaf.extras) {
|
||||||
if (useNoNativesFlatLaf) {
|
if (useNoNativesFlatLaf) {
|
||||||
exclude(group = "com.formdev", module = "flatlaf")
|
exclude(group = "com.formdev", module = "flatlaf")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
implementation(libs.flatlaf.swingx) {
|
api(libs.flatlaf.swingx) {
|
||||||
if (useNoNativesFlatLaf) {
|
if (useNoNativesFlatLaf) {
|
||||||
exclude(group = "com.formdev", module = "flatlaf")
|
exclude(group = "com.formdev", module = "flatlaf")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation(libs.kotlinx.serialization.json)
|
api(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.swingx)
|
api(libs.swingx)
|
||||||
implementation(libs.jgoodies.forms)
|
api(libs.jgoodies.forms)
|
||||||
implementation(libs.jna)
|
api(libs.jna)
|
||||||
implementation(libs.jna.platform)
|
api(libs.jna.platform)
|
||||||
implementation(libs.versioncompare)
|
api(libs.versioncompare)
|
||||||
implementation(libs.oshi.core)
|
api(libs.oshi.core)
|
||||||
implementation(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
api(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
||||||
implementation(libs.jfa) { exclude(group = "*", module = "*") }
|
api(libs.jfa) { exclude(group = "*", module = "*") }
|
||||||
implementation(libs.jbr.api)
|
api(libs.jbr.api)
|
||||||
implementation(libs.okhttp)
|
api(libs.okhttp)
|
||||||
implementation(libs.okhttp.logging)
|
api(libs.okhttp.logging)
|
||||||
implementation(libs.sshd.core)
|
api(libs.sshd.core)
|
||||||
implementation(libs.commonmark)
|
api(libs.commonmark)
|
||||||
implementation(libs.jgit)
|
api(libs.jgit)
|
||||||
implementation(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
api(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
||||||
implementation(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
||||||
implementation(libs.eddsa)
|
api(libs.eddsa)
|
||||||
implementation(libs.jnafilechooser)
|
api(libs.jnafilechooser)
|
||||||
implementation(libs.xodus.vfs)
|
api(libs.xodus.vfs)
|
||||||
implementation(libs.xodus.openAPI)
|
api(libs.xodus.openAPI)
|
||||||
implementation(libs.xodus.environment)
|
api(libs.xodus.environment)
|
||||||
implementation(libs.bip39)
|
api(libs.bip39)
|
||||||
implementation(libs.colorpicker)
|
api(libs.colorpicker)
|
||||||
implementation(libs.mixpanel)
|
api(libs.mixpanel)
|
||||||
implementation(libs.jSerialComm)
|
api(libs.jSerialComm)
|
||||||
implementation(libs.ini4j)
|
api(libs.ini4j)
|
||||||
implementation(libs.restart4j)
|
api(libs.restart4j)
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
@@ -147,6 +148,37 @@ application {
|
|||||||
mainClass = "app.termora.MainKt"
|
mainClass = "app.termora.MainKt"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
create<MavenPublication>("mavenJava") {
|
||||||
|
from(components["java"])
|
||||||
|
pom {
|
||||||
|
name = project.name
|
||||||
|
description = "Termora is a terminal emulator and SSH client for Windows, macOS and Linux"
|
||||||
|
url = "https://github.com/TermoraDev/termora"
|
||||||
|
|
||||||
|
licenses {
|
||||||
|
license {
|
||||||
|
name = "AGPL-3.0"
|
||||||
|
url = "https://opensource.org/license/agpl-v3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
developers {
|
||||||
|
developer {
|
||||||
|
name = "hstyi"
|
||||||
|
url = "https://github.com/hstyi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scm {
|
||||||
|
url = "https://github.com/TermoraDev/termora"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "2.1.20"
|
kotlin = "2.1.21"
|
||||||
slf4j = "2.0.17"
|
slf4j = "2.0.17"
|
||||||
pty4j = "0.13.4"
|
pty4j = "0.13.6"
|
||||||
tinylog = "2.7.0"
|
tinylog = "2.7.0"
|
||||||
kotlinx-coroutines = "1.10.2"
|
kotlinx-coroutines = "1.10.2"
|
||||||
flatlaf = "3.6"
|
flatlaf = "3.6"
|
||||||
@@ -35,7 +35,7 @@ bip39 = "1.0.9"
|
|||||||
colorpicker = "2.0.1"
|
colorpicker = "2.0.1"
|
||||||
rhino = "1.8.0"
|
rhino = "1.8.0"
|
||||||
delight-rhino-sandbox = "0.0.17"
|
delight-rhino-sandbox = "0.0.17"
|
||||||
testcontainers = "1.21.0"
|
testcontainers = "1.21.1"
|
||||||
mixpanel = "1.5.3"
|
mixpanel = "1.5.3"
|
||||||
jSerialComm = "2.11.0"
|
jSerialComm = "2.11.0"
|
||||||
ini4j = "0.5.5-2"
|
ini4j = "0.5.5-2"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0"
|
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||||
}
|
}
|
||||||
rootProject.name = "termora"
|
rootProject.name = "termora"
|
||||||
|
|
||||||
|
|||||||
@@ -523,6 +523,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
*/
|
*/
|
||||||
var beep by BooleanPropertyDelegate(true)
|
var beep by BooleanPropertyDelegate(true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 超链接
|
||||||
|
*/
|
||||||
|
var hyperlink by BooleanPropertyDelegate(true)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 光标闪烁
|
* 光标闪烁
|
||||||
*/
|
*/
|
||||||
@@ -643,6 +648,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
*/
|
*/
|
||||||
var backgroundRunning by BooleanPropertyDelegate(false)
|
var backgroundRunning by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签关闭前确认
|
||||||
|
*/
|
||||||
|
var confirmTabClose by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 背景图片的地址
|
* 背景图片的地址
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
|||||||
isModal = true
|
isModal = true
|
||||||
title = I18n.getString("termora.new-host.title")
|
title = I18n.getString("termora.new-host.title")
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(null)
|
||||||
|
pane.setSelectedIndex(0)
|
||||||
|
|
||||||
init()
|
init()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.apache.commons.lang3.RegExUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.PageantConnector
|
import org.eclipse.jgit.internal.transport.sshd.agent.connector.PageantConnector
|
||||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocketConnector
|
import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocketConnector
|
||||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.WinPipeConnector
|
import org.eclipse.jgit.internal.transport.sshd.agent.connector.WinPipeConnector
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
|
import java.awt.datatransfer.DataFlavor
|
||||||
import java.awt.event.*
|
import java.awt.event.*
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
@@ -221,7 +223,24 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
val nameTextField = OutlineTextField(128)
|
val nameTextField = OutlineTextField(128)
|
||||||
val protocolTypeComboBox = FlatComboBox<Protocol>()
|
val protocolTypeComboBox = FlatComboBox<Protocol>()
|
||||||
val usernameTextField = OutlineTextField(128)
|
val usernameTextField = OutlineTextField(128)
|
||||||
val hostTextField = OutlineTextField(255)
|
val hostTextField = object : OutlineTextField(255) {
|
||||||
|
override fun paste() {
|
||||||
|
if (!toolkit.systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = toolkit.systemClipboard.getData(DataFlavor.stringFlavor)?.toString() ?: return
|
||||||
|
if (text.isBlank()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除所有不可见字符
|
||||||
|
text = RegExUtils.replaceAll(text, "[\\p{C}\\s]", StringUtils.EMPTY)
|
||||||
|
|
||||||
|
// text
|
||||||
|
replaceSelection(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
private val passwordPanel = JPanel(BorderLayout())
|
private val passwordPanel = JPanel(BorderLayout())
|
||||||
private val chooseKeyBtn = JButton(Icons.greyKey)
|
private val chooseKeyBtn = JButton(Icons.greyKey)
|
||||||
val passwordTextField = OutlinePasswordField(255)
|
val passwordTextField = OutlinePasswordField(255)
|
||||||
|
|||||||
@@ -135,10 +135,12 @@ class NewHostTree : SimpleTree() {
|
|||||||
// double click
|
// double click
|
||||||
addMouseListener(object : MouseAdapter() {
|
addMouseListener(object : MouseAdapter() {
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
if (getPathForLocation(e.x, e.y) == null) return
|
|
||||||
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||||
val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return
|
val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return
|
||||||
if (lastNode.host.protocol != Protocol.Folder) {
|
if (lastNode.host.protocol != Protocol.Folder) {
|
||||||
|
val path = tree.getClosestPathForLocation(e.x, e.y) ?: return
|
||||||
|
val bounds = tree.getRowBounds(tree.getRowForPath(path)) ?: return
|
||||||
|
if ((e.y >= bounds.y && e.y < (bounds.y + bounds.height)).not()) return
|
||||||
openHostAction?.actionPerformed(OpenHostActionEvent(e.source, lastNode.host, e))
|
openHostAction?.actionPerformed(OpenHostActionEvent(e.source, lastNode.host, e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -854,7 +856,8 @@ class NewHostTree : SimpleTree() {
|
|||||||
val port = map["Port"]?.toIntOrNull() ?: 22
|
val port = map["Port"]?.toIntOrNull() ?: 22
|
||||||
val username = map["Username"] ?: StringUtils.EMPTY
|
val username = map["Username"] ?: StringUtils.EMPTY
|
||||||
val protocol = map["Protocol"] ?: "SSH"
|
val protocol = map["Protocol"] ?: "SSH"
|
||||||
if (!StringUtils.equalsIgnoreCase(protocol, "SSH")) continue
|
// 仅支持 SSH、RDP 协议
|
||||||
|
if (StringUtils.equalsAnyIgnoreCase(protocol, "SSH", "RDP").not()) continue
|
||||||
if (StringUtils.isAllBlank(hostname, label)) continue
|
if (StringUtils.isAllBlank(hostname, label)) continue
|
||||||
|
|
||||||
var p: HostTreeNode? = null
|
var p: HostTreeNode? = null
|
||||||
@@ -889,7 +892,7 @@ class NewHostTree : SimpleTree() {
|
|||||||
host = hostname,
|
host = hostname,
|
||||||
port = port,
|
port = port,
|
||||||
username = username,
|
username = username,
|
||||||
protocol = Protocol.SSH,
|
protocol = runCatching { Protocol.valueOf(protocol) }.getOrNull() ?: Protocol.SSH,
|
||||||
parentId = p?.host?.id ?: StringUtils.EMPTY,
|
parentId = p?.host?.id ?: StringUtils.EMPTY,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class NewHostTreeDialog(
|
|||||||
|
|
||||||
|
|
||||||
init()
|
init()
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(owner)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ open class OptionsPane : JPanel(BorderLayout()) {
|
|||||||
}
|
}
|
||||||
private val cardLayout = CardLayout()
|
private val cardLayout = CardLayout()
|
||||||
private val contentPanel = JPanel(cardLayout)
|
private val contentPanel = JPanel(cardLayout)
|
||||||
|
private val loadedComponents = mutableMapOf<String, JComponent>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -103,16 +104,15 @@ open class OptionsPane : JPanel(BorderLayout()) {
|
|||||||
throw UnsupportedOperationException("Title already exists")
|
throw UnsupportedOperationException("Title already exists")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
contentPanel.add(option.getJComponent(), option.getTitle())
|
|
||||||
tabListModel.addElement(option)
|
tabListModel.addElement(option)
|
||||||
|
|
||||||
if (tabList.selectedIndex < 0) {
|
|
||||||
tabList.selectedIndex = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeOption(option: Option) {
|
fun removeOption(option: Option) {
|
||||||
contentPanel.remove(option.getJComponent())
|
val title = option.getTitle()
|
||||||
|
loadedComponents[title]?.let {
|
||||||
|
contentPanel.remove(it)
|
||||||
|
loadedComponents.remove(title)
|
||||||
|
}
|
||||||
tabListModel.removeElement(option)
|
tabListModel.removeElement(option)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +123,17 @@ open class OptionsPane : JPanel(BorderLayout()) {
|
|||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
tabList.addListSelectionListener {
|
tabList.addListSelectionListener {
|
||||||
if (tabList.selectedIndex >= 0) {
|
if (tabList.selectedIndex >= 0) {
|
||||||
cardLayout.show(contentPanel, tabListModel.get(tabList.selectedIndex).getTitle())
|
val option = tabListModel.get(tabList.selectedIndex)
|
||||||
|
val title = option.getTitle()
|
||||||
|
|
||||||
|
if (!loadedComponents.containsKey(title)) {
|
||||||
|
val component = option.getJComponent()
|
||||||
|
loadedComponents[title] = component
|
||||||
|
contentPanel.add(component, title)
|
||||||
|
SwingUtilities.updateComponentTreeUI(component)
|
||||||
|
}
|
||||||
|
|
||||||
|
cardLayout.show(contentPanel, title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,10 +223,9 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun willBeClose(): Boolean {
|
override fun beforeClose() {
|
||||||
// 保存窗口状态
|
// 保存窗口状态
|
||||||
terminalPanel.storeVisualWindows(host.id)
|
terminalPanel.storeVisualWindows(host.id)
|
||||||
return super.willBeClose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class MySessionListener : SessionListener, Disposable {
|
private inner class MySessionListener : SessionListener, Disposable {
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package app.termora
|
|||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
import java.awt.event.WindowAdapter
|
|
||||||
import java.awt.event.WindowEvent
|
|
||||||
import javax.swing.BorderFactory
|
import javax.swing.BorderFactory
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.JPanel
|
import javax.swing.JPanel
|
||||||
@@ -20,8 +18,10 @@ class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
title = I18n.getString("termora.setting")
|
title = I18n.getString("termora.setting")
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
init()
|
val index = properties.getString("Settings-SelectedOption")?.toIntOrNull() ?: 0
|
||||||
|
optionsPane.setSelectedIndex(index)
|
||||||
|
|
||||||
|
init()
|
||||||
initEvents()
|
initEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,14 +31,6 @@ class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
properties.putString("Settings-SelectedOption", optionsPane.getSelectedIndex().toString())
|
properties.putString("Settings-SelectedOption", optionsPane.getSelectedIndex().toString())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
addWindowListener(object : WindowAdapter() {
|
|
||||||
override fun windowActivated(e: WindowEvent) {
|
|
||||||
removeWindowListener(this)
|
|
||||||
val index = properties.getString("Settings-SelectedOption")?.toIntOrNull() ?: return
|
|
||||||
optionsPane.setSelectedIndex(index)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createCenterPanel(): JComponent {
|
override fun createCenterPanel(): JComponent {
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ import com.jgoodies.forms.builder.FormBuilder
|
|||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
import com.jthemedetecor.OsThemeDetector
|
import com.jthemedetecor.OsThemeDetector
|
||||||
import com.sun.jna.LastErrorException
|
import com.sun.jna.LastErrorException
|
||||||
|
import com.sun.jna.Native
|
||||||
|
import com.sun.jna.platform.win32.Shell32
|
||||||
|
import com.sun.jna.platform.win32.ShlObj
|
||||||
|
import com.sun.jna.platform.win32.WinDef
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
@@ -132,6 +136,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
val themeComboBox = FlatComboBox<String>()
|
val themeComboBox = FlatComboBox<String>()
|
||||||
val languageComboBox = FlatComboBox<String>()
|
val languageComboBox = FlatComboBox<String>()
|
||||||
val backgroundComBoBox = YesOrNoComboBox()
|
val backgroundComBoBox = YesOrNoComboBox()
|
||||||
|
val confirmTabCloseComBoBox = YesOrNoComboBox()
|
||||||
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
|
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
|
||||||
val preferredThemeBtn = JButton(Icons.settings)
|
val preferredThemeBtn = JButton(Icons.settings)
|
||||||
val opacitySpinner = NumberSpinner(100, 0, 100)
|
val opacitySpinner = NumberSpinner(100, 0, 100)
|
||||||
@@ -180,6 +185,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
followSystemCheckBox.isSelected = appearance.followSystem
|
followSystemCheckBox.isSelected = appearance.followSystem
|
||||||
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
|
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
|
||||||
backgroundComBoBox.selectedItem = appearance.backgroundRunning
|
backgroundComBoBox.selectedItem = appearance.backgroundRunning
|
||||||
|
confirmTabCloseComBoBox.selectedItem = appearance.confirmTabClose
|
||||||
|
|
||||||
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
||||||
themeManager.themes.keys.forEach { themeComboBox.addItem(it) }
|
themeManager.themes.keys.forEach { themeComboBox.addItem(it) }
|
||||||
@@ -230,6 +236,13 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
confirmTabCloseComBoBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
appearance.confirmTabClose = confirmTabCloseComBoBox.selectedItem as Boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
followSystemCheckBox.addActionListener {
|
followSystemCheckBox.addActionListener {
|
||||||
appearance.followSystem = followSystemCheckBox.isSelected
|
appearance.followSystem = followSystemCheckBox.isSelected
|
||||||
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
||||||
@@ -368,7 +381,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private fun getFormPanel(): JPanel {
|
private fun getFormPanel(): JPanel {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $formMargin, default:grow, $formMargin, default, default:grow",
|
"left:pref, $formMargin, default:grow, $formMargin, default, default:grow",
|
||||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
)
|
)
|
||||||
val box = FlatToolBar()
|
val box = FlatToolBar()
|
||||||
box.add(followSystemCheckBox)
|
box.add(followSystemCheckBox)
|
||||||
@@ -401,7 +414,13 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
.add(opacitySpinner).xy(3, rows).apply { rows += step }
|
.add(opacitySpinner).xy(3, rows).apply { rows += step }
|
||||||
|
|
||||||
builder.add("${I18n.getString("termora.settings.appearance.background-running")}:").xy(1, rows)
|
builder.add("${I18n.getString("termora.settings.appearance.background-running")}:").xy(1, rows)
|
||||||
.add(backgroundComBoBox).xy(3, rows)
|
.add(backgroundComBoBox).xy(3, rows).apply { rows += step }
|
||||||
|
|
||||||
|
val confirmTabCloseBox = Box.createHorizontalBox()
|
||||||
|
confirmTabCloseBox.add(JLabel("${I18n.getString("termora.settings.appearance.confirm-tab-close")}:"))
|
||||||
|
confirmTabCloseBox.add(Box.createHorizontalStrut(8))
|
||||||
|
confirmTabCloseBox.add(confirmTabCloseComBoBox)
|
||||||
|
builder.add(confirmTabCloseBox).xyw(1, rows, 3).apply { rows += step }
|
||||||
|
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
@@ -422,6 +441,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private val selectCopyComboBox = YesOrNoComboBox()
|
private val selectCopyComboBox = YesOrNoComboBox()
|
||||||
private val autoCloseTabComboBox = YesOrNoComboBox()
|
private val autoCloseTabComboBox = YesOrNoComboBox()
|
||||||
private val floatingToolbarComboBox = YesOrNoComboBox()
|
private val floatingToolbarComboBox = YesOrNoComboBox()
|
||||||
|
private val hyperlinkComboBox = YesOrNoComboBox()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -499,6 +519,13 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hyperlinkComboBox.addItemListener { e ->
|
||||||
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
|
terminalSetting.hyperlink = hyperlinkComboBox.selectedItem as Boolean
|
||||||
|
TerminalPanelFactory.getInstance().repaintAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cursorBlinkComboBox.addItemListener { e ->
|
cursorBlinkComboBox.addItemListener { e ->
|
||||||
if (e.stateChange == ItemEvent.SELECTED) {
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
terminalSetting.cursorBlink = cursorBlinkComboBox.selectedItem as Boolean
|
terminalSetting.cursorBlink = cursorBlinkComboBox.selectedItem as Boolean
|
||||||
@@ -577,20 +604,33 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
shellComboBox.selectedItem = terminalSetting.localShell
|
shellComboBox.selectedItem = terminalSetting.localShell
|
||||||
|
|
||||||
val fonts = linkedSetOf("JetBrains Mono", "Source Code Pro", "Monospaced")
|
fontComboBox.addItem(terminalSetting.font)
|
||||||
FontUtils.getAllFonts().forEach {
|
var fontsLoaded = false
|
||||||
if (!fonts.contains(it.family)) {
|
|
||||||
fonts.addLast(it.family)
|
fontComboBox.addPopupMenuListener(object : PopupMenuListener {
|
||||||
|
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
||||||
|
if (!fontsLoaded) {
|
||||||
|
val selectedItem = fontComboBox.selectedItem
|
||||||
|
fontComboBox.removeAllItems();
|
||||||
|
fontComboBox.addItem("JetBrains Mono")
|
||||||
|
fontComboBox.addItem("Source Code Pro")
|
||||||
|
fontComboBox.addItem("Monospaced")
|
||||||
|
FontUtils.getAvailableFontFamilyNames().forEach {
|
||||||
|
fontComboBox.addItem(it)
|
||||||
|
}
|
||||||
|
fontComboBox.selectedItem = selectedItem
|
||||||
|
fontsLoaded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (font in fonts) {
|
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {}
|
||||||
fontComboBox.addItem(font)
|
override fun popupMenuCanceled(e: PopupMenuEvent) {}
|
||||||
}
|
})
|
||||||
|
|
||||||
fontComboBox.selectedItem = terminalSetting.font
|
fontComboBox.selectedItem = terminalSetting.font
|
||||||
debugComboBox.selectedItem = terminalSetting.debug
|
debugComboBox.selectedItem = terminalSetting.debug
|
||||||
beepComboBox.selectedItem = terminalSetting.beep
|
beepComboBox.selectedItem = terminalSetting.beep
|
||||||
|
hyperlinkComboBox.selectedItem = terminalSetting.hyperlink
|
||||||
cursorBlinkComboBox.selectedItem = terminalSetting.cursorBlink
|
cursorBlinkComboBox.selectedItem = terminalSetting.cursorBlink
|
||||||
cursorStyleComboBox.selectedItem = terminalSetting.cursor
|
cursorStyleComboBox.selectedItem = terminalSetting.cursor
|
||||||
selectCopyComboBox.selectedItem = terminalSetting.selectCopy
|
selectCopyComboBox.selectedItem = terminalSetting.selectCopy
|
||||||
@@ -613,7 +653,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private fun getCenterComponent(): JComponent {
|
private fun getCenterComponent(): JComponent {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow",
|
"left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow",
|
||||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
)
|
)
|
||||||
|
|
||||||
val beepBtn = JButton(Icons.run)
|
val beepBtn = JButton(Icons.run)
|
||||||
@@ -636,6 +676,8 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
.add("${I18n.getString("termora.settings.terminal.beep")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.beep")}:").xy(1, rows)
|
||||||
.add(beepComboBox).xy(3, rows)
|
.add(beepComboBox).xy(3, rows)
|
||||||
.add(beepBtn).xy(5, rows).apply { rows += step }
|
.add(beepBtn).xy(5, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.settings.terminal.hyperlink")}:").xy(1, rows)
|
||||||
|
.add(hyperlinkComboBox).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.select-copy")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.select-copy")}:").xy(1, rows)
|
||||||
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
|
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
|
||||||
@@ -1476,6 +1518,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private val sftpCommandField = OutlineTextField(255)
|
private val sftpCommandField = OutlineTextField(255)
|
||||||
private val defaultDirectoryField = OutlineTextField(255)
|
private val defaultDirectoryField = OutlineTextField(255)
|
||||||
private val browseDirectoryBtn = JButton(Icons.folder)
|
private val browseDirectoryBtn = JButton(Icons.folder)
|
||||||
|
private val browseEditCommandBtn = JButton(Icons.folder)
|
||||||
private val pinTabComboBox = YesOrNoComboBox()
|
private val pinTabComboBox = YesOrNoComboBox()
|
||||||
private val preserveModificationTimeComboBox = YesOrNoComboBox()
|
private val preserveModificationTimeComboBox = YesOrNoComboBox()
|
||||||
private val sftp get() = database.sftp
|
private val sftp get() = database.sftp
|
||||||
@@ -1547,6 +1590,41 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
browseEditCommandBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val chooser = FileChooser()
|
||||||
|
chooser.allowsMultiSelection = false
|
||||||
|
chooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
||||||
|
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
chooser.defaultDirectory = "/Applications"
|
||||||
|
} else {
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
val pszPath = CharArray(WinDef.MAX_PATH)
|
||||||
|
Shell32.INSTANCE.SHGetFolderPath(
|
||||||
|
null,
|
||||||
|
ShlObj.CSIDL_DESKTOPDIRECTORY, null, ShlObj.SHGFP_TYPE_CURRENT,
|
||||||
|
pszPath
|
||||||
|
)
|
||||||
|
chooser.defaultDirectory = Native.toString(pszPath)
|
||||||
|
} else {
|
||||||
|
chooser.defaultDirectory = SystemUtils.USER_HOME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chooser.showOpenDialog(owner).thenAccept { files ->
|
||||||
|
if (files.isNotEmpty()) {
|
||||||
|
val file = files.first()
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
editCommandField.text = "open -a ${file.absolutePath} {0}"
|
||||||
|
} else {
|
||||||
|
editCommandField.text = "${file.absolutePath} {0}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1563,6 +1641,8 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
sftpCommandField.placeholderText = "sftp"
|
sftpCommandField.placeholderText = "sftp"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editCommandField.trailingComponent = browseEditCommandBtn
|
||||||
|
|
||||||
defaultDirectoryField.placeholderText = SystemUtils.USER_HOME
|
defaultDirectoryField.placeholderText = SystemUtils.USER_HOME
|
||||||
defaultDirectoryField.trailingComponent = browseDirectoryBtn
|
defaultDirectoryField.trailingComponent = browseDirectoryBtn
|
||||||
|
|
||||||
|
|||||||
@@ -265,9 +265,14 @@ object SshClients {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e !is SshException || e.disconnectCode != SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) throw e
|
if (e !is SshException || e.disconnectCode != SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) throw e
|
||||||
val owner = client.properties["owner"] as Window? ?: throw e
|
val owner = client.properties["owner"] as Window? ?: throw e
|
||||||
val authentication = ask(host, owner) ?: throw e
|
val askUserInfo = ask(host, entry, owner) ?: throw e
|
||||||
if (authentication.type == AuthenticationType.No) throw e
|
if (askUserInfo.authentication.type == AuthenticationType.No) throw e
|
||||||
return doOpenSession(host.copy(authentication = authentication), client)
|
return doOpenSession(
|
||||||
|
host.copy(
|
||||||
|
authentication = askUserInfo.authentication,
|
||||||
|
username = askUserInfo.username
|
||||||
|
), client
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
session.setAttribute(HOST_KEY, host)
|
session.setAttribute(HOST_KEY, host)
|
||||||
@@ -414,21 +419,33 @@ object SshClients {
|
|||||||
return sshClient
|
return sshClient
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ask(host: Host, owner: Window): Authentication? {
|
|
||||||
val ref = AtomicReference<Authentication>(null)
|
private data class AskUserInfo(val username: String, val authentication: Authentication)
|
||||||
|
|
||||||
|
private fun ask(host: Host, entry: HostConfigEntry, owner: Window): AskUserInfo? {
|
||||||
|
val ref = AtomicReference<AskUserInfo>(null)
|
||||||
|
|
||||||
SwingUtilities.invokeAndWait {
|
SwingUtilities.invokeAndWait {
|
||||||
val dialog = RequestAuthenticationDialog(owner, host)
|
val dialog = RequestAuthenticationDialog(owner, host)
|
||||||
dialog.setLocationRelativeTo(owner)
|
dialog.setLocationRelativeTo(owner)
|
||||||
val authentication = dialog.getAuthentication().apply { ref.set(this) }
|
val authentication = dialog.getAuthentication()
|
||||||
|
ref.set(AskUserInfo(dialog.getUsername(), authentication))
|
||||||
|
|
||||||
// save
|
// save
|
||||||
if (dialog.isRemembered()) {
|
if (dialog.isRemembered()) {
|
||||||
|
// fix https://github.com/TermoraDev/termora/issues/609
|
||||||
|
val hostId = entry.getProperty("Host", host.id)
|
||||||
|
val h = hostManager.getHost(hostId)
|
||||||
|
if (h != null) {
|
||||||
hostManager.addHost(
|
hostManager.addHost(
|
||||||
host.copy(
|
h.copy(
|
||||||
authentication = authentication,
|
authentication = authentication,
|
||||||
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ref.get()
|
return ref.get()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import app.termora.actions.AnActionEvent
|
|||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.actions.MultipleAction
|
import app.termora.actions.MultipleAction
|
||||||
import app.termora.highlight.KeywordHighlightPaintListener
|
import app.termora.highlight.KeywordHighlightPaintListener
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
import app.termora.terminal.Terminal
|
import app.termora.terminal.Terminal
|
||||||
import app.termora.terminal.panel.TerminalHyperlinkPaintListener
|
import app.termora.terminal.panel.TerminalHyperlinkPaintListener
|
||||||
@@ -40,6 +41,10 @@ class TerminalPanelFactory : Disposable {
|
|||||||
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
||||||
val writer = MyTerminalWriter(ptyConnector)
|
val writer = MyTerminalWriter(ptyConnector)
|
||||||
val terminalPanel = TerminalPanel(terminal, writer)
|
val terminalPanel = TerminalPanel(terminal, writer)
|
||||||
|
|
||||||
|
// processDeviceStatusReport
|
||||||
|
terminal.getTerminalModel().setData(DataKey.TerminalWriter, writer)
|
||||||
|
|
||||||
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
||||||
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
||||||
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.Database.Appearance
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
@@ -44,10 +45,15 @@ interface TerminalTab : Disposable, DataProvider {
|
|||||||
fun canClose(): Boolean = true
|
fun canClose(): Boolean = true
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 返回 true 表示可以关闭
|
* 返回 true 表示可以关闭,只有当 [Appearance.confirmTabClose] 为 false 时才会调用
|
||||||
*/
|
*/
|
||||||
fun willBeClose(): Boolean = true
|
fun willBeClose(): Boolean = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 即将关闭,已经无法挽回
|
||||||
|
*/
|
||||||
|
fun beforeClose() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否可以克隆
|
* 是否可以克隆
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class TerminalTabbed(
|
|||||||
private val actionManager = ActionManager.getInstance()
|
private val actionManager = ActionManager.getInstance()
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
private val titleProperty = UUID.randomUUID().toSimpleString()
|
private val titleProperty = UUID.randomUUID().toSimpleString()
|
||||||
|
private val appearance get() = Database.getDatabase().appearance
|
||||||
private val iconListener = PropertyChangeListener { e ->
|
private val iconListener = PropertyChangeListener { e ->
|
||||||
val source = e.source
|
val source = e.source
|
||||||
if (e.propertyName == "icon" && source is TerminalTab) {
|
if (e.propertyName == "icon" && source is TerminalTab) {
|
||||||
@@ -153,8 +154,29 @@ class TerminalTabbed(
|
|||||||
if (tabbedPane.isTabClosable(index)) {
|
if (tabbedPane.isTabClosable(index)) {
|
||||||
val tab = tabs[index]
|
val tab = tabs[index]
|
||||||
|
|
||||||
|
// 询问是否可以关闭
|
||||||
if (disposable) {
|
if (disposable) {
|
||||||
if (!tab.willBeClose()) {
|
// 如果开启了关闭确认,那么直接询问用户
|
||||||
|
if (appearance.confirmTabClose) {
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
windowScope.window,
|
||||||
|
I18n.getString("termora.tabbed.tab.close-prompt"),
|
||||||
|
messageType = JOptionPane.QUESTION_MESSAGE,
|
||||||
|
optionType = JOptionPane.OK_CANCEL_OPTION
|
||||||
|
) != JOptionPane.OK_OPTION
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (!tab.willBeClose()) { // 如果没有开启则询问用户
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知即将关闭
|
||||||
|
if (disposable) {
|
||||||
|
try {
|
||||||
|
tab.beforeClose()
|
||||||
|
} catch (_: Exception) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -400,10 +422,12 @@ class TerminalTabbed(
|
|||||||
private fun showContextMenu(event: MouseEvent) {
|
private fun showContextMenu(event: MouseEvent) {
|
||||||
val popupMenu = FlatPopupMenu()
|
val popupMenu = FlatPopupMenu()
|
||||||
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
|
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
|
||||||
|
val owner = SwingUtilities.getWindowAncestor(this@TerminalTabbed)
|
||||||
val dialog = CustomizeToolBarDialog(
|
val dialog = CustomizeToolBarDialog(
|
||||||
SwingUtilities.getWindowAncestor(this@TerminalTabbed),
|
owner,
|
||||||
termoraToolBar
|
termoraToolBar
|
||||||
)
|
)
|
||||||
|
dialog.setLocationRelativeTo(owner)
|
||||||
if (dialog.open()) {
|
if (dialog.open()) {
|
||||||
termoraToolBar.rebuild()
|
termoraToolBar.rebuild()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -645,9 +645,13 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ohKeyPair.remark.isEmpty()) {
|
|
||||||
ohKeyPair = ohKeyPair.copy(
|
ohKeyPair = ohKeyPair.copy(
|
||||||
name = nameTextField.text,
|
name = nameTextField.text,
|
||||||
|
remark = remarkTextField.text,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (ohKeyPair.remark.isEmpty()) {
|
||||||
|
ohKeyPair = ohKeyPair.copy(
|
||||||
remark = "Import on " + DateFormatUtils.format(Date(), I18n.getString("termora.date-format"))
|
remark = "Import on " + DateFormatUtils.format(Date(), I18n.getString("termora.date-format"))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class MacroDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
initEvents()
|
initEvents()
|
||||||
|
|
||||||
init()
|
init()
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(owner)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import app.termora.*
|
|||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.actions.SettingsAction
|
import app.termora.actions.SettingsAction
|
||||||
import app.termora.sftp.FileSystemViewTable.AskTransfer.Action
|
import app.termora.sftp.FileSystemViewTable.AskTransfer.Action
|
||||||
|
import app.termora.vfs2.VFSWalker
|
||||||
import app.termora.vfs2.sftp.MySftpFileObject
|
import app.termora.vfs2.sftp.MySftpFileObject
|
||||||
import app.termora.vfs2.sftp.MySftpFileSystem
|
import app.termora.vfs2.sftp.MySftpFileSystem
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
@@ -37,7 +38,6 @@ import java.nio.file.FileVisitor
|
|||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.nio.file.StandardOpenOption
|
import java.nio.file.StandardOpenOption
|
||||||
import java.nio.file.attribute.BasicFileAttributes
|
import java.nio.file.attribute.BasicFileAttributes
|
||||||
import java.nio.file.attribute.FileTime
|
|
||||||
import java.text.MessageFormat
|
import java.text.MessageFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
@@ -45,6 +45,21 @@ import java.util.regex.Pattern
|
|||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.table.DefaultTableCellRenderer
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
import kotlin.collections.ArrayDeque
|
import kotlin.collections.ArrayDeque
|
||||||
|
import kotlin.collections.List
|
||||||
|
import kotlin.collections.all
|
||||||
|
import kotlin.collections.contains
|
||||||
|
import kotlin.collections.filter
|
||||||
|
import kotlin.collections.filterIsInstance
|
||||||
|
import kotlin.collections.find
|
||||||
|
import kotlin.collections.forEach
|
||||||
|
import kotlin.collections.isEmpty
|
||||||
|
import kotlin.collections.isNotEmpty
|
||||||
|
import kotlin.collections.last
|
||||||
|
import kotlin.collections.listOf
|
||||||
|
import kotlin.collections.map
|
||||||
|
import kotlin.collections.mapOf
|
||||||
|
import kotlin.collections.mutableListOf
|
||||||
|
import kotlin.collections.sortedArray
|
||||||
import kotlin.io.path.absolutePathString
|
import kotlin.io.path.absolutePathString
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
@@ -218,7 +233,7 @@ class FileSystemViewTable(
|
|||||||
val localTarget = sftpPanel.getLocalTarget()
|
val localTarget = sftpPanel.getLocalTarget()
|
||||||
val table = localTarget.getData(SFTPDataProviders.FileSystemViewTable) ?: return false
|
val table = localTarget.getData(SFTPDataProviders.FileSystemViewTable) ?: return false
|
||||||
// 委托最左侧的本地文件系统传输
|
// 委托最左侧的本地文件系统传输
|
||||||
table.transfer(paths, true, targetWorkdir)
|
table.transfer(paths, true, targetWorkdir, fileSystemViewPanel)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -360,34 +375,7 @@ class FileSystemViewTable(
|
|||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
val last = files.last()
|
val last = files.last()
|
||||||
if (last !is MySftpFileObject) return
|
if (last !is MySftpFileObject) return
|
||||||
|
changePermission(last)
|
||||||
val dialog = PosixFilePermissionDialog(
|
|
||||||
SwingUtilities.getWindowAncestor(table),
|
|
||||||
model.getFilePermissions(last)
|
|
||||||
)
|
|
||||||
val permissions = dialog.open() ?: return
|
|
||||||
|
|
||||||
if (fileSystemViewPanel.requestLoading()) {
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
|
||||||
val c = runCatching { last.setPosixFilePermissions(permissions) }.onFailure {
|
|
||||||
withContext(Dispatchers.Swing) {
|
|
||||||
OptionPane.showMessageDialog(
|
|
||||||
owner,
|
|
||||||
ExceptionUtils.getMessage(it),
|
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop loading
|
|
||||||
fileSystemViewPanel.stopLoading()
|
|
||||||
|
|
||||||
// reload
|
|
||||||
if (c.isSuccess) {
|
|
||||||
fileSystemViewPanel.reload(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
refresh.addActionListener { fileSystemViewPanel.reload() }
|
refresh.addActionListener { fileSystemViewPanel.reload() }
|
||||||
@@ -409,6 +397,80 @@ class FileSystemViewTable(
|
|||||||
popupMenu.show(table, e.x, e.y)
|
popupMenu.show(table, e.x, e.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun changePermission(file: MySftpFileObject) {
|
||||||
|
|
||||||
|
val dialog = PosixFilePermissionDialog(
|
||||||
|
SwingUtilities.getWindowAncestor(table),
|
||||||
|
model.getFilePermissions(file)
|
||||||
|
)
|
||||||
|
val permissions = dialog.open() ?: return
|
||||||
|
val isIncludeSubdirectories = dialog.isIncludeSubdirectories()
|
||||||
|
|
||||||
|
if (fileSystemViewPanel.requestLoading()) {
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
val c = runCatching {
|
||||||
|
file.setPosixFilePermissions(permissions)
|
||||||
|
if (isIncludeSubdirectories && file.isFolder) {
|
||||||
|
file.refresh()
|
||||||
|
VFSWalker.walk(file, object : FileVisitor<FileObject> {
|
||||||
|
override fun preVisitDirectory(
|
||||||
|
dir: FileObject,
|
||||||
|
attrs: BasicFileAttributes
|
||||||
|
): FileVisitResult {
|
||||||
|
dir.refresh()
|
||||||
|
if (dir is MySftpFileObject) {
|
||||||
|
dir.setPosixFilePermissions(permissions)
|
||||||
|
}
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitFile(
|
||||||
|
file: FileObject,
|
||||||
|
attrs: BasicFileAttributes
|
||||||
|
): FileVisitResult {
|
||||||
|
if (file is MySftpFileObject) {
|
||||||
|
file.setPosixFilePermissions(permissions)
|
||||||
|
}
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitFileFailed(
|
||||||
|
file: FileObject,
|
||||||
|
exc: IOException
|
||||||
|
): FileVisitResult {
|
||||||
|
return FileVisitResult.TERMINATE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun postVisitDirectory(
|
||||||
|
dir: FileObject,
|
||||||
|
exc: IOException?
|
||||||
|
): FileVisitResult {
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
ExceptionUtils.getMessage(it),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop loading
|
||||||
|
fileSystemViewPanel.stopLoading()
|
||||||
|
|
||||||
|
// reload
|
||||||
|
if (c.isSuccess) {
|
||||||
|
fileSystemViewPanel.reload(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun renameSelection() {
|
private fun renameSelection() {
|
||||||
val index = selectedRow
|
val index = selectedRow
|
||||||
if (index < 0) return
|
if (index < 0) return
|
||||||
@@ -619,12 +681,13 @@ class FileSystemViewTable(
|
|||||||
private fun transfer(
|
private fun transfer(
|
||||||
files: List<FileObject>,
|
files: List<FileObject>,
|
||||||
fromLocalSystem: Boolean = false,
|
fromLocalSystem: Boolean = false,
|
||||||
targetWorkdir: FileObject? = null
|
targetWorkdir: FileObject? = null,
|
||||||
|
target: FileSystemViewPanel? = null,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
assertEventDispatchThread()
|
assertEventDispatchThread()
|
||||||
|
|
||||||
val target = sftpPanel.getTarget(table) ?: return
|
val target = (target ?: sftpPanel.getTarget(table)) ?: return
|
||||||
val table = target.getData(SFTPDataProviders.FileSystemViewTable) ?: return
|
val table = target.getData(SFTPDataProviders.FileSystemViewTable) ?: return
|
||||||
var isApplyAll = false
|
var isApplyAll = false
|
||||||
var lastAction = Action.Overwrite
|
var lastAction = Action.Overwrite
|
||||||
@@ -650,7 +713,7 @@ class FileSystemViewTable(
|
|||||||
|
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
try {
|
try {
|
||||||
doTransfer(file, lastAction, fromLocalSystem, targetWorkdir)
|
doTransfer(file, lastAction, fromLocalSystem, targetWorkdir, target)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (log.isErrorEnabled) {
|
if (log.isErrorEnabled) {
|
||||||
log.error(e.message, e)
|
log.error(e.message, e)
|
||||||
@@ -801,10 +864,11 @@ class FileSystemViewTable(
|
|||||||
file: FileObject,
|
file: FileObject,
|
||||||
action: Action,
|
action: Action,
|
||||||
fromLocalSystem: Boolean,
|
fromLocalSystem: Boolean,
|
||||||
targetWorkdir: FileObject?
|
targetWorkdir: FileObject?,
|
||||||
|
target: FileSystemViewPanel? = null
|
||||||
) {
|
) {
|
||||||
val sftpPanel = this.sftpPanel
|
val sftpPanel = this.sftpPanel
|
||||||
val target = sftpPanel.getTarget(table) ?: return
|
val target = (target ?: sftpPanel.getTarget(table)) ?: return
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定义一个添加器,它可以自动的判断导入/拖拽行为
|
* 定义一个添加器,它可以自动的判断导入/拖拽行为
|
||||||
@@ -886,36 +950,7 @@ class FileSystemViewTable(
|
|||||||
dir: FileObject,
|
dir: FileObject,
|
||||||
visitor: FileVisitor<FileObject>,
|
visitor: FileVisitor<FileObject>,
|
||||||
): FileVisitResult {
|
): FileVisitResult {
|
||||||
|
return VFSWalker.walk(dir, visitor)
|
||||||
// clear cache
|
|
||||||
if (visitor.preVisitDirectory(dir, EmptyBasicFileAttributes.INSTANCE) == FileVisitResult.TERMINATE) {
|
|
||||||
return FileVisitResult.TERMINATE
|
|
||||||
}
|
|
||||||
|
|
||||||
for (e in dir.children) {
|
|
||||||
if (e.name.baseName == ".." || e.name.baseName == ".") continue
|
|
||||||
if (e.isFolder) {
|
|
||||||
if (walk(dir.resolveFile(e.name.baseName), visitor) == FileVisitResult.TERMINATE) {
|
|
||||||
return FileVisitResult.TERMINATE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val result = visitor.visitFile(
|
|
||||||
dir.resolveFile(e.name.baseName),
|
|
||||||
EmptyBasicFileAttributes.INSTANCE
|
|
||||||
)
|
|
||||||
if (result == FileVisitResult.TERMINATE) {
|
|
||||||
return FileVisitResult.TERMINATE
|
|
||||||
} else if (result == FileVisitResult.SKIP_SUBTREE) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visitor.postVisitDirectory(dir, null) == FileVisitResult.TERMINATE) {
|
|
||||||
return FileVisitResult.TERMINATE
|
|
||||||
}
|
|
||||||
|
|
||||||
return FileVisitResult.CONTINUE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addTransport(
|
private fun addTransport(
|
||||||
@@ -974,47 +1009,5 @@ class FileSystemViewTable(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class EmptyBasicFileAttributes : BasicFileAttributes {
|
|
||||||
companion object {
|
|
||||||
val INSTANCE = EmptyBasicFileAttributes()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun lastModifiedTime(): FileTime {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun lastAccessTime(): FileTime {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun creationTime(): FileTime {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isRegularFile(): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isDirectory(): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isSymbolicLink(): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isOther(): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun size(): Long {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fileKey(): Any {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -157,8 +157,12 @@ class FileSystemViewTableModel : DefaultTableModel() {
|
|||||||
fun getPathNames(): Set<String> {
|
fun getPathNames(): Set<String> {
|
||||||
val names = linkedSetOf<String>()
|
val names = linkedSetOf<String>()
|
||||||
for (i in 0 until rowCount) {
|
for (i in 0 until rowCount) {
|
||||||
|
if (hasParent && i == 0) {
|
||||||
|
names.add("..")
|
||||||
|
} else {
|
||||||
names.add(getFileObject(i).name.baseName)
|
names.add(getFileObject(i).name.baseName)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class PosixFilePermissionDialog(
|
|||||||
private val otherRead = JCheckBox(I18n.getString("termora.transport.permissions.read"))
|
private val otherRead = JCheckBox(I18n.getString("termora.transport.permissions.read"))
|
||||||
private val otherWrite = JCheckBox(I18n.getString("termora.transport.permissions.write"))
|
private val otherWrite = JCheckBox(I18n.getString("termora.transport.permissions.write"))
|
||||||
private val otherExecute = JCheckBox(I18n.getString("termora.transport.permissions.execute"))
|
private val otherExecute = JCheckBox(I18n.getString("termora.transport.permissions.execute"))
|
||||||
|
private val includeSubFolder = JCheckBox(I18n.getString("termora.transport.permissions.include-subfolder"))
|
||||||
|
|
||||||
private var isCancelled = false
|
private var isCancelled = false
|
||||||
|
|
||||||
@@ -60,13 +61,14 @@ class PosixFilePermissionDialog(
|
|||||||
otherRead.isFocusable = false
|
otherRead.isFocusable = false
|
||||||
otherWrite.isFocusable = false
|
otherWrite.isFocusable = false
|
||||||
otherExecute.isFocusable = false
|
otherExecute.isFocusable = false
|
||||||
|
includeSubFolder.isFocusable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createCenterPanel(): JComponent {
|
override fun createCenterPanel(): JComponent {
|
||||||
val formMargin = "7dlu"
|
val formMargin = "7dlu"
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"default:grow, $formMargin, default:grow, $formMargin, default:grow",
|
"default:grow, $formMargin, default:grow, $formMargin, default:grow",
|
||||||
"pref, $formMargin, pref, $formMargin, pref"
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
)
|
)
|
||||||
|
|
||||||
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
|
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
|
||||||
@@ -95,6 +97,8 @@ class PosixFilePermissionDialog(
|
|||||||
otherBox.border = BorderFactory.createTitledBorder(I18n.getString("termora.transport.permissions.others"))
|
otherBox.border = BorderFactory.createTitledBorder(I18n.getString("termora.transport.permissions.others"))
|
||||||
builder.add(otherBox).xy(5, 3)
|
builder.add(otherBox).xy(5, 3)
|
||||||
|
|
||||||
|
builder.add(includeSubFolder).xyw(1, 5, 5)
|
||||||
|
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +107,10 @@ class PosixFilePermissionDialog(
|
|||||||
super.doCancelAction()
|
super.doCancelAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isIncludeSubdirectories(): Boolean {
|
||||||
|
return includeSubFolder.isSelected
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return 返回空表示取消了
|
* @return 返回空表示取消了
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -56,6 +56,16 @@ class SFTPAction : AnAction("SFTP", Icons.folder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val host = hostManager.getHost(hostId) ?: return
|
val host = hostManager.getHost(hostId) ?: return
|
||||||
|
for (i in 0 until tabbed.tabCount) {
|
||||||
|
val c = tabbed.getComponentAt(i)
|
||||||
|
if (c is SFTPFileSystemViewPanel) {
|
||||||
|
if (c.state == SFTPFileSystemViewPanel.State.Initialized) {
|
||||||
|
c.selectHost(host)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tabbed.addSFTPFileSystemViewPanelTab(host)
|
tabbed.addSFTPFileSystemViewPanelTab(host)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import java.awt.event.MouseAdapter
|
|||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
import javax.swing.event.TreeExpansionEvent
|
||||||
|
import javax.swing.event.TreeExpansionListener
|
||||||
|
|
||||||
class SFTPFileSystemViewPanel(
|
class SFTPFileSystemViewPanel(
|
||||||
var host: Host? = null,
|
var host: Host? = null,
|
||||||
@@ -35,17 +37,18 @@ class SFTPFileSystemViewPanel(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(SFTPFileSystemViewPanel::class.java)
|
private val log = LoggerFactory.getLogger(SFTPFileSystemViewPanel::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
private enum class State {
|
enum class State {
|
||||||
Initialized,
|
Initialized,
|
||||||
Connecting,
|
Connecting,
|
||||||
Connected,
|
Connected,
|
||||||
ConnectFailed,
|
ConnectFailed,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var state = State.Initialized
|
var state = State.Initialized
|
||||||
|
private set
|
||||||
private val cardLayout = CardLayout()
|
private val cardLayout = CardLayout()
|
||||||
private val cardPanel = JPanel(cardLayout)
|
private val cardPanel = JPanel(cardLayout)
|
||||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
@@ -283,12 +286,20 @@ class SFTPFileSystemViewPanel(
|
|||||||
val node = tree.getLastSelectedPathNode() ?: return
|
val node = tree.getLastSelectedPathNode() ?: return
|
||||||
if (node.isFolder) return
|
if (node.isFolder) return
|
||||||
val host = node.data as Host
|
val host = node.data as Host
|
||||||
that.setTabTitle(host.name)
|
selectHost(host)
|
||||||
that.host = host
|
|
||||||
that.connect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
tree.addTreeExpansionListener(object : TreeExpansionListener {
|
||||||
|
override fun treeExpanded(event: TreeExpansionEvent) {
|
||||||
|
properties.putString("SFTPTabbed.Tree.state", TreeUtils.saveExpansionState(tree))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun treeCollapsed(event: TreeExpansionEvent) {
|
||||||
|
properties.putString("SFTPTabbed.Tree.state", TreeUtils.saveExpansionState(tree))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
@@ -305,6 +316,12 @@ class SFTPFileSystemViewPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun selectHost(host: Host) {
|
||||||
|
that.setTabTitle(host.name)
|
||||||
|
that.host = host
|
||||||
|
that.connect()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setTabTitle(title: String) {
|
private fun setTabTitle(title: String) {
|
||||||
val tabbed = SwingUtilities.getAncestorOfClass(JTabbedPane::class.java, that)
|
val tabbed = SwingUtilities.getAncestorOfClass(JTabbedPane::class.java, that)
|
||||||
if (tabbed is JTabbedPane) {
|
if (tabbed is JTabbedPane) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import app.termora.actions.AnAction
|
|||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
import java.awt.Point
|
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
@@ -13,7 +12,6 @@ import javax.swing.JButton
|
|||||||
import javax.swing.JToolBar
|
import javax.swing.JToolBar
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
@Suppress("DuplicatedCode")
|
@Suppress("DuplicatedCode")
|
||||||
class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPane(), Disposable {
|
class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPane(), Disposable {
|
||||||
@@ -43,23 +41,20 @@ class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPan
|
|||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
addBtn.addActionListener(object : AnAction() {
|
addBtn.addActionListener(object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val dialog = NewHostTreeDialog(SwingUtilities.getWindowAncestor(tabbed))
|
for (i in 0 until tabCount) {
|
||||||
dialog.location = Point(
|
val c = getComponentAt(i)
|
||||||
max(0, addBtn.locationOnScreen.x - dialog.width / 2 + addBtn.width / 2),
|
if (c !is SFTPFileSystemViewPanel) continue
|
||||||
addBtn.locationOnScreen.y + max(tabHeight, addBtn.height)
|
if (c.state != SFTPFileSystemViewPanel.State.Initialized) continue
|
||||||
)
|
selectedIndex = i
|
||||||
dialog.setFilter { it.host.protocol == Protocol.SSH }
|
return
|
||||||
dialog.setTreeName("SFTPTabbed.Tree")
|
|
||||||
dialog.allowMulti = true
|
|
||||||
dialog.isVisible = true
|
|
||||||
|
|
||||||
val hosts = dialog.hosts
|
|
||||||
if (hosts.isEmpty()) return
|
|
||||||
|
|
||||||
for (host in hosts) {
|
|
||||||
addSFTPFileSystemViewPanelTab(host)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加一个新的
|
||||||
|
addTab(
|
||||||
|
I18n.getString("termora.transport.sftp.select-host"),
|
||||||
|
SFTPFileSystemViewPanel(transportManager = transportManager)
|
||||||
|
)
|
||||||
|
selectedIndex = tabCount - 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ class SnippetAction private constructor() : AnAction(I18n.getString("termora.sni
|
|||||||
}
|
}
|
||||||
|
|
||||||
const val SNIPPET = "SnippetAction"
|
const val SNIPPET = "SnippetAction"
|
||||||
|
|
||||||
|
// \r \n \t \a \e \b
|
||||||
|
private val SpecialChars = mutableMapOf(
|
||||||
|
'r' to '\r',
|
||||||
|
'n' to '\n',
|
||||||
|
't' to '\t',
|
||||||
|
'a' to ControlCharacters.BEL,
|
||||||
|
'e' to ControlCharacters.ESC,
|
||||||
|
'b' to ControlCharacters.BS
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
@@ -27,31 +37,39 @@ class SnippetAction private constructor() : AnAction(I18n.getString("termora.sni
|
|||||||
|
|
||||||
fun runSnippet(snippet: Snippet, writer: TerminalWriter) {
|
fun runSnippet(snippet: Snippet, writer: TerminalWriter) {
|
||||||
if (snippet.type != SnippetType.Snippet) return
|
if (snippet.type != SnippetType.Snippet) return
|
||||||
val map = mapOf(
|
writer.write(TerminalWriter.WriteRequest.fromBytes(unescape(snippet.snippet).toByteArray(writer.getCharset())))
|
||||||
"\n" to ControlCharacters.LF,
|
}
|
||||||
"\r" to ControlCharacters.CR,
|
|
||||||
"\t" to ControlCharacters.TAB,
|
private fun unescape(text: String): String {
|
||||||
"\b" to ControlCharacters.BS,
|
val chars = text.toCharArray()
|
||||||
"\\a" to ControlCharacters.BEL,
|
val sb = StringBuilder()
|
||||||
"\\e" to ControlCharacters.ESC,
|
|
||||||
)
|
|
||||||
val chars = snippet.snippet.toCharArray()
|
|
||||||
for (i in chars.indices) {
|
for (i in chars.indices) {
|
||||||
val c = chars[i]
|
val c = chars[i]
|
||||||
if (i == 0) continue
|
|
||||||
if (c != '\n') continue
|
// 不是特殊字符不处理
|
||||||
if (chars[i - 1] != '\\') continue
|
if (SpecialChars.containsKey(c).not()) {
|
||||||
// 每一行的最后一个 \ 比较特殊,先转成 null 然后再去 unescapeJava
|
sb.append(c)
|
||||||
chars[i - 1] = Char.Null
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var text = chars.joinToString(StringUtils.EMPTY)
|
// 特殊字符前面不是 `\` 不处理
|
||||||
text = StringEscapeUtils.unescapeJava(text)
|
if (chars.getOrNull(i - 1) != '\\') {
|
||||||
for (e in map.entries) {
|
sb.append(c)
|
||||||
text = text.replace(e.key, e.value.toString())
|
continue
|
||||||
}
|
}
|
||||||
text = snippet.snippet.replace(Char.Null, '\\')
|
|
||||||
|
|
||||||
writer.write(TerminalWriter.WriteRequest.fromBytes(text.toByteArray(writer.getCharset())))
|
// 如果构成的字符串是:\\r 就会生成 \r 字符串,并非转译成:CR
|
||||||
|
if (chars.getOrNull(i - 2) == '\\') {
|
||||||
|
sb.deleteCharAt(sb.length - 1)
|
||||||
|
sb.append(c)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 命中条件之后,那么 sb 最后一个字符肯定是 \
|
||||||
|
sb.deleteCharAt(sb.length - 1)
|
||||||
|
sb.append(SpecialChars.getValue(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,8 +41,10 @@ class SnippetPanel : JPanel(BorderLayout()), Disposable {
|
|||||||
private fun initViews() {
|
private fun initViews() {
|
||||||
val splitPane = JSplitPane()
|
val splitPane = JSplitPane()
|
||||||
splitPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
splitPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||||
|
val scrollPane = JScrollPane(snippetTree)
|
||||||
|
scrollPane.border = BorderFactory.createEmptyBorder()
|
||||||
|
|
||||||
leftPanel.add(snippetTree, BorderLayout.CENTER)
|
leftPanel.add(scrollPane, BorderLayout.CENTER)
|
||||||
leftPanel.border = BorderFactory.createCompoundBorder(
|
leftPanel.border = BorderFactory.createCompoundBorder(
|
||||||
BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor),
|
BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor),
|
||||||
BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
|
|||||||
@@ -360,8 +360,9 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Send Device Attributes (Primary DA).
|
// Send Device Attributes (Primary DA).
|
||||||
'c' -> {
|
'c' -> {
|
||||||
|
sendDeviceAttributes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSI Ps M Delete Ps Line(s) (default = 1) (DL).
|
// CSI Ps M Delete Ps Line(s) (default = 1) (DL).
|
||||||
@@ -505,6 +506,22 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sendDeviceAttributes() {
|
||||||
|
|
||||||
|
assertEventDispatchThread()
|
||||||
|
|
||||||
|
if (!terminalModel.hasData(DataKey.TerminalWriter)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val writer = terminalModel.getData(DataKey.TerminalWriter)
|
||||||
|
|
||||||
|
// VT102_RESPONSE
|
||||||
|
val bytes = "${ControlCharacters.ESC}[?6c".toByteArray(writer.getCharset())
|
||||||
|
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-?-Pm-h.1D0E
|
* https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-?-Pm-h.1D0E
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ interface SelectionModel {
|
|||||||
*/
|
*/
|
||||||
fun setSelection(startPosition: Position, endPosition: Position)
|
fun setSelection(startPosition: Position, endPosition: Position)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置块选中模式
|
||||||
|
*/
|
||||||
|
fun setBlockSelection(block: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否是块选中模式
|
||||||
|
*/
|
||||||
|
fun isBlockSelection(): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取开始选中的位置
|
* 获取开始选中的位置
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import kotlin.math.min
|
|||||||
open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
||||||
private var startPosition = Position.unknown
|
private var startPosition = Position.unknown
|
||||||
private var endPosition = Position.unknown
|
private var endPosition = Position.unknown
|
||||||
|
private var block = false
|
||||||
private val document = terminal.getDocument()
|
private val document = terminal.getDocument()
|
||||||
|
|
||||||
internal companion object {
|
internal companion object {
|
||||||
@@ -67,7 +68,37 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
|||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
val iterator = getChars(getSelectionStartPosition(), getSelectionEndPosition())
|
val start = getSelectionStartPosition()
|
||||||
|
val end = getSelectionEndPosition()
|
||||||
|
|
||||||
|
if (isBlockSelection()) {
|
||||||
|
val left = min(start.x, end.x)
|
||||||
|
val right = max(start.x, end.x)
|
||||||
|
val top = min(start.y, end.y)
|
||||||
|
val bottom = max(start.y, end.y)
|
||||||
|
|
||||||
|
for (lineNum in top..bottom) {
|
||||||
|
val line = document.getLine(lineNum)
|
||||||
|
val chars = line.chars()
|
||||||
|
|
||||||
|
// 块选中要处理超出边界
|
||||||
|
val from = (left - 1).coerceAtLeast(0)
|
||||||
|
val to = right.coerceAtMost(chars.size)
|
||||||
|
|
||||||
|
if (from < to) {
|
||||||
|
val selected = chars.subList(from, to)
|
||||||
|
.filter { !it.first.isNull && !it.first.isSoftHyphen }
|
||||||
|
.joinToString("") { it.first.toString() }
|
||||||
|
sb.append(selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineNum != bottom) {
|
||||||
|
sb.appendLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
val iterator = getChars(start, end)
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
val line = iterator.next()
|
val line = iterator.next()
|
||||||
val chars = line.chars()
|
val chars = line.chars()
|
||||||
@@ -92,6 +123,7 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
|||||||
sb.appendLine()
|
sb.appendLine()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (sb.isNotEmpty() && sb.last() == ControlCharacters.LF) {
|
if (sb.isNotEmpty() && sb.last() == ControlCharacters.LF) {
|
||||||
sb.deleteCharAt(sb.length - 1)
|
sb.deleteCharAt(sb.length - 1)
|
||||||
@@ -171,6 +203,12 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
|||||||
fireSelectionChanged()
|
fireSelectionChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setBlockSelection(block: Boolean) {
|
||||||
|
this.block = block
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isBlockSelection() = block
|
||||||
|
|
||||||
override fun getSelectionStartPosition(): Position {
|
override fun getSelectionStartPosition(): Position {
|
||||||
return startPosition
|
return startPosition
|
||||||
}
|
}
|
||||||
@@ -202,13 +240,20 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun hasSelection(x: Int, y: Int): Boolean {
|
override fun hasSelection(x: Int, y: Int): Boolean {
|
||||||
return hasSelection() && isPointInsideArea(
|
|
||||||
startPosition,
|
if (hasSelection().not()) return false
|
||||||
endPosition,
|
|
||||||
x,
|
// 如果是块选中
|
||||||
y,
|
if (isBlockSelection()) {
|
||||||
terminal.getTerminalModel().getCols()
|
val left = min(startPosition.x, endPosition.x)
|
||||||
)
|
val right = max(startPosition.x, endPosition.x)
|
||||||
|
val top = min(startPosition.y, endPosition.y)
|
||||||
|
val bottom = max(startPosition.y, endPosition.y)
|
||||||
|
|
||||||
|
return x in left..right && y in top..bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
return isPointInsideArea(startPosition, endPosition, x, y, terminal.getTerminalModel().getCols())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app.termora.terminal.panel
|
|||||||
|
|
||||||
import app.termora.Application
|
import app.termora.Application
|
||||||
import app.termora.ApplicationScope
|
import app.termora.ApplicationScope
|
||||||
|
import app.termora.Database
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import java.awt.Graphics
|
import java.awt.Graphics
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@@ -16,6 +17,7 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val regex = Regex("https?://\\S*[^.\\s'\",()<>\\[\\]]")
|
private val regex = Regex("https?://\\S*[^.\\s'\",()<>\\[\\]]")
|
||||||
|
private val isEnableHyperlink get() = Database.getDatabase().terminal.hyperlink
|
||||||
|
|
||||||
override fun before(
|
override fun before(
|
||||||
offset: Int,
|
offset: Int,
|
||||||
@@ -25,6 +27,9 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
|
|||||||
terminalDisplay: TerminalDisplay,
|
terminalDisplay: TerminalDisplay,
|
||||||
terminal: Terminal
|
terminal: Terminal
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
if (isEnableHyperlink.not()) return
|
||||||
|
|
||||||
val document = terminal.getDocument()
|
val document = terminal.getDocument()
|
||||||
var startOffset = offset
|
var startOffset = offset
|
||||||
var endOffset = startOffset + count
|
var endOffset = startOffset + count
|
||||||
@@ -91,4 +96,18 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun after(
|
||||||
|
offset: Int,
|
||||||
|
count: Int,
|
||||||
|
g: Graphics,
|
||||||
|
terminalPanel: TerminalPanel,
|
||||||
|
terminalDisplay: TerminalDisplay,
|
||||||
|
terminal: Terminal
|
||||||
|
) {
|
||||||
|
if (isEnableHyperlink.not()) {
|
||||||
|
// 删除之前的
|
||||||
|
terminal.getMarkupModel().removeAllHighlighters(Highlighter.HYPERLINK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -134,6 +134,8 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
|
|||||||
// 如果不判断的话可能会导致移动了一点点就就进入选择状态了
|
// 如果不判断的话可能会导致移动了一点点就就进入选择状态了
|
||||||
val diff = terminalPanel.getAverageCharWidth() / 5.0
|
val diff = terminalPanel.getAverageCharWidth() / 5.0
|
||||||
if (abs(mousePressedPoint.y - e.y) >= diff || abs(mousePressedPoint.x - e.x) >= diff) {
|
if (abs(mousePressedPoint.y - e.y) >= diff || abs(mousePressedPoint.x - e.x) >= diff) {
|
||||||
|
// 设置选中模式
|
||||||
|
terminal.getSelectionModel().setBlockSelection(isOnlyAltDown(e))
|
||||||
beginSelect(
|
beginSelect(
|
||||||
Position(x = mousePressedPoint.x, y = mousePressedPoint.y),
|
Position(x = mousePressedPoint.x, y = mousePressedPoint.y),
|
||||||
)
|
)
|
||||||
@@ -141,6 +143,13 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isOnlyAltDown(e: MouseEvent): Boolean {
|
||||||
|
return e.isAltDown &&
|
||||||
|
e.isMetaDown.not() &&
|
||||||
|
e.isControlDown.not() &&
|
||||||
|
e.isShiftDown.not() &&
|
||||||
|
e.isAltGraphDown.not()
|
||||||
|
}
|
||||||
|
|
||||||
private fun beginSelect(position: Position) {
|
private fun beginSelect(position: Position) {
|
||||||
|
|
||||||
|
|||||||
88
src/main/kotlin/app/termora/vfs2/VFSWalker.kt
Normal file
88
src/main/kotlin/app/termora/vfs2/VFSWalker.kt
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package app.termora.vfs2
|
||||||
|
|
||||||
|
import org.apache.commons.vfs2.FileObject
|
||||||
|
import java.nio.file.FileVisitResult
|
||||||
|
import java.nio.file.FileVisitor
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes
|
||||||
|
import java.nio.file.attribute.FileTime
|
||||||
|
|
||||||
|
object VFSWalker {
|
||||||
|
fun walk(
|
||||||
|
dir: FileObject,
|
||||||
|
visitor: FileVisitor<FileObject>,
|
||||||
|
): FileVisitResult {
|
||||||
|
|
||||||
|
// clear cache
|
||||||
|
if (visitor.preVisitDirectory(dir, EmptyBasicFileAttributes.INSTANCE) == FileVisitResult.TERMINATE) {
|
||||||
|
return FileVisitResult.TERMINATE
|
||||||
|
}
|
||||||
|
|
||||||
|
for (e in dir.children) {
|
||||||
|
if (e.name.baseName == ".." || e.name.baseName == ".") continue
|
||||||
|
if (e.isFolder) {
|
||||||
|
if (walk(dir.resolveFile(e.name.baseName), visitor) == FileVisitResult.TERMINATE) {
|
||||||
|
return FileVisitResult.TERMINATE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val result = visitor.visitFile(
|
||||||
|
dir.resolveFile(e.name.baseName),
|
||||||
|
EmptyBasicFileAttributes.INSTANCE
|
||||||
|
)
|
||||||
|
if (result == FileVisitResult.TERMINATE) {
|
||||||
|
return FileVisitResult.TERMINATE
|
||||||
|
} else if (result == FileVisitResult.SKIP_SUBTREE) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visitor.postVisitDirectory(dir, null) == FileVisitResult.TERMINATE) {
|
||||||
|
return FileVisitResult.TERMINATE
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
|
||||||
|
private class EmptyBasicFileAttributes : BasicFileAttributes {
|
||||||
|
companion object {
|
||||||
|
val INSTANCE = EmptyBasicFileAttributes()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun lastModifiedTime(): FileTime {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun lastAccessTime(): FileTime {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun creationTime(): FileTime {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isRegularFile(): Boolean {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isDirectory(): Boolean {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isSymbolicLink(): Boolean {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isOther(): Boolean {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun size(): Long {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fileKey(): Any {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,6 +57,7 @@ termora.settings.appearance.follow-system=Sync with OS
|
|||||||
termora.settings.appearance.opacity=Opacity
|
termora.settings.appearance.opacity=Opacity
|
||||||
termora.settings.appearance.background-image=BG Image
|
termora.settings.appearance.background-image=BG Image
|
||||||
termora.settings.appearance.background-running=Backgrounding
|
termora.settings.appearance.background-running=Backgrounding
|
||||||
|
termora.settings.appearance.confirm-tab-close=Confirm tab close
|
||||||
|
|
||||||
termora.setting.security=Security
|
termora.setting.security=Security
|
||||||
termora.setting.security.enter-password=Enter password
|
termora.setting.security.enter-password=Enter password
|
||||||
@@ -73,6 +74,7 @@ termora.settings.terminal.size=Size
|
|||||||
termora.settings.terminal.max-rows=Max rows
|
termora.settings.terminal.max-rows=Max rows
|
||||||
termora.settings.terminal.debug=Debug mode
|
termora.settings.terminal.debug=Debug mode
|
||||||
termora.settings.terminal.beep=Beep
|
termora.settings.terminal.beep=Beep
|
||||||
|
termora.settings.terminal.hyperlink=Hyperlink
|
||||||
termora.settings.terminal.select-copy=Select copy
|
termora.settings.terminal.select-copy=Select copy
|
||||||
termora.settings.terminal.cursor-style=Cursor type
|
termora.settings.terminal.cursor-style=Cursor type
|
||||||
termora.settings.terminal.cursor-blink=Cursor blink
|
termora.settings.terminal.cursor-blink=Cursor blink
|
||||||
@@ -232,6 +234,7 @@ termora.tabbed.contextmenu.close-other-tabs=Close Other Tabs
|
|||||||
termora.tabbed.contextmenu.close-all-tabs=Close All Tabs
|
termora.tabbed.contextmenu.close-all-tabs=Close All Tabs
|
||||||
termora.tabbed.contextmenu.reconnect=Reconnect
|
termora.tabbed.contextmenu.reconnect=Reconnect
|
||||||
termora.tabbed.local-tab.close-prompt=Do you want to terminal a running process in this terminal?
|
termora.tabbed.local-tab.close-prompt=Do you want to terminal a running process in this terminal?
|
||||||
|
termora.tabbed.tab.close-prompt=Are you sure you want to close this tab?
|
||||||
|
|
||||||
# Terminal logger
|
# Terminal logger
|
||||||
termora.terminal-logger=Terminal Logger
|
termora.terminal-logger=Terminal Logger
|
||||||
@@ -309,6 +312,7 @@ termora.transport.permissions.execute=Execute
|
|||||||
termora.transport.permissions.owner=Owner
|
termora.transport.permissions.owner=Owner
|
||||||
termora.transport.permissions.group=Group
|
termora.transport.permissions.group=Group
|
||||||
termora.transport.permissions.others=Others
|
termora.transport.permissions.others=Others
|
||||||
|
termora.transport.permissions.include-subfolder=Include subdirectories
|
||||||
|
|
||||||
termora.transport.sftp.retry=Retry
|
termora.transport.sftp.retry=Retry
|
||||||
termora.transport.sftp.select-another-host=Select another host
|
termora.transport.sftp.select-another-host=Select another host
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ termora.settings.appearance.follow-system=跟随系统
|
|||||||
termora.settings.appearance.opacity=透明度
|
termora.settings.appearance.opacity=透明度
|
||||||
termora.settings.appearance.background-image=背景图
|
termora.settings.appearance.background-image=背景图
|
||||||
termora.settings.appearance.background-running=后台运行
|
termora.settings.appearance.background-running=后台运行
|
||||||
|
termora.settings.appearance.confirm-tab-close=标签关闭前确认
|
||||||
|
|
||||||
termora.setting.security=安全
|
termora.setting.security=安全
|
||||||
termora.setting.security.enter-password=请输入密码
|
termora.setting.security.enter-password=请输入密码
|
||||||
@@ -77,6 +78,7 @@ termora.settings.terminal.size=大小
|
|||||||
termora.settings.terminal.max-rows=最大行数
|
termora.settings.terminal.max-rows=最大行数
|
||||||
termora.settings.terminal.debug=调试模式
|
termora.settings.terminal.debug=调试模式
|
||||||
termora.settings.terminal.beep=蜂鸣声
|
termora.settings.terminal.beep=蜂鸣声
|
||||||
|
termora.settings.terminal.hyperlink=超链接
|
||||||
termora.settings.terminal.select-copy=选中复制
|
termora.settings.terminal.select-copy=选中复制
|
||||||
termora.settings.terminal.cursor-style=光标样式
|
termora.settings.terminal.cursor-style=光标样式
|
||||||
termora.settings.terminal.cursor-blink=光标闪烁
|
termora.settings.terminal.cursor-blink=光标闪烁
|
||||||
@@ -221,6 +223,7 @@ termora.tabbed.contextmenu.close-other-tabs=关闭其他标签页
|
|||||||
termora.tabbed.contextmenu.close-all-tabs=关闭所有标签页
|
termora.tabbed.contextmenu.close-all-tabs=关闭所有标签页
|
||||||
termora.tabbed.contextmenu.reconnect=重新连接
|
termora.tabbed.contextmenu.reconnect=重新连接
|
||||||
termora.tabbed.local-tab.close-prompt=你想要终止这个终端中正在运行的进程吗?
|
termora.tabbed.local-tab.close-prompt=你想要终止这个终端中正在运行的进程吗?
|
||||||
|
termora.tabbed.tab.close-prompt=你确定要关闭这个标签页吗?
|
||||||
|
|
||||||
|
|
||||||
# Terminal logger
|
# Terminal logger
|
||||||
@@ -322,6 +325,7 @@ termora.transport.permissions.execute=执行
|
|||||||
termora.transport.permissions.owner=所有者
|
termora.transport.permissions.owner=所有者
|
||||||
termora.transport.permissions.group=组
|
termora.transport.permissions.group=组
|
||||||
termora.transport.permissions.others=其他
|
termora.transport.permissions.others=其他
|
||||||
|
termora.transport.permissions.include-subfolder=包含子目录
|
||||||
|
|
||||||
# transport job
|
# transport job
|
||||||
termora.transport.jobs.table.name=名称
|
termora.transport.jobs.table.name=名称
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ termora.settings.appearance.follow-system=跟隨系統
|
|||||||
termora.settings.appearance.opacity=透明度
|
termora.settings.appearance.opacity=透明度
|
||||||
termora.settings.appearance.background-image=背景圖
|
termora.settings.appearance.background-image=背景圖
|
||||||
termora.settings.appearance.background-running=後台運行
|
termora.settings.appearance.background-running=後台運行
|
||||||
|
termora.settings.appearance.confirm-tab-close=關閉分頁確認
|
||||||
|
|
||||||
termora.setting.security=安全
|
termora.setting.security=安全
|
||||||
termora.setting.security.enter-password=請輸入密碼
|
termora.setting.security.enter-password=請輸入密碼
|
||||||
@@ -88,7 +89,8 @@ termora.settings.terminal.font=字體
|
|||||||
termora.settings.terminal.size=大小
|
termora.settings.terminal.size=大小
|
||||||
termora.settings.terminal.max-rows=最大行數
|
termora.settings.terminal.max-rows=最大行數
|
||||||
termora.settings.terminal.debug=偵錯模式
|
termora.settings.terminal.debug=偵錯模式
|
||||||
termora.settings.terminal.beep=蜂鳴聲
|
termora.settings.terminal.beep=超連結
|
||||||
|
termora.settings.terminal.hyperlink=Hyperlink
|
||||||
termora.settings.terminal.select-copy=選取複製
|
termora.settings.terminal.select-copy=選取複製
|
||||||
termora.settings.terminal.cursor-style=遊標風格
|
termora.settings.terminal.cursor-style=遊標風格
|
||||||
termora.settings.terminal.cursor-blink=遊標閃爍
|
termora.settings.terminal.cursor-blink=遊標閃爍
|
||||||
@@ -217,7 +219,7 @@ termora.tabbed.contextmenu.close-other-tabs=關閉其他標籤頁
|
|||||||
termora.tabbed.contextmenu.close-all-tabs=關閉所有標籤
|
termora.tabbed.contextmenu.close-all-tabs=關閉所有標籤
|
||||||
termora.tabbed.contextmenu.reconnect=重新連接
|
termora.tabbed.contextmenu.reconnect=重新連接
|
||||||
termora.tabbed.local-tab.close-prompt=你想要終止這個終端機中正在運作的進程嗎?
|
termora.tabbed.local-tab.close-prompt=你想要終止這個終端機中正在運作的進程嗎?
|
||||||
|
termora.tabbed.tab.close-prompt=你確定要關閉這個分頁嗎?
|
||||||
|
|
||||||
|
|
||||||
# Terminal logger
|
# Terminal logger
|
||||||
@@ -305,6 +307,17 @@ termora.transport.sftp.already-exists.destination=目標文件
|
|||||||
termora.transport.sftp.already-exists.source=原始檔
|
termora.transport.sftp.already-exists.source=原始檔
|
||||||
termora.transport.sftp.already-exists.actions=操作
|
termora.transport.sftp.already-exists.actions=操作
|
||||||
|
|
||||||
|
# permissions
|
||||||
|
termora.transport.permissions=更改權限
|
||||||
|
termora.transport.permissions.file-folder-permissions=檔案/資料夾權限
|
||||||
|
termora.transport.permissions.read=讀取
|
||||||
|
termora.transport.permissions.write=寫入
|
||||||
|
termora.transport.permissions.execute=執行
|
||||||
|
termora.transport.permissions.owner=所有者
|
||||||
|
termora.transport.permissions.group=群組
|
||||||
|
termora.transport.permissions.others=其他
|
||||||
|
termora.transport.permissions.include-subfolder=包含子目錄
|
||||||
|
|
||||||
# transport job
|
# transport job
|
||||||
termora.transport.jobs.table.name=名稱
|
termora.transport.jobs.table.name=名稱
|
||||||
termora.transport.jobs.table.status=狀態
|
termora.transport.jobs.table.status=狀態
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ Source: "{#MySourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdir
|
|||||||
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||||
|
|
||||||
|
[InstallDelete]
|
||||||
|
Type: files; Name: "{app}\app\*.jar"
|
||||||
|
Type: filesandordirs; Name: "{app}\runtime\*"
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall; Check: ShouldPromptStart
|
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall; Check: ShouldPromptStart
|
||||||
Filename: "{app}\{#MyAppExeName}"; Flags: nowait runhidden; Check: ShouldAutoStart
|
Filename: "{app}\{#MyAppExeName}"; Flags: nowait runhidden; Check: ShouldAutoStart
|
||||||
|
|||||||
29
src/test/resources/issue-564/Dockerfile
Normal file
29
src/test/resources/issue-564/Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
FROM debian:bookworm-slim
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
# 安装基础包 + sshd + nvim 依赖
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
openssh-server curl ca-certificates tzdata git unzip \
|
||||||
|
libfuse2 locales && \
|
||||||
|
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
# 安装 nvim 最新版(AppImage 提取)
|
||||||
|
RUN curl -LO https://github.com/neovim/neovim/releases/download/v0.11.1/nvim-linux-arm64.appimage && \
|
||||||
|
mv nvim-linux-arm64.appimage nvim.appimage && chmod u+x nvim.appimage && ./nvim.appimage --appimage-extract && \
|
||||||
|
mv squashfs-root/usr/bin/nvim /usr/local/bin/nvim && \
|
||||||
|
rm -rf squashfs-root nvim.appimage
|
||||||
|
# 配置 SSH
|
||||||
|
RUN mkdir /var/run/sshd && \
|
||||||
|
echo 'root:root' | chpasswd && \
|
||||||
|
echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config && \
|
||||||
|
echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config
|
||||||
|
# 设置语言环境(可选)
|
||||||
|
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \
|
||||||
|
apt-get update && apt-get install -y locales && \
|
||||||
|
locale-gen en_US.UTF-8 && \
|
||||||
|
update-locale LANG=en_US.UTF-8
|
||||||
|
ENV LANG=en_US.UTF-8 \
|
||||||
|
LANGUAGE=en_US:en \
|
||||||
|
LC_ALL=en_US.UTF-8
|
||||||
|
# 启动 SSHD
|
||||||
|
EXPOSE 22
|
||||||
|
CMD ["/usr/sbin/sshd", "-D"]
|
||||||
Reference in New Issue
Block a user