feat: WSL support on Windows

This commit is contained in:
hstyi
2025-06-25 11:52:53 +08:00
committed by hstyi
parent 01d0f9d4bd
commit 3d47840aa8
27 changed files with 622 additions and 68 deletions

View File

@@ -92,6 +92,10 @@ object Icons {
val terminalUnread by lazy { DynamicIcon("icons/terminalUnread.svg", "icons/terminalUnread_dark.svg") }
val dbPrimitive by lazy { DynamicIcon("icons/dbPrimitive.svg", "icons/dbPrimitive_dark.svg") }
val linux by lazy { DynamicIcon("icons/linux.svg", "icons/linux_dark.svg") }
val debian by lazy { DynamicIcon("icons/debian.svg") }
val fedora by lazy { DynamicIcon("icons/fedora.svg") }
val almalinux by lazy { DynamicIcon("icons/almalinux.svg") }
val ubuntu by lazy { DynamicIcon("icons/ubuntu.svg") }
val success by lazy { DynamicIcon("icons/success.svg", "icons/success_dark.svg") }
val errorDialog by lazy { DynamicIcon("icons/errorDialog.svg", "icons/errorDialog_dark.svg") }
val network by lazy { DynamicIcon("icons/network.svg", "icons/network_dark.svg") }

View File

@@ -59,6 +59,7 @@ class NewHostDialogV2(owner: Window, private val editHost: Host? = null) : Dialo
toolbar.add(Box.createHorizontalGlue())
val extensions = ProtocolHostPanelExtension.extensions
.filter { it.canCreateProtocolHostPanel() }
for ((index, extension) in extensions.withIndex()) {
val protocol = extension.getProtocolProvider().getProtocol()
val icon = FlatSVGIcon(

View File

@@ -2,7 +2,6 @@ package app.termora.actions
import app.termora.NewHostDialogV2
import app.termora.tree.HostTreeNode
import app.termora.tree.NewHostTreeModel
import javax.swing.tree.TreePath
class NewHostAction : AnAction() {
@@ -38,12 +37,9 @@ class NewHostAction : AnAction() {
)
val newNode = HostTreeNode(host)
val model = tree.model
if (model is NewHostTreeModel) {
model.insertNodeInto(newNode, lastNode, lastNode.childCount)
tree.selectionPath = TreePath(model.getPathToRoot(newNode))
}
val model = tree.simpleTreeModel
model.insertNodeInto(newNode, lastNode, lastNode.childCount)
tree.selectionPath = TreePath(model.getPathToRoot(newNode))
}
}

View File

@@ -11,6 +11,7 @@ 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.ssh.SSHInternalPlugin
import app.termora.plugin.internal.wsl.WSLInternalPlugin
import app.termora.swingCoroutineScope
import app.termora.transfer.internal.local.LocalPlugin
import app.termora.transfer.internal.sftp.SFTPPlugin
@@ -115,6 +116,10 @@ internal class PluginManager private constructor() {
plugins.add(PluginDescriptor(LocalInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// rdp plugin
plugins.add(PluginDescriptor(RDPInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// wsl plugin
// if (SystemUtils.IS_OS_WINDOWS) {
plugins.add(PluginDescriptor(WSLInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// }
// sftp pty plugin
plugins.add(PluginDescriptor(SFTPPtyInternalPlugin(), origin = PluginOrigin.Internal, version = version))

View File

@@ -0,0 +1,8 @@
package app.termora.plugin.internal.wsl
data class WSLDistribution(
val guid: String,
val distributionName: String,
val flavor:String,
val basePath: String,
)

View File

@@ -0,0 +1,298 @@
package app.termora.plugin.internal.wsl
import app.termora.*
import com.formdev.flatlaf.FlatClientProperties
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.*
internal open class WSLHostOptionsPane : OptionsPane() {
protected val generalOption = GeneralOption()
protected val terminalOption = TerminalOption()
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
init {
addOption(generalOption)
addOption(terminalOption)
}
open fun getHost(): Host {
val name = generalOption.nameTextField.text
val protocol = WSLProtocolProvider.PROTOCOL
val host = (generalOption.hostComboBox.selectedItem as WSLDistribution).distributionName
val options = Options.Companion.Default.copy(
encoding = terminalOption.charsetComboBox.selectedItem as String,
env = terminalOption.environmentTextArea.text,
startupCommand = terminalOption.startupCommandTextField.text,
)
return Host(
name = name,
protocol = protocol,
host = host,
options = options,
sort = System.currentTimeMillis(),
remark = generalOption.remarkTextArea.text,
)
}
fun setHost(host: Host) {
generalOption.nameTextField.text = host.name
generalOption.hostComboBox.selectedItem = host.host
generalOption.remarkTextArea.text = host.remark
generalOption.hostComboBox.selectedItem = null
terminalOption.startupCommandTextField.text = host.options.startupCommand
terminalOption.environmentTextArea.text = host.options.env
terminalOption.charsetComboBox.selectedItem = host.options.encoding
for (i in 0 until generalOption.hostComboBox.itemCount) {
if (generalOption.hostComboBox.getItemAt(i).distributionName == host.host) {
generalOption.hostComboBox.selectedIndex = i
break
}
}
}
fun validateFields(): Boolean {
// general
return (validateField(generalOption.nameTextField)
|| validateField(generalOption.hostComboBox)).not()
}
/**
* 返回 true 表示有错误
*/
private fun validateField(textField: JTextField): Boolean {
if (textField.isEnabled && textField.text.isBlank()) {
setOutlineError(textField)
return true
}
return false
}
/**
* 返回 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
}
private fun setOutlineError(textField: JTextField) {
selectOptionJComponent(textField)
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
textField.requestFocusInWindow()
}
protected inner class GeneralOption : JPanel(BorderLayout()), Option {
val nameTextField = OutlineTextField(128)
val hostComboBox = OutlineComboBox<WSLDistribution>()
val remarkTextArea = FixedLengthTextArea(512)
init {
initView()
initEvents()
}
private fun initView() {
hostComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component? {
val text = if (value is WSLDistribution) value.distributionName else value
val c = super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
icon = null
if (value is WSLDistribution) {
icon = if (StringUtils.containsIgnoreCase(value.flavor, "debian")) {
Icons.debian
} else if (StringUtils.containsIgnoreCase(value.flavor, "ubuntu")) {
Icons.ubuntu
} else if (StringUtils.containsIgnoreCase(value.flavor, "fedora")) {
Icons.fedora
} else if (StringUtils.containsIgnoreCase(value.flavor, "alma")) {
Icons.almalinux
} else {
Icons.linux
}
}
return c
}
}
add(getCenterComponent(), BorderLayout.CENTER)
}
private fun initEvents() {
for (distribution in WSLSupport.getDistributions()) {
hostComboBox.addItem(distribution)
}
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",
"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).debug(false)
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
.add(nameTextField).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.wsl.distribution")}:").xy(1, rows)
.add(hostComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
.xy(3, rows).apply { rows += step }
.build()
return panel
}
}
protected inner class TerminalOption : JPanel(BorderLayout()), Option {
val charsetComboBox = JComboBox<String>()
val startupCommandTextField = OutlineTextField()
val environmentTextArea = FixedLengthTextArea(2048)
init {
initView()
initEvents()
}
private fun initView() {
add(getCenterComponent(), BorderLayout.CENTER)
startupCommandTextField.placeholderText = "--cd ~"
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"
)
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.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
}
}
}

View File

@@ -0,0 +1,58 @@
package app.termora.plugin.internal.wsl
import app.termora.Host
import app.termora.PtyConnectorFactory
import app.termora.PtyHostTerminalTab
import app.termora.WindowScope
import app.termora.terminal.PtyConnector
import org.apache.commons.io.Charsets
import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.StringUtils
import java.nio.charset.StandardCharsets
import java.util.regex.Pattern
class WSLHostTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
companion object {
fun parseCommand(command: String): List<String> {
val result = mutableListOf<String>()
val matcher = Pattern.compile("\"([^\"]*)\"|(\\S+)").matcher(command)
while (matcher.find()) {
if (matcher.group(1) != null) {
result.add(matcher.group(1)) // 处理双引号部分
} else {
result.add(matcher.group(2).replace("\\\\ ", " "))
}
}
return result
}
}
override suspend fun openPtyConnector(): PtyConnector {
val winSize = terminalPanel.winSize()
val drive = System.getenv("SystemRoot")
val wsl = FileUtils.getFile(drive, "System32", "wsl.exe").absolutePath
val commands = mutableListOf<String>()
commands.add(wsl)
commands.add("-d")
commands.add(host.host)
if (StringUtils.isNoneBlank(host.options.startupCommand)) {
commands.addAll(parseCommand(host.options.startupCommand))
}
val ptyConnector = PtyConnectorFactory.getInstance().createPtyConnector(
commands = commands.toTypedArray(),
rows = winSize.rows, cols = winSize.cols,
env = host.options.envs(),
charset = Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8),
)
return ptyConnector
}
override fun sendStartupCommand(ptyConnector: PtyConnector, bytes: ByteArray) {
// Nothing
}
}

View File

@@ -0,0 +1,24 @@
package app.termora.plugin.internal.wsl
import app.termora.plugin.Extension
import app.termora.plugin.InternalPlugin
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProviderExtension
internal class WSLInternalPlugin : InternalPlugin() {
init {
support.addExtension(ProtocolProviderExtension::class.java) { WSLProtocolProviderExtension.instance }
support.addExtension(ProtocolHostPanelExtension::class.java) { WSLProtocolHostPanelExtension.instance }
}
override fun getName(): String {
return "WSL Protocol"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,36 @@
package app.termora.plugin.internal.wsl
import app.termora.Disposer
import app.termora.Host
import app.termora.protocol.ProtocolHostPanel
import java.awt.BorderLayout
class WSLProtocolHostPanel : ProtocolHostPanel() {
private val pane = WSLHostOptionsPane()
init {
initView()
initEvents()
}
private fun initView() {
add(pane, BorderLayout.CENTER)
Disposer.register(this, pane)
}
private fun initEvents() {}
override fun getHost(): Host {
return pane.getHost()
}
override fun setHost(host: Host) {
pane.setHost(host)
}
override fun validateFields(): Boolean {
return pane.validateFields()
}
}

View File

@@ -0,0 +1,24 @@
package app.termora.plugin.internal.wsl
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
internal class WSLProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
companion object {
val instance by lazy { WSLProtocolHostPanelExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return WSLProtocolProvider.instance
}
override fun canCreateProtocolHostPanel(): Boolean {
return WSLSupport.isSupported
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
return WSLProtocolHostPanel()
}
}

View File

@@ -0,0 +1,30 @@
package app.termora.plugin.internal.wsl
import app.termora.*
import app.termora.actions.DataProvider
import app.termora.protocol.GenericProtocolProvider
internal class WSLProtocolProvider private constructor() : GenericProtocolProvider {
companion object {
val instance by lazy { WSLProtocolProvider() }
const val PROTOCOL = "WSL"
}
override fun getProtocol(): String {
return PROTOCOL
}
override fun createTerminalTab(dataProvider: DataProvider, windowScope: WindowScope, host: Host): TerminalTab {
return WSLHostTerminalTab(windowScope, host)
}
override fun getIcon(width: Int, height: Int): DynamicIcon {
return Icons.linux
}
override fun canCreateTerminalTab(dataProvider: DataProvider, windowScope: WindowScope, host: Host): Boolean {
return WSLSupport.isSupported
}
}

View File

@@ -0,0 +1,14 @@
package app.termora.plugin.internal.wsl
import app.termora.protocol.ProtocolProvider
import app.termora.protocol.ProtocolProviderExtension
internal class WSLProtocolProviderExtension private constructor() : ProtocolProviderExtension {
companion object {
val instance by lazy { WSLProtocolProviderExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return WSLProtocolProvider.instance
}
}

View File

@@ -0,0 +1,45 @@
package app.termora.plugin.internal.wsl
import com.formdev.flatlaf.util.SystemInfo
import com.sun.jna.platform.win32.Advapi32Util
import com.sun.jna.platform.win32.WinReg
import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.StringUtils
object WSLSupport {
val isSupported by lazy { checkSupported() }
private fun checkSupported(): Boolean {
if (SystemInfo.isWindows.not()) return false
val drive = System.getenv("SystemRoot") ?: return false
val wsl = FileUtils.getFile(drive, "System32", "wsl.exe")
return wsl.exists()
}
fun getDistributions(): List<WSLDistribution> {
if (isSupported.not()) return emptyList()
val baseKeyPath = "Software\\Microsoft\\Windows\\CurrentVersion\\Lxss"
val guids = Advapi32Util.registryGetKeys(WinReg.HKEY_CURRENT_USER, baseKeyPath)
val distributions = mutableListOf<WSLDistribution>()
for (guid in guids) {
val key = baseKeyPath + "\\" + guid
val distroName = Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "DistributionName")
val basePath = Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "BasePath")
val flavor = Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "Flavor")
if (StringUtils.isAnyBlank(distroName, guid, basePath, flavor)) continue
distributions.add(
WSLDistribution(
guid = guid,
flavor = flavor,
basePath = basePath,
distributionName = distroName
)
)
}
return distributions
}
}

View File

@@ -16,6 +16,11 @@ interface ProtocolHostPanelExtension : Extension {
*/
fun getProtocolProvider(): ProtocolProvider
/**
* 是否可以创建协议主机面板
*/
fun canCreateProtocolHostPanel(): Boolean = true
/**
* 创建协议主机面板
*/

View File

@@ -15,7 +15,7 @@ import javax.swing.SwingUtilities
import javax.swing.tree.TreePath
class SnippetTree : SimpleTree() {
override val model = SnippetTreeModel()
override val simpleTreeModel = SnippetTreeModel()
private val snippetManager get() = SnippetManager.getInstance()
@@ -25,7 +25,7 @@ class SnippetTree : SimpleTree() {
}
private fun initViews() {
super.setModel(model)
super.setModel(simpleTreeModel)
isEditable = true
dragEnabled = true
dropMode = DropMode.ON_OR_INSERT
@@ -68,16 +68,16 @@ class SnippetTree : SimpleTree() {
newFile(SnippetTreeNode(snippet))
}
rename.addActionListener { startEditingAtPath(TreePath(model.getPathToRoot(lastNode))) }
refresh.addActionListener { model.reload(lastNode) }
rename.addActionListener { startEditingAtPath(TreePath(simpleTreeModel.getPathToRoot(lastNode))) }
refresh.addActionListener { simpleTreeModel.reload(lastNode) }
expandAll.addActionListener {
for (node in getSelectionSimpleTreeNodes(true)) {
expandPath(TreePath(model.getPathToRoot(node)))
expandPath(TreePath(simpleTreeModel.getPathToRoot(node)))
}
}
colspanAll.addActionListener {
for (node in getSelectionSimpleTreeNodes(true).reversed()) {
collapsePath(TreePath(model.getPathToRoot(node)))
collapsePath(TreePath(simpleTreeModel.getPathToRoot(node)))
}
}
remove.addActionListener(object : AnAction() {
@@ -94,7 +94,7 @@ class SnippetTree : SimpleTree() {
) {
for (c in nodes) {
snippetManager.addSnippet(c.data.copy(deleted = true, updateDate = System.currentTimeMillis()))
model.removeNodeFromParent(c)
simpleTreeModel.removeNodeFromParent(c)
// 将所有子孙也删除
for (child in c.getAllChildren()) {
snippetManager.addSnippet(
@@ -110,7 +110,7 @@ class SnippetTree : SimpleTree() {
})
rename.isEnabled = lastNode != model.root
rename.isEnabled = lastNode != simpleTreeModel.root
remove.isEnabled = rename.isEnabled
newFolder.isEnabled = lastNode.data.type == SnippetType.Folder
newSnippet.isEnabled = newFolder.isEnabled
@@ -130,18 +130,18 @@ class SnippetTree : SimpleTree() {
val n = node as? SnippetTreeNode ?: return
n.data = n.data.copy(name = text, updateDate = System.currentTimeMillis())
snippetManager.addSnippet(n.data)
model.nodeStructureChanged(n)
simpleTreeModel.nodeStructureChanged(n)
}
override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>, index: Int) {
// 从原来的父移除
model.removeNodeFromParent(node)
simpleTreeModel.removeNodeFromParent(node)
val nNode = node as? SnippetTreeNode ?: return
val nParent = parent as? SnippetTreeNode ?: return
nNode.data = nNode.data.copy(parentId = nParent.data.id, updateDate = System.currentTimeMillis())
model.insertNodeInto(nNode, nParent, index)
simpleTreeModel.insertNodeInto(nNode, nParent, index)
}
override fun getSelectionSimpleTreeNodes(include: Boolean): List<SnippetTreeNode> {

View File

@@ -5,6 +5,7 @@ import app.termora.*
import app.termora.actions.DataProvider
import app.termora.database.DatabaseManager
import app.termora.plugin.ExtensionManager
import app.termora.plugin.internal.wsl.WSLHostTerminalTab
import app.termora.transfer.TransportTableModel.Attributes
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatToolBar
@@ -47,7 +48,6 @@ import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
import java.util.concurrent.atomic.AtomicBoolean
import java.util.regex.Pattern
import java.util.stream.Stream
import javax.swing.*
import javax.swing.TransferHandler
@@ -952,7 +952,7 @@ class TransportPanel(
val p = localPath.absolutePathString()
if (editCommand.isNotBlank()) {
ProcessBuilder(parseCommand(MessageFormat.format(editCommand, p))).start()
ProcessBuilder(WSLHostTerminalTab.parseCommand(MessageFormat.format(editCommand, p))).start()
} else if (SystemInfo.isMacOS) {
ProcessBuilder("open", "-a", "TextEdit", "-W", p).start().onExit()
.whenComplete { _, _ -> if (disposed.get().not()) Disposer.dispose(disposable) }
@@ -973,20 +973,6 @@ class TransportPanel(
return disposable
}
private fun parseCommand(command: String): List<String> {
val result = mutableListOf<String>()
val matcher = Pattern.compile("\"([^\"]*)\"|(\\S+)").matcher(command)
while (matcher.find()) {
if (matcher.group(1) != null) {
result.add(matcher.group(1)) // 处理双引号部分
} else {
result.add(matcher.group(2).replace("\\\\ ", " "))
}
}
return result
}
override fun dispose() {
transferIds.clear()
}

View File

@@ -73,7 +73,7 @@ class NewHostTree : SimpleTree(), Disposable {
get() = enableManager.isShowTags()
set(value) = enableManager.setShowTags(value)
private var isPopupMenu = false
override val model = NewHostTreeModel.getInstance()
override val simpleTreeModel = NewHostTreeModel.getInstance()
/**
* 是否允许显示右键菜单
@@ -95,7 +95,7 @@ class NewHostTree : SimpleTree(), Disposable {
}
private fun initViews() {
super.setModel(model)
super.setModel(simpleTreeModel)
isEditable = true
dragEnabled = true
isRootVisible = false
@@ -124,7 +124,7 @@ class NewHostTree : SimpleTree(), Disposable {
if (e.keyCode == KeyEvent.VK_ENTER && doubleClickConnection) {
val nodes = getSelectionSimpleTreeNodes()
if (nodes.size == 1 && nodes.first().isFolder) {
val path = TreePath(model.getPathToRoot(nodes.first()))
val path = TreePath(simpleTreeModel.getPathToRoot(nodes.first()))
if (isExpanded(path)) {
collapsePath(path)
} else {
@@ -161,7 +161,7 @@ class NewHostTree : SimpleTree(), Disposable {
override fun canImport(support: TransferHandler.TransferSupport): Boolean {
val dropLocation = support.dropLocation as? DropLocation ?: return false
val node = dropLocation.path.lastPathComponent as? SimpleTreeNode<*> ?: return false
return node != model.getRoot()
return node != simpleTreeModel.getRoot()
}
override fun canCreateTransferable(c: JComponent): Boolean {
@@ -178,7 +178,7 @@ class NewHostTree : SimpleTree(), Disposable {
val tags = TagManager.getInstance().getTags(lastNode.host.ownerId)
val nodes = getSelectionSimpleTreeNodes()
val fullNodes = getSelectionSimpleTreeNodes(true)
val lastNodeParent = lastNode.parent ?: model.root
val lastNodeParent = lastNode.parent ?: simpleTreeModel.root
val lastHost = lastNode.host
val hasTeamNode = nodes.any { it is TeamTreeNode }
@@ -252,8 +252,8 @@ class NewHostTree : SimpleTree(), Disposable {
parentId = lastNode.id,
)
val node = HostTreeNode(host)
model.insertNodeInto(node, lastNode, lastNode.folderCount)
selectionPath = TreePath(model.getPathToRoot(node))
simpleTreeModel.insertNodeInto(node, lastNode, lastNode.folderCount)
selectionPath = TreePath(simpleTreeModel.getPathToRoot(node))
startEditingAtPath(selectionPath)
}
remove.addActionListener(object : ActionListener {
@@ -268,7 +268,7 @@ class NewHostTree : SimpleTree(), Disposable {
) == JOptionPane.YES_OPTION
) {
for (c in nodes) {
model.removeNodeFromParent(c)
simpleTreeModel.removeNodeFromParent(c)
}
}
}
@@ -278,20 +278,20 @@ class NewHostTree : SimpleTree(), Disposable {
val p = c.parent ?: continue
val newNode = copyNode(c, p.host.id)
// 先入 Model
model.insertNodeInto(newNode, p, lastNodeParent.getIndex(c) + 1)
simpleTreeModel.insertNodeInto(newNode, p, lastNodeParent.getIndex(c) + 1)
// 开启编辑
selectionPath = TreePath(model.getPathToRoot(newNode))
selectionPath = TreePath(simpleTreeModel.getPathToRoot(newNode))
}
}
rename.addActionListener { startEditingAtPath(TreePath(model.getPathToRoot(lastNode))) }
rename.addActionListener { startEditingAtPath(TreePath(simpleTreeModel.getPathToRoot(lastNode))) }
expandAll.addActionListener {
for (node in fullNodes) {
expandPath(TreePath(model.getPathToRoot(node)))
expandPath(TreePath(simpleTreeModel.getPathToRoot(node)))
}
}
colspanAll.addActionListener {
for (node in fullNodes.reversed()) {
collapsePath(TreePath(model.getPathToRoot(node)))
collapsePath(TreePath(simpleTreeModel.getPathToRoot(node)))
}
}
newHost.addActionListener(object : ActionListener {
@@ -306,8 +306,8 @@ class NewHostTree : SimpleTree(), Disposable {
)
val newNode = HostTreeNode(host)
model.insertNodeInto(newNode, lastNode, lastNode.childCount)
selectionPath = TreePath(model.getPathToRoot(newNode))
simpleTreeModel.insertNodeInto(newNode, lastNode, lastNode.childCount)
selectionPath = TreePath(simpleTreeModel.getPathToRoot(newNode))
}
})
property.addActionListener(object : ActionListener {
@@ -318,10 +318,10 @@ class NewHostTree : SimpleTree(), Disposable {
dialog.isVisible = true
val host = dialog.host ?: return
lastNode.host = host
model.nodeStructureChanged(lastNode)
simpleTreeModel.nodeStructureChanged(lastNode)
}
})
refresh.addActionListener { model.reload(lastNode) }
refresh.addActionListener { simpleTreeModel.reload(lastNode) }
newMenu.isEnabled = lastNode.isFolder
remove.isEnabled = getSelectionSimpleTreeNodes().none { it.id == "0" } && hasTeamNode.not()
@@ -353,7 +353,7 @@ class NewHostTree : SimpleTree(), Disposable {
tags.add(tag.id)
}
lastNode.host = lastHost.copy(options = lastHost.options.copy(tags = tags))
model.nodeStructureChanged(lastNode)
simpleTreeModel.nodeStructureChanged(lastNode)
}
}
@@ -387,7 +387,7 @@ class NewHostTree : SimpleTree(), Disposable {
override fun onRenamed(node: SimpleTreeNode<*>, text: String) {
val lastNode = node as? HostTreeNode ?: return
lastNode.host = lastNode.host.copy(name = text)
model.nodeStructureChanged(lastNode)
simpleTreeModel.nodeStructureChanged(lastNode)
}
override fun createTreeModelListener(): TreeModelListener {
@@ -402,7 +402,7 @@ class NewHostTree : SimpleTree(), Disposable {
override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>, index: Int) {
if (parent !is HostTreeNode || node !is HostTreeNode) return
// 从原来的父移除
model.removeNodeFromParent(node)
simpleTreeModel.removeNodeFromParent(node)
node.data = node.data.copy(
id = randomUUID(),
@@ -411,7 +411,7 @@ class NewHostTree : SimpleTree(), Disposable {
ownerType = parent.host.ownerType,
)
model.insertNodeInto(node, parent, index)
simpleTreeModel.insertNodeInto(node, parent, index)
// 子也需要变基
for ((idx, e) in node.childrenNode().withIndex()) {
@@ -445,7 +445,7 @@ class NewHostTree : SimpleTree(), Disposable {
if (host.isFolder) {
for (child in node.children()) {
if (child is HostTreeNode) {
model.insertNodeInto(
simpleTreeModel.insertNodeInto(
copyNode(child, newHost.id, idGenerator, level + 1),
newNode, node.getIndex(child)
)
@@ -650,10 +650,10 @@ class NewHostTree : SimpleTree(), Disposable {
}
// 重新加载
model.reload(folder)
simpleTreeModel.reload(folder)
// expand root
expandPath(TreePath(model.getPathToRoot(folder)))
expandPath(TreePath(simpleTreeModel.getPathToRoot(folder)))
}
private fun parseFromWindTerm(folder: HostTreeNode, file: File): List<HostTreeNode> {

View File

@@ -5,6 +5,7 @@ import app.termora.plugin.internal.extension.DynamicExtensionHandler
import app.termora.plugin.internal.rdp.RDPProtocolProvider
import app.termora.plugin.internal.serial.SerialProtocolProvider
import app.termora.plugin.internal.ssh.SSHProtocolProvider
import app.termora.plugin.internal.wsl.WSLProtocolProvider
import org.apache.commons.lang3.StringUtils
import java.awt.Graphics2D
import javax.swing.JComponent
@@ -32,6 +33,12 @@ class ShowMoreInfoSimpleTreeCellRendererExtension private constructor() : Simple
.let { Disposer.register(this, it) }
}
// wsl
// key: guid
// value: name
private val map = mutableMapOf<String, String?>()
@Suppress("CascadeIf")
override fun createAnnotations(
tree: JTree,
value: Any?,
@@ -58,6 +65,8 @@ class ShowMoreInfoSimpleTreeCellRendererExtension private constructor() : Simple
}
} else if (host.protocol == SerialProtocolProvider.PROTOCOL) {
text = host.options.serialComm.port
} else if (host.protocol == WSLProtocolProvider.PROTOCOL) {
text = host.host
}
}

View File

@@ -22,7 +22,7 @@ import kotlin.math.min
open class SimpleTree : JXTree() {
protected open val model get() = super.getModel() as SimpleTreeModel<*>
open val simpleTreeModel get() = super.getModel() as SimpleTreeModel<*>
private val editor = OutlineTextField(64)
protected val tree get() = this
@@ -123,12 +123,12 @@ open class SimpleTree : JXTree() {
if (tree.canCreateTransferable(c).not()) return null
val nodes = getSelectionSimpleTreeNodes().toMutableList()
if (nodes.isEmpty()) return null
if (nodes.contains(model.root)) return null
if (nodes.contains(simpleTreeModel.root)) return null
val iterator = nodes.iterator()
while (iterator.hasNext()) {
val node = iterator.next()
val parents = model.getPathToRoot(node).filter { it != node }
val parents = simpleTreeModel.getPathToRoot(node).filter { it != node }
if (parents.any { nodes.contains(it) }) {
iterator.remove()
}
@@ -211,11 +211,11 @@ open class SimpleTree : JXTree() {
}
rebase(e, node, min(index, node.childCount))
selectionPath = TreePath(model.getPathToRoot(e))
selectionPath = TreePath(simpleTreeModel.getPathToRoot(e))
}
// 先展开最顶级的
expandPath(TreePath(model.getPathToRoot(node)))
expandPath(TreePath(simpleTreeModel.getPathToRoot(node)))
return true
}
@@ -245,8 +245,8 @@ open class SimpleTree : JXTree() {
private fun newNode(newNode: SimpleTreeNode<*>, index: Int): Boolean {
val lastNode = lastSelectedPathComponent
if (lastNode !is SimpleTreeNode<*>) return false
model.insertNodeInto(newNode, lastNode, index)
selectionPath = TreePath(model.getPathToRoot(newNode))
simpleTreeModel.insertNodeInto(newNode, lastNode, index)
selectionPath = TreePath(simpleTreeModel.getPathToRoot(newNode))
startEditingAtPath(selectionPath)
return true
}
@@ -291,7 +291,7 @@ open class SimpleTree : JXTree() {
}
protected open fun isCellEditable(e: EventObject?): Boolean {
return getLastSelectedPathNode() != model.root
return getLastSelectedPathNode() != simpleTreeModel.root
}
protected open fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>, index: Int) {

View File

@@ -87,9 +87,9 @@ open class SimpleTreeCellRenderer : DefaultTreeCellRenderer() {
val icon = this.icon
if (icon is DynamicIcon && FlatLaf.isLafDark().not()) {
val oldColorFilter = icon.colorFilter
icon.colorFilter = colorFilter
// icon.colorFilter = colorFilter
icon.paintIcon(c, g, x, y)
icon.colorFilter = oldColorFilter
// icon.colorFilter = oldColorFilter
return
}
}