mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 10:22:58 +08:00
feat: WSL support on Windows
This commit is contained in:
@@ -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") }
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,11 @@ interface ProtocolHostPanelExtension : Extension {
|
||||
*/
|
||||
fun getProtocolProvider(): ProtocolProvider
|
||||
|
||||
/**
|
||||
* 是否可以创建协议主机面板
|
||||
*/
|
||||
fun canCreateProtocolHostPanel(): Boolean = true
|
||||
|
||||
/**
|
||||
* 创建协议主机面板
|
||||
*/
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user