feat: support RDP protocol (#524)

This commit is contained in:
hstyi
2025-04-20 15:33:09 +08:00
committed by GitHub
parent 8755c4ad23
commit 993f220b8b
8 changed files with 96 additions and 10 deletions

View File

@@ -25,6 +25,7 @@ enum class Protocol {
SSH, SSH,
Local, Local,
Serial, Serial,
RDP,
/** /**
* 交互式的 SFTP此协议只在系统内部交互不应该暴露给用户也不应该持久化 * 交互式的 SFTP此协议只在系统内部交互不应该暴露给用户也不应该持久化

View File

@@ -320,6 +320,7 @@ open class HostOptionsPane : OptionsPane() {
protocolTypeComboBox.addItem(Protocol.SSH) protocolTypeComboBox.addItem(Protocol.SSH)
protocolTypeComboBox.addItem(Protocol.Local) protocolTypeComboBox.addItem(Protocol.Local)
protocolTypeComboBox.addItem(Protocol.Serial) protocolTypeComboBox.addItem(Protocol.Serial)
protocolTypeComboBox.addItem(Protocol.RDP)
authenticationTypeComboBox.addItem(AuthenticationType.No) authenticationTypeComboBox.addItem(AuthenticationType.No)
authenticationTypeComboBox.addItem(AuthenticationType.Password) authenticationTypeComboBox.addItem(AuthenticationType.Password)

View File

@@ -49,6 +49,7 @@ class HostTreeNode(host: Host) : SimpleTreeNode<Host>(host) {
return when (host.protocol) { return when (host.protocol) {
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon() Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
Protocol.Serial -> if (selected && hasFocus) Icons.plugin.dark else Icons.plugin Protocol.Serial -> if (selected && hasFocus) Icons.plugin.dark else Icons.plugin
Protocol.RDP -> if (selected && hasFocus) Icons.microsoftWindows.dark else Icons.microsoftWindows
else -> if (selected && hasFocus) Icons.terminal.dark else Icons.terminal else -> if (selected && hasFocus) Icons.terminal.dark else Icons.terminal
} }
} }

View File

@@ -64,6 +64,7 @@ object Icons {
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") } val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") } val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") }
val microsoft by lazy { DynamicIcon("icons/microsoft.svg", "icons/microsoft_dark.svg") } val microsoft by lazy { DynamicIcon("icons/microsoft.svg", "icons/microsoft_dark.svg") }
val microsoftWindows by lazy { DynamicIcon("icons/microsoftWindows.svg", "icons/microsoftWindows_dark.svg") }
val tencent by lazy { DynamicIcon("icons/tencent.svg") } val tencent by lazy { DynamicIcon("icons/tencent.svg") }
val google by lazy { DynamicIcon("icons/google-small.svg") } val google by lazy { DynamicIcon("icons/google-small.svg") }
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") } val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }

View File

@@ -37,6 +37,7 @@ import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.XPathConstants import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory import javax.xml.xpath.XPathFactory
@Suppress("CascadeIf")
class NewHostTree : SimpleTree() { class NewHostTree : SimpleTree() {
companion object { companion object {
@@ -48,7 +49,7 @@ class NewHostTree : SimpleTree() {
private val properties get() = Database.getDatabase().properties private val properties get() = Database.getDatabase().properties
private val owner get() = SwingUtilities.getWindowAncestor(this) private val owner get() = SwingUtilities.getWindowAncestor(this)
private val openHostAction get() = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST) private val openHostAction get() = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
private val sftpAction get() = ActionManager.getInstance().getAction(app.termora.Actions.SFTP) private val sftpAction get() = ActionManager.getInstance().getAction(Actions.SFTP)
private var isShowMoreInfo private var isShowMoreInfo
get() = properties.getString("HostTree.showMoreInfo", "false").toBoolean() get() = properties.getString("HostTree.showMoreInfo", "false").toBoolean()
set(value) = properties.putString("HostTree.showMoreInfo", value.toString()) set(value) = properties.putString("HostTree.showMoreInfo", value.toString())
@@ -97,7 +98,7 @@ class NewHostTree : SimpleTree() {
// 是否显示更多信息 // 是否显示更多信息
if (isShowMoreInfo) { if (isShowMoreInfo) {
val color = if (sel) { val color = if (sel) {
if (tree.hasFocus()) { if (tree.hasFocus() || isPopupMenu) {
UIManager.getColor("textHighlightText") UIManager.getColor("textHighlightText")
} else { } else {
this.foreground this.foreground
@@ -110,15 +111,15 @@ class NewHostTree : SimpleTree() {
"""<font color=rgb(${color.red},${color.green},${color.blue})>${it}</font>""" """<font color=rgb(${color.red},${color.green},${color.blue})>${it}</font>"""
} }
if (host.protocol == Protocol.SSH) { // @formatter:off
text = if (host.protocol == Protocol.SSH || host.protocol == Protocol.RDP) {
"<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;${fontTag.apply("${host.username}@${host.host}")}</html>" text = "<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;${fontTag.apply("${host.username}@${host.host}")}</html>"
} else if (host.protocol == Protocol.Serial) { } else if (host.protocol == Protocol.Serial) {
text = text = "<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;${fontTag.apply(host.options.serialComm.port)}</html>"
"<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;${fontTag.apply(host.options.serialComm.port)}</html>"
} else if (host.protocol == Protocol.Folder) { } else if (host.protocol == Protocol.Folder) {
text = "<html>${host.name}${fontTag.apply(" (${node.childCount})")}</html>" text = "<html>${host.name}${fontTag.apply(" (${node.childCount})")}</html>"
} }
// @formatter:on
} }
val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus) val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus)
@@ -810,7 +811,7 @@ class NewHostTree : SimpleTree() {
var group = bookmarkGroups.find { it.bookmarkIds.contains(id) } var group = bookmarkGroups.find { it.bookmarkIds.contains(id) }
while (group != null && group.id != "default") { while (group != null && group.id != "default") {
folderNames.addFirst(group.title) folderNames.addFirst(group.title)
group = bookmarkGroups.find { it.bookmarkGroupIds.contains(group?.id ?: StringUtils.EMPTY) } group = bookmarkGroups.find { it.bookmarkGroupIds.contains(group.id) }
} }
printer.printRecord( printer.printRecord(

View File

@@ -1,6 +1,19 @@
package app.termora.actions package app.termora.actions
import app.termora.* import app.termora.*
import com.formdev.flatlaf.util.SystemInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
import java.net.URI
import java.util.*
import javax.swing.JOptionPane
import kotlin.time.Duration.Companion.seconds
class OpenHostAction : AnAction() { class OpenHostAction : AnAction() {
companion object { companion object {
@@ -26,10 +39,70 @@ class OpenHostAction : AnAction() {
Protocol.SSH -> SSHTerminalTab(windowScope, evt.host) Protocol.SSH -> SSHTerminalTab(windowScope, evt.host)
Protocol.Serial -> SerialTerminalTab(windowScope, evt.host) Protocol.Serial -> SerialTerminalTab(windowScope, evt.host)
Protocol.SFTPPty -> SFTPPtyTerminalTab(windowScope, evt.host) Protocol.SFTPPty -> SFTPPtyTerminalTab(windowScope, evt.host)
Protocol.RDP -> openRDP(windowScope, evt.host)
else -> LocalTerminalTab(windowScope, evt.host) else -> LocalTerminalTab(windowScope, evt.host)
} }
if (tab is TerminalTab) {
terminalTabbedManager.addTerminalTab(tab) terminalTabbedManager.addTerminalTab(tab)
if (tab is PtyHostTerminalTab) {
tab.start() tab.start()
} }
}
}
private fun openRDP(windowScope: WindowScope, host: Host) {
if (SystemInfo.isLinux) {
OptionPane.showMessageDialog(
windowScope.window,
"Linux cannot connect to Windows Remote Server, Supported only for macOS and Windows",
messageType = JOptionPane.WARNING_MESSAGE
)
return
}
if (SystemInfo.isMacOS) {
if (!FileUtils.getFile("/Applications/Windows App.app").exists()) {
val option = OptionPane.showConfirmDialog(
windowScope.window,
"If you want to connect to a Windows Remote Server, You have to install the Windows App",
optionType = JOptionPane.OK_CANCEL_OPTION
)
if (option == JOptionPane.OK_OPTION) {
Application.browse(URI.create("https://apps.apple.com/app/windows-app/id1295203466"))
}
return
}
}
val sb = StringBuilder()
sb.append("full address:s:").append(host.host).append(':').append(host.port).appendLine()
sb.append("username:s:").append(host.username).appendLine()
val file = FileUtils.getFile(Application.getTemporaryDir(), UUID.randomUUID().toSimpleString() + ".rdp")
file.outputStream().use { IOUtils.write(sb.toString(), it, Charsets.UTF_8) }
if (host.authentication.type == AuthenticationType.Password) {
val systemClipboard = windowScope.window.toolkit.systemClipboard
val password = host.authentication.password
systemClipboard.setContents(StringSelection(password), null)
// clear password
swingCoroutineScope.launch(Dispatchers.IO) {
delay(30.seconds)
if (systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
if (systemClipboard.getData(DataFlavor.stringFlavor) == password) {
systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
}
}
}
}
if (SystemInfo.isMacOS) {
ProcessBuilder("open", file.absolutePath).start()
} else if (SystemInfo.isWindows) {
ProcessBuilder("mstsc", file.absolutePath).start()
}
}
} }

View File

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.00098 2H7.00195L7.00098 7H2L2.00098 2ZM8.00293 2H13V7H8.00293V2ZM2 7.99902L7 8V13.001L2 13V7.99902ZM8.00195 8H12.999L12.998 13.001H8.00195" fill="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 399 B

View File

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.00098 2H7.00195L7.00098 7H2L2.00098 2ZM8.00293 2H13V7H8.00293V2ZM2 7.99902L7 8V13.001L2 13V7.99902ZM8.00195 8H12.999L12.998 13.001H8.00195" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 399 B