mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 10:22:58 +08:00
Compare commits
33 Commits
2.0.0-beta
...
2.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0020fede1 | ||
|
|
6f1eaab456 | ||
|
|
6173eae772 | ||
|
|
0bb366b1f7 | ||
|
|
9a4d6f7f4d | ||
|
|
a4ae11e301 | ||
|
|
5af0acb619 | ||
|
|
042434b8f8 | ||
|
|
eddc7ef0c6 | ||
|
|
c96ca2d424 | ||
|
|
45be9008fd | ||
|
|
057da4e297 | ||
|
|
e4e41667ff | ||
|
|
95ca0a4af7 | ||
|
|
702dee7983 | ||
|
|
165d544448 | ||
|
|
ecf61bedc4 | ||
|
|
66a81a5da3 | ||
|
|
574c816ebb | ||
|
|
e7cafb74e4 | ||
|
|
5050aa37f5 | ||
|
|
53d3d96a06 | ||
|
|
d40b8a4c9c | ||
|
|
728671509c | ||
|
|
b7178a30fb | ||
|
|
939d6a1fd7 | ||
|
|
2986a9cc46 | ||
|
|
f36afaf5d3 | ||
|
|
8cec835583 | ||
|
|
a32838dad6 | ||
|
|
d54671757e | ||
|
|
d1dba56bcd | ||
|
|
919c06779d |
4
.github/workflows/osx-aarch64.yml
vendored
4
.github/workflows/osx-aarch64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install the Apple certificate
|
- name: Install the Apple certificate
|
||||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora' && env.BUILD_CERTIFICATE_BASE64 != ''
|
||||||
env:
|
env:
|
||||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
|
||||||
- name: Setup the Notary information
|
- name: Setup the Notary information
|
||||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.APPLE_ID != ''"
|
||||||
env:
|
env:
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
|||||||
4
.github/workflows/osx-x86-64.yml
vendored
4
.github/workflows/osx-x86-64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install the Apple certificate
|
- name: Install the Apple certificate
|
||||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora' && env.BUILD_CERTIFICATE_BASE64 != ''
|
||||||
env:
|
env:
|
||||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
|
||||||
- name: Setup the Notary information
|
- name: Setup the Notary information
|
||||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.APPLE_ID != ''"
|
||||||
env:
|
env:
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
|||||||
@@ -62,9 +62,6 @@ dependencies {
|
|||||||
testImplementation(libs.h2)
|
testImplementation(libs.h2)
|
||||||
testImplementation(libs.exposed.migration)
|
testImplementation(libs.exposed.migration)
|
||||||
|
|
||||||
// implementation(platform(libs.koin.bom))
|
|
||||||
// implementation(libs.koin.core)
|
|
||||||
|
|
||||||
api(kotlin("reflect"))
|
api(kotlin("reflect"))
|
||||||
api(libs.slf4j.api)
|
api(libs.slf4j.api)
|
||||||
api(libs.pty4j)
|
api(libs.pty4j)
|
||||||
@@ -105,7 +102,6 @@ dependencies {
|
|||||||
|
|
||||||
api(libs.colorpicker)
|
api(libs.colorpicker)
|
||||||
api(libs.mixpanel)
|
api(libs.mixpanel)
|
||||||
api(libs.jSerialComm)
|
|
||||||
api(libs.ini4j)
|
api(libs.ini4j)
|
||||||
api(libs.restart4j)
|
api(libs.restart4j)
|
||||||
api(libs.exposed.core)
|
api(libs.exposed.core)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ slf4j = "2.0.17"
|
|||||||
pty4j = "0.13.6"
|
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.1-SNAPSHOT"
|
||||||
kotlinx-serialization-json = "1.9.0"
|
kotlinx-serialization-json = "1.9.0"
|
||||||
commons-codec = "1.18.0"
|
commons-codec = "1.18.0"
|
||||||
commons-lang3 = "3.17.0"
|
commons-lang3 = "3.17.0"
|
||||||
@@ -22,9 +22,9 @@ jna = "5.17.0"
|
|||||||
jSystemThemeDetector = "3.9.1"
|
jSystemThemeDetector = "3.9.1"
|
||||||
commons-io = "2.19.0"
|
commons-io = "2.19.0"
|
||||||
jbr-api = "17.1.10.1"
|
jbr-api = "17.1.10.1"
|
||||||
hutool = "5.8.37"
|
hutool = "5.8.39"
|
||||||
jsch = "0.2.26"
|
jsch = "2.27.2"
|
||||||
okhttp = "4.12.0"
|
okhttp = "5.1.0"
|
||||||
sshj = "0.39.0"
|
sshj = "0.39.0"
|
||||||
sshd-core = "2.15.0"
|
sshd-core = "2.15.0"
|
||||||
jgit = "7.2.0.202503040940-r"
|
jgit = "7.2.0.202503040940-r"
|
||||||
@@ -46,7 +46,7 @@ h2 = "2.3.232"
|
|||||||
sqlite = "3.50.2.0"
|
sqlite = "3.50.2.0"
|
||||||
jug = "5.1.0"
|
jug = "5.1.0"
|
||||||
semver4j = "6.0.0"
|
semver4j = "6.0.0"
|
||||||
jsvg = "1.4.0"
|
jsvg = "2.0.0"
|
||||||
dom4j = "2.2.0"
|
dom4j = "2.2.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
project.version = "0.0.4"
|
project.version = "0.0.5"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.termora.plugins.bg
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.database.DatabaseManager
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
@@ -96,9 +95,7 @@ internal class BackgroundManager private constructor() : Disposable, GlassPaneAw
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val body = response.body
|
val body = response.body
|
||||||
if (body != null) {
|
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
|
||||||
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
|
|
||||||
}
|
|
||||||
IOUtils.closeQuietly(body)
|
IOUtils.closeQuietly(body)
|
||||||
return@use tempFile
|
return@use tempFile
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
project.version = "0.0.1"
|
project.version = "0.0.1"
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
compileOnly(project(":"))
|
compileOnly(project(":"))
|
||||||
|
implementation("org.apache.commons:commons-pool2:2.12.1")
|
||||||
|
testImplementation(project(":"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
package app.termora.plugins.ftp
|
|
||||||
|
|
||||||
import org.apache.commons.vfs2.Capability
|
|
||||||
import org.apache.commons.vfs2.FileName
|
|
||||||
import org.apache.commons.vfs2.FileSystem
|
|
||||||
import org.apache.commons.vfs2.FileSystemOptions
|
|
||||||
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
|
|
||||||
|
|
||||||
class FTPFileProvider private constructor() : AbstractOriginatingFileProvider() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val instance by lazy { FTPFileProvider() }
|
|
||||||
val capabilities = listOf(
|
|
||||||
Capability.CREATE,
|
|
||||||
Capability.DELETE,
|
|
||||||
Capability.RENAME,
|
|
||||||
Capability.GET_TYPE,
|
|
||||||
Capability.LIST_CHILDREN,
|
|
||||||
Capability.READ_CONTENT,
|
|
||||||
Capability.URI,
|
|
||||||
Capability.WRITE_CONTENT,
|
|
||||||
Capability.GET_LAST_MODIFIED,
|
|
||||||
Capability.SET_LAST_MODIFIED_FILE,
|
|
||||||
Capability.RANDOM_ACCESS_READ,
|
|
||||||
Capability.APPEND_CONTENT
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCapabilities(): Collection<Capability> {
|
|
||||||
return FTPFileProvider.capabilities
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun doCreateFileSystem(
|
|
||||||
rootFileName: FileName,
|
|
||||||
fileSystemOptions: FileSystemOptions
|
|
||||||
): FileSystem? {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3FileSystem
|
||||||
|
import app.termora.transfer.s3.S3Path
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.net.ftp.FTPClient
|
||||||
|
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||||
|
|
||||||
|
class FTPFileSystem(private val pool: GenericObjectPool<FTPClient>) : S3FileSystem(FTPSystemProvider(pool)) {
|
||||||
|
|
||||||
|
override fun create(root: String?, names: List<String>): S3Path {
|
||||||
|
val path = FTPPath(this, root, names)
|
||||||
|
if (names.isEmpty()) {
|
||||||
|
path.attributes = path.attributes.copy(directory = true)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
IOUtils.closeQuietly(pool)
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.plugin.internal.BasicProxyOption
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.KeyboardFocusManager
|
||||||
|
import java.awt.event.ComponentAdapter
|
||||||
|
import java.awt.event.ComponentEvent
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
class FTPHostOptionsPane : OptionsPane() {
|
||||||
|
private val generalOption = GeneralOption()
|
||||||
|
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
|
||||||
|
private val sftpOption = SFTPOption()
|
||||||
|
|
||||||
|
init {
|
||||||
|
addOption(generalOption)
|
||||||
|
addOption(proxyOption)
|
||||||
|
addOption(sftpOption)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHost(): Host {
|
||||||
|
val name = generalOption.nameTextField.text
|
||||||
|
val protocol = FTPProtocolProvider.PROTOCOL
|
||||||
|
val port = generalOption.portTextField.value as Int
|
||||||
|
var authentication = Authentication.Companion.No
|
||||||
|
var proxy = Proxy.Companion.No
|
||||||
|
val authenticationType = AuthenticationType.Password
|
||||||
|
|
||||||
|
authentication = authentication.copy(
|
||||||
|
type = authenticationType,
|
||||||
|
password = String(generalOption.passwordTextField.password)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||||
|
proxy = proxy.copy(
|
||||||
|
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
|
||||||
|
host = proxyOption.proxyHostTextField.text,
|
||||||
|
username = proxyOption.proxyUsernameTextField.text,
|
||||||
|
password = String(proxyOption.proxyPasswordTextField.password),
|
||||||
|
port = proxyOption.proxyPortTextField.value as Int,
|
||||||
|
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val options = Options.Default.copy(
|
||||||
|
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||||
|
encoding = sftpOption.charsetComboBox.selectedItem as String,
|
||||||
|
extras = mutableMapOf("passive" to (sftpOption.passiveComboBox.selectedItem as PassiveMode).name)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Host(
|
||||||
|
name = name,
|
||||||
|
protocol = protocol,
|
||||||
|
port = port,
|
||||||
|
host = generalOption.hostTextField.text,
|
||||||
|
username = generalOption.usernameTextField.text,
|
||||||
|
authentication = authentication,
|
||||||
|
proxy = proxy,
|
||||||
|
sort = System.currentTimeMillis(),
|
||||||
|
remark = generalOption.remarkTextArea.text,
|
||||||
|
options = options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHost(host: Host) {
|
||||||
|
generalOption.nameTextField.text = host.name
|
||||||
|
generalOption.usernameTextField.text = host.username
|
||||||
|
generalOption.remarkTextArea.text = host.remark
|
||||||
|
generalOption.passwordTextField.text = host.authentication.password
|
||||||
|
generalOption.hostTextField.text = host.host
|
||||||
|
generalOption.portTextField.value = host.port
|
||||||
|
|
||||||
|
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||||
|
proxyOption.proxyHostTextField.text = host.proxy.host
|
||||||
|
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
||||||
|
proxyOption.proxyUsernameTextField.text = host.proxy.username
|
||||||
|
proxyOption.proxyPortTextField.value = host.proxy.port
|
||||||
|
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
|
||||||
|
|
||||||
|
|
||||||
|
val passive = host.options.extras["passive"] ?: PassiveMode.Local.name
|
||||||
|
sftpOption.charsetComboBox.selectedItem = host.options.encoding
|
||||||
|
sftpOption.passiveComboBox.selectedItem = runCatching { PassiveMode.valueOf(passive) }
|
||||||
|
.getOrNull() ?: PassiveMode.Local
|
||||||
|
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateFields(): Boolean {
|
||||||
|
val host = getHost()
|
||||||
|
|
||||||
|
// general
|
||||||
|
if (validateField(generalOption.nameTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (validateField(generalOption.hostTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(generalOption.usernameTextField.text) || generalOption.passwordTextField.password.isNotEmpty()) {
|
||||||
|
if (validateField(generalOption.usernameTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateField(generalOption.passwordTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxy
|
||||||
|
if (host.proxy.type != ProxyType.No) {
|
||||||
|
if (validateField(proxyOption.proxyHostTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.proxy.authenticationType != AuthenticationType.No) {
|
||||||
|
if (validateField(proxyOption.proxyUsernameTextField)
|
||||||
|
|| validateField(proxyOption.proxyPasswordTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示有错误
|
||||||
|
*/
|
||||||
|
private fun validateField(textField: JTextField): Boolean {
|
||||||
|
if (textField.isEnabled && (if (textField is JPasswordField) textField.password.isEmpty() else textField.text.isBlank())) {
|
||||||
|
setOutlineError(textField)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setOutlineError(c: JComponent) {
|
||||||
|
selectOptionJComponent(c)
|
||||||
|
c.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
|
c.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||||
|
val portTextField = PortSpinner(21)
|
||||||
|
val nameTextField = OutlineTextField(128)
|
||||||
|
val usernameTextField = OutlineTextField(128)
|
||||||
|
val hostTextField = OutlineTextField(255)
|
||||||
|
val passwordTextField = OutlinePasswordField(255)
|
||||||
|
val publicKeyComboBox = OutlineComboBox<String>()
|
||||||
|
val remarkTextArea = FixedLengthTextArea(512)
|
||||||
|
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
|
||||||
|
publicKeyComboBox.isEditable = false
|
||||||
|
|
||||||
|
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = StringUtils.EMPTY
|
||||||
|
if (value is String) {
|
||||||
|
text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
text,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = value?.toString() ?: ""
|
||||||
|
when (value) {
|
||||||
|
AuthenticationType.Password -> {
|
||||||
|
text = "Password"
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationType.PublicKey -> {
|
||||||
|
text = "Public Key"
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationType.KeyboardInteractive -> {
|
||||||
|
text = "Keyboard Interactive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
text,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||||
|
|
||||||
|
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
addComponentListener(object : ComponentAdapter() {
|
||||||
|
override fun componentResized(e: ComponentEvent) {
|
||||||
|
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
|
||||||
|
removeComponentListener(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.settings
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.new-host.general")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
|
||||||
|
remarkTextArea.rows = 8
|
||||||
|
remarkTextArea.lineWrap = true
|
||||||
|
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
|
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
|
||||||
|
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
||||||
|
.add(hostTextField).xy(3, rows)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||||
|
.add(portTextField).xy(7, rows).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
||||||
|
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
|
||||||
|
.add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
||||||
|
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
||||||
|
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
||||||
|
.xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||||
|
val defaultDirectoryField = OutlineTextField(255)
|
||||||
|
val charsetComboBox = JComboBox<String>()
|
||||||
|
val passiveComboBox = JComboBox<PassiveMode>()
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
|
||||||
|
for (e in Charset.availableCharsets()) {
|
||||||
|
charsetComboBox.addItem(e.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
charsetComboBox.selectedItem = "UTF-8"
|
||||||
|
|
||||||
|
passiveComboBox.addItem(PassiveMode.Local)
|
||||||
|
passiveComboBox.addItem(PassiveMode.Remote)
|
||||||
|
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.folder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.transport.sftp")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
|
||||||
|
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${FTPI18n.getString("termora.plugins.ftp.passive")}:").xy(1, rows)
|
||||||
|
.add(passiveComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||||
|
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PassiveMode {
|
||||||
|
Local,
|
||||||
|
Remote,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
import app.termora.NamedI18n
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object FTPI18n : NamedI18n("i18n/messages") {
|
||||||
|
private val log = LoggerFactory.getLogger(FTPI18n::class.java)
|
||||||
|
|
||||||
|
override fun getLogger(): Logger {
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(key: String): String {
|
||||||
|
return try {
|
||||||
|
substitutor.replace(getBundle().getString(key))
|
||||||
|
} catch (_: MissingResourceException) {
|
||||||
|
I18n.getString(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3Path
|
||||||
|
|
||||||
|
class FTPPath(fileSystem: FTPFileSystem, root: String?, names: List<String>) : S3Path(fileSystem, root, names) {
|
||||||
|
override val isBucket: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
|
override val bucketName: String
|
||||||
|
get() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override val objectName: String
|
||||||
|
get() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun getCustomType(): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
package app.termora.plugins.ftp
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
import app.termora.DynamicIcon
|
|
||||||
import app.termora.I18n
|
|
||||||
import app.termora.Icons
|
|
||||||
import app.termora.plugin.Extension
|
import app.termora.plugin.Extension
|
||||||
import app.termora.plugin.ExtensionSupport
|
import app.termora.plugin.ExtensionSupport
|
||||||
import app.termora.plugin.PaidPlugin
|
import app.termora.plugin.PaidPlugin
|
||||||
@@ -27,6 +24,7 @@ class FTPPlugin : PaidPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
return support.getExtensions(clazz)
|
return support.getExtensions(clazz)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,36 @@
|
|||||||
package app.termora.plugins.ftp
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.Disposer
|
||||||
import app.termora.Host
|
import app.termora.Host
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import org.apache.commons.lang3.StringUtils
|
import java.awt.BorderLayout
|
||||||
|
|
||||||
class FTPProtocolHostPanel : ProtocolHostPanel() {
|
class FTPProtocolHostPanel : ProtocolHostPanel() {
|
||||||
|
|
||||||
|
private val pane = FTPHostOptionsPane()
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(pane, BorderLayout.CENTER)
|
||||||
|
Disposer.register(this, pane)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {}
|
||||||
|
|
||||||
override fun getHost(): Host {
|
override fun getHost(): Host {
|
||||||
return Host(
|
return pane.getHost()
|
||||||
name = StringUtils.EMPTY,
|
|
||||||
protocol = FTPProtocolProvider.PROTOCOL
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setHost(host: Host) {
|
override fun setHost(host: Host) {
|
||||||
|
pane.setHost(host)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun validateFields(): Boolean {
|
override fun validateFields(): Boolean {
|
||||||
return true
|
return pane.validateFields()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
package app.termora.plugins.ftp
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
|
||||||
class FTPProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
class FTPProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||||
companion object {
|
companion object {
|
||||||
val instance by lazy { FTPProtocolHostPanelExtension() }
|
val instance = FTPProtocolHostPanelExtension()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getProtocolProvider(): ProtocolProvider {
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
return FTPProtocolProvider.instance
|
return FTPProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return FTPProtocolHostPanel()
|
return FTPProtocolHostPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,33 @@
|
|||||||
package app.termora.plugins.ftp
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.AuthenticationType
|
||||||
import app.termora.DynamicIcon
|
import app.termora.DynamicIcon
|
||||||
import app.termora.Icons
|
import app.termora.Icons
|
||||||
import app.termora.protocol.FileObjectHandler
|
import app.termora.ProxyType
|
||||||
import app.termora.protocol.FileObjectRequest
|
import app.termora.protocol.PathHandler
|
||||||
|
import app.termora.protocol.PathHandlerRequest
|
||||||
import app.termora.protocol.TransferProtocolProvider
|
import app.termora.protocol.TransferProtocolProvider
|
||||||
import org.apache.commons.vfs2.provider.FileProvider
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.net.ftp.FTPClient
|
||||||
|
import org.apache.commons.pool2.BasePooledObjectFactory
|
||||||
|
import org.apache.commons.pool2.PooledObject
|
||||||
|
import org.apache.commons.pool2.impl.DefaultPooledObject
|
||||||
|
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||||
|
import org.apache.commons.pool2.impl.GenericObjectPoolConfig
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
|
||||||
class FTPProtocolProvider private constructor() : TransferProtocolProvider {
|
class FTPProtocolProvider private constructor() : TransferProtocolProvider {
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val instance by lazy { FTPProtocolProvider() }
|
private val log = LoggerFactory.getLogger(FTPProtocolProvider::class.java)
|
||||||
|
|
||||||
|
val instance = FTPProtocolProvider()
|
||||||
const val PROTOCOL = "FTP"
|
const val PROTOCOL = "FTP"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,12 +39,82 @@ class FTPProtocolProvider private constructor() : TransferProtocolProvider {
|
|||||||
return Icons.ftp
|
return Icons.ftp
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFileProvider(): FileProvider {
|
override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
|
||||||
return FTPFileProvider.instance
|
val host = requester.host
|
||||||
|
|
||||||
|
val config = GenericObjectPoolConfig<FTPClient>().apply {
|
||||||
|
maxTotal = 12
|
||||||
|
// 与 transfer 最大传输量匹配
|
||||||
|
maxIdle = 6
|
||||||
|
minIdle = 1
|
||||||
|
testOnBorrow = false
|
||||||
|
testWhileIdle = true
|
||||||
|
// 检测空闲对象线程每次运行时检测的空闲对象的数量
|
||||||
|
timeBetweenEvictionRuns = Duration.ofSeconds(30)
|
||||||
|
// 连接空闲的最小时间,达到此值后空闲链接将会被移除,且保留 minIdle 个空闲连接数
|
||||||
|
softMinEvictableIdleDuration = Duration.ofSeconds(30)
|
||||||
|
// 连接的最小空闲时间,达到此值后该空闲连接可能会被移除(还需看是否已达最大空闲连接数)
|
||||||
|
minEvictableIdleDuration = Duration.ofMinutes(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
val ftpClientPool = GenericObjectPool(object : BasePooledObjectFactory<FTPClient>() {
|
||||||
|
override fun create(): FTPClient {
|
||||||
|
val client = FTPClient()
|
||||||
|
client.charset = Charset.forName(host.options.encoding)
|
||||||
|
client.controlEncoding = client.charset.name()
|
||||||
|
client.connect(host.host, host.port)
|
||||||
|
if (client.isConnected.not()) {
|
||||||
|
throw IllegalStateException("FTP client is not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.proxy.type == ProxyType.HTTP) {
|
||||||
|
client.proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress(host.proxy.host, host.proxy.port))
|
||||||
|
} else if (host.proxy.type == ProxyType.SOCKS5) {
|
||||||
|
client.proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(host.proxy.host, host.proxy.port))
|
||||||
|
}
|
||||||
|
|
||||||
|
val password = if (host.authentication.type == AuthenticationType.Password)
|
||||||
|
host.authentication.password else StringUtils.EMPTY
|
||||||
|
if (client.login(host.username, password).not()) {
|
||||||
|
throw IllegalStateException("Incorrect account or password")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.options.extras["passive"] == FTPHostOptionsPane.PassiveMode.Remote.name) {
|
||||||
|
client.enterRemotePassiveMode()
|
||||||
|
} else {
|
||||||
|
client.enterLocalPassiveMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
client.listHiddenFiles = true
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun wrap(obj: FTPClient): PooledObject<FTPClient> {
|
||||||
|
return DefaultPooledObject(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun validateObject(p: PooledObject<FTPClient>): Boolean {
|
||||||
|
val ftp = p.`object`
|
||||||
|
return ftp.isConnected.not() && ftp.sendNoOp()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun destroyObject(p: PooledObject<FTPClient>) {
|
||||||
|
try {
|
||||||
|
p.`object`.disconnect()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, config)
|
||||||
|
|
||||||
|
val defaultPath = host.options.sftpDefaultDirectory
|
||||||
|
val fs = FTPFileSystem(ftpClientPool)
|
||||||
|
return PathHandler(fs, fs.getPath(defaultPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -5,10 +5,10 @@ import app.termora.protocol.ProtocolProviderExtension
|
|||||||
|
|
||||||
class FTPProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
class FTPProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||||
companion object {
|
companion object {
|
||||||
val instance by lazy { FTPProtocolProviderExtension() }
|
val instance = FTPProtocolProviderExtension()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getProtocolProvider(): ProtocolProvider {
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
return FTPProtocolProvider.Companion.instance
|
return FTPProtocolProvider.instance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3FileSystemProvider
|
||||||
|
import app.termora.transfer.s3.S3Path
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.net.ftp.FTPClient
|
||||||
|
import org.apache.commons.net.ftp.FTPFile
|
||||||
|
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.nio.file.AccessMode
|
||||||
|
import java.nio.file.CopyOption
|
||||||
|
import java.nio.file.NoSuchFileException
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.attribute.FileAttribute
|
||||||
|
import java.nio.file.attribute.PosixFilePermission
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
|
||||||
|
class FTPSystemProvider(private val pool: GenericObjectPool<FTPClient>) : S3FileSystemProvider() {
|
||||||
|
|
||||||
|
|
||||||
|
override fun getScheme(): String? {
|
||||||
|
return "ftp"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOutputStream(path: S3Path): OutputStream {
|
||||||
|
return createStreamer(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInputStream(path: S3Path): InputStream {
|
||||||
|
val ftp = pool.borrowObject()
|
||||||
|
val fs = ftp.retrieveFileStream(path.absolutePathString())
|
||||||
|
return object : InputStream() {
|
||||||
|
override fun read(): Int {
|
||||||
|
return fs.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
IOUtils.closeQuietly(fs)
|
||||||
|
ftp.completePendingCommand()
|
||||||
|
pool.returnObject(ftp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createStreamer(path: S3Path): OutputStream {
|
||||||
|
val ftp = pool.borrowObject()
|
||||||
|
val os = ftp.storeFileStream(path.absolutePathString())
|
||||||
|
return object : OutputStream() {
|
||||||
|
override fun write(b: Int) {
|
||||||
|
os.write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
IOUtils.closeQuietly(os)
|
||||||
|
ftp.completePendingCommand()
|
||||||
|
pool.returnObject(ftp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
|
||||||
|
val paths = mutableListOf<S3Path>()
|
||||||
|
if (path.exists().not()) {
|
||||||
|
throw NoSuchFileException(path.absolutePathString())
|
||||||
|
}
|
||||||
|
|
||||||
|
withFtpClient {
|
||||||
|
val files = it.listFiles(path.absolutePathString())
|
||||||
|
for (file in files) {
|
||||||
|
val p = path.resolve(file.name)
|
||||||
|
p.attributes = p.attributes.copy(
|
||||||
|
directory = file.isDirectory,
|
||||||
|
regularFile = file.isFile,
|
||||||
|
size = file.size,
|
||||||
|
lastModifiedTime = file.timestamp.timeInMillis,
|
||||||
|
)
|
||||||
|
p.attributes.permissions = ftpPermissionsToPosix(file)
|
||||||
|
paths.add(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun ftpPermissionsToPosix(file: FTPFile): Set<PosixFilePermission> {
|
||||||
|
val perms = mutableSetOf<PosixFilePermission>()
|
||||||
|
|
||||||
|
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OWNER_READ)
|
||||||
|
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OWNER_WRITE)
|
||||||
|
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OWNER_EXECUTE)
|
||||||
|
|
||||||
|
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.READ_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.GROUP_READ)
|
||||||
|
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.GROUP_WRITE)
|
||||||
|
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.GROUP_EXECUTE)
|
||||||
|
|
||||||
|
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.READ_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OTHERS_READ)
|
||||||
|
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OTHERS_WRITE)
|
||||||
|
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OTHERS_EXECUTE)
|
||||||
|
|
||||||
|
return perms
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) {
|
||||||
|
withFtpClient { it.mkd(dir.absolutePathString()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun move(source: Path?, target: Path?, vararg options: CopyOption?) {
|
||||||
|
if (source != null && target != null) {
|
||||||
|
withFtpClient {
|
||||||
|
it.rename(source.absolutePathString(), target.absolutePathString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(path: S3Path, isDirectory: Boolean) {
|
||||||
|
withFtpClient {
|
||||||
|
if (isDirectory) {
|
||||||
|
it.rmd(path.absolutePathString())
|
||||||
|
} else {
|
||||||
|
it.deleteFile(path.absolutePathString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
|
||||||
|
withFtpClient {
|
||||||
|
if (it.cwd(path.absolutePathString()) == 250) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (it.listFiles(path.absolutePathString()).isNotEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NoSuchFileException(path.absolutePathString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T> withFtpClient(block: (FTPClient) -> T): T {
|
||||||
|
val client = pool.borrowObject()
|
||||||
|
return try {
|
||||||
|
block(client)
|
||||||
|
} finally {
|
||||||
|
pool.returnObject(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<descriptions>
|
<descriptions>
|
||||||
<description>Connecting to FTP</description>
|
<description>Connecting to FTP</description>
|
||||||
<description language="zh_CN">支持连接到到 FTP</description>
|
<description language="zh_CN">支持连接到 FTP</description>
|
||||||
<description language="zh_TW">支援連接到 FTP</description>
|
<description language="zh_TW">支援連接到 FTP</description>
|
||||||
</descriptions>
|
</descriptions>
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#6C707E"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#6C707E"></path></svg>
|
<svg t="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#6C707E"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#6C707E"></path></svg>
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1 +1 @@
|
|||||||
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#CED0D6"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#CED0D6"></path></svg>
|
<svg t="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#CED0D6"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#CED0D6"></path></svg>
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
1
plugins/ftp/src/main/resources/i18n/messages.properties
Normal file
1
plugins/ftp/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
termora.plugins.ftp.passive=Passive Mode
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
termora.plugins.ftp.passive=被动模式
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
termora.plugins.ftp.passive=被動模式
|
||||||
@@ -2,14 +2,14 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
}
|
}
|
||||||
|
|
||||||
project.version = "0.0.6"
|
project.version = "0.0.7"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
compileOnly(project(":"))
|
compileOnly(project(":"))
|
||||||
implementation("com.maxmind.geoip2:geoip2:4.3.1")
|
implementation("com.maxmind.geoip2:geoip2:4.3.1")
|
||||||
// https://github.com/hstyi/geolite2
|
// https://github.com/hstyi/geolite2
|
||||||
implementation("com.github.hstyi:geolite2:v1.0-202506280327")
|
implementation("com.github.hstyi:geolite2:v1.0-202507040118")
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
project.version = "0.0.2"
|
project.version = "0.0.3"
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
|||||||
controlsVisible = false
|
controlsVisible = false
|
||||||
|
|
||||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
title = I18n.getString("termora.doorman.safe")
|
title = MigrationI18n.getString("termora.doorman.safe")
|
||||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,8 +65,8 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun createCenterPanel(): JComponent {
|
override fun createCenterPanel(): JComponent {
|
||||||
label.text = I18n.getString("termora.doorman.safe")
|
label.text = MigrationI18n.getString("termora.doorman.safe")
|
||||||
tip.text = I18n.getString("termora.doorman.unlock-data")
|
tip.text = MigrationI18n.getString("termora.doorman.unlock-data")
|
||||||
icon.icon = FlatSVGIcon(Icons.role.name, 80, 80)
|
icon.icon = FlatSVGIcon(Icons.role.name, 80, 80)
|
||||||
safeBtn.icon = Icons.unlocked
|
safeBtn.icon = Icons.unlocked
|
||||||
|
|
||||||
@@ -95,24 +95,24 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
|||||||
.add(passwordTextField).xy(2, rows)
|
.add(passwordTextField).xy(2, rows)
|
||||||
.add(safeBtn).xy(4, rows).apply { rows += step }
|
.add(safeBtn).xy(4, rows).apply { rows += step }
|
||||||
.add(tip).xyw(2, rows, 4, "center, fill").apply { rows += step }
|
.add(tip).xyw(2, rows, 4, "center, fill").apply { rows += step }
|
||||||
.add(JXHyperlink(object : AnAction(I18n.getString("termora.doorman.forget-password")) {
|
.add(JXHyperlink(object : AnAction(MigrationI18n.getString("termora.doorman.forget-password")) {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val option = OptionPane.showConfirmDialog(
|
val option = OptionPane.showConfirmDialog(
|
||||||
this@DoormanDialog, I18n.getString("termora.doorman.forget-password-message"),
|
this@DoormanDialog, MigrationI18n.getString("termora.doorman.forget-password-message"),
|
||||||
options = arrayOf(
|
options = arrayOf(
|
||||||
I18n.getString("termora.doorman.have-a-mnemonic"),
|
MigrationI18n.getString("termora.doorman.have-a-mnemonic"),
|
||||||
I18n.getString("termora.doorman.dont-have-a-mnemonic"),
|
MigrationI18n.getString("termora.doorman.dont-have-a-mnemonic"),
|
||||||
),
|
),
|
||||||
optionType = JOptionPane.YES_NO_OPTION,
|
optionType = JOptionPane.YES_NO_OPTION,
|
||||||
messageType = JOptionPane.INFORMATION_MESSAGE,
|
messageType = JOptionPane.INFORMATION_MESSAGE,
|
||||||
initialValue = I18n.getString("termora.doorman.have-a-mnemonic")
|
initialValue = MigrationI18n.getString("termora.doorman.have-a-mnemonic")
|
||||||
)
|
)
|
||||||
if (option == JOptionPane.YES_OPTION) {
|
if (option == JOptionPane.YES_OPTION) {
|
||||||
showMnemonicsDialog()
|
showMnemonicsDialog()
|
||||||
} else if (option == JOptionPane.NO_OPTION) {
|
} else if (option == JOptionPane.NO_OPTION) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
this@DoormanDialog,
|
this@DoormanDialog,
|
||||||
I18n.getString("termora.doorman.delete-data"),
|
MigrationI18n.getString("termora.doorman.delete-data"),
|
||||||
messageType = JOptionPane.WARNING_MESSAGE
|
messageType = JOptionPane.WARNING_MESSAGE
|
||||||
)
|
)
|
||||||
Application.browse(MigrationApplicationRunnerExtension.instance.getDatabaseFile().toURI())
|
Application.browse(MigrationApplicationRunnerExtension.instance.getDatabaseFile().toURI())
|
||||||
@@ -141,7 +141,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
|||||||
log.error(e.message, e)
|
log.error(e.message, e)
|
||||||
}
|
}
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
|
this, MigrationI18n.getString("termora.doorman.mnemonic-data-corrupted"),
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
)
|
)
|
||||||
passwordTextField.outline = "error"
|
passwordTextField.outline = "error"
|
||||||
@@ -166,7 +166,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is PasswordWrongException) {
|
if (e is PasswordWrongException) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
this, I18n.getString("termora.doorman.password-wrong"),
|
this, MigrationI18n.getString("termora.doorman.password-wrong"),
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -197,7 +197,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
|||||||
isModal = true
|
isModal = true
|
||||||
isResizable = true
|
isResizable = true
|
||||||
controlsVisible = false
|
controlsVisible = false
|
||||||
title = I18n.getString("termora.doorman.mnemonic.title")
|
title = MigrationI18n.getString("termora.doorman.mnemonic.title")
|
||||||
init()
|
init()
|
||||||
pack()
|
pack()
|
||||||
size = Dimension(max(size.width, UIManager.getInt("Dialog.width") - 250), size.height)
|
size = Dimension(max(size.width, UIManager.getInt("Dialog.width") - 250), size.height)
|
||||||
@@ -251,7 +251,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
this,
|
this,
|
||||||
I18n.getString("termora.doorman.mnemonic.incorrect"),
|
MigrationI18n.getString("termora.doorman.mnemonic.incorrect"),
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -7,3 +7,18 @@ termora.plugins.migration.message=<html> \
|
|||||||
<h3 align="center">📎 For more information, please see: <a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
<h3 align="center">📎 For more information, please see: <a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||||
</html>
|
</html>
|
||||||
termora.plugins.migration.migrate=Migrate
|
termora.plugins.migration.migrate=Migrate
|
||||||
|
|
||||||
|
# Doorman
|
||||||
|
termora.doorman.safe=Data is encrypted
|
||||||
|
termora.doorman.unlock-data=Enter password to unlock data
|
||||||
|
termora.doorman.password-wrong=Wrong password
|
||||||
|
termora.doorman.forget-password=Forgot password?
|
||||||
|
termora.doorman.delete-data=Delete the data catalog and restart, This will lose all data
|
||||||
|
termora.doorman.forget-password-message=Unlock data with a mnemonic. Without it, data cannot be accessed
|
||||||
|
termora.doorman.have-a-mnemonic=I have a mnemonic
|
||||||
|
termora.doorman.dont-have-a-mnemonic=I don't have a mnemonic
|
||||||
|
termora.doorman.mnemonic-data-corrupted=Unable to decrypt data with the mnemonic, the data maybe corrupted
|
||||||
|
|
||||||
|
termora.doorman.mnemonic.title=Enter 12 mnemonic words
|
||||||
|
termora.doorman.mnemonic.incorrect=Incorrect mnemonic
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
# Doorman
|
||||||
|
termora.doorman.safe=Данные защифрованы
|
||||||
|
termora.doorman.unlock-data=Введите пароль для разблокировки данных
|
||||||
|
termora.doorman.verify-password=Введите пароль для проверки
|
||||||
|
termora.doorman.password-wrong=Неверный пароль
|
||||||
|
termora.doorman.password-correct=Пароль верный
|
||||||
|
termora.doorman.unsafe=Данные не зашифрованы
|
||||||
|
termora.doorman.lock-data=Спрашивать пароль при запуске
|
||||||
|
termora.doorman.forget-password=Забыли пароль?
|
||||||
|
termora.doorman.delete-data=Удалить данные и перезапустить, это приведет к потере всех данных
|
||||||
|
termora.doorman.forget-password-message=Разблокировать данные с помощью мнемоники. Без него доступ к данным невозможен.
|
||||||
|
termora.doorman.have-a-mnemonic=У меня есть мнемоники
|
||||||
|
termora.doorman.dont-have-a-mnemonic=У меня нет мнемоники
|
||||||
|
termora.doorman.mnemonic-data-corrupted=Невозможно расшифровать данные с помощью мнемоники, возможно, данные повреждены.
|
||||||
|
|
||||||
|
termora.doorman.mnemonic.title=Введите 12 слов мнемоники
|
||||||
|
termora.doorman.mnemonic.incorrect=Неверные мнемоники
|
||||||
@@ -7,3 +7,17 @@ termora.plugins.migration.message=<html> \
|
|||||||
<h3 align="center">📎 更多信息请查看:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
<h3 align="center">📎 更多信息请查看:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||||
</html>
|
</html>
|
||||||
termora.plugins.migration.migrate=迁移
|
termora.plugins.migration.migrate=迁移
|
||||||
|
|
||||||
|
# Doorman
|
||||||
|
termora.doorman.safe=数据已加密
|
||||||
|
termora.doorman.unlock-data=输入密码解锁数据
|
||||||
|
termora.doorman.password-wrong=密码错误
|
||||||
|
termora.doorman.forget-password=忘记密码?
|
||||||
|
termora.doorman.delete-data=删除数据目录后重新启动程序,这样会丢失所有数据
|
||||||
|
termora.doorman.forget-password-message=通过助记词解锁数据,没有助记词则无法解锁
|
||||||
|
termora.doorman.have-a-mnemonic=我有助记词
|
||||||
|
termora.doorman.dont-have-a-mnemonic=我没有助记词
|
||||||
|
termora.doorman.mnemonic-data-corrupted=无法从助记词解密数据,数据可能已经损坏
|
||||||
|
|
||||||
|
termora.doorman.mnemonic.title=输入 12 个助记词
|
||||||
|
termora.doorman.mnemonic.incorrect=助记词错误
|
||||||
|
|||||||
@@ -7,3 +7,18 @@ termora.plugins.migration.message=<html> \
|
|||||||
<h3 align="center">📎 更多資訊請參見:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
<h3 align="center">📎 更多資訊請參見:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||||
</html>
|
</html>
|
||||||
termora.plugins.migration.migrate=遷移
|
termora.plugins.migration.migrate=遷移
|
||||||
|
|
||||||
|
|
||||||
|
# Doorman
|
||||||
|
termora.doorman.safe=資料已加密
|
||||||
|
termora.doorman.unlock-data=輸入密碼解鎖資料
|
||||||
|
termora.doorman.password-wrong=密碼錯誤
|
||||||
|
termora.doorman.forget-password=忘記密碼?
|
||||||
|
termora.doorman.delete-data=刪除資料目錄後重新啟動程序,這樣會遺失所有數據
|
||||||
|
termora.doorman.forget-password-message=透過助記詞解鎖數據,沒有助記詞則無法解鎖
|
||||||
|
termora.doorman.have-a-mnemonic=我有助記詞
|
||||||
|
termora.doorman.dont-have-a-mnemonic=我沒有助記詞
|
||||||
|
termora.doorman.mnemonic-data-corrupted=無法從助記詞解密數據,資料可能已損壞
|
||||||
|
termora.doorman.mnemonic.title=輸入 12 個助記詞
|
||||||
|
termora.doorman.mnemonic.incorrect=助記詞錯誤
|
||||||
|
|
||||||
|
|||||||
17
plugins/serial/build.gradle.kts
Normal file
17
plugins/serial/build.gradle.kts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
project.version = "0.0.1"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
compileOnly(project(":"))
|
||||||
|
implementation("com.fazecast:jSerialComm:2.11.2")
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package app.termora.plugin.internal.serial
|
package app.termora.plugins.serial
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.plugin.internal.BasicGeneralOption
|
import app.termora.plugin.internal.BasicGeneralOption
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package app.termora.plugins.serial
|
||||||
|
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.ExtensionSupport
|
||||||
|
import app.termora.plugin.Plugin
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
internal class SerialPlugin : Plugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(ProtocolProviderExtension::class.java) { SerialProtocolProviderExtension.instance }
|
||||||
|
support.addExtension(ProtocolHostPanelExtension::class.java) { SerialProtocolHostPanelExtension.instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "Serial Comm"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package app.termora
|
package app.termora.plugins.serial
|
||||||
|
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
import com.fazecast.jSerialComm.SerialPort
|
import com.fazecast.jSerialComm.SerialPort
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package app.termora.plugin.internal.serial
|
package app.termora.plugins.serial
|
||||||
|
|
||||||
import app.termora.Disposer
|
import app.termora.Disposer
|
||||||
import app.termora.Host
|
import app.termora.Host
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package app.termora.plugin.internal.serial
|
package app.termora.plugins.serial
|
||||||
|
|
||||||
import app.termora.account.AccountOwner
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
@@ -19,4 +19,7 @@ internal class SerialProtocolHostPanelExtension private constructor() : Protocol
|
|||||||
return SerialProtocolHostPanel()
|
return SerialProtocolHostPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 5
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package app.termora.plugin.internal.serial
|
package app.termora.plugins.serial
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package app.termora.plugin.internal.serial
|
package app.termora.plugins.serial
|
||||||
|
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
import app.termora.protocol.ProtocolProviderExtension
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package app.termora.plugin.internal.serial
|
package app.termora.plugins.serial
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.Host
|
||||||
|
import app.termora.Icons
|
||||||
|
import app.termora.PtyHostTerminalTab
|
||||||
|
import app.termora.WindowScope
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
import org.apache.commons.io.Charsets
|
import org.apache.commons.io.Charsets
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
package app.termora
|
package app.termora.plugins.serial
|
||||||
|
|
||||||
|
import app.termora.Host
|
||||||
|
import app.termora.SerialCommFlowControl
|
||||||
|
import app.termora.SerialCommParity
|
||||||
import com.fazecast.jSerialComm.SerialPort
|
import com.fazecast.jSerialComm.SerialPort
|
||||||
|
|
||||||
object Serials {
|
object Serials {
|
||||||
22
plugins/serial/src/main/resources/META-INF/plugin.xml
Normal file
22
plugins/serial/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>serial</id>
|
||||||
|
|
||||||
|
<name>Serial Comm</name>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.serial.SerialPlugin</entry>
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Supports access to serial ports</description>
|
||||||
|
<description language="zh_CN">支持访问串口</description>
|
||||||
|
<description language="zh_TW">支援訪問串口</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1747210120200" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1169" width="16" height="16"><path d="M806.11718723 531.44140652l-78.2578125 78.50390571L410.22265625 291.21874973l82.08984402-78.53906223a231.46874973 231.46874973 0 0 1 162.49218723-68.66015625 206.54296902 206.54296902 0 0 1 131.02734402 44.296875L856.98828125 117.125a35.15625027 35.15625027 0 0 1 49.67578125-0.03515652h0.03515652a35.15625027 35.15625027 0 0 1 0 49.74609429l-74.35546875 74.35546875a225.21093777 225.21093777 0 0 1-26.22656304 290.25z m-191.63671875 27.07031223a24.57421875 24.57421875 0 0 1-1.51171821 33.08203098l-57.72656277 57.76171929 63.98437473 63.73828071-83.42578125 83.46093777a230.62499973 230.62499973 0 0 1-161.71874946 68.66015625 204.92578125 204.92578125 0 0 1-136.75781304-49.21874973l-95.83593723 95.80078071a24.18750027 24.18750027 0 0 1-34.24218723-0.07031223 24.890625 24.890625 0 0 1-0.35156277-34.76953098l95.94140598-100.12500027a225.98437473 225.98437473 0 0 1 21.09375-299.28515598L305.98437473 394.0859375l68.66015625 68.66015625L427.13281277 411.10156277a24.39843777 24.39843777 0 0 1 34.34765598 34.59375L409.94140652 497.234375l117.9140625 117.63281277 56.53125-57.5859375a20.28515652 20.28515652 0 0 1 30.09374946 1.23046848z m-112.32421875 199.19531223l44.05078179-44.05078098-240.22265679-240.46875-44.296875 44.57812473a169.91015625 169.91015625 0 0 0-4.67578071 240.18750027l4.640625 4.92187473a151.875 151.875 0 0 0 117.9140625 48.97265652 175.35937473 175.35937473 0 0 0 122.58984321-54.70312473v0.56249946zM933.875 512c0 232.98046875-188.89453125 421.875-421.875 421.875-7.87499973 0-15.71484375-0.2109375-23.48437473-0.6328125l73.12499946-73.16015598a351.77343777 351.77343777 0 0 0 298.44140679-298.40625027l73.12499946-73.16015598c0.45703152 7.76953098 0.66796902 15.609375 0.66796902 23.48437473zM535.48437473 90.7578125L462.35937527 163.953125a351.77343777 351.77343777 0 0 0-298.44140679 298.40625027l-73.12499946 73.16015598A428.625 428.625 0 0 1 90.125 512C90.125 279.01953125 279.01953125 90.125 512 90.125c7.87499973 0 15.71484375 0.2109375 23.48437473 0.6328125z" fill="#6C707E" p-id="1170"></path></svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg t="1747210120200" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1169"
|
||||||
|
width="16" height="16">
|
||||||
|
<path d="M806.11718723 531.44140652l-78.2578125 78.50390571L410.22265625 291.21874973l82.08984402-78.53906223a231.46874973 231.46874973 0 0 1 162.49218723-68.66015625 206.54296902 206.54296902 0 0 1 131.02734402 44.296875L856.98828125 117.125a35.15625027 35.15625027 0 0 1 49.67578125-0.03515652h0.03515652a35.15625027 35.15625027 0 0 1 0 49.74609429l-74.35546875 74.35546875a225.21093777 225.21093777 0 0 1-26.22656304 290.25z m-191.63671875 27.07031223a24.57421875 24.57421875 0 0 1-1.51171821 33.08203098l-57.72656277 57.76171929 63.98437473 63.73828071-83.42578125 83.46093777a230.62499973 230.62499973 0 0 1-161.71874946 68.66015625 204.92578125 204.92578125 0 0 1-136.75781304-49.21874973l-95.83593723 95.80078071a24.18750027 24.18750027 0 0 1-34.24218723-0.07031223 24.890625 24.890625 0 0 1-0.35156277-34.76953098l95.94140598-100.12500027a225.98437473 225.98437473 0 0 1 21.09375-299.28515598L305.98437473 394.0859375l68.66015625 68.66015625L427.13281277 411.10156277a24.39843777 24.39843777 0 0 1 34.34765598 34.59375L409.94140652 497.234375l117.9140625 117.63281277 56.53125-57.5859375a20.28515652 20.28515652 0 0 1 30.09374946 1.23046848z m-112.32421875 199.19531223l44.05078179-44.05078098-240.22265679-240.46875-44.296875 44.57812473a169.91015625 169.91015625 0 0 0-4.67578071 240.18750027l4.640625 4.92187473a151.875 151.875 0 0 0 117.9140625 48.97265652 175.35937473 175.35937473 0 0 0 122.58984321-54.70312473v0.56249946zM933.875 512c0 232.98046875-188.89453125 421.875-421.875 421.875-7.87499973 0-15.71484375-0.2109375-23.48437473-0.6328125l73.12499946-73.16015598a351.77343777 351.77343777 0 0 0 298.44140679-298.40625027l73.12499946-73.16015598c0.45703152 7.76953098 0.66796902 15.609375 0.66796902 23.48437473zM535.48437473 90.7578125L462.35937527 163.953125a351.77343777 351.77343777 0 0 0-298.44140679 298.40625027l-73.12499946 73.16015598A428.625 428.625 0 0 1 90.125 512C90.125 279.01953125 279.01953125 90.125 512 90.125c7.87499973 0 15.71484375 0.2109375 23.48437473 0.6328125z"
|
||||||
|
fill="#CED0D6" p-id="1170"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -2,7 +2,7 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
}
|
}
|
||||||
|
|
||||||
project.version = "0.0.2"
|
project.version = "0.0.3"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
package app.termora.plugins.smb
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
import app.termora.transfer.s3.S3FileSystem
|
import app.termora.transfer.s3.S3FileSystem
|
||||||
|
import app.termora.transfer.s3.S3Path
|
||||||
import com.hierynomus.smbj.session.Session
|
import com.hierynomus.smbj.session.Session
|
||||||
import com.hierynomus.smbj.share.DiskShare
|
import com.hierynomus.smbj.share.DiskShare
|
||||||
|
|
||||||
class SMBFileSystem(private val share: DiskShare, session: Session) :
|
class SMBFileSystem(private val share: DiskShare, session: Session) :
|
||||||
S3FileSystem(SMBFileSystemProvider(share, session)) {
|
S3FileSystem(SMBFileSystemProvider(share, session)) {
|
||||||
|
|
||||||
|
override fun create(root: String?, names: List<String>): S3Path {
|
||||||
|
val path = SMBPath(this, root, names)
|
||||||
|
if (names.isEmpty()) {
|
||||||
|
path.attributes = path.attributes.copy(directory = true)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
share.close()
|
share.close()
|
||||||
super.close()
|
super.close()
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3Path
|
||||||
|
|
||||||
|
class SMBPath(fileSystem: SMBFileSystem, root: String?, names: List<String>) : S3Path(fileSystem, root, names) {
|
||||||
|
override val isBucket: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
|
override val bucketName: String
|
||||||
|
get() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override val objectName: String
|
||||||
|
get() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun getCustomType(): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ include("plugins:s3")
|
|||||||
include("plugins:oss")
|
include("plugins:oss")
|
||||||
include("plugins:cos")
|
include("plugins:cos")
|
||||||
include("plugins:obs")
|
include("plugins:obs")
|
||||||
//include("plugins:ftp")
|
include("plugins:ftp")
|
||||||
include("plugins:bg")
|
include("plugins:bg")
|
||||||
include("plugins:sync")
|
include("plugins:sync")
|
||||||
include("plugins:migration")
|
include("plugins:migration")
|
||||||
@@ -15,3 +15,4 @@ include("plugins:editor")
|
|||||||
include("plugins:geo")
|
include("plugins:geo")
|
||||||
include("plugins:webdav")
|
include("plugins:webdav")
|
||||||
include("plugins:smb")
|
include("plugins:smb")
|
||||||
|
include("plugins:serial")
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.actions.ActionManager
|
|
||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
import app.termora.keymap.KeymapManager
|
|
||||||
import app.termora.plugin.ExtensionManager
|
import app.termora.plugin.ExtensionManager
|
||||||
import app.termora.plugin.PluginManager
|
import app.termora.plugin.PluginManager
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
@@ -22,10 +20,7 @@ import org.apache.commons.lang3.LocaleUtils
|
|||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.MenuItem
|
import java.awt.*
|
||||||
import java.awt.PopupMenu
|
|
||||||
import java.awt.SystemTray
|
|
||||||
import java.awt.TrayIcon
|
|
||||||
import java.awt.desktop.AppReopenedEvent
|
import java.awt.desktop.AppReopenedEvent
|
||||||
import java.awt.desktop.AppReopenedListener
|
import java.awt.desktop.AppReopenedListener
|
||||||
import java.awt.desktop.SystemEventListener
|
import java.awt.desktop.SystemEventListener
|
||||||
@@ -57,12 +52,6 @@ class ApplicationRunner {
|
|||||||
// 统计
|
// 统计
|
||||||
enableAnalytics()
|
enableAnalytics()
|
||||||
|
|
||||||
// init ActionManager、KeymapManager、VFS
|
|
||||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
|
||||||
ActionManager.getInstance()
|
|
||||||
KeymapManager.getInstance()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置 LAF
|
// 设置 LAF
|
||||||
setupLaf()
|
setupLaf()
|
||||||
|
|
||||||
@@ -173,7 +162,6 @@ class ApplicationRunner {
|
|||||||
private fun setupLaf() {
|
private fun setupLaf() {
|
||||||
|
|
||||||
System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux || SystemInfo.isWindows}")
|
System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux || SystemInfo.isWindows}")
|
||||||
System.setProperty(FlatSystemProperties.USE_ROUNDED_POPUP_BORDER, "false")
|
|
||||||
|
|
||||||
if (SystemInfo.isLinux) {
|
if (SystemInfo.isLinux) {
|
||||||
JFrame.setDefaultLookAndFeelDecorated(true)
|
JFrame.setDefaultLookAndFeelDecorated(true)
|
||||||
@@ -197,12 +185,13 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
themeManager.change(theme, true)
|
themeManager.change(theme, true)
|
||||||
|
|
||||||
|
if (Application.isBetaVersion()) {
|
||||||
FlatInspector.install("ctrl shift X")
|
FlatInspector.install("ctrl shift X")
|
||||||
|
}
|
||||||
|
|
||||||
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
||||||
UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false)
|
UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false)
|
||||||
UIManager.put("TitlePane.useWindowDecorations", false)
|
UIManager.put(FlatClientProperties.POPUP_FORCE_HEAVY_WEIGHT, true)
|
||||||
|
|
||||||
UIManager.put("Component.arc", 5)
|
UIManager.put("Component.arc", 5)
|
||||||
UIManager.put("TextComponent.arc", UIManager.getInt("Component.arc"))
|
UIManager.put("TextComponent.arc", UIManager.getInt("Component.arc"))
|
||||||
@@ -213,7 +202,6 @@ class ApplicationRunner {
|
|||||||
UIManager.put("Dialog.width", 650)
|
UIManager.put("Dialog.width", 650)
|
||||||
UIManager.put("Dialog.height", 550)
|
UIManager.put("Dialog.height", 550)
|
||||||
|
|
||||||
|
|
||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isMacOS) {
|
||||||
UIManager.put("TabbedPane.tabHeight", UIManager.getInt("TitleBar.height"))
|
UIManager.put("TabbedPane.tabHeight", UIManager.getInt("TitleBar.height"))
|
||||||
} else if (SystemInfo.isLinux) {
|
} else if (SystemInfo.isLinux) {
|
||||||
@@ -231,15 +219,33 @@ class ApplicationRunner {
|
|||||||
UIManager.put("Table.rowHeight", 24)
|
UIManager.put("Table.rowHeight", 24)
|
||||||
UIManager.put("Table.focusCellHighlightBorder", FlatTableCellBorder.Default())
|
UIManager.put("Table.focusCellHighlightBorder", FlatTableCellBorder.Default())
|
||||||
UIManager.put("Table.focusSelectedCellHighlightBorder", FlatTableCellBorder.Default())
|
UIManager.put("Table.focusSelectedCellHighlightBorder", FlatTableCellBorder.Default())
|
||||||
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
|
||||||
|
|
||||||
UIManager.put("Tree.rowHeight", 24)
|
UIManager.put("Tree.rowHeight", 24)
|
||||||
UIManager.put("Tree.background", DynamicColor("window"))
|
UIManager.put("Tree.background", DynamicColor("window"))
|
||||||
UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc"))
|
|
||||||
UIManager.put("Tree.showCellFocusIndicator", false)
|
UIManager.put("Tree.showCellFocusIndicator", false)
|
||||||
UIManager.put("Tree.repaintWholeRow", true)
|
UIManager.put("Tree.repaintWholeRow", true)
|
||||||
|
|
||||||
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
// Linux 更多的是尖锐风格
|
||||||
|
if (SystemInfo.isMacOS || SystemInfo.isWindows) {
|
||||||
|
val selectionInsets = Insets(0, 2, 0, 2)
|
||||||
|
UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("Tree.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
|
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("List.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
|
UIManager.put("ComboBox.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("ComboBox.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
|
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("Table.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
|
UIManager.put("MenuBar.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("MenuBar.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
|
UIManager.put("MenuItem.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("MenuItem.selectionInsets", selectionInsets)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.Application.ohMyJson
|
|
||||||
import app.termora.actions.MultipleAction
|
|
||||||
import app.termora.database.DatabaseManager
|
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
@@ -18,10 +15,10 @@ import javax.swing.event.ListDataListener
|
|||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class CustomizeToolBarDialog(
|
internal class CustomizeToolBarDialog(
|
||||||
owner: Window,
|
owner: Window,
|
||||||
private val windowScope: WindowScope,
|
private val windowScope: WindowScope,
|
||||||
private val toolbar: TermoraToolBar
|
private val model: TermoraToolbarModel,
|
||||||
) : DialogWrapper(owner) {
|
) : DialogWrapper(owner) {
|
||||||
|
|
||||||
private val moveTopBtn = JButton(Icons.moveUp)
|
private val moveTopBtn = JButton(Icons.moveUp)
|
||||||
@@ -40,6 +37,7 @@ class CustomizeToolBarDialog(
|
|||||||
private val actionManager get() = ActionManager.getInstance()
|
private val actionManager get() = ActionManager.getInstance()
|
||||||
|
|
||||||
private var isOk = false
|
private var isOk = false
|
||||||
|
private val actions = mutableListOf<ToolBarAction>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
size = Dimension(UIManager.getInt("Dialog.width") - 150, UIManager.getInt("Dialog.height") - 100)
|
size = Dimension(UIManager.getInt("Dialog.width") - 150, UIManager.getInt("Dialog.height") - 100)
|
||||||
@@ -147,7 +145,7 @@ class CustomizeToolBarDialog(
|
|||||||
resetBtn.addActionListener {
|
resetBtn.addActionListener {
|
||||||
leftList.model.removeAllElements()
|
leftList.model.removeAllElements()
|
||||||
rightList.model.removeAllElements()
|
rightList.model.removeAllElements()
|
||||||
for (action in toolbar.getAllActions()) {
|
for (action in model.getAllActions()) {
|
||||||
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
|
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,7 +256,7 @@ class CustomizeToolBarDialog(
|
|||||||
override fun windowOpened(e: WindowEvent) {
|
override fun windowOpened(e: WindowEvent) {
|
||||||
removeWindowListener(this)
|
removeWindowListener(this)
|
||||||
|
|
||||||
for (action in toolbar.getActions()) {
|
for (action in model.getActions()) {
|
||||||
if (action.visible) {
|
if (action.visible) {
|
||||||
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
|
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
|
||||||
} else {
|
} else {
|
||||||
@@ -271,12 +269,7 @@ class CustomizeToolBarDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getActionHolder(actionId: String): ActionHolder? {
|
private fun getActionHolder(actionId: String): ActionHolder? {
|
||||||
var action = actionManager.getAction(actionId)
|
val action = actionManager.getAction(actionId)
|
||||||
if (action == null) {
|
|
||||||
if (actionId == MultipleAction.MULTIPLE) {
|
|
||||||
action = MultipleAction.getInstance(windowScope)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (action == null) return null
|
if (action == null) return null
|
||||||
return ActionHolder(actionId, action)
|
return ActionHolder(actionId, action)
|
||||||
}
|
}
|
||||||
@@ -365,12 +358,14 @@ class CustomizeToolBarDialog(
|
|||||||
actions.add(ToolBarAction(leftList.model.getElementAt(i).id, false))
|
actions.add(ToolBarAction(leftList.model.getElementAt(i).id, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
DatabaseManager.getInstance()
|
this.actions.clear()
|
||||||
.properties.putString("Termora.ToolBar.Actions", ohMyJson.encodeToString(actions))
|
this.actions.addAll(actions)
|
||||||
|
|
||||||
super.doOKAction()
|
super.doOKAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getActions()=actions
|
||||||
|
|
||||||
fun open(): Boolean {
|
fun open(): Boolean {
|
||||||
isModal = true
|
isModal = true
|
||||||
isVisible = true
|
isVisible = true
|
||||||
|
|||||||
@@ -344,6 +344,11 @@ data class Host(
|
|||||||
|
|
||||||
val isFolder get() = StringUtils.equalsIgnoreCase(protocol, "Folder")
|
val isFolder get() = StringUtils.equalsIgnoreCase(protocol, "Folder")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 临时的 SSH 不可以保存
|
||||||
|
*/
|
||||||
|
val isTemporary get() = options.extras["Temporary"] != null
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|||||||
@@ -21,9 +21,13 @@ class HostManager private constructor() : Disposable {
|
|||||||
*/
|
*/
|
||||||
fun addHost(host: Host, source: DatabaseChangedExtension.Source = DatabaseChangedExtension.Source.User) {
|
fun addHost(host: Host, source: DatabaseChangedExtension.Source = DatabaseChangedExtension.Source.User) {
|
||||||
assertEventDispatchThread()
|
assertEventDispatchThread()
|
||||||
if (host.ownerType.isBlank()) {
|
|
||||||
|
if (host.isTemporary)
|
||||||
|
throw IllegalArgumentException("Temporary host")
|
||||||
|
|
||||||
|
if (host.ownerType.isBlank())
|
||||||
throw IllegalArgumentException("Owner type cannot be null")
|
throw IllegalArgumentException("Owner type cannot be null")
|
||||||
}
|
|
||||||
databaseManager.saveAndIncrementVersion(
|
databaseManager.saveAndIncrementVersion(
|
||||||
Data(
|
Data(
|
||||||
id = host.id,
|
id = host.id,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ object I18n : AbstractI18n() {
|
|||||||
"en_US" to "English",
|
"en_US" to "English",
|
||||||
"zh_CN" to "简体中文",
|
"zh_CN" to "简体中文",
|
||||||
"zh_TW" to "繁體中文",
|
"zh_TW" to "繁體中文",
|
||||||
|
"ru_RU" to "Русский",
|
||||||
)
|
)
|
||||||
|
|
||||||
fun containsLanguage(locale: Locale): String? {
|
fun containsLanguage(locale: Locale): String? {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ object Icons {
|
|||||||
val empty by lazy { DynamicIcon("icons/empty.svg") }
|
val empty by lazy { DynamicIcon("icons/empty.svg") }
|
||||||
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.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 add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") }
|
||||||
|
val breakpoint by lazy { DynamicIcon("icons/breakpoint.svg", "icons/breakpoint_dark.svg") }
|
||||||
val softWrap by lazy { DynamicIcon("icons/softWrap.svg", "icons/softWrap_dark.svg") }
|
val softWrap by lazy { DynamicIcon("icons/softWrap.svg", "icons/softWrap_dark.svg") }
|
||||||
val scrollUp by lazy { DynamicIcon("icons/scrollUp.svg", "icons/scrollUp_dark.svg") }
|
val scrollUp by lazy { DynamicIcon("icons/scrollUp.svg", "icons/scrollUp_dark.svg") }
|
||||||
val reformatCode by lazy { DynamicIcon("icons/reformatCode.svg", "icons/reformatCode_dark.svg") }
|
val reformatCode by lazy { DynamicIcon("icons/reformatCode.svg", "icons/reformatCode_dark.svg") }
|
||||||
@@ -78,6 +79,7 @@ object Icons {
|
|||||||
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") }
|
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") }
|
||||||
val export by lazy { DynamicIcon("icons/export.svg", "icons/export_dark.svg") }
|
val export by lazy { DynamicIcon("icons/export.svg", "icons/export_dark.svg") }
|
||||||
val terminal by lazy { DynamicIcon("icons/terminal.svg", "icons/terminal_dark.svg") }
|
val terminal by lazy { DynamicIcon("icons/terminal.svg", "icons/terminal_dark.svg") }
|
||||||
|
val telnet by lazy { DynamicIcon("icons/telnet.svg", "icons/telnet_dark.svg") }
|
||||||
val ssh by lazy { DynamicIcon("icons/ssh.svg", "icons/ssh_dark.svg") }
|
val ssh by lazy { DynamicIcon("icons/ssh.svg", "icons/ssh_dark.svg") }
|
||||||
val ftp by lazy { DynamicIcon("icons/ftp.svg", "icons/ftp_dark.svg") }
|
val ftp by lazy { DynamicIcon("icons/ftp.svg", "icons/ftp_dark.svg") }
|
||||||
val minio by lazy { DynamicIcon("icons/minio.svg", "icons/minio_dark.svg") }
|
val minio by lazy { DynamicIcon("icons/minio.svg", "icons/minio_dark.svg") }
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class JSplitPaneWithZeroSizeDivider(
|
|||||||
synchronized(treeLock) {
|
synchronized(treeLock) {
|
||||||
for (c in components) {
|
for (c in components) {
|
||||||
if (c == divider) {
|
if (c == divider) {
|
||||||
|
c.isVisible = splitPane.leftComponent.isVisible
|
||||||
c.setBounds(
|
c.setBounds(
|
||||||
splitPane.dividerLocation - w,
|
splitPane.dividerLocation - w,
|
||||||
topOffset.get(),
|
topOffset.get(),
|
||||||
@@ -109,8 +110,10 @@ class JSplitPaneWithZeroSizeDivider(
|
|||||||
|
|
||||||
override fun paint(g: Graphics) {
|
override fun paint(g: Graphics) {
|
||||||
super.paint(g)
|
super.paint(g)
|
||||||
g.color = UIManager.getColor("controlShadow")
|
if (divider.isVisible) {
|
||||||
g.fillRect(splitPane.dividerLocation, 0, 1, topOffset.get())
|
g.color = UIManager.getColor("controlShadow")
|
||||||
|
g.fillRect(splitPane.dividerLocation, 0, 1, topOffset.get())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class MultipleTerminalListener : TerminalPaintListener {
|
|||||||
) {
|
) {
|
||||||
val windowScope = AnActionEvent(terminalPanel, StringUtils.EMPTY, EventObject(terminalPanel))
|
val windowScope = AnActionEvent(terminalPanel, StringUtils.EMPTY, EventObject(terminalPanel))
|
||||||
.getData(DataProviders.WindowScope) ?: return
|
.getData(DataProviders.WindowScope) ?: return
|
||||||
if (!MultipleAction.getInstance(windowScope).isSelected) return
|
if (MultipleAction.getInstance().isSelected(windowScope).not()) return
|
||||||
|
|
||||||
val oldFont = g.font
|
val oldFont = g.font
|
||||||
val colorPalette = terminal.getTerminalModel().getColorPalette()
|
val colorPalette = terminal.getTerminalModel().getColorPalette()
|
||||||
|
|||||||
165
src/main/kotlin/app/termora/MyTermoraToolbar.kt
Normal file
165
src/main/kotlin/app/termora/MyTermoraToolbar.kt
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.StateAction
|
||||||
|
import app.termora.findeverywhere.FindEverywhereAction
|
||||||
|
import app.termora.plugin.internal.badge.Badge
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
|
import java.awt.AWTEvent
|
||||||
|
import java.awt.Rectangle
|
||||||
|
import java.awt.event.AWTEventListener
|
||||||
|
import java.awt.event.ActionEvent
|
||||||
|
import java.awt.event.MouseEvent
|
||||||
|
import java.beans.PropertyChangeEvent
|
||||||
|
import java.beans.PropertyChangeListener
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
internal class MyTermoraToolbar(private val windowScope: WindowScope) : FlatToolBar() {
|
||||||
|
|
||||||
|
|
||||||
|
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
|
||||||
|
private val model get() = TermoraToolbarModel.getInstance()
|
||||||
|
private val actionManager get() = model.getActionManager()
|
||||||
|
private val toolbar get() = this
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一次性生命周期 每次刷新都会重置
|
||||||
|
*/
|
||||||
|
private var disposable = Disposer.newDisposable()
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
refreshActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
isFloatable = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
Disposer.register(windowScope, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
Disposer.dispose(disposable)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// 监听全局事件
|
||||||
|
toolkit.addAWTEventListener(customizeToolBarAWTEventListener, AWTEvent.MOUSE_EVENT_MASK)
|
||||||
|
Disposer.register(windowScope, customizeToolBarAWTEventListener)
|
||||||
|
|
||||||
|
// 监听变化
|
||||||
|
model.addTermoraToolbarModelListener(object : TermoraToolbarModel.TermoraToolbarModelListener {
|
||||||
|
override fun onChanged() {
|
||||||
|
refreshActions()
|
||||||
|
}
|
||||||
|
}).let { Disposer.register(windowScope, it) }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshActions() {
|
||||||
|
Disposer.dispose(disposable)
|
||||||
|
disposable = Disposer.newDisposable()
|
||||||
|
|
||||||
|
removeAll()
|
||||||
|
|
||||||
|
add(JButton(object : AbstractAction() {
|
||||||
|
init {
|
||||||
|
putValue(SMALL_ICON, Icons.add)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: ActionEvent) {
|
||||||
|
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
add(Box.createHorizontalGlue())
|
||||||
|
|
||||||
|
for (action in model.getActions()) {
|
||||||
|
if (action.visible.not()) continue
|
||||||
|
val action = actionManager.getAction(action.id) ?: continue
|
||||||
|
add(redirectAction(action, disposable))
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidate()
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun redirectAction(action: Action, disposable: Disposable): AbstractButton {
|
||||||
|
val button = if (action is StateAction) JToggleButton() else JButton()
|
||||||
|
button.toolTipText = action.getValue(Action.SHORT_DESCRIPTION) as? String
|
||||||
|
button.icon = action.getValue(Action.SMALL_ICON) as? Icon
|
||||||
|
button.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
action.actionPerformed(e)
|
||||||
|
if (action is StateAction) {
|
||||||
|
button.isSelected = action.isSelected(windowScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
val listener = object : PropertyChangeListener, Disposable {
|
||||||
|
private val badge get() = Badge.getInstance(windowScope)
|
||||||
|
override fun propertyChange(evt: PropertyChangeEvent) {
|
||||||
|
if (evt.propertyName == "Badge") {
|
||||||
|
if (action.getValue("Badge") == true) {
|
||||||
|
badge.addBadge(button)
|
||||||
|
} else {
|
||||||
|
badge.removeBadge(button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action is StateAction) {
|
||||||
|
button.isSelected = action.isSelected(windowScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
action.removePropertyChangeListener(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
action.addPropertyChangeListener(listener)
|
||||||
|
Disposer.register(disposable, listener)
|
||||||
|
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对着 ToolBar 右键
|
||||||
|
*/
|
||||||
|
private inner class CustomizeToolBarAWTEventListener : AWTEventListener, Disposable {
|
||||||
|
override fun eventDispatched(event: AWTEvent) {
|
||||||
|
if (event !is MouseEvent || event.id != MouseEvent.MOUSE_CLICKED
|
||||||
|
|| SwingUtilities.isRightMouseButton(event).not()
|
||||||
|
) return
|
||||||
|
|
||||||
|
// 如果 ToolBar 没有显示
|
||||||
|
if (toolbar.isShowing.not()) return
|
||||||
|
|
||||||
|
// 如果不是作用于在 ToolBar 上面
|
||||||
|
if (Rectangle(toolbar.locationOnScreen, toolbar.size).contains(event.locationOnScreen).not()) return
|
||||||
|
|
||||||
|
// 显示右键菜单
|
||||||
|
showContextMenu(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showContextMenu(event: MouseEvent) {
|
||||||
|
val popupMenu = FlatPopupMenu()
|
||||||
|
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
|
||||||
|
val owner = windowScope.window
|
||||||
|
val dialog = CustomizeToolBarDialog(owner, windowScope, model)
|
||||||
|
dialog.setLocationRelativeTo(owner)
|
||||||
|
if (dialog.open()) {
|
||||||
|
model.setActions(dialog.getActions())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
popupMenu.show(event.component, event.x, event.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
toolkit.removeAWTEventListener(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,8 @@ class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(own
|
|||||||
preferredSize = size
|
preferredSize = size
|
||||||
minimumSize = size
|
minimumSize = size
|
||||||
|
|
||||||
|
rememberCheckBox.isVisible = host.isTemporary.not()
|
||||||
|
|
||||||
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
override fun getListCellRendererComponent(
|
override fun getListCellRendererComponent(
|
||||||
list: JList<*>?,
|
list: JList<*>?,
|
||||||
@@ -84,7 +86,7 @@ class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(own
|
|||||||
|
|
||||||
switchPasswordComponent()
|
switchPasswordComponent()
|
||||||
|
|
||||||
return FormBuilder.create().padding("$formMargin, $formMargin, $formMargin, $formMargin")
|
return FormBuilder.create().padding("1dlu, $formMargin, $formMargin, $formMargin")
|
||||||
.layout(layout)
|
.layout(layout)
|
||||||
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, 1)
|
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, 1)
|
||||||
.add(authenticationTypeComboBox).xy(3, 1)
|
.add(authenticationTypeComboBox).xy(3, 1)
|
||||||
|
|||||||
@@ -133,8 +133,8 @@ class TerminalPanelFactory : Disposable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val multipleAction = MultipleAction.getInstance(windowScope)
|
val multipleAction = MultipleAction.getInstance()
|
||||||
if (!multipleAction.isSelected) {
|
if (multipleAction.isSelected(windowScope).not()) {
|
||||||
ptyConnector.write(request.buffer)
|
ptyConnector.write(request.buffer)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,35 +9,28 @@ import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
|
|||||||
import app.termora.findeverywhere.FindEverywhereProvider
|
import app.termora.findeverywhere.FindEverywhereProvider
|
||||||
import app.termora.findeverywhere.FindEverywhereProviderExtension
|
import app.termora.findeverywhere.FindEverywhereProviderExtension
|
||||||
import app.termora.findeverywhere.FindEverywhereResult
|
import app.termora.findeverywhere.FindEverywhereResult
|
||||||
|
import app.termora.plugin.ExtensionManager
|
||||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||||
import app.termora.plugin.internal.sftppty.SFTPPtyProtocolProvider
|
|
||||||
import app.termora.plugin.internal.sftppty.SFTPPtyTerminalTab
|
|
||||||
import app.termora.plugin.internal.ssh.SSHProtocolProvider
|
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
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 org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
import java.awt.event.AWTEventListener
|
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import java.util.*
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class TerminalTabbed(
|
class TerminalTabbed(
|
||||||
private val windowScope: WindowScope,
|
private val windowScope: WindowScope,
|
||||||
private val termoraToolBar: TermoraToolBar,
|
|
||||||
private val tabbedPane: FlatTabbedPane,
|
private val tabbedPane: FlatTabbedPane,
|
||||||
private val layout: TermoraLayout,
|
private val layout: TermoraLayout,
|
||||||
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager, DataProvider {
|
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager, DataProvider {
|
||||||
private val tabs = mutableListOf<TerminalTab>()
|
private val tabs = mutableListOf<TerminalTab>()
|
||||||
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
|
|
||||||
private val toolbar = termoraToolBar.getJToolBar()
|
|
||||||
private val actionManager = ActionManager.getInstance()
|
private val actionManager = ActionManager.getInstance()
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
private val appearance get() = DatabaseManager.getInstance().appearance
|
private val appearance get() = DatabaseManager.getInstance().appearance
|
||||||
@@ -63,8 +56,6 @@ class TerminalTabbed(
|
|||||||
tabbedPane.isTabsClosable = true
|
tabbedPane.isTabsClosable = true
|
||||||
tabbedPane.tabType = FlatTabbedPane.TabType.card
|
tabbedPane.tabType = FlatTabbedPane.TabType.card
|
||||||
|
|
||||||
tabbedPane.trailingComponent = toolbar
|
|
||||||
|
|
||||||
add(tabbedPane, BorderLayout.CENTER)
|
add(tabbedPane, BorderLayout.CENTER)
|
||||||
|
|
||||||
windowScope.getOrCreate(TerminalTabbedManager::class) { this }
|
windowScope.getOrCreate(TerminalTabbedManager::class) { this }
|
||||||
@@ -75,7 +66,6 @@ class TerminalTabbed(
|
|||||||
|
|
||||||
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
Disposer.register(this, customizeToolBarAWTEventListener)
|
|
||||||
|
|
||||||
// 关闭 tab
|
// 关闭 tab
|
||||||
tabbedPane.setTabCloseCallback { _, i -> removeTabAt(i, true) }
|
tabbedPane.setTabCloseCallback { _, i -> removeTabAt(i, true) }
|
||||||
@@ -149,9 +139,6 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
}).let { Disposer.register(this, it) }
|
}).let { Disposer.register(this, it) }
|
||||||
|
|
||||||
// 监听全局事件
|
|
||||||
toolkit.addAWTEventListener(customizeToolBarAWTEventListener, AWTEvent.MOUSE_EVENT_MASK)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeTabAt(index: Int, disposable: Boolean = true) {
|
private fun removeTabAt(index: Int, disposable: Boolean = true) {
|
||||||
@@ -211,6 +198,15 @@ class TerminalTabbed(
|
|||||||
private fun showContextMenu(tabIndex: Int, e: MouseEvent) {
|
private fun showContextMenu(tabIndex: Int, e: MouseEvent) {
|
||||||
val c = tabbedPane.getComponentAt(tabIndex) as JComponent
|
val c = tabbedPane.getComponentAt(tabIndex) as JComponent
|
||||||
val tab = tabs[tabIndex]
|
val tab = tabs[tabIndex]
|
||||||
|
val extensions = ExtensionManager.getInstance().getExtensions(TerminalTabbedContextMenuExtension::class.java)
|
||||||
|
val menuItems = mutableListOf<JMenuItem>()
|
||||||
|
for (extension in extensions) {
|
||||||
|
try {
|
||||||
|
menuItems.add(extension.createJMenuItem(windowScope, tab))
|
||||||
|
} catch (_: UnsupportedOperationException) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val popupMenu = FlatPopupMenu()
|
val popupMenu = FlatPopupMenu()
|
||||||
|
|
||||||
@@ -232,7 +228,7 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 克隆
|
// 克隆
|
||||||
val clone = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.clone"))
|
val clone = popupMenu.add(I18n.getString("termora.copy"))
|
||||||
clone.addActionListener { evt ->
|
clone.addActionListener { evt ->
|
||||||
if (tab is HostTerminalTab) {
|
if (tab is HostTerminalTab) {
|
||||||
actionManager
|
actionManager
|
||||||
@@ -284,14 +280,10 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (tab is HostTerminalTab) {
|
if (menuItems.isNotEmpty()) {
|
||||||
val openHostAction = actionManager.getAction(OpenHostAction.OPEN_HOST)
|
popupMenu.addSeparator()
|
||||||
if (openHostAction != null) {
|
for (item in menuItems) {
|
||||||
if (tab.host.protocol == SSHProtocolProvider.PROTOCOL || tab.host.protocol == SFTPPtyProtocolProvider.PROTOCOL) {
|
popupMenu.add(item)
|
||||||
popupMenu.addSeparator()
|
|
||||||
val sftpCommand = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
|
|
||||||
sftpCommand.addActionListener { openSFTPPtyTab(tab, openHostAction, it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,9 +291,7 @@ class TerminalTabbed(
|
|||||||
|
|
||||||
// 关闭
|
// 关闭
|
||||||
val close = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close"))
|
val close = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close"))
|
||||||
close.addActionListener {
|
close.addActionListener { tabbedPane.tabCloseCallback?.accept(tabbedPane, tabIndex) }
|
||||||
tabbedPane.tabCloseCallback?.accept(tabbedPane, tabIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭其他标签页
|
// 关闭其他标签页
|
||||||
popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close-other-tabs")).addActionListener {
|
popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close-other-tabs")).addActionListener {
|
||||||
@@ -324,7 +314,7 @@ class TerminalTabbed(
|
|||||||
close.isEnabled = tab.canClose()
|
close.isEnabled = tab.canClose()
|
||||||
rename.isEnabled = close.isEnabled
|
rename.isEnabled = close.isEnabled
|
||||||
clone.isEnabled = close.isEnabled
|
clone.isEnabled = close.isEnabled
|
||||||
edit.isEnabled = tab is HostTerminalTab && tab.host.id != "local"
|
edit.isEnabled = tab is HostTerminalTab && tab.host.id != "local" && tab.host.isTemporary.not()
|
||||||
openInNewWindow.isEnabled = close.isEnabled
|
openInNewWindow.isEnabled = close.isEnabled
|
||||||
|
|
||||||
// 如果不允许克隆
|
// 如果不允许克隆
|
||||||
@@ -335,12 +325,7 @@ class TerminalTabbed(
|
|||||||
if (close.isEnabled) {
|
if (close.isEnabled) {
|
||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
|
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
|
||||||
reconnect.addActionListener {
|
reconnect.addActionListener { tabs[tabIndex].reconnect() }
|
||||||
if (tabIndex > 0) {
|
|
||||||
tabs[tabIndex].reconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reconnect.isEnabled = tabs[tabIndex].canReconnect()
|
reconnect.isEnabled = tabs[tabIndex].canReconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,90 +367,6 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun openSFTPPtyTab(tab: HostTerminalTab, openHostAction: Action, evt: EventObject) {
|
|
||||||
if (!SFTPPtyTerminalTab.canSupports) {
|
|
||||||
OptionPane.showMessageDialog(
|
|
||||||
SwingUtilities.getWindowAncestor(this),
|
|
||||||
I18n.getString("termora.tabbed.contextmenu.sftp-not-install"),
|
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var host = tab.host
|
|
||||||
|
|
||||||
if (host.protocol == SSHProtocolProvider.PROTOCOL) {
|
|
||||||
val envs = tab.host.options.envs().toMutableMap()
|
|
||||||
val currentDir = tab.getData(DataProviders.Terminal)?.getTerminalModel()
|
|
||||||
?.getData(DataKey.CurrentDir, StringUtils.EMPTY) ?: StringUtils.EMPTY
|
|
||||||
|
|
||||||
if (currentDir.isNotBlank()) {
|
|
||||||
envs["CurrentDir"] = currentDir
|
|
||||||
}
|
|
||||||
|
|
||||||
host = host.copy(
|
|
||||||
protocol = SFTPPtyProtocolProvider.PROTOCOL,
|
|
||||||
options = host.options.copy(env = envs.toPropertiesString())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
openHostAction.actionPerformed(OpenHostActionEvent(this, host, evt))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 对着 ToolBar 右键
|
|
||||||
*/
|
|
||||||
private inner class CustomizeToolBarAWTEventListener : AWTEventListener, Disposable {
|
|
||||||
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 owner = SwingUtilities.getWindowAncestor(this@TerminalTabbed)
|
|
||||||
val dialog = CustomizeToolBarDialog(owner, windowScope, termoraToolBar)
|
|
||||||
dialog.setLocationRelativeTo(owner)
|
|
||||||
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 inner class SwitchFindEverywhereResult(
|
||||||
private val title: String,
|
private val title: String,
|
||||||
private val icon: Icon?,
|
private val icon: Icon?,
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import javax.swing.JMenuItem
|
||||||
|
|
||||||
|
interface TerminalTabbedContextMenuExtension : Extension {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抛出 [UnsupportedOperationException] 表示不支持
|
||||||
|
*/
|
||||||
|
fun createJMenuItem(windowScope: WindowScope, tab: TerminalTab): JMenuItem
|
||||||
|
}
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnAction
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.tree.NewHostTree
|
import app.termora.tree.NewHostTree
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Font
|
import java.awt.Font
|
||||||
|
import java.awt.event.ComponentAdapter
|
||||||
|
import java.awt.event.ComponentEvent
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
|
||||||
class TermoraFencePanel(
|
class TermoraFencePanel(
|
||||||
@@ -24,6 +31,8 @@ class TermoraFencePanel(
|
|||||||
private val leftTreePanel = LeftTreePanel()
|
private val leftTreePanel = LeftTreePanel()
|
||||||
private val mySplitPane = JSplitPaneWithZeroSizeDivider(splitPane) { tabbed.tabHeight }
|
private val mySplitPane = JSplitPaneWithZeroSizeDivider(splitPane) { tabbed.tabHeight }
|
||||||
private val enableManager get() = EnableManager.getInstance()
|
private val enableManager get() = EnableManager.getInstance()
|
||||||
|
private val toolbar = FlatToolBar().apply { isFloatable = false }
|
||||||
|
private var dividerLocation = 0
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -44,12 +53,39 @@ class TermoraFencePanel(
|
|||||||
tabbed.tabType = FlatTabbedPane.TabType.underlined
|
tabbed.tabType = FlatTabbedPane.TabType.underlined
|
||||||
tabbed.tabAreaInsets = null
|
tabbed.tabAreaInsets = null
|
||||||
|
|
||||||
|
// macOS 避开控制栏
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
toolbar.add(Box.createHorizontalStrut(76))
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbar.add(createColspanAction())
|
||||||
|
tabbed.leadingComponent = toolbar
|
||||||
|
toolbar.isVisible = false
|
||||||
|
|
||||||
add(mySplitPane, BorderLayout.CENTER)
|
add(mySplitPane, BorderLayout.CENTER)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
Disposer.register(this, leftTreePanel)
|
Disposer.register(this, leftTreePanel)
|
||||||
splitPane.addPropertyChangeListener("dividerLocation") { mySplitPane.doLayout() }
|
splitPane.addPropertyChangeListener("dividerLocation") { mySplitPane.doLayout() }
|
||||||
|
|
||||||
|
leftTreePanel.addComponentListener(object : ComponentAdapter() {
|
||||||
|
override fun componentHidden(e: ComponentEvent) {
|
||||||
|
toolbar.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun componentShown(e: ComponentEvent) {
|
||||||
|
toolbar.isVisible = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
actionMap.put("toggle", createColspanAction())
|
||||||
|
getInputMap(WHEN_IN_FOCUSED_WINDOW).put(
|
||||||
|
KeyStroke.getKeyStroke(
|
||||||
|
KeyEvent.VK_B,
|
||||||
|
toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK
|
||||||
|
), "toggle"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class LeftTreePanel : JPanel(BorderLayout()), Disposable {
|
private inner class LeftTreePanel : JPanel(BorderLayout()), Disposable {
|
||||||
@@ -70,9 +106,14 @@ class TermoraFencePanel(
|
|||||||
val label = JLabel(Application.getName())
|
val label = JLabel(Application.getName())
|
||||||
label.foreground = UIManager.getColor("textInactiveText")
|
label.foreground = UIManager.getColor("textInactiveText")
|
||||||
label.font = label.font.deriveFont(Font.BOLD)
|
label.font = label.font.deriveFont(Font.BOLD)
|
||||||
|
// 与最后一个按钮对冲,使其宽度和谐
|
||||||
|
box.add(Box.createHorizontalStrut(24))
|
||||||
box.add(Box.createHorizontalGlue())
|
box.add(Box.createHorizontalGlue())
|
||||||
if (SystemInfo.isMacOS.not()) box.add(label)
|
if (SystemInfo.isMacOS.not()) {
|
||||||
|
box.add(label)
|
||||||
|
}
|
||||||
box.add(Box.createHorizontalGlue())
|
box.add(Box.createHorizontalGlue())
|
||||||
|
box.add(createColspanAction())
|
||||||
|
|
||||||
if (SystemInfo.isMacOS || SystemInfo.isLinux) {
|
if (SystemInfo.isMacOS || SystemInfo.isLinux) {
|
||||||
box.addMouseListener(moveMouseAdapter)
|
box.addMouseListener(moveMouseAdapter)
|
||||||
@@ -95,8 +136,25 @@ class TermoraFencePanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createColspanAction(): Action {
|
||||||
|
return object : AnAction(Icons.dataColumn) {
|
||||||
|
init {
|
||||||
|
val text = I18n.getString("termora.welcome.toggle-sidebar")
|
||||||
|
putValue(SHORT_DESCRIPTION, "$text (${if (SystemInfo.isMacOS) '⌘' else "Ctrl"} + Shift + B)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
if (leftTreePanel.isVisible) dividerLocation = splitPane.dividerLocation
|
||||||
|
leftTreePanel.isVisible = leftTreePanel.isVisible.not()
|
||||||
|
if (leftTreePanel.isVisible) splitPane.dividerLocation = dividerLocation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
enableManager.setFlag("Termora.Fence.dividerLocation", splitPane.dividerLocation)
|
if (leftTreePanel.isVisible)
|
||||||
|
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getHostTree(): NewHostTree {
|
fun getHostTree(): NewHostTree {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.*
|
||||||
import app.termora.actions.DataProviderSupport
|
import app.termora.database.DatabaseChangedExtension
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.database.DatabasePropertiesChangedExtension
|
||||||
import app.termora.actions.OpenHostAction
|
|
||||||
import app.termora.findeverywhere.FindEverywhereProvider
|
import app.termora.findeverywhere.FindEverywhereProvider
|
||||||
import app.termora.findeverywhere.FindEverywhereProviderExtension
|
import app.termora.findeverywhere.FindEverywhereProviderExtension
|
||||||
import app.termora.findeverywhere.FindEverywhereResult
|
import app.termora.findeverywhere.FindEverywhereResult
|
||||||
|
import app.termora.keymap.KeyShortcut
|
||||||
|
import app.termora.keymap.KeymapManager
|
||||||
import app.termora.plugin.ExtensionManager
|
import app.termora.plugin.ExtensionManager
|
||||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||||
import app.termora.plugin.internal.ssh.SSHProtocolProvider
|
import app.termora.plugin.internal.ssh.SSHProtocolProvider
|
||||||
@@ -21,16 +22,12 @@ import com.formdev.flatlaf.util.SystemInfo
|
|||||||
import com.jetbrains.JBR
|
import com.jetbrains.JBR
|
||||||
import org.apache.commons.lang3.ArrayUtils
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
import java.awt.event.*
|
import java.awt.event.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.imageio.ImageIO
|
import javax.imageio.ImageIO
|
||||||
import javax.swing.Icon
|
import javax.swing.*
|
||||||
import javax.swing.JComponent
|
|
||||||
import javax.swing.JFrame
|
|
||||||
import javax.swing.SwingUtilities.isEventDispatchThread
|
import javax.swing.SwingUtilities.isEventDispatchThread
|
||||||
import javax.swing.UIManager
|
|
||||||
|
|
||||||
|
|
||||||
fun assertEventDispatchThread() {
|
fun assertEventDispatchThread() {
|
||||||
@@ -45,22 +42,25 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
private val id = UUID.randomUUID().toString()
|
private val id = UUID.randomUUID().toString()
|
||||||
private val windowScope = ApplicationScope.forWindowScope(this)
|
private val windowScope = ApplicationScope.forWindowScope(this)
|
||||||
private val tabbedPane = MyTabbedPane().apply { tabHeight = titleBarHeight }
|
private val tabbedPane = MyTabbedPane().apply { tabHeight = titleBarHeight }
|
||||||
private val toolbar = TermoraToolBar(windowScope, this)
|
private val toolbar = MyTermoraToolbar(windowScope)
|
||||||
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane, layout)
|
private val terminalTabbed = TerminalTabbed(windowScope, tabbedPane, layout)
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
private var notifyListeners = emptyArray<NotifyListener>()
|
private var notifyListeners = emptyArray<NotifyListener>()
|
||||||
private val moveMouseAdapter = createMoveMouseAdaptor()
|
private val moveMouseAdapter = createMoveMouseAdaptor()
|
||||||
|
private val keymapManager get() = KeymapManager.getInstance()
|
||||||
|
private val actionManager get() = ActionManager.getInstance()
|
||||||
|
private val dynamicExtensionHandler get() = DynamicExtensionHandler.getInstance()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
initEvents()
|
initEvents()
|
||||||
|
initKeymap()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
if (SystemInfo.isLinux) {
|
if (SystemInfo.isLinux) {
|
||||||
toolbar.getJToolBar().addMouseListener(moveMouseAdapter)
|
toolbar.addMouseListener(moveMouseAdapter)
|
||||||
toolbar.getJToolBar().addMouseMotionListener(moveMouseAdapter)
|
toolbar.addMouseMotionListener(moveMouseAdapter)
|
||||||
} else if (SystemInfo.isMacOS) {
|
} else if (SystemInfo.isMacOS) {
|
||||||
terminalTabbed.addMouseListener(moveMouseAdapter)
|
terminalTabbed.addMouseListener(moveMouseAdapter)
|
||||||
terminalTabbed.addMouseMotionListener(moveMouseAdapter)
|
terminalTabbed.addMouseMotionListener(moveMouseAdapter)
|
||||||
@@ -68,12 +68,20 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
tabbedPane.addMouseListener(moveMouseAdapter)
|
tabbedPane.addMouseListener(moveMouseAdapter)
|
||||||
tabbedPane.addMouseMotionListener(moveMouseAdapter)
|
tabbedPane.addMouseMotionListener(moveMouseAdapter)
|
||||||
|
|
||||||
toolbar.getJToolBar().addMouseListener(moveMouseAdapter)
|
toolbar.addMouseListener(moveMouseAdapter)
|
||||||
toolbar.getJToolBar().addMouseMotionListener(moveMouseAdapter)
|
toolbar.addMouseMotionListener(moveMouseAdapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 快捷键变动时重新监听
|
||||||
|
val refresher = KeymapRefresher()
|
||||||
|
dynamicExtensionHandler.register(DatabasePropertiesChangedExtension::class.java, refresher)
|
||||||
|
.let { Disposer.register(windowScope, it) }
|
||||||
|
dynamicExtensionHandler.register(DatabaseChangedExtension::class.java, refresher)
|
||||||
|
.let { Disposer.register(windowScope, it) }
|
||||||
|
|
||||||
|
|
||||||
// FindEverywhere
|
// FindEverywhere
|
||||||
DynamicExtensionHandler.getInstance()
|
dynamicExtensionHandler
|
||||||
.register(FindEverywhereProviderExtension::class.java, object : FindEverywhereProviderExtension {
|
.register(FindEverywhereProviderExtension::class.java, object : FindEverywhereProviderExtension {
|
||||||
private val hostTreeModel get() = NewHostTreeModel.getInstance()
|
private val hostTreeModel get() = NewHostTreeModel.getInstance()
|
||||||
|
|
||||||
@@ -115,8 +123,7 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
private val showMoreInfo get() = EnableManager.getInstance().isShowMoreInfo()
|
private val showMoreInfo get() = EnableManager.getInstance().isShowMoreInfo()
|
||||||
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
ActionManager.getInstance()
|
actionManager.getAction(OpenHostAction.OPEN_HOST)
|
||||||
.getAction(OpenHostAction.OPEN_HOST)
|
|
||||||
?.actionPerformed(OpenHostActionEvent(e.source, host, e))
|
?.actionPerformed(OpenHostActionEvent(e.source, host, e))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +156,6 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
|
|
||||||
// macOS 要避开左边的控制栏
|
// macOS 要避开左边的控制栏
|
||||||
@@ -162,6 +168,8 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
|
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tabbedPane.trailingComponent = toolbar
|
||||||
|
|
||||||
val height = UIManager.getInt("TabbedPane.tabHeight") + tabbedPane.tabAreaInsets.top
|
val height = UIManager.getInt("TabbedPane.tabHeight") + tabbedPane.tabAreaInsets.top
|
||||||
|
|
||||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
@@ -169,14 +177,6 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICON, false)
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICON, false)
|
||||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, height)
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, height)
|
||||||
} else if (SystemInfo.isMacOS) {
|
|
||||||
rootPane.putClientProperty("apple.awt.windowTitleVisible", false)
|
|
||||||
rootPane.putClientProperty("apple.awt.fullWindowContent", true)
|
|
||||||
rootPane.putClientProperty("apple.awt.transparentTitleBar", true)
|
|
||||||
rootPane.putClientProperty(
|
|
||||||
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING,
|
|
||||||
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING_MEDIUM
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
@@ -217,6 +217,61 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
|
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initKeymap() {
|
||||||
|
assertEventDispatchThread()
|
||||||
|
|
||||||
|
val keymap = keymapManager.getActiveKeymap()
|
||||||
|
val inputMap = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
|
||||||
|
val actionMap = rootPane.actionMap
|
||||||
|
|
||||||
|
// 移除之前所有的快捷键
|
||||||
|
inputMap.clear()
|
||||||
|
actionMap.clear()
|
||||||
|
|
||||||
|
for ((shortcut, actionIds) in keymap.getShortcuts()) {
|
||||||
|
if (shortcut !is KeyShortcut) continue
|
||||||
|
if (actionIds.contains(SwitchTabAction.SWITCH_TAB)) continue
|
||||||
|
registerKeyStroke(actionMap, inputMap, shortcut.keyStroke, actionIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (shortcut in keymap.getShortcut(SwitchTabAction.SWITCH_TAB)) {
|
||||||
|
if (shortcut !is KeyShortcut) continue
|
||||||
|
registerKeyStroke(actionMap, inputMap, shortcut.keyStroke, listOf(SwitchTabAction.SWITCH_TAB))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerKeyStroke(
|
||||||
|
actionMap: ActionMap,
|
||||||
|
inputMap: InputMap,
|
||||||
|
keyStroke: KeyStroke,
|
||||||
|
actionIds: List<String>
|
||||||
|
) {
|
||||||
|
val keyShortcutActionId = "KeyShortcutAction_${randomUUID()}"
|
||||||
|
actionMap.put(keyShortcutActionId, redirectAction(actionIds))
|
||||||
|
inputMap.put(keyStroke, keyShortcutActionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun redirectAction(actionIds: List<String>): Action {
|
||||||
|
return object : AbstractAction() {
|
||||||
|
private val keyboardFocusManager get() = KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
var source = e.source
|
||||||
|
if (source == rootPane) {
|
||||||
|
val focusOwner = keyboardFocusManager.focusOwner
|
||||||
|
if (focusOwner is JComponent) {
|
||||||
|
source = focusOwner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (actionId in actionIds) {
|
||||||
|
val action = actionManager.getAction(actionId) ?: continue
|
||||||
|
action.actionPerformed(RedirectAnActionEvent(source, e.actionCommand, EventQueue.getCurrentEvent()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
return dataProviderSupport.getData(dataKey) ?: terminalTabbed.getData(dataKey)
|
return dataProviderSupport.getData(dataKey) ?: terminalTabbed.getData(dataKey)
|
||||||
}
|
}
|
||||||
@@ -363,6 +418,35 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
return object : MouseAdapter() {}
|
return object : MouseAdapter() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class KeymapRefresher : DatabasePropertiesChangedExtension, DatabaseChangedExtension {
|
||||||
|
|
||||||
|
override fun onDataChanged(
|
||||||
|
id: String,
|
||||||
|
type: String,
|
||||||
|
action: DatabaseChangedExtension.Action,
|
||||||
|
source: DatabaseChangedExtension.Source
|
||||||
|
) {
|
||||||
|
if (type != "Keymap") return
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPropertyChanged(name: String, key: String, value: String) {
|
||||||
|
if (name != "Setting.Properties") return
|
||||||
|
if (key != "Keymap.Active") return
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refresh() {
|
||||||
|
initKeymap()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class RedirectAnActionEvent(
|
||||||
|
source: Any,
|
||||||
|
command: String,
|
||||||
|
event: EventObject
|
||||||
|
) : AnActionEvent(source, command, event)
|
||||||
|
|
||||||
private inner class GlassPane : JComponent() {
|
private inner class GlassPane : JComponent() {
|
||||||
|
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
import app.termora.Application.ohMyJson
|
|
||||||
import app.termora.actions.*
|
|
||||||
import app.termora.database.DatabaseManager
|
|
||||||
import app.termora.findeverywhere.FindEverywhereAction
|
|
||||||
import app.termora.snippet.SnippetAction
|
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.jdesktop.swingx.action.ActionContainerFactory
|
|
||||||
import java.awt.Rectangle
|
|
||||||
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 windowScope: WindowScope,
|
|
||||||
private val frame: TermoraFrame,
|
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun rebuild() {
|
|
||||||
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
|
||||||
val toolbars = SwingUtils.getDescendantsOfClass(MyToolBar::class.java, frame)
|
|
||||||
for (toolbar in toolbars) {
|
|
||||||
toolbar.rebuild()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val properties get() = DatabaseManager.getInstance().properties
|
|
||||||
private val toolbar by lazy { MyToolBar().apply { rebuild() } }
|
|
||||||
|
|
||||||
|
|
||||||
fun getJToolBar(): JToolBar {
|
|
||||||
return toolbar
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取到所有的 Action
|
|
||||||
*/
|
|
||||||
fun getAllActions(): List<ToolBarAction> {
|
|
||||||
return listOf(
|
|
||||||
ToolBarAction(SnippetAction.SNIPPET, true),
|
|
||||||
ToolBarAction(Actions.SFTP, true),
|
|
||||||
ToolBarAction(Actions.TERMINAL_LOGGER, true),
|
|
||||||
ToolBarAction(Actions.MACRO, true),
|
|
||||||
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
|
|
||||||
ToolBarAction(Actions.KEY_MANAGER, true),
|
|
||||||
ToolBarAction(MultipleAction.MULTIPLE, true),
|
|
||||||
ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
|
|
||||||
ToolBarAction(SettingsAction.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
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class MyToolBar : JToolBar() {
|
|
||||||
init {
|
|
||||||
// 监听窗口大小变动,然后修改边距避开控制按钮
|
|
||||||
addComponentListener(object : ComponentAdapter() {
|
|
||||||
override fun componentResized(e: ComponentEvent) {
|
|
||||||
adjust()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun adjust() {
|
|
||||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
|
||||||
val rectangle =
|
|
||||||
frame.rootPane.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
|
||||||
as? Rectangle ?: return
|
|
||||||
val right = rectangle.width
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun rebuild() {
|
|
||||||
val toolbar: JToolBar = this
|
|
||||||
val actionManager = ActionManager.getInstance()
|
|
||||||
val actionContainerFactory = ActionContainerFactory(actionManager)
|
|
||||||
|
|
||||||
toolbar.removeAll()
|
|
||||||
|
|
||||||
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
|
||||||
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isEnabled(): Boolean {
|
|
||||||
return actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.isEnabled ?: false
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
toolbar.add(Box.createHorizontalGlue())
|
|
||||||
|
|
||||||
if (SystemInfo.isLinux || SystemInfo.isWindows) {
|
|
||||||
toolbar.add(Box.createHorizontalStrut(16))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
val ac = actionManager.getAction(action.id)
|
|
||||||
if (ac == null) {
|
|
||||||
if (action.id == MultipleAction.MULTIPLE) {
|
|
||||||
toolbar.add(actionContainerFactory.createButton(MultipleAction.getInstance(windowScope)))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toolbar.add(actionContainerFactory.createButton(ac))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (toolbar is MyToolBar) {
|
|
||||||
toolbar.adjust()
|
|
||||||
}
|
|
||||||
|
|
||||||
toolbar.revalidate()
|
|
||||||
toolbar.repaint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
103
src/main/kotlin/app/termora/TermoraToolbarModel.kt
Normal file
103
src/main/kotlin/app/termora/TermoraToolbarModel.kt
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.Application.ohMyJson
|
||||||
|
import app.termora.actions.ActionManager
|
||||||
|
import app.termora.actions.MultipleAction
|
||||||
|
import app.termora.actions.SettingsAction
|
||||||
|
import app.termora.database.DatabaseManager
|
||||||
|
import app.termora.findeverywhere.FindEverywhereAction
|
||||||
|
import app.termora.snippet.SnippetAction
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.event.EventListenerList
|
||||||
|
|
||||||
|
internal class TermoraToolbarModel private constructor() {
|
||||||
|
companion object {
|
||||||
|
fun getInstance(): TermoraToolbarModel {
|
||||||
|
return ApplicationScope.forApplicationScope()
|
||||||
|
.getOrCreate(TermoraToolbarModel::class) { TermoraToolbarModel() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val properties get() = DatabaseManager.getInstance().properties
|
||||||
|
private val eventListener = EventListenerList()
|
||||||
|
|
||||||
|
|
||||||
|
fun getActionManager() = ActionManager.getInstance()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取到所有的 Action
|
||||||
|
*/
|
||||||
|
fun getAllActions(): List<ToolBarAction> {
|
||||||
|
return listOf(
|
||||||
|
ToolBarAction(SnippetAction.SNIPPET, true),
|
||||||
|
ToolBarAction(Actions.SFTP, true),
|
||||||
|
ToolBarAction(Actions.TERMINAL_LOGGER, true),
|
||||||
|
ToolBarAction(Actions.MACRO, true),
|
||||||
|
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
|
||||||
|
ToolBarAction(Actions.KEY_MANAGER, true),
|
||||||
|
ToolBarAction(MultipleAction.MULTIPLE, true),
|
||||||
|
ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
|
||||||
|
ToolBarAction(SettingsAction.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 setActions(actions: List<ToolBarAction>) {
|
||||||
|
assertEventDispatchThread()
|
||||||
|
properties.putString("Termora.ToolBar.Actions", ohMyJson.encodeToString(actions))
|
||||||
|
for (listener in eventListener.getListeners(TermoraToolbarModelListener::class.java)) {
|
||||||
|
listener.onChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun addTermoraToolbarModelListener(listener: TermoraToolbarModelListener): Disposable {
|
||||||
|
eventListener.add(TermoraToolbarModelListener::class.java, listener)
|
||||||
|
return object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
removeTermoraToolbarModelListener(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeTermoraToolbarModelListener(listener: TermoraToolbarModelListener) {
|
||||||
|
eventListener.remove(TermoraToolbarModelListener::class.java, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TermoraToolbarModelListener : EventListener {
|
||||||
|
fun onChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
import javax.swing.PopupFactory
|
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
|
|
||||||
@@ -118,11 +117,7 @@ internal class ThemeManager private constructor() {
|
|||||||
|
|
||||||
private fun immediateChange(classname: String) {
|
private fun immediateChange(classname: String) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
val oldPopupFactory = PopupFactory.getSharedInstance()
|
|
||||||
UIManager.setLookAndFeel(classname)
|
UIManager.setLookAndFeel(classname)
|
||||||
PopupFactory.setSharedInstance(oldPopupFactory)
|
|
||||||
|
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
log.error(ex.message, ex)
|
log.error(ex.message, ex)
|
||||||
}
|
}
|
||||||
|
|||||||
9
src/main/kotlin/app/termora/ToolBarAction.kt
Normal file
9
src/main/kotlin/app/termora/ToolBarAction.kt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ToolBarAction(
|
||||||
|
val id: String,
|
||||||
|
val visible: Boolean,
|
||||||
|
)
|
||||||
@@ -3,7 +3,6 @@ package app.termora.account
|
|||||||
import app.termora.AES
|
import app.termora.AES
|
||||||
import app.termora.database.*
|
import app.termora.database.*
|
||||||
import app.termora.database.Data.Companion.toData
|
import app.termora.database.Data.Companion.toData
|
||||||
import okhttp3.internal.EMPTY_BYTE_ARRAY
|
|
||||||
import org.apache.commons.codec.binary.Base64
|
import org.apache.commons.codec.binary.Base64
|
||||||
import org.apache.commons.codec.digest.DigestUtils
|
import org.apache.commons.codec.digest.DigestUtils
|
||||||
import org.apache.commons.lang3.ObjectUtils
|
import org.apache.commons.lang3.ObjectUtils
|
||||||
@@ -88,7 +87,7 @@ abstract class SyncService {
|
|||||||
return accountManager.getSecretKey()
|
return accountManager.getSecretKey()
|
||||||
}
|
}
|
||||||
val team = accountManager.getTeams().firstOrNull { it.id == ownerId }
|
val team = accountManager.getTeams().firstOrNull { it.id == ownerId }
|
||||||
return team?.secretKey ?: EMPTY_BYTE_ARRAY
|
return team?.secretKey ?: byteArrayOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun decryptData(id: String, data: String, ownerId: String): String {
|
protected fun decryptData(id: String, data: String, ownerId: String): String {
|
||||||
|
|||||||
@@ -28,12 +28,14 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
|||||||
private fun registerActions() {
|
private fun registerActions() {
|
||||||
addAction(NewWindowAction.NEW_WINDOW, NewWindowAction())
|
addAction(NewWindowAction.NEW_WINDOW, NewWindowAction())
|
||||||
addAction(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
|
addAction(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
|
||||||
|
addAction(QuickConnectAction.QUICK_CONNECT, QuickConnectAction.instance)
|
||||||
|
|
||||||
addAction(Actions.APP_UPDATE, AppUpdateAction.getInstance())
|
addAction(Actions.APP_UPDATE, AppUpdateAction.getInstance())
|
||||||
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())
|
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())
|
||||||
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
|
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
|
||||||
addAction(Actions.SFTP, TransferAnAction())
|
addAction(Actions.SFTP, TransferAnAction())
|
||||||
addAction(SFTPCommandAction.SFTP_COMMAND, SFTPCommandAction())
|
addAction(SFTPCommandAction.SFTP_COMMAND, SFTPCommandAction())
|
||||||
|
addAction(MultipleAction.MULTIPLE, MultipleAction.getInstance())
|
||||||
addAction(SnippetAction.SNIPPET, SnippetAction.getInstance())
|
addAction(SnippetAction.SNIPPET, SnippetAction.getInstance())
|
||||||
addAction(Actions.MACRO, MacroAction())
|
addAction(Actions.MACRO, MacroAction())
|
||||||
addAction(Actions.KEY_MANAGER, KeyManagerAction())
|
addAction(Actions.KEY_MANAGER, KeyManagerAction())
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
import app.termora.I18n
|
import app.termora.*
|
||||||
import app.termora.Icons
|
|
||||||
import app.termora.TerminalPanelFactory
|
|
||||||
import app.termora.WindowScope
|
|
||||||
|
|
||||||
class MultipleAction private constructor() : AnAction(
|
class MultipleAction private constructor() : AnAction(
|
||||||
I18n.getString("termora.tools.multiple"),
|
I18n.getString("termora.tools.multiple"),
|
||||||
Icons.vcs
|
Icons.vcs
|
||||||
) {
|
), StateAction {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@@ -17,8 +14,8 @@ class MultipleAction private constructor() : AnAction(
|
|||||||
*/
|
*/
|
||||||
const val MULTIPLE = "MultipleAction"
|
const val MULTIPLE = "MultipleAction"
|
||||||
|
|
||||||
fun getInstance(windowScope: WindowScope): MultipleAction {
|
fun getInstance(): MultipleAction {
|
||||||
return windowScope.getOrCreate(MultipleAction::class) { MultipleAction() }
|
return ApplicationScope.forApplicationScope().getOrCreate(MultipleAction::class) { MultipleAction() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +24,24 @@ class MultipleAction private constructor() : AnAction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
super.setSelected(false)
|
||||||
|
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
|
||||||
|
setSelected(windowScope, isSelected(windowScope).not())
|
||||||
TerminalPanelFactory.getInstance().repaintAll()
|
TerminalPanelFactory.getInstance().repaintAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isSelected(): Boolean {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isSelected(windowScope: WindowScope): Boolean {
|
||||||
|
return windowScope.getBoolean("MultipleAction.isSelected", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSelected(windowScope: WindowScope, selected: Boolean) {
|
||||||
|
windowScope.putBoolean("MultipleAction.isSelected", selected)
|
||||||
|
putValue("MultipleAction.isSelected", selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ class OpenHostAction : AnAction() {
|
|||||||
if (providers.first { StringUtils.equalsIgnoreCase(it.getProtocol(), host.protocol) }
|
if (providers.first { StringUtils.equalsIgnoreCase(it.getProtocol(), host.protocol) }
|
||||||
.isTransfer()) {
|
.isTransfer()) {
|
||||||
ActionManager.getInstance().getAction(Actions.SFTP)
|
ActionManager.getInstance().getAction(Actions.SFTP)
|
||||||
.actionPerformed(TransferActionEvent(evt.source, evt.host.id, evt.event))
|
.actionPerformed(TransferActionEvent(evt.source, host, evt.event))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
178
src/main/kotlin/app/termora/actions/QuickConnectAction.kt
Normal file
178
src/main/kotlin/app/termora/actions/QuickConnectAction.kt
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.Application.ohMyJson
|
||||||
|
import app.termora.OptionsPane.Companion.FORM_MARGIN
|
||||||
|
import app.termora.database.DatabaseManager
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.net.URI
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
class QuickConnectAction private constructor() : AnAction(I18n.getString("termora.actions.quick-connect"), Icons.find) {
|
||||||
|
companion object {
|
||||||
|
const val QUICK_CONNECT = "QuickConnectAction"
|
||||||
|
val instance = QuickConnectAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.quick-connect"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val scope = evt.getData(DataProviders.WindowScope) ?: return
|
||||||
|
val dialog = QuickConnectDialog(scope.window)
|
||||||
|
dialog.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private class QuickConnectDialog(owner: Window) : DialogWrapper(owner) {
|
||||||
|
private val properties get() = DatabaseManager.getInstance().properties
|
||||||
|
private val hostComboBox = OutlineComboBox<String>()
|
||||||
|
private val usernameTextField = OutlineTextField(256)
|
||||||
|
private val passwordTextField = OutlinePasswordField(256)
|
||||||
|
|
||||||
|
init {
|
||||||
|
isModal = true
|
||||||
|
title = I18n.getString("termora.actions.quick-connect")
|
||||||
|
isResizable = false
|
||||||
|
init()
|
||||||
|
pack()
|
||||||
|
size = Dimension(UIManager.getInt("Dialog.width") - 250, preferredSize.height)
|
||||||
|
setLocationRelativeTo(owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
hostComboBox.isEditable = true
|
||||||
|
hostComboBox.placeholderText = "ssh://127.0.0.1:22"
|
||||||
|
|
||||||
|
val histories = getHistories()
|
||||||
|
for (history in histories) {
|
||||||
|
if (histories.first() == history) {
|
||||||
|
usernameTextField.text = history.host.username
|
||||||
|
passwordTextField.text = history.host.authentication.password
|
||||||
|
}
|
||||||
|
hostComboBox.addItem(history.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
usernameTextField.placeholderText = I18n.getString("termora.new-host.general.username")
|
||||||
|
passwordTextField.placeholderText = I18n.getString("termora.new-host.general.password")
|
||||||
|
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
return FormBuilder.create().layout(layout)
|
||||||
|
.border(BorderFactory.createEmptyBorder(0, 8, 8, 8))
|
||||||
|
.add("${I18n.getString("termora.new-host.general.protocol")}:").xy(1, 1)
|
||||||
|
.add(hostComboBox).xy(3, 1)
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, 3)
|
||||||
|
.add(usernameTextField).xy(3, 3)
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, 5)
|
||||||
|
.add(passwordTextField).xy(3, 5)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doOKAction() {
|
||||||
|
val host = hostComboBox.selectedItem as? String
|
||||||
|
if (host.isNullOrBlank()) {
|
||||||
|
hostComboBox.requestFocusInWindow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val historyHost: HistoryHost
|
||||||
|
try {
|
||||||
|
historyHost = getHistoryHost(host.trim())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
hostComboBox.requestFocusInWindow()
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
this,
|
||||||
|
e.message ?: ExceptionUtils.getRootCauseMessage(e),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val action = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
|
||||||
|
if (action is OpenHostAction) {
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
action.actionPerformed(OpenHostActionEvent(this, historyHost.host, EventObject(this)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.doOKAction()
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createOkAction(): AbstractAction {
|
||||||
|
return OkAction(I18n.getString("termora.welcome.contextmenu.connect"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getHistoryHost(host: String): HistoryHost {
|
||||||
|
|
||||||
|
|
||||||
|
val uri = URI.create(host)
|
||||||
|
val protocolProvider = ProtocolProvider.valueOf(uri.scheme)
|
||||||
|
if (protocolProvider == null) {
|
||||||
|
throw UnsupportedOperationException(I18n.getString("termora.protocol.not-supported", uri.scheme))
|
||||||
|
}
|
||||||
|
|
||||||
|
val historyHost = HistoryHost(
|
||||||
|
host, Host(
|
||||||
|
name = uri.host,
|
||||||
|
protocol = uri.scheme,
|
||||||
|
host = uri.host,
|
||||||
|
port = uri.port,
|
||||||
|
username = usernameTextField.text.trim(),
|
||||||
|
authentication = Authentication.No.copy(
|
||||||
|
type = AuthenticationType.Password,
|
||||||
|
password = String(passwordTextField.password)
|
||||||
|
),
|
||||||
|
options = Options.Default.copy(
|
||||||
|
extras = mutableMapOf("Temporary" to "true")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val histories = getHistories().toMutableList()
|
||||||
|
histories.removeIf { it.url == host }
|
||||||
|
histories.addFirst(historyHost)
|
||||||
|
|
||||||
|
if (histories.size > 20) {
|
||||||
|
histories.removeLast()
|
||||||
|
}
|
||||||
|
|
||||||
|
properties.putString("QuickConnect.historyHosts", ohMyJson.encodeToString(histories))
|
||||||
|
|
||||||
|
return historyHost
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getHistories(): List<HistoryHost> {
|
||||||
|
val text = properties.getString("QuickConnect.historyHosts", "[]")
|
||||||
|
return ohMyJson.runCatching { ohMyJson.decodeFromString<List<HistoryHost>>(text) }
|
||||||
|
.getOrNull() ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addNotify() {
|
||||||
|
super.addNotify()
|
||||||
|
controlsVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class HistoryHost(
|
||||||
|
val url: String,
|
||||||
|
val host: Host,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
8
src/main/kotlin/app/termora/actions/StateAction.kt
Normal file
8
src/main/kotlin/app/termora/actions/StateAction.kt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.WindowScope
|
||||||
|
|
||||||
|
interface StateAction {
|
||||||
|
fun isSelected(windowScope: WindowScope): Boolean
|
||||||
|
fun setSelected(windowScope: WindowScope, selected: Boolean)
|
||||||
|
}
|
||||||
@@ -504,6 +504,8 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
|
|
||||||
protected open fun putString(key: String, value: String) {
|
protected open fun putString(key: String, value: String) {
|
||||||
databaseManager.setSetting("${name}.$key", value)
|
databaseManager.setSetting("${name}.$key", value)
|
||||||
|
// 触发变动
|
||||||
|
DatabasePropertiesChangedExtension.onPropertyChanged(name, key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package app.termora.database
|
||||||
|
|
||||||
|
import app.termora.database.DatabaseManager.Companion.log
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.ExtensionManager
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
internal interface DatabasePropertiesChangedExtension : Extension {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun onPropertyChanged(name: String, key: String, value: String) {
|
||||||
|
if (SwingUtilities.isEventDispatchThread()) {
|
||||||
|
for (extension in ExtensionManager.getInstance()
|
||||||
|
.getExtensions(DatabasePropertiesChangedExtension::class.java)) {
|
||||||
|
try {
|
||||||
|
extension.onPropertyChanged(name, key, value)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SwingUtilities.invokeLater { onPropertyChanged(name, key, value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 属性数据变动
|
||||||
|
*
|
||||||
|
* @param name 属性名
|
||||||
|
* @param key key
|
||||||
|
*/
|
||||||
|
fun onPropertyChanged(name: String, key: String, value: String)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -22,9 +22,7 @@ class QuickActionsFindEverywhereProvider(private val windowScope: WindowScope) :
|
|||||||
for (action in actions) {
|
for (action in actions) {
|
||||||
val ac = actionManager.getAction(action)
|
val ac = actionManager.getAction(action)
|
||||||
if (ac == null) {
|
if (ac == null) {
|
||||||
if (action == MultipleAction.MULTIPLE) {
|
continue
|
||||||
results.add(ActionFindEverywhereResult(MultipleAction.getInstance(windowScope)))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
results.add(ActionFindEverywhereResult(ac))
|
results.add(ActionFindEverywhereResult(ac))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import app.termora.Icons
|
|||||||
import app.termora.Scope
|
import app.termora.Scope
|
||||||
import app.termora.actions.NewHostAction
|
import app.termora.actions.NewHostAction
|
||||||
import app.termora.actions.OpenLocalTerminalAction
|
import app.termora.actions.OpenLocalTerminalAction
|
||||||
|
import app.termora.actions.QuickConnectAction
|
||||||
import app.termora.snippet.SnippetAction
|
import app.termora.snippet.SnippetAction
|
||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
@@ -19,19 +20,13 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
|
|||||||
actionManager.let { list.add(CreateHostFindEverywhereResult()) }
|
actionManager.let { list.add(CreateHostFindEverywhereResult()) }
|
||||||
|
|
||||||
// Local terminal
|
// Local terminal
|
||||||
actionManager.getAction(OpenLocalTerminalAction.LOCAL_TERMINAL)?.let {
|
actionManager.getAction(OpenLocalTerminalAction.LOCAL_TERMINAL)?.let { list.add(ActionFindEverywhereResult(it)) }
|
||||||
list.add(ActionFindEverywhereResult(it))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snippet
|
// Snippet
|
||||||
actionManager.getAction(SnippetAction.SNIPPET)?.let {
|
actionManager.getAction(SnippetAction.SNIPPET)?.let { list.add(ActionFindEverywhereResult(it)) }
|
||||||
list.add(ActionFindEverywhereResult(it))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SFTP
|
// SFTP
|
||||||
actionManager.getAction(Actions.SFTP)?.let {
|
actionManager.getAction(Actions.SFTP)?.let { list.add(ActionFindEverywhereResult(it)) }
|
||||||
list.add(ActionFindEverywhereResult(it))
|
// quick connect
|
||||||
}
|
actionManager.getAction(QuickConnectAction.QUICK_CONNECT)?.let { list.add(ActionFindEverywhereResult(it)) }
|
||||||
|
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,14 @@
|
|||||||
package app.termora.keymap
|
package app.termora.keymap
|
||||||
|
|
||||||
import app.termora.ApplicationScope
|
import app.termora.ApplicationScope
|
||||||
import app.termora.DialogWrapper
|
|
||||||
import app.termora.Disposable
|
import app.termora.Disposable
|
||||||
import app.termora.SwingUtils
|
|
||||||
import app.termora.account.AccountManager
|
import app.termora.account.AccountManager
|
||||||
import app.termora.actions.AnActionEvent
|
|
||||||
import app.termora.database.Data
|
import app.termora.database.Data
|
||||||
import app.termora.database.DataType
|
import app.termora.database.DataType
|
||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
import app.termora.database.OwnerType
|
import app.termora.database.OwnerType
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.Container
|
|
||||||
import java.awt.KeyEventDispatcher
|
|
||||||
import java.awt.KeyboardFocusManager
|
|
||||||
import java.awt.event.KeyEvent
|
|
||||||
import javax.swing.JComponent
|
|
||||||
import javax.swing.JDialog
|
|
||||||
import javax.swing.JPopupMenu
|
|
||||||
import javax.swing.KeyStroke
|
|
||||||
|
|
||||||
class KeymapManager private constructor() : Disposable {
|
class KeymapManager private constructor() : Disposable {
|
||||||
|
|
||||||
@@ -34,17 +21,13 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val keymapKeyEventDispatcher = KeymapKeyEventDispatcher()
|
|
||||||
private val database get() = DatabaseManager.getInstance()
|
private val database get() = DatabaseManager.getInstance()
|
||||||
private val properties get() = DatabaseManager.getInstance().properties
|
private val properties get() = DatabaseManager.getInstance().properties
|
||||||
private val keymaps = linkedMapOf<String, Keymap>()
|
private val keymaps = linkedMapOf<String, Keymap>()
|
||||||
private val accountManager get() = AccountManager.getInstance()
|
private val accountManager get() = AccountManager.getInstance()
|
||||||
private val activeKeymap get() = properties.getString("Keymap.Active")
|
private val activeKeymap get() = properties.getString("Keymap.Active")
|
||||||
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
keyboardFocusManager.addKeyEventDispatcher(keymapKeyEventDispatcher)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (data in database.rawData(DataType.Keymap)) {
|
for (data in database.rawData(DataType.Keymap)) {
|
||||||
try {
|
try {
|
||||||
@@ -63,13 +46,8 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MacOSKeymap.getInstance().let {
|
MacOSKeymap.getInstance().let { keymaps[it.name] = it }
|
||||||
keymaps[it.name] = it
|
WindowsKeymap.getInstance().let { keymaps[it.name] = it }
|
||||||
}
|
|
||||||
|
|
||||||
WindowsKeymap.getInstance().let {
|
|
||||||
keymaps[it.name] = it
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +80,7 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
keymaps.putFirst(keymap.name, keymap)
|
keymaps.putFirst(keymap.name, keymap)
|
||||||
val accountId = accountManager.getAccountId()
|
val accountId = accountManager.getAccountId()
|
||||||
|
|
||||||
database.save(
|
database.saveAndIncrementVersion(
|
||||||
Data(
|
Data(
|
||||||
id = keymap.id,
|
id = keymap.id,
|
||||||
ownerId = accountId,
|
ownerId = accountId,
|
||||||
@@ -122,84 +100,4 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
database.delete(id, DataType.Keymap.name)
|
database.delete(id, DataType.Keymap.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class KeymapKeyEventDispatcher : KeyEventDispatcher {
|
|
||||||
|
|
||||||
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
|
|
||||||
if (e.isConsumed || e.id != KeyEvent.KEY_PRESSED || e.modifiersEx == 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val keyStroke = KeyStroke.getKeyStrokeForEvent(e)
|
|
||||||
val component = e.source
|
|
||||||
|
|
||||||
if (component is JComponent) {
|
|
||||||
// 如果这个键已经被组件注册了,那么忽略
|
|
||||||
if (getConditionForKeyStroke(component, keyStroke) != JComponent.UNDEFINED_CONDITION) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val shortcuts = getActiveKeymap()
|
|
||||||
val actionIds = shortcuts.getActionIds(KeyShortcut(keyStroke))
|
|
||||||
if (actionIds.isEmpty()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val focusedWindow = keyboardFocusManager.focusedWindow
|
|
||||||
if (focusedWindow is DialogWrapper) {
|
|
||||||
if (!focusedWindow.processGlobalKeymap) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else if (focusedWindow is JDialog) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果当前有 Popup ,那么不派发事件
|
|
||||||
val c = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
|
||||||
if (c is Container) {
|
|
||||||
val popups: List<JPopupMenu> = SwingUtils.getDescendantsOfType(
|
|
||||||
JPopupMenu::class.java,
|
|
||||||
c, true
|
|
||||||
)
|
|
||||||
if (popups.isNotEmpty()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val evt = AnActionEvent(e.source, StringUtils.EMPTY, e)
|
|
||||||
for (actionId in actionIds) {
|
|
||||||
val action = ActionManager.getInstance().getAction(actionId) ?: continue
|
|
||||||
if (!action.isEnabled) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
action.actionPerformed(evt)
|
|
||||||
if (evt.isConsumed) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getConditionForKeyStroke(c: JComponent, keyStroke: KeyStroke): Int {
|
|
||||||
val condition = c.getConditionForKeyStroke(keyStroke)
|
|
||||||
|
|
||||||
// 如果这个键已经被组件注册了,那么忽略
|
|
||||||
if (condition != JComponent.UNDEFINED_CONDITION) {
|
|
||||||
return condition
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c.parent is JComponent) {
|
|
||||||
return getConditionForKeyStroke(c.parent as JComponent, keyStroke)
|
|
||||||
}
|
|
||||||
|
|
||||||
return JComponent.UNDEFINED_CONDITION
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispose() {
|
|
||||||
keyboardFocusManager.removeKeyEventDispatcher(keymapKeyEventDispatcher)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package app.termora.keymgr
|
|||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
|
import app.termora.plugin.internal.ssh.SshClients
|
||||||
import app.termora.terminal.ControlCharacters
|
import app.termora.terminal.ControlCharacters
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.terminal.PtyConnectorDelegate
|
import app.termora.terminal.PtyConnectorDelegate
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import app.termora.plugin.internal.extension.DynamicExtensionPlugin
|
|||||||
import app.termora.plugin.internal.local.LocalInternalPlugin
|
import app.termora.plugin.internal.local.LocalInternalPlugin
|
||||||
import app.termora.plugin.internal.plugin.PluginInternalPlugin
|
import app.termora.plugin.internal.plugin.PluginInternalPlugin
|
||||||
import app.termora.plugin.internal.rdp.RDPInternalPlugin
|
import app.termora.plugin.internal.rdp.RDPInternalPlugin
|
||||||
import app.termora.plugin.internal.serial.SerialInternalPlugin
|
|
||||||
import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin
|
import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin
|
||||||
import app.termora.plugin.internal.ssh.SSHInternalPlugin
|
import app.termora.plugin.internal.ssh.SSHInternalPlugin
|
||||||
|
import app.termora.plugin.internal.telnet.TelnetInternalPlugin
|
||||||
import app.termora.plugin.internal.wsl.WSLInternalPlugin
|
import app.termora.plugin.internal.wsl.WSLInternalPlugin
|
||||||
import app.termora.swingCoroutineScope
|
import app.termora.swingCoroutineScope
|
||||||
import app.termora.terminal.panel.vw.FloatingToolbarPlugin
|
import app.termora.terminal.panel.vw.FloatingToolbarPlugin
|
||||||
@@ -111,12 +111,12 @@ internal class PluginManager private constructor() {
|
|||||||
|
|
||||||
// ssh plugin
|
// ssh plugin
|
||||||
plugins.add(PluginDescriptor(SSHInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
plugins.add(PluginDescriptor(SSHInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
// serial plugin
|
|
||||||
plugins.add(PluginDescriptor(SerialInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
|
||||||
// local plugin
|
// local plugin
|
||||||
plugins.add(PluginDescriptor(LocalInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
plugins.add(PluginDescriptor(LocalInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
// rdp plugin
|
// rdp plugin
|
||||||
plugins.add(PluginDescriptor(RDPInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
plugins.add(PluginDescriptor(RDPInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
|
// telnet plugin
|
||||||
|
plugins.add(PluginDescriptor(TelnetInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
// wsl plugin
|
// wsl plugin
|
||||||
if (SystemUtils.IS_OS_WINDOWS) {
|
if (SystemUtils.IS_OS_WINDOWS) {
|
||||||
plugins.add(PluginDescriptor(WSLInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
plugins.add(PluginDescriptor(WSLInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import java.awt.event.ComponentAdapter
|
|||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
internal class BasicGeneralOption : JPanel(BorderLayout()), OptionsPane.Option {
|
class BasicGeneralOption : JPanel(BorderLayout()), OptionsPane.Option {
|
||||||
val nameTextField = OutlineTextField(128)
|
val nameTextField = OutlineTextField(128)
|
||||||
val remarkTextArea = FixedLengthTextArea(512)
|
val remarkTextArea = FixedLengthTextArea(512)
|
||||||
private val formMargin = "7dlu"
|
private val formMargin = "7dlu"
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import java.awt.Component
|
|||||||
import java.awt.event.ItemEvent
|
import java.awt.event.ItemEvent
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
class BasicProxyOption(private val proxyTypes: List<ProxyType> = listOf(ProxyType.HTTP, ProxyType.SOCKS5)) :
|
class BasicProxyOption(
|
||||||
|
private val proxyTypes: List<ProxyType> = listOf(ProxyType.HTTP, ProxyType.SOCKS5),
|
||||||
|
private val authenticationTypes: List<AuthenticationType> = listOf(AuthenticationType.Password),
|
||||||
|
) :
|
||||||
JPanel(BorderLayout()), Option {
|
JPanel(BorderLayout()), Option {
|
||||||
private val formMargin = "7dlu"
|
private val formMargin = "7dlu"
|
||||||
|
|
||||||
@@ -21,6 +24,10 @@ class BasicProxyOption(private val proxyTypes: List<ProxyType> = listOf(ProxyTyp
|
|||||||
val proxyPortTextField = PortSpinner(1080)
|
val proxyPortTextField = PortSpinner(1080)
|
||||||
val proxyAuthenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
val proxyAuthenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||||
|
|
||||||
|
constructor(proxyTypes: List<ProxyType> = listOf(ProxyType.HTTP, ProxyType.SOCKS5)) : this(
|
||||||
|
proxyTypes,
|
||||||
|
listOf(AuthenticationType.Password)
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -67,9 +74,9 @@ class BasicProxyOption(private val proxyTypes: List<ProxyType> = listOf(ProxyTyp
|
|||||||
}
|
}
|
||||||
|
|
||||||
proxyAuthenticationTypeComboBox.addItem(AuthenticationType.No)
|
proxyAuthenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||||
proxyAuthenticationTypeComboBox.addItem(AuthenticationType.Password)
|
for (type in authenticationTypes) {
|
||||||
|
proxyAuthenticationTypeComboBox.addItem(type)
|
||||||
proxyUsernameTextField.text = "root"
|
}
|
||||||
|
|
||||||
refreshStates()
|
refreshStates()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,30 @@
|
|||||||
package app.termora.plugin.internal.badge
|
package app.termora.plugin.internal.badge
|
||||||
|
|
||||||
import app.termora.WindowScope
|
import app.termora.*
|
||||||
import java.awt.Color
|
import java.awt.Color
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
|
|
||||||
class Badge private constructor() {
|
class Badge private constructor(scope: Scope) {
|
||||||
companion object {
|
companion object {
|
||||||
fun getInstance(scope: WindowScope): Badge {
|
fun getInstance(scope: WindowScope): Badge {
|
||||||
return scope.getOrCreate(Badge::class) { Badge() }
|
return scope.getOrCreate(Badge::class) { Badge(scope) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInstance(): Badge {
|
||||||
|
val scope = ApplicationScope.forApplicationScope()
|
||||||
|
return scope.getOrCreate(Badge::class) { Badge(scope) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
Disposer.register(scope, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
map.clear()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private val map = WeakHashMap<JComponent, BadgePresentation>()
|
private val map = WeakHashMap<JComponent, BadgePresentation>()
|
||||||
|
|
||||||
|
|||||||
@@ -18,4 +18,8 @@ internal class LocalProtocolHostPanelExtension private constructor() : ProtocolH
|
|||||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return LocalProtocolHostPanel()
|
return LocalProtocolHostPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,8 @@ package app.termora.plugin.internal.local
|
|||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.protocol.GenericProtocolProvider
|
import app.termora.protocol.GenericProtocolProvider
|
||||||
import app.termora.protocol.ProtocolTestRequest
|
|
||||||
import app.termora.protocol.ProtocolTester
|
|
||||||
|
|
||||||
internal class LocalProtocolProvider private constructor() : GenericProtocolProvider, ProtocolTester {
|
internal class LocalProtocolProvider private constructor() : GenericProtocolProvider {
|
||||||
companion object {
|
companion object {
|
||||||
val instance by lazy { LocalProtocolProvider() }
|
val instance by lazy { LocalProtocolProvider() }
|
||||||
const val PROTOCOL = "local"
|
const val PROTOCOL = "local"
|
||||||
@@ -20,9 +18,6 @@ internal class LocalProtocolProvider private constructor() : GenericProtocolProv
|
|||||||
return Icons.powershell
|
return Icons.powershell
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun canTestConnection(requester: ProtocolTestRequest): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createTerminalTab(dataProvider: DataProvider, windowScope: WindowScope, host: Host): TerminalTab {
|
override fun createTerminalTab(dataProvider: DataProvider, windowScope: WindowScope, host: Host): TerminalTab {
|
||||||
return LocalTerminalTab(windowScope, host)
|
return LocalTerminalTab(windowScope, host)
|
||||||
|
|||||||
@@ -232,7 +232,8 @@ class PluginPanel(val descriptor: PluginPluginDescriptor) : JPanel(), Disposable
|
|||||||
|
|
||||||
// 当有多个插件正在安装时,那么最后一个安装成功的询问是否重启
|
// 当有多个插件正在安装时,那么最后一个安装成功的询问是否重启
|
||||||
if (installing.get() <= 1) {
|
if (installing.get() <= 1) {
|
||||||
restarter.scheduleRestart(owner)
|
// 不阻塞按钮状态变更
|
||||||
|
SwingUtilities.invokeLater { restarter.scheduleRestart(owner) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是更新,那么也需要刷新 InstalledPanel 下的按钮状态
|
// 如果是更新,那么也需要刷新 InstalledPanel 下的按钮状态
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.formdev.flatlaf.FlatLaf
|
|||||||
import com.formdev.flatlaf.util.UIScale
|
import com.formdev.flatlaf.util.UIScale
|
||||||
import com.github.weisj.jsvg.SVGDocument
|
import com.github.weisj.jsvg.SVGDocument
|
||||||
import com.github.weisj.jsvg.parser.SVGLoader
|
import com.github.weisj.jsvg.parser.SVGLoader
|
||||||
|
import com.github.weisj.jsvg.parser.impl.MutableLoaderContext
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.Graphics
|
import java.awt.Graphics
|
||||||
import java.awt.Graphics2D
|
import java.awt.Graphics2D
|
||||||
@@ -21,8 +22,8 @@ class PluginSVGIcon(input: InputStream, dark: InputStream? = null) : Icon {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val document = svgLoader.load(input)
|
private val document = svgLoader.load(input, null, MutableLoaderContext.createDefault())
|
||||||
private val darkDocument = dark?.let { svgLoader.load(it) }
|
private val darkDocument = dark?.let { svgLoader.load(it, null, MutableLoaderContext.createDefault()) }
|
||||||
|
|
||||||
override fun getIconHeight(): Int {
|
override fun getIconHeight(): Int {
|
||||||
return 32
|
return 32
|
||||||
|
|||||||
@@ -18,4 +18,8 @@ internal class RDPProtocolHostPanelExtension private constructor() : ProtocolHos
|
|||||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return RDPProtocolHostPanel()
|
return RDPProtocolHostPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import app.termora.*
|
|||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
import app.termora.keymgr.KeyManager
|
import app.termora.keymgr.KeyManager
|
||||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||||
|
import app.termora.plugin.internal.ssh.SshClients
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.apache.commons.io.Charsets
|
import org.apache.commons.io.Charsets
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
import app.termora.TerminalTab
|
||||||
|
import app.termora.TerminalTabbedContextMenuExtension
|
||||||
|
import app.termora.WindowScope
|
||||||
|
import app.termora.actions.AnAction
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
|
import javax.swing.JMenuItem
|
||||||
|
|
||||||
|
class CloneSessionTerminalTabbedContextMenuExtension private constructor() : TerminalTabbedContextMenuExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = CloneSessionTerminalTabbedContextMenuExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createJMenuItem(
|
||||||
|
windowScope: WindowScope,
|
||||||
|
tab: TerminalTab
|
||||||
|
): JMenuItem {
|
||||||
|
if (tab is SSHTerminalTab) {
|
||||||
|
if (tab.host.protocol == SSHProtocolProvider.PROTOCOL) {
|
||||||
|
val cloneSession = JMenuItem(I18n.getString("termora.tabbed.contextmenu.clone-session"))
|
||||||
|
val c = tab.getData(SSHTerminalTab.MySshHandler)
|
||||||
|
cloneSession.isEnabled = c?.channel?.isOpen == true
|
||||||
|
if (c != null) {
|
||||||
|
cloneSession.addActionListener(object : AnAction() {
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||||
|
val handler = c.copy(channel = null)
|
||||||
|
val newTab = SSHTerminalTab(windowScope, tab.host, handler)
|
||||||
|
terminalTabbedManager.addTerminalTab(newTab)
|
||||||
|
newTab.start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return cloneSession
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugin.internal.ssh
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import app.termora.TerminalTabbedContextMenuExtension
|
||||||
import app.termora.plugin.Extension
|
import app.termora.plugin.Extension
|
||||||
import app.termora.plugin.InternalPlugin
|
import app.termora.plugin.InternalPlugin
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
@@ -9,6 +10,8 @@ internal class SSHInternalPlugin : InternalPlugin() {
|
|||||||
init {
|
init {
|
||||||
support.addExtension(ProtocolProviderExtension::class.java) { SSHProtocolProviderExtension.instance }
|
support.addExtension(ProtocolProviderExtension::class.java) { SSHProtocolProviderExtension.instance }
|
||||||
support.addExtension(ProtocolHostPanelExtension::class.java) { SSHProtocolHostPanelExtension.instance }
|
support.addExtension(ProtocolHostPanelExtension::class.java) { SSHProtocolHostPanelExtension.instance }
|
||||||
|
support.addExtension(TerminalTabbedContextMenuExtension::class.java) { SftpCommandTerminalTabbedContextMenuExtension.instance }
|
||||||
|
support.addExtension(TerminalTabbedContextMenuExtension::class.java) { CloneSessionTerminalTabbedContextMenuExtension.instance }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String {
|
||||||
|
|||||||
@@ -19,4 +19,8 @@ internal class SSHProtocolHostPanelExtension private constructor() : ProtocolHos
|
|||||||
return SSHProtocolHostPanel(accountOwner)
|
return SSHProtocolHostPanel(accountOwner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,41 +10,36 @@ import app.termora.keymap.KeymapManager
|
|||||||
import app.termora.terminal.ControlCharacters
|
import app.termora.terminal.ControlCharacters
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.apache.commons.io.Charsets
|
import org.apache.commons.io.Charsets
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.apache.sshd.client.SshClient
|
import org.apache.sshd.client.SshClient
|
||||||
import org.apache.sshd.client.channel.ChannelShell
|
import org.apache.sshd.client.channel.ChannelShell
|
||||||
import org.apache.sshd.client.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
import org.apache.sshd.common.SshConstants
|
import org.apache.sshd.common.future.CloseFuture
|
||||||
import org.apache.sshd.common.channel.Channel
|
import org.apache.sshd.common.future.SshFutureListener
|
||||||
import org.apache.sshd.common.channel.ChannelListener
|
|
||||||
import org.apache.sshd.common.session.Session
|
|
||||||
import org.apache.sshd.common.session.SessionListener
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
class SSHTerminalTab(
|
||||||
|
windowScope: WindowScope, host: Host,
|
||||||
|
private val handler: SshHandler = SshHandler()
|
||||||
|
) : PtyHostTerminalTab(windowScope, host) {
|
||||||
|
|
||||||
class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|
||||||
PtyHostTerminalTab(windowScope, host) {
|
|
||||||
companion object {
|
companion object {
|
||||||
val SSHSession = DataKey(ClientSession::class)
|
val SSHSession = DataKey(ClientSession::class)
|
||||||
|
internal val MySshHandler = DataKey(SshHandler::class)
|
||||||
private val log = LoggerFactory.getLogger(SSHTerminalTab::class.java)
|
private val log = LoggerFactory.getLogger(SSHTerminalTab::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
private val tab = this
|
private val owner get() = SwingUtilities.getWindowAncestor(terminalPanel)
|
||||||
|
private val tab get() = this
|
||||||
private var sshClient: SshClient? = null
|
|
||||||
private var sshSession: ClientSession? = null
|
|
||||||
private var sshChannelShell: ChannelShell? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
terminalPanel.dropFiles = false
|
terminalPanel.dropFiles = false
|
||||||
@@ -55,12 +50,10 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
return terminalPanel
|
return terminalPanel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun canReconnect(): Boolean {
|
override fun canReconnect(): Boolean {
|
||||||
return !mutex.isLocked
|
return mutex.isLocked.not()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override suspend fun openPtyConnector(): PtyConnector {
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
if (mutex.tryLock()) {
|
if (mutex.tryLock()) {
|
||||||
try {
|
try {
|
||||||
@@ -82,74 +75,32 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
// hide cursor
|
// hide cursor
|
||||||
terminalModel.setData(DataKey.Companion.ShowCursor, false)
|
terminalModel.setData(DataKey.Companion.ShowCursor, false)
|
||||||
// print
|
// print
|
||||||
terminal.write("SSH client is opening...\r\n")
|
terminal.write("Connecting to remote server ")
|
||||||
}
|
}
|
||||||
|
|
||||||
val owner = SwingUtilities.getWindowAncestor(terminalPanel)
|
val loading = coroutineScope.launch(Dispatchers.Swing) {
|
||||||
val client = SshClients.openClient(host, owner).also { sshClient = it }
|
var c = 0
|
||||||
val sessionListener = MySessionListener()
|
while (isActive) {
|
||||||
val channelListener = MyChannelListener()
|
if (++c > 6) c = 1
|
||||||
|
terminal.write("${ControlCharacters.ESC}[1;32m")
|
||||||
withContext(Dispatchers.Swing) { terminal.write("SSH client opened successfully.\r\n\r\n") }
|
terminal.write(".".repeat(c))
|
||||||
|
terminal.write(" ".repeat(6 - c))
|
||||||
client.addSessionListener(sessionListener)
|
terminal.write("${ControlCharacters.ESC}[0m")
|
||||||
client.addChannelListener(channelListener)
|
delay(350.milliseconds)
|
||||||
|
terminal.write("${ControlCharacters.BS}".repeat(6))
|
||||||
val (session, channel) = try {
|
|
||||||
val session = SshClients.openSession(host, client).also { sshSession = it }
|
|
||||||
val channel = SshClients.openShell(
|
|
||||||
host,
|
|
||||||
terminalPanel.winSize(),
|
|
||||||
session
|
|
||||||
).also { sshChannelShell = it }
|
|
||||||
Pair(session, channel)
|
|
||||||
} finally {
|
|
||||||
client.removeSessionListener(sessionListener)
|
|
||||||
client.removeChannelListener(channelListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newline
|
|
||||||
withContext(Dispatchers.Swing) {
|
|
||||||
terminal.write("\r\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
channel.addChannelListener(object : ChannelListener {
|
|
||||||
private val reconnectShortcut
|
|
||||||
get() = KeymapManager.Companion.getInstance().getActiveKeymap()
|
|
||||||
.getShortcut(TabReconnectAction.Companion.RECONNECT_TAB).firstOrNull()
|
|
||||||
|
|
||||||
override fun channelClosed(channel: Channel, reason: Throwable?) {
|
|
||||||
coroutineScope.launch(Dispatchers.Swing) {
|
|
||||||
terminal.write("\r\n\r\n${ControlCharacters.Companion.ESC}[31m")
|
|
||||||
terminal.write(I18n.getString("termora.terminal.channel-disconnected"))
|
|
||||||
if (reconnectShortcut is KeyShortcut) {
|
|
||||||
terminal.write(
|
|
||||||
I18n.getString(
|
|
||||||
"termora.terminal.channel-reconnect",
|
|
||||||
reconnectShortcut.toString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
terminal.write("\r\n")
|
|
||||||
terminal.write("${ControlCharacters.Companion.ESC}[0m")
|
|
||||||
terminalModel.setData(DataKey.Companion.ShowCursor, false)
|
|
||||||
if (DatabaseManager.getInstance().terminal.autoCloseTabWhenDisconnected) {
|
|
||||||
terminalTabbedManager?.let { manager ->
|
|
||||||
SwingUtilities.invokeLater {
|
|
||||||
manager.closeTerminalTab(tab, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop
|
|
||||||
stop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
// 打开隧道
|
val channel: ChannelShell
|
||||||
openTunnelings(session, host)
|
try {
|
||||||
|
val client = openClient()
|
||||||
|
val session = openSession(client)
|
||||||
|
channel = openChannel(session)
|
||||||
|
// 打开隧道
|
||||||
|
openTunnelings(session, host)
|
||||||
|
} finally {
|
||||||
|
loading.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
// 隐藏提示
|
// 隐藏提示
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
@@ -194,10 +145,68 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openClient(): SshClient {
|
||||||
|
val client = handler.client
|
||||||
|
if (client != null) return client
|
||||||
|
return SshClients.openClient(host, owner).also { handler.client = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openSession(client: SshClient): ClientSession {
|
||||||
|
val session = handler.session
|
||||||
|
if (session != null) return SshSessionPool.register(session, client)
|
||||||
|
return SshClients.openSession(host, client).also { handler.session = SshSessionPool.register(it, client) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openChannel(session: ClientSession): ChannelShell {
|
||||||
|
val channel = SshClients.openShell(host, terminalPanel.winSize(), session)
|
||||||
|
handler.channel = channel
|
||||||
|
|
||||||
|
channel.addCloseFutureListener(object : SshFutureListener<CloseFuture> {
|
||||||
|
private val reconnectShortcut
|
||||||
|
get() = KeymapManager.Companion.getInstance().getActiveKeymap()
|
||||||
|
.getShortcut(TabReconnectAction.Companion.RECONNECT_TAB).firstOrNull()
|
||||||
|
private val autoCloseTabWhenDisconnected get() = DatabaseManager.getInstance().terminal.autoCloseTabWhenDisconnected
|
||||||
|
|
||||||
|
override fun operationComplete(future: CloseFuture) {
|
||||||
|
coroutineScope.launch(Dispatchers.Swing) {
|
||||||
|
terminal.write("\r\n\r\n${ControlCharacters.Companion.ESC}[31m")
|
||||||
|
terminal.write(I18n.getString("termora.terminal.channel-disconnected"))
|
||||||
|
if (reconnectShortcut is KeyShortcut) {
|
||||||
|
terminal.write(
|
||||||
|
I18n.getString(
|
||||||
|
"termora.terminal.channel-reconnect",
|
||||||
|
reconnectShortcut.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
terminal.write("\r\n")
|
||||||
|
terminal.write("${ControlCharacters.Companion.ESC}[0m")
|
||||||
|
terminalModel.setData(DataKey.Companion.ShowCursor, false)
|
||||||
|
|
||||||
|
if (autoCloseTabWhenDisconnected) {
|
||||||
|
terminalTabbedManager?.let { manager ->
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
manager.closeTerminalTab(tab, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
if (dataKey == SSHSession) {
|
if (dataKey == SSHSession) {
|
||||||
return sshSession as T?
|
return handler.session as T?
|
||||||
|
}
|
||||||
|
if (dataKey == MySshHandler) {
|
||||||
|
return handler as T?
|
||||||
}
|
}
|
||||||
return super.getData(dataKey)
|
return super.getData(dataKey)
|
||||||
}
|
}
|
||||||
@@ -206,16 +215,7 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
if (mutex.tryLock()) {
|
if (mutex.tryLock()) {
|
||||||
try {
|
try {
|
||||||
super.stop()
|
super.stop()
|
||||||
|
handler.close()
|
||||||
sshChannelShell?.close(true)
|
|
||||||
sshSession?.disableSessionHeartbeat()
|
|
||||||
sshSession?.disconnect(SshConstants.SSH2_DISCONNECT_BY_APPLICATION, StringUtils.EMPTY)
|
|
||||||
sshSession?.close(true)
|
|
||||||
sshClient?.close(true)
|
|
||||||
|
|
||||||
sshChannelShell = null
|
|
||||||
sshSession = null
|
|
||||||
sshClient = null
|
|
||||||
} finally {
|
} finally {
|
||||||
mutex.unlock()
|
mutex.unlock()
|
||||||
}
|
}
|
||||||
@@ -231,36 +231,4 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
terminalPanel.storeVisualWindows(host.id)
|
terminalPanel.storeVisualWindows(host.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class MySessionListener : SessionListener, Disposable {
|
|
||||||
override fun sessionEvent(session: Session, event: SessionListener.Event) {
|
|
||||||
coroutineScope.launch {
|
|
||||||
when (event) {
|
|
||||||
SessionListener.Event.KeyEstablished -> terminal.write("Session Key exchange successful.\r\n")
|
|
||||||
SessionListener.Event.Authenticated -> terminal.write("Session authentication successful.\r\n\r\n")
|
|
||||||
SessionListener.Event.KexCompleted -> terminal.write("Session KEX negotiation successful.\r\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun sessionEstablished(session: Session) {
|
|
||||||
coroutineScope.launch { terminal.write("Session established.\r\n") }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun sessionCreated(session: Session?) {
|
|
||||||
coroutineScope.launch { terminal.write("Session created.\r\n") }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class MyChannelListener : ChannelListener, Disposable {
|
|
||||||
override fun channelOpenSuccess(channel: Channel) {
|
|
||||||
coroutineScope.launch { terminal.write("Channel shell opened successfully.\r\n") }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun channelInitialized(channel: Channel) {
|
|
||||||
coroutineScope.launch { terminal.write("Channel shell initialization successful.\r\n") }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.actions.*
|
||||||
|
import app.termora.plugin.internal.sftppty.SFTPPtyProtocolProvider
|
||||||
|
import app.termora.plugin.internal.sftppty.SFTPPtyTerminalTab
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.Action
|
||||||
|
import javax.swing.JMenuItem
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
|
||||||
|
class SftpCommandTerminalTabbedContextMenuExtension private constructor() : TerminalTabbedContextMenuExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = SftpCommandTerminalTabbedContextMenuExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val actionManager = ActionManager.getInstance()
|
||||||
|
|
||||||
|
override fun createJMenuItem(
|
||||||
|
windowScope: WindowScope,
|
||||||
|
tab: TerminalTab
|
||||||
|
): JMenuItem {
|
||||||
|
if (tab is HostTerminalTab) {
|
||||||
|
val openHostAction = actionManager.getAction(OpenHostAction.OPEN_HOST)
|
||||||
|
if (openHostAction != null) {
|
||||||
|
if (tab.host.protocol == SSHProtocolProvider.PROTOCOL || tab.host.protocol == SFTPPtyProtocolProvider.PROTOCOL) {
|
||||||
|
val sftpCommand = JMenuItem(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
|
||||||
|
sftpCommand.addActionListener(object : AnAction() {
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
openSFTPPtyTab(tab, openHostAction, evt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return sftpCommand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openSFTPPtyTab(tab: HostTerminalTab, openHostAction: Action, evt: EventObject) {
|
||||||
|
if (SFTPPtyTerminalTab.canSupports.not()) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
tab.windowScope.window,
|
||||||
|
I18n.getString("termora.tabbed.contextmenu.sftp-not-install"),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var host = tab.host
|
||||||
|
|
||||||
|
if (host.protocol == SSHProtocolProvider.PROTOCOL) {
|
||||||
|
val envs = tab.host.options.envs().toMutableMap()
|
||||||
|
val currentDir = tab.getData(DataProviders.Terminal)?.getTerminalModel()
|
||||||
|
?.getData(DataKey.CurrentDir, StringUtils.EMPTY) ?: StringUtils.EMPTY
|
||||||
|
|
||||||
|
if (currentDir.isNotBlank()) {
|
||||||
|
envs["CurrentDir"] = currentDir
|
||||||
|
}
|
||||||
|
|
||||||
|
host = host.copy(
|
||||||
|
protocol = SFTPPtyProtocolProvider.PROTOCOL,
|
||||||
|
options = host.options.copy(env = envs.toPropertiesString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
openHostAction.actionPerformed(OpenHostActionEvent(evt.source, host, evt))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||||
import app.termora.terminal.TerminalSize
|
import app.termora.terminal.TerminalSize
|
||||||
@@ -29,7 +30,6 @@ import org.apache.sshd.client.session.ClientSession
|
|||||||
import org.apache.sshd.client.session.ClientSessionImpl
|
import org.apache.sshd.client.session.ClientSessionImpl
|
||||||
import org.apache.sshd.client.session.SessionFactory
|
import org.apache.sshd.client.session.SessionFactory
|
||||||
import org.apache.sshd.common.AttributeRepository
|
import org.apache.sshd.common.AttributeRepository
|
||||||
import org.apache.sshd.common.SshConstants
|
|
||||||
import org.apache.sshd.common.SshException
|
import org.apache.sshd.common.SshException
|
||||||
import org.apache.sshd.common.channel.ChannelFactory
|
import org.apache.sshd.common.channel.ChannelFactory
|
||||||
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
||||||
@@ -63,7 +63,7 @@ import org.eclipse.jgit.internal.transport.sshd.proxy.AbstractClientProxyConnect
|
|||||||
import org.eclipse.jgit.internal.transport.sshd.proxy.HttpClientConnector
|
import org.eclipse.jgit.internal.transport.sshd.proxy.HttpClientConnector
|
||||||
import org.eclipse.jgit.internal.transport.sshd.proxy.Socks5ClientConnector
|
import org.eclipse.jgit.internal.transport.sshd.proxy.Socks5ClientConnector
|
||||||
import org.eclipse.jgit.transport.CredentialsProvider
|
import org.eclipse.jgit.transport.CredentialsProvider
|
||||||
import org.eclipse.jgit.transport.SshConstants.IDENTITY_AGENT
|
import org.eclipse.jgit.transport.SshConstants
|
||||||
import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
|
import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
|
||||||
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory
|
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -89,7 +89,7 @@ object SshClients {
|
|||||||
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
|
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
|
||||||
|
|
||||||
private val timeout = Duration.ofSeconds(30)
|
private val timeout = Duration.ofSeconds(30)
|
||||||
private val hostManager get() = HostManager.getInstance()
|
private val hostManager get() = HostManager.Companion.getInstance()
|
||||||
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
|
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,7 +166,7 @@ object SshClients {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val jumpHosts = mutableListOf<Host>()
|
val jumpHosts = mutableListOf<Host>()
|
||||||
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
val hosts = HostManager.Companion.getInstance().hosts().associateBy { it.id }
|
||||||
for (jumpHostId in h.options.jumpHosts) {
|
for (jumpHostId in h.options.jumpHosts) {
|
||||||
val e = hosts[jumpHostId]
|
val e = hosts[jumpHostId]
|
||||||
if (e == null) {
|
if (e == null) {
|
||||||
@@ -235,22 +235,23 @@ object SshClients {
|
|||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isMacOS) {
|
||||||
val file = FileUtils.getFile(Application.getBaseDataDir(), "config", "ssh-agent.sock")
|
val file = FileUtils.getFile(Application.getBaseDataDir(), "config", "ssh-agent.sock")
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
entry.setProperty(IDENTITY_AGENT, file.absolutePath)
|
entry.setProperty(SshConstants.IDENTITY_AGENT, file.absolutePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (entry.getProperty(IDENTITY_AGENT).isNullOrBlank()) {
|
if (entry.getProperty(SshConstants.IDENTITY_AGENT).isNullOrBlank()) {
|
||||||
if (host.authentication.password.isNotBlank())
|
if (host.authentication.password.isNotBlank())
|
||||||
entry.setProperty(IDENTITY_AGENT, host.authentication.password)
|
entry.setProperty(SshConstants.IDENTITY_AGENT, host.authentication.password)
|
||||||
else if (SystemInfo.isWindows)
|
else if (SystemInfo.isWindows)
|
||||||
entry.setProperty(IDENTITY_AGENT, PageantConnector.DESCRIPTOR.identityAgent)
|
entry.setProperty(SshConstants.IDENTITY_AGENT, PageantConnector.DESCRIPTOR.identityAgent)
|
||||||
else
|
else
|
||||||
entry.setProperty(IDENTITY_AGENT, UnixDomainSocketConnector.DESCRIPTOR.identityAgent)
|
entry.setProperty(SshConstants.IDENTITY_AGENT, UnixDomainSocketConnector.DESCRIPTOR.identityAgent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val session = client.connect(entry).verify(timeout).session
|
val session = client.connect(entry).verify(timeout).session
|
||||||
if (host.authentication.type == AuthenticationType.Password) {
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
session.addPasswordIdentity(host.authentication.password)
|
if (StringUtils.isNotBlank(host.authentication.password))
|
||||||
|
session.addPasswordIdentity(host.authentication.password)
|
||||||
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||||
session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password)
|
session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password)
|
||||||
}
|
}
|
||||||
@@ -272,7 +273,7 @@ object SshClients {
|
|||||||
throw SshException("Authentication failed")
|
throw SshException("Authentication failed")
|
||||||
}
|
}
|
||||||
} 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 != org.apache.sshd.common.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 askUserInfo = ask(host, entry, owner) ?: throw e
|
val askUserInfo = ask(host, entry, owner) ?: throw e
|
||||||
if (askUserInfo.authentication.type == AuthenticationType.No) throw e
|
if (askUserInfo.authentication.type == AuthenticationType.No) throw e
|
||||||
@@ -383,7 +384,7 @@ object SshClients {
|
|||||||
|
|
||||||
val channelFactories = mutableListOf<ChannelFactory>()
|
val channelFactories = mutableListOf<ChannelFactory>()
|
||||||
channelFactories.addAll(ClientBuilder.DEFAULT_CHANNEL_FACTORIES)
|
channelFactories.addAll(ClientBuilder.DEFAULT_CHANNEL_FACTORIES)
|
||||||
channelFactories.add(X11ChannelFactory.INSTANCE)
|
channelFactories.add(X11ChannelFactory.Companion.INSTANCE)
|
||||||
builder.channelFactories(channelFactories)
|
builder.channelFactories(channelFactories)
|
||||||
|
|
||||||
val sshClient = builder.build() as JGitSshClient
|
val sshClient = builder.build() as JGitSshClient
|
||||||
@@ -726,4 +727,3 @@ object SshClients {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import org.apache.sshd.client.SshClient
|
||||||
|
import org.apache.sshd.client.session.ClientSession
|
||||||
|
import org.apache.sshd.common.channel.Channel
|
||||||
|
|
||||||
|
data class SshHandler(
|
||||||
|
var client: SshClient? = null,
|
||||||
|
var session: ClientSession? = null,
|
||||||
|
var channel: Channel? = null
|
||||||
|
) : AutoCloseable {
|
||||||
|
override fun close() {
|
||||||
|
|
||||||
|
channel?.close(true)?.await()
|
||||||
|
session?.close(true)?.await()
|
||||||
|
|
||||||
|
channel = null
|
||||||
|
session = null
|
||||||
|
|
||||||
|
|
||||||
|
// client 由 SshSessionPool 负责关闭
|
||||||
|
if (client?.isClosing == true || client?.isClosed == true) {
|
||||||
|
client = null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import org.apache.sshd.client.SshClient
|
||||||
|
import org.apache.sshd.client.channel.ChannelExec
|
||||||
|
import org.apache.sshd.client.channel.ChannelShell
|
||||||
|
import org.apache.sshd.client.session.ClientSession
|
||||||
|
import org.apache.sshd.client.session.forward.DynamicPortForwardingTracker
|
||||||
|
import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker
|
||||||
|
import org.apache.sshd.common.AttributeRepository
|
||||||
|
import org.apache.sshd.common.channel.Channel
|
||||||
|
import org.apache.sshd.common.channel.PtyChannelConfigurationHolder
|
||||||
|
import org.apache.sshd.common.channel.throttle.ChannelStreamWriter
|
||||||
|
import org.apache.sshd.common.channel.throttle.ChannelStreamWriterResolver
|
||||||
|
import org.apache.sshd.common.future.CloseFuture
|
||||||
|
import org.apache.sshd.common.future.DefaultCloseFuture
|
||||||
|
import org.apache.sshd.common.io.IoWriteFuture
|
||||||
|
import org.apache.sshd.common.session.SessionHeartbeatController
|
||||||
|
import org.apache.sshd.common.util.buffer.Buffer
|
||||||
|
import org.apache.sshd.common.util.net.SshdSocketAddress
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.net.SocketAddress
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import java.util.function.Function
|
||||||
|
|
||||||
|
internal object SshSessionPool {
|
||||||
|
private val map = WeakHashMap<ClientSession, MyClientSession>()
|
||||||
|
|
||||||
|
fun register(session: ClientSession, client: SshClient): ClientSession {
|
||||||
|
if (session is MyClientSession) {
|
||||||
|
session.refCount.incrementAndGet()
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(session) {
|
||||||
|
val delegate = map[session] ?: MyClientSession(client, session)
|
||||||
|
map[session] = delegate
|
||||||
|
delegate.refCount.incrementAndGet()
|
||||||
|
return delegate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class MyClientSession(
|
||||||
|
private val client: SshClient,
|
||||||
|
private val delegate: ClientSession
|
||||||
|
) : ClientSession by delegate {
|
||||||
|
val refCount = AtomicInteger(0)
|
||||||
|
|
||||||
|
override fun createShellChannel(): ChannelShell? {
|
||||||
|
return delegate.createShellChannel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createExecChannel(command: String?): ChannelExec? {
|
||||||
|
return delegate.createExecChannel(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createExecChannel(
|
||||||
|
command: String?,
|
||||||
|
ptyConfig: PtyChannelConfigurationHolder?,
|
||||||
|
env: Map<String?, *>?
|
||||||
|
): ChannelExec? {
|
||||||
|
return delegate.createExecChannel(command, ptyConfig, env)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun executeRemoteCommand(command: String?): String? {
|
||||||
|
return delegate.executeRemoteCommand(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun executeRemoteCommand(
|
||||||
|
command: String?,
|
||||||
|
stderr: OutputStream?,
|
||||||
|
charset: Charset?
|
||||||
|
): String? {
|
||||||
|
return delegate.executeRemoteCommand(command, stderr, charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun executeRemoteCommand(
|
||||||
|
command: String?,
|
||||||
|
stdout: OutputStream?,
|
||||||
|
stderr: OutputStream?,
|
||||||
|
charset: Charset?
|
||||||
|
) {
|
||||||
|
delegate.executeRemoteCommand(command, stdout, stderr, charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createLocalPortForwardingTracker(
|
||||||
|
localPort: Int,
|
||||||
|
remote: SshdSocketAddress?
|
||||||
|
): ExplicitPortForwardingTracker? {
|
||||||
|
return delegate.createLocalPortForwardingTracker(localPort, remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createLocalPortForwardingTracker(
|
||||||
|
local: SshdSocketAddress?,
|
||||||
|
remote: SshdSocketAddress?
|
||||||
|
): ExplicitPortForwardingTracker? {
|
||||||
|
return delegate.createLocalPortForwardingTracker(local, remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createRemotePortForwardingTracker(
|
||||||
|
remote: SshdSocketAddress?,
|
||||||
|
local: SshdSocketAddress?
|
||||||
|
): ExplicitPortForwardingTracker? {
|
||||||
|
return delegate.createRemotePortForwardingTracker(remote, local)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createDynamicPortForwardingTracker(local: SshdSocketAddress?): DynamicPortForwardingTracker? {
|
||||||
|
return delegate.createDynamicPortForwardingTracker(local)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun waitFor(
|
||||||
|
mask: Collection<ClientSession.ClientSessionEvent?>?,
|
||||||
|
timeout: Duration?
|
||||||
|
): Set<ClientSession.ClientSessionEvent?>? {
|
||||||
|
return delegate.waitFor(mask, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createBuffer(cmd: Byte): Buffer? {
|
||||||
|
return delegate.createBuffer(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writePacket(
|
||||||
|
buffer: Buffer?,
|
||||||
|
timeout: Duration?
|
||||||
|
): IoWriteFuture? {
|
||||||
|
return delegate.writePacket(buffer, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writePacket(
|
||||||
|
buffer: Buffer?,
|
||||||
|
maxWaitMillis: Long
|
||||||
|
): IoWriteFuture? {
|
||||||
|
return delegate.writePacket(buffer, maxWaitMillis)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun request(
|
||||||
|
request: String?,
|
||||||
|
buffer: Buffer?,
|
||||||
|
timeout: Long,
|
||||||
|
unit: TimeUnit?
|
||||||
|
): Buffer? {
|
||||||
|
return delegate.request(request, buffer, timeout, unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun request(
|
||||||
|
request: String?,
|
||||||
|
buffer: Buffer?,
|
||||||
|
timeout: Duration?
|
||||||
|
): Buffer? {
|
||||||
|
return delegate.request(request, buffer, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLocalAddress(): SocketAddress? {
|
||||||
|
return delegate.getLocalAddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRemoteAddress(): SocketAddress? {
|
||||||
|
return delegate.getRemoteAddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : Any?> resolveAttribute(key: AttributeRepository.AttributeKey<T?>?): T? {
|
||||||
|
return delegate.resolveAttribute(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSessionHeartbeatType(): SessionHeartbeatController.HeartbeatType? {
|
||||||
|
return delegate.getSessionHeartbeatType()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSessionHeartbeatInterval(): Duration? {
|
||||||
|
return delegate.getSessionHeartbeatInterval()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disableSessionHeartbeat() {
|
||||||
|
delegate.disableSessionHeartbeat()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSessionHeartbeat(
|
||||||
|
type: SessionHeartbeatController.HeartbeatType?,
|
||||||
|
unit: TimeUnit?,
|
||||||
|
count: Long
|
||||||
|
) {
|
||||||
|
delegate.setSessionHeartbeat(type, unit, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSessionHeartbeat(
|
||||||
|
type: SessionHeartbeatController.HeartbeatType?,
|
||||||
|
interval: Duration?
|
||||||
|
) {
|
||||||
|
delegate.setSessionHeartbeat(type, interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isEmpty(): Boolean {
|
||||||
|
return delegate.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLongProperty(name: String?, def: Long): Long {
|
||||||
|
return delegate.getLongProperty(name, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLong(name: String?): Long? {
|
||||||
|
return delegate.getLong(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIntProperty(name: String?, def: Int): Int {
|
||||||
|
return delegate.getIntProperty(name, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInteger(name: String?): Int? {
|
||||||
|
return delegate.getInteger(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBooleanProperty(name: String?, def: Boolean): Boolean {
|
||||||
|
return delegate.getBooleanProperty(name, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBoolean(name: String?): Boolean? {
|
||||||
|
return delegate.getBoolean(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStringProperty(name: String?, def: String?): String? {
|
||||||
|
return delegate.getStringProperty(name, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(name: String?): String? {
|
||||||
|
return delegate.getString(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getObject(name: String?): Any? {
|
||||||
|
return delegate.getObject(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCharset(
|
||||||
|
name: String?,
|
||||||
|
defaultValue: Charset?
|
||||||
|
): Charset? {
|
||||||
|
return delegate.getCharset(name, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : Any?> computeAttributeIfAbsent(
|
||||||
|
key: AttributeRepository.AttributeKey<T?>?,
|
||||||
|
resolver: Function<in AttributeRepository.AttributeKey<T>, out T?>?
|
||||||
|
): T? {
|
||||||
|
return delegate.computeAttributeIfAbsent(key, resolver)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
close(true)?.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close(immediately: Boolean): CloseFuture? {
|
||||||
|
synchronized(delegate) {
|
||||||
|
if (refCount.decrementAndGet() < 1) {
|
||||||
|
delegate.close(immediately).await()
|
||||||
|
return client.close(immediately)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DefaultCloseFuture(this, this).apply { setClosed() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isOpen(): Boolean {
|
||||||
|
return delegate.isOpen()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCipherFactoriesNameList(): String? {
|
||||||
|
return delegate.getCipherFactoriesNameList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCipherFactoriesNames(): List<String?>? {
|
||||||
|
return delegate.getCipherFactoriesNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCipherFactoriesNameList(names: String?) {
|
||||||
|
delegate.setCipherFactoriesNameList(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCipherFactoriesNames(vararg names: String?) {
|
||||||
|
delegate.setCipherFactoriesNames(*names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCipherFactoriesNames(names: Collection<String?>?) {
|
||||||
|
delegate.setCipherFactoriesNames(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCompressionFactoriesNameList(): String? {
|
||||||
|
return delegate.getCompressionFactoriesNameList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCompressionFactoriesNames(): List<String?>? {
|
||||||
|
return delegate.getCompressionFactoriesNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCompressionFactoriesNameList(names: String?) {
|
||||||
|
delegate.setCompressionFactoriesNameList(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCompressionFactoriesNames(vararg names: String?) {
|
||||||
|
delegate.setCompressionFactoriesNames(*names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCompressionFactoriesNames(names: Collection<String?>?) {
|
||||||
|
delegate.setCompressionFactoriesNames(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMacFactoriesNameList(): String? {
|
||||||
|
return delegate.getMacFactoriesNameList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMacFactoriesNames(): List<String?>? {
|
||||||
|
return delegate.getMacFactoriesNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMacFactoriesNameList(names: String?) {
|
||||||
|
delegate.setMacFactoriesNameList(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMacFactoriesNames(vararg names: String?) {
|
||||||
|
delegate.setMacFactoriesNames(*names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMacFactoriesNames(names: Collection<String?>?) {
|
||||||
|
delegate.setMacFactoriesNames(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSignatureFactoriesNameList(names: String?) {
|
||||||
|
delegate.setSignatureFactoriesNameList(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSignatureFactoriesNames(vararg names: String?) {
|
||||||
|
delegate.setSignatureFactoriesNames(*names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSignatureFactoriesNames(names: Collection<String?>?) {
|
||||||
|
delegate.setSignatureFactoriesNames(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSignatureFactoriesNameList(): String? {
|
||||||
|
return delegate.getSignatureFactoriesNameList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSignatureFactoriesNames(): List<String?>? {
|
||||||
|
return delegate.getSignatureFactoriesNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolveChannelStreamWriterResolver(): ChannelStreamWriterResolver? {
|
||||||
|
return delegate.resolveChannelStreamWriterResolver()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolveChannelStreamWriter(
|
||||||
|
channel: Channel?,
|
||||||
|
cmd: Byte
|
||||||
|
): ChannelStreamWriter? {
|
||||||
|
return delegate.resolveChannelStreamWriter(channel, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isLocalPortForwardingStartedForPort(port: Int): Boolean {
|
||||||
|
return delegate.isLocalPortForwardingStartedForPort(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isRemotePortForwardingStartedForPort(port: Int): Boolean {
|
||||||
|
return delegate.isRemotePortForwardingStartedForPort(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setUserAuthFactoriesNames(names: Collection<String?>?) {
|
||||||
|
delegate.setUserAuthFactoriesNames(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setUserAuthFactoriesNames(vararg names: String?) {
|
||||||
|
delegate.setUserAuthFactoriesNames(*names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getUserAuthFactoriesNameList(): String? {
|
||||||
|
return delegate.getUserAuthFactoriesNameList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getUserAuthFactoriesNames(): List<String?>? {
|
||||||
|
return delegate.getUserAuthFactoriesNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setUserAuthFactoriesNameList(names: String?) {
|
||||||
|
delegate.setUserAuthFactoriesNameList(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startLocalPortForwarding(
|
||||||
|
localPort: Int,
|
||||||
|
remote: SshdSocketAddress?
|
||||||
|
): SshdSocketAddress? {
|
||||||
|
return delegate.startLocalPortForwarding(localPort, remote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,448 @@
|
|||||||
|
package app.termora.plugin.internal.telnet
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.plugin.internal.BasicProxyOption
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.KeyboardFocusManager
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.event.ComponentAdapter
|
||||||
|
import java.awt.event.ComponentEvent
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() {
|
||||||
|
protected val generalOption = GeneralOption()
|
||||||
|
|
||||||
|
// telnet 不支持代理密码
|
||||||
|
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
|
||||||
|
private val terminalOption = TerminalOption()
|
||||||
|
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
|
init {
|
||||||
|
addOption(generalOption)
|
||||||
|
addOption(proxyOption)
|
||||||
|
addOption(terminalOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getHost(): Host {
|
||||||
|
val name = generalOption.nameTextField.text
|
||||||
|
val protocol = TelnetProtocolProvider.PROTOCOL
|
||||||
|
val host = generalOption.hostTextField.text
|
||||||
|
val port = (generalOption.portTextField.value ?: 23) as Int
|
||||||
|
var authentication = Authentication.No
|
||||||
|
var proxy = Proxy.Companion.No
|
||||||
|
val authenticationType = generalOption.authenticationTypeComboBox.selectedItem as AuthenticationType
|
||||||
|
|
||||||
|
if (authenticationType == AuthenticationType.Password) {
|
||||||
|
authentication = authentication.copy(
|
||||||
|
type = authenticationType,
|
||||||
|
password = String(generalOption.passwordTextField.password)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||||
|
proxy = proxy.copy(
|
||||||
|
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
|
||||||
|
host = proxyOption.proxyHostTextField.text,
|
||||||
|
username = proxyOption.proxyUsernameTextField.text,
|
||||||
|
password = String(proxyOption.proxyPasswordTextField.password),
|
||||||
|
port = proxyOption.proxyPortTextField.value as Int,
|
||||||
|
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val serialComm = SerialComm()
|
||||||
|
|
||||||
|
val options = Options.Companion.Default.copy(
|
||||||
|
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
||||||
|
env = terminalOption.environmentTextArea.text,
|
||||||
|
startupCommand = terminalOption.startupCommandTextField.text,
|
||||||
|
serialComm = serialComm,
|
||||||
|
extras = mutableMapOf("backspace" to (terminalOption.backspaceComboBox.selectedItem as Backspace).name)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Host(
|
||||||
|
name = name,
|
||||||
|
protocol = protocol,
|
||||||
|
host = host,
|
||||||
|
port = port,
|
||||||
|
username = generalOption.usernameTextField.text,
|
||||||
|
authentication = authentication,
|
||||||
|
proxy = proxy,
|
||||||
|
sort = System.currentTimeMillis(),
|
||||||
|
remark = generalOption.remarkTextArea.text,
|
||||||
|
options = options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHost(host: Host) {
|
||||||
|
generalOption.portTextField.value = host.port
|
||||||
|
generalOption.nameTextField.text = host.name
|
||||||
|
generalOption.usernameTextField.text = host.username
|
||||||
|
generalOption.hostTextField.text = host.host
|
||||||
|
generalOption.remarkTextArea.text = host.remark
|
||||||
|
generalOption.authenticationTypeComboBox.selectedItem = host.authentication.type
|
||||||
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
|
generalOption.passwordTextField.text = host.authentication.password
|
||||||
|
}
|
||||||
|
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||||
|
proxyOption.proxyHostTextField.text = host.proxy.host
|
||||||
|
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
||||||
|
proxyOption.proxyUsernameTextField.text = host.proxy.username
|
||||||
|
proxyOption.proxyPortTextField.value = host.proxy.port
|
||||||
|
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
|
||||||
|
|
||||||
|
terminalOption.charsetComboBox.selectedItem = host.options.encoding
|
||||||
|
terminalOption.environmentTextArea.text = host.options.env
|
||||||
|
terminalOption.startupCommandTextField.text = host.options.startupCommand
|
||||||
|
terminalOption.backspaceComboBox.selectedItem =
|
||||||
|
Backspace.valueOf(host.options.extras["backspace"] ?: Backspace.Delete.name)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateFields(): Boolean {
|
||||||
|
val host = getHost()
|
||||||
|
|
||||||
|
// general
|
||||||
|
if (validateField(generalOption.nameTextField)
|
||||||
|
|| validateField(generalOption.hostTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
|
if (validateField(generalOption.usernameTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (validateField(generalOption.passwordTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxy
|
||||||
|
if (host.proxy.type != ProxyType.No) {
|
||||||
|
if (validateField(proxyOption.proxyHostTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.proxy.authenticationType != AuthenticationType.No) {
|
||||||
|
if (validateField(proxyOption.proxyUsernameTextField)
|
||||||
|
|| validateField(proxyOption.proxyPasswordTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示有错误
|
||||||
|
*/
|
||||||
|
private fun validateField(textField: JTextField): Boolean {
|
||||||
|
if (textField.isEnabled && textField.text.isBlank()) {
|
||||||
|
setOutlineError(textField)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setOutlineError(textField: JTextField) {
|
||||||
|
selectOptionJComponent(textField)
|
||||||
|
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
|
textField.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示有错误
|
||||||
|
*/
|
||||||
|
private fun validateField(comboBox: JComboBox<*>): Boolean {
|
||||||
|
val selectedItem = comboBox.selectedItem
|
||||||
|
if (comboBox.isEnabled && (selectedItem == null || (selectedItem is String && selectedItem.isBlank()))) {
|
||||||
|
selectOptionJComponent(comboBox)
|
||||||
|
comboBox.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
|
comboBox.requestFocusInWindow()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
protected inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||||
|
val portTextField = PortSpinner(23)
|
||||||
|
val nameTextField = OutlineTextField(128)
|
||||||
|
val usernameTextField = OutlineTextField(128)
|
||||||
|
val hostTextField = OutlineTextField(255)
|
||||||
|
val passwordTextField = OutlinePasswordField(255)
|
||||||
|
val publicKeyComboBox = OutlineComboBox<String>()
|
||||||
|
val remarkTextArea = FixedLengthTextArea(512)
|
||||||
|
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
|
||||||
|
publicKeyComboBox.isEditable = false
|
||||||
|
|
||||||
|
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = StringUtils.EMPTY
|
||||||
|
if (value is String) {
|
||||||
|
text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
text,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = value?.toString() ?: ""
|
||||||
|
when (value) {
|
||||||
|
AuthenticationType.Password -> {
|
||||||
|
text = "Password"
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationType.PublicKey -> {
|
||||||
|
text = "Public Key"
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationType.KeyboardInteractive -> {
|
||||||
|
text = "Keyboard Interactive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
text,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||||
|
|
||||||
|
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
addComponentListener(object : ComponentAdapter() {
|
||||||
|
override fun componentResized(e: ComponentEvent) {
|
||||||
|
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
|
||||||
|
removeComponentListener(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.settings
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.new-host.general")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
|
||||||
|
remarkTextArea.rows = 8
|
||||||
|
remarkTextArea.lineWrap = true
|
||||||
|
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
|
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
|
||||||
|
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
||||||
|
.add(hostTextField).xy(3, rows)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||||
|
.add(portTextField).xy(7, rows).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
||||||
|
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
|
||||||
|
.add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
||||||
|
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
||||||
|
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
||||||
|
.xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class TerminalOption : JPanel(BorderLayout()), Option {
|
||||||
|
val charsetComboBox = JComboBox<String>()
|
||||||
|
val backspaceComboBox = JComboBox<Backspace>()
|
||||||
|
val startupCommandTextField = OutlineTextField()
|
||||||
|
val environmentTextArea = FixedLengthTextArea(2048)
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
|
||||||
|
backspaceComboBox.addItem(Backspace.Delete)
|
||||||
|
backspaceComboBox.addItem(Backspace.Backspace)
|
||||||
|
backspaceComboBox.addItem(Backspace.VT220)
|
||||||
|
|
||||||
|
|
||||||
|
environmentTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
environmentTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
|
||||||
|
environmentTextArea.rows = 8
|
||||||
|
environmentTextArea.lineWrap = true
|
||||||
|
environmentTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
|
|
||||||
|
for (e in Charset.availableCharsets()) {
|
||||||
|
charsetComboBox.addItem(e.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
charsetComboBox.selectedItem = "UTF-8"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.terminal
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.new-host.terminal")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
|
||||||
|
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.backspace")}:").xy(1, rows)
|
||||||
|
.add(backspaceComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows)
|
||||||
|
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
|
||||||
|
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
|
||||||
|
.apply { rows += step }
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Backspace {
|
||||||
|
/**
|
||||||
|
* 0x08
|
||||||
|
*/
|
||||||
|
Backspace,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 0x7F 默认
|
||||||
|
*/
|
||||||
|
Delete,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ESC[3~
|
||||||
|
*/
|
||||||
|
VT220, ;
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return when (this) {
|
||||||
|
Backspace -> "ASCII Backspace (0x08)"
|
||||||
|
Delete -> "ASCII Delete (0x7F)"
|
||||||
|
VT220 -> "VT220 Delete (ESC[3~)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
package app.termora.plugin.internal.serial
|
package app.termora.plugin.internal.telnet
|
||||||
|
|
||||||
import app.termora.plugin.Extension
|
import app.termora.plugin.Extension
|
||||||
import app.termora.plugin.InternalPlugin
|
import app.termora.plugin.InternalPlugin
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProviderExtension
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
internal class SerialInternalPlugin : InternalPlugin() {
|
internal class TelnetInternalPlugin : InternalPlugin() {
|
||||||
init {
|
init {
|
||||||
support.addExtension(ProtocolProviderExtension::class.java) { SerialProtocolProviderExtension.instance }
|
support.addExtension(ProtocolProviderExtension::class.java) { TelnetProtocolProviderExtension.instance }
|
||||||
support.addExtension(ProtocolHostPanelExtension::class.java) { SerialProtocolHostPanelExtension.instance }
|
support.addExtension(ProtocolHostPanelExtension::class.java) { TelnetProtocolHostPanelExtension.instance }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String {
|
||||||
return "Serial Protocol"
|
return "Telnet Protocol"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user