mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12: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 terminalUnread by lazy { DynamicIcon("icons/terminalUnread.svg", "icons/terminalUnread_dark.svg") }
|
||||||
val dbPrimitive by lazy { DynamicIcon("icons/dbPrimitive.svg", "icons/dbPrimitive_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 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 success by lazy { DynamicIcon("icons/success.svg", "icons/success_dark.svg") }
|
||||||
val errorDialog by lazy { DynamicIcon("icons/errorDialog.svg", "icons/errorDialog_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") }
|
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())
|
toolbar.add(Box.createHorizontalGlue())
|
||||||
|
|
||||||
val extensions = ProtocolHostPanelExtension.extensions
|
val extensions = ProtocolHostPanelExtension.extensions
|
||||||
|
.filter { it.canCreateProtocolHostPanel() }
|
||||||
for ((index, extension) in extensions.withIndex()) {
|
for ((index, extension) in extensions.withIndex()) {
|
||||||
val protocol = extension.getProtocolProvider().getProtocol()
|
val protocol = extension.getProtocolProvider().getProtocol()
|
||||||
val icon = FlatSVGIcon(
|
val icon = FlatSVGIcon(
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package app.termora.actions
|
|||||||
|
|
||||||
import app.termora.NewHostDialogV2
|
import app.termora.NewHostDialogV2
|
||||||
import app.termora.tree.HostTreeNode
|
import app.termora.tree.HostTreeNode
|
||||||
import app.termora.tree.NewHostTreeModel
|
|
||||||
import javax.swing.tree.TreePath
|
import javax.swing.tree.TreePath
|
||||||
|
|
||||||
class NewHostAction : AnAction() {
|
class NewHostAction : AnAction() {
|
||||||
@@ -38,12 +37,9 @@ class NewHostAction : AnAction() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val newNode = HostTreeNode(host)
|
val newNode = HostTreeNode(host)
|
||||||
val model = tree.model
|
val model = tree.simpleTreeModel
|
||||||
|
model.insertNodeInto(newNode, lastNode, lastNode.childCount)
|
||||||
if (model is NewHostTreeModel) {
|
tree.selectionPath = TreePath(model.getPathToRoot(newNode))
|
||||||
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.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.wsl.WSLInternalPlugin
|
||||||
import app.termora.swingCoroutineScope
|
import app.termora.swingCoroutineScope
|
||||||
import app.termora.transfer.internal.local.LocalPlugin
|
import app.termora.transfer.internal.local.LocalPlugin
|
||||||
import app.termora.transfer.internal.sftp.SFTPPlugin
|
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))
|
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))
|
||||||
|
// wsl plugin
|
||||||
|
// if (SystemUtils.IS_OS_WINDOWS) {
|
||||||
|
plugins.add(PluginDescriptor(WSLInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
|
// }
|
||||||
// sftp pty plugin
|
// sftp pty plugin
|
||||||
plugins.add(PluginDescriptor(SFTPPtyInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
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 getProtocolProvider(): ProtocolProvider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否可以创建协议主机面板
|
||||||
|
*/
|
||||||
|
fun canCreateProtocolHostPanel(): Boolean = true
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建协议主机面板
|
* 创建协议主机面板
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import javax.swing.SwingUtilities
|
|||||||
import javax.swing.tree.TreePath
|
import javax.swing.tree.TreePath
|
||||||
|
|
||||||
class SnippetTree : SimpleTree() {
|
class SnippetTree : SimpleTree() {
|
||||||
override val model = SnippetTreeModel()
|
override val simpleTreeModel = SnippetTreeModel()
|
||||||
|
|
||||||
private val snippetManager get() = SnippetManager.getInstance()
|
private val snippetManager get() = SnippetManager.getInstance()
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ class SnippetTree : SimpleTree() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initViews() {
|
private fun initViews() {
|
||||||
super.setModel(model)
|
super.setModel(simpleTreeModel)
|
||||||
isEditable = true
|
isEditable = true
|
||||||
dragEnabled = true
|
dragEnabled = true
|
||||||
dropMode = DropMode.ON_OR_INSERT
|
dropMode = DropMode.ON_OR_INSERT
|
||||||
@@ -68,16 +68,16 @@ class SnippetTree : SimpleTree() {
|
|||||||
newFile(SnippetTreeNode(snippet))
|
newFile(SnippetTreeNode(snippet))
|
||||||
}
|
}
|
||||||
|
|
||||||
rename.addActionListener { startEditingAtPath(TreePath(model.getPathToRoot(lastNode))) }
|
rename.addActionListener { startEditingAtPath(TreePath(simpleTreeModel.getPathToRoot(lastNode))) }
|
||||||
refresh.addActionListener { model.reload(lastNode) }
|
refresh.addActionListener { simpleTreeModel.reload(lastNode) }
|
||||||
expandAll.addActionListener {
|
expandAll.addActionListener {
|
||||||
for (node in getSelectionSimpleTreeNodes(true)) {
|
for (node in getSelectionSimpleTreeNodes(true)) {
|
||||||
expandPath(TreePath(model.getPathToRoot(node)))
|
expandPath(TreePath(simpleTreeModel.getPathToRoot(node)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
colspanAll.addActionListener {
|
colspanAll.addActionListener {
|
||||||
for (node in getSelectionSimpleTreeNodes(true).reversed()) {
|
for (node in getSelectionSimpleTreeNodes(true).reversed()) {
|
||||||
collapsePath(TreePath(model.getPathToRoot(node)))
|
collapsePath(TreePath(simpleTreeModel.getPathToRoot(node)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
remove.addActionListener(object : AnAction() {
|
remove.addActionListener(object : AnAction() {
|
||||||
@@ -94,7 +94,7 @@ class SnippetTree : SimpleTree() {
|
|||||||
) {
|
) {
|
||||||
for (c in nodes) {
|
for (c in nodes) {
|
||||||
snippetManager.addSnippet(c.data.copy(deleted = true, updateDate = System.currentTimeMillis()))
|
snippetManager.addSnippet(c.data.copy(deleted = true, updateDate = System.currentTimeMillis()))
|
||||||
model.removeNodeFromParent(c)
|
simpleTreeModel.removeNodeFromParent(c)
|
||||||
// 将所有子孙也删除
|
// 将所有子孙也删除
|
||||||
for (child in c.getAllChildren()) {
|
for (child in c.getAllChildren()) {
|
||||||
snippetManager.addSnippet(
|
snippetManager.addSnippet(
|
||||||
@@ -110,7 +110,7 @@ class SnippetTree : SimpleTree() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
rename.isEnabled = lastNode != model.root
|
rename.isEnabled = lastNode != simpleTreeModel.root
|
||||||
remove.isEnabled = rename.isEnabled
|
remove.isEnabled = rename.isEnabled
|
||||||
newFolder.isEnabled = lastNode.data.type == SnippetType.Folder
|
newFolder.isEnabled = lastNode.data.type == SnippetType.Folder
|
||||||
newSnippet.isEnabled = newFolder.isEnabled
|
newSnippet.isEnabled = newFolder.isEnabled
|
||||||
@@ -130,18 +130,18 @@ class SnippetTree : SimpleTree() {
|
|||||||
val n = node as? SnippetTreeNode ?: return
|
val n = node as? SnippetTreeNode ?: return
|
||||||
n.data = n.data.copy(name = text, updateDate = System.currentTimeMillis())
|
n.data = n.data.copy(name = text, updateDate = System.currentTimeMillis())
|
||||||
snippetManager.addSnippet(n.data)
|
snippetManager.addSnippet(n.data)
|
||||||
model.nodeStructureChanged(n)
|
simpleTreeModel.nodeStructureChanged(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>, index: Int) {
|
override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>, index: Int) {
|
||||||
// 从原来的父移除
|
// 从原来的父移除
|
||||||
model.removeNodeFromParent(node)
|
simpleTreeModel.removeNodeFromParent(node)
|
||||||
|
|
||||||
val nNode = node as? SnippetTreeNode ?: return
|
val nNode = node as? SnippetTreeNode ?: return
|
||||||
val nParent = parent as? SnippetTreeNode ?: return
|
val nParent = parent as? SnippetTreeNode ?: return
|
||||||
nNode.data = nNode.data.copy(parentId = nParent.data.id, updateDate = System.currentTimeMillis())
|
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> {
|
override fun getSelectionSimpleTreeNodes(include: Boolean): List<SnippetTreeNode> {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import app.termora.*
|
|||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
import app.termora.plugin.ExtensionManager
|
import app.termora.plugin.ExtensionManager
|
||||||
|
import app.termora.plugin.internal.wsl.WSLHostTerminalTab
|
||||||
import app.termora.transfer.TransportTableModel.Attributes
|
import app.termora.transfer.TransportTableModel.Attributes
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
@@ -47,7 +48,6 @@ import java.util.*
|
|||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.Future
|
import java.util.concurrent.Future
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.regex.Pattern
|
|
||||||
import java.util.stream.Stream
|
import java.util.stream.Stream
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.TransferHandler
|
import javax.swing.TransferHandler
|
||||||
@@ -952,7 +952,7 @@ class TransportPanel(
|
|||||||
|
|
||||||
val p = localPath.absolutePathString()
|
val p = localPath.absolutePathString()
|
||||||
if (editCommand.isNotBlank()) {
|
if (editCommand.isNotBlank()) {
|
||||||
ProcessBuilder(parseCommand(MessageFormat.format(editCommand, p))).start()
|
ProcessBuilder(WSLHostTerminalTab.parseCommand(MessageFormat.format(editCommand, p))).start()
|
||||||
} else if (SystemInfo.isMacOS) {
|
} else if (SystemInfo.isMacOS) {
|
||||||
ProcessBuilder("open", "-a", "TextEdit", "-W", p).start().onExit()
|
ProcessBuilder("open", "-a", "TextEdit", "-W", p).start().onExit()
|
||||||
.whenComplete { _, _ -> if (disposed.get().not()) Disposer.dispose(disposable) }
|
.whenComplete { _, _ -> if (disposed.get().not()) Disposer.dispose(disposable) }
|
||||||
@@ -973,20 +973,6 @@ class TransportPanel(
|
|||||||
return disposable
|
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() {
|
override fun dispose() {
|
||||||
transferIds.clear()
|
transferIds.clear()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
get() = enableManager.isShowTags()
|
get() = enableManager.isShowTags()
|
||||||
set(value) = enableManager.setShowTags(value)
|
set(value) = enableManager.setShowTags(value)
|
||||||
private var isPopupMenu = false
|
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() {
|
private fun initViews() {
|
||||||
super.setModel(model)
|
super.setModel(simpleTreeModel)
|
||||||
isEditable = true
|
isEditable = true
|
||||||
dragEnabled = true
|
dragEnabled = true
|
||||||
isRootVisible = false
|
isRootVisible = false
|
||||||
@@ -124,7 +124,7 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
if (e.keyCode == KeyEvent.VK_ENTER && doubleClickConnection) {
|
if (e.keyCode == KeyEvent.VK_ENTER && doubleClickConnection) {
|
||||||
val nodes = getSelectionSimpleTreeNodes()
|
val nodes = getSelectionSimpleTreeNodes()
|
||||||
if (nodes.size == 1 && nodes.first().isFolder) {
|
if (nodes.size == 1 && nodes.first().isFolder) {
|
||||||
val path = TreePath(model.getPathToRoot(nodes.first()))
|
val path = TreePath(simpleTreeModel.getPathToRoot(nodes.first()))
|
||||||
if (isExpanded(path)) {
|
if (isExpanded(path)) {
|
||||||
collapsePath(path)
|
collapsePath(path)
|
||||||
} else {
|
} else {
|
||||||
@@ -161,7 +161,7 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
override fun canImport(support: TransferHandler.TransferSupport): Boolean {
|
override fun canImport(support: TransferHandler.TransferSupport): Boolean {
|
||||||
val dropLocation = support.dropLocation as? DropLocation ?: return false
|
val dropLocation = support.dropLocation as? DropLocation ?: return false
|
||||||
val node = dropLocation.path.lastPathComponent as? SimpleTreeNode<*> ?: 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 {
|
override fun canCreateTransferable(c: JComponent): Boolean {
|
||||||
@@ -178,7 +178,7 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
val tags = TagManager.getInstance().getTags(lastNode.host.ownerId)
|
val tags = TagManager.getInstance().getTags(lastNode.host.ownerId)
|
||||||
val nodes = getSelectionSimpleTreeNodes()
|
val nodes = getSelectionSimpleTreeNodes()
|
||||||
val fullNodes = getSelectionSimpleTreeNodes(true)
|
val fullNodes = getSelectionSimpleTreeNodes(true)
|
||||||
val lastNodeParent = lastNode.parent ?: model.root
|
val lastNodeParent = lastNode.parent ?: simpleTreeModel.root
|
||||||
val lastHost = lastNode.host
|
val lastHost = lastNode.host
|
||||||
val hasTeamNode = nodes.any { it is TeamTreeNode }
|
val hasTeamNode = nodes.any { it is TeamTreeNode }
|
||||||
|
|
||||||
@@ -252,8 +252,8 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
parentId = lastNode.id,
|
parentId = lastNode.id,
|
||||||
)
|
)
|
||||||
val node = HostTreeNode(host)
|
val node = HostTreeNode(host)
|
||||||
model.insertNodeInto(node, lastNode, lastNode.folderCount)
|
simpleTreeModel.insertNodeInto(node, lastNode, lastNode.folderCount)
|
||||||
selectionPath = TreePath(model.getPathToRoot(node))
|
selectionPath = TreePath(simpleTreeModel.getPathToRoot(node))
|
||||||
startEditingAtPath(selectionPath)
|
startEditingAtPath(selectionPath)
|
||||||
}
|
}
|
||||||
remove.addActionListener(object : ActionListener {
|
remove.addActionListener(object : ActionListener {
|
||||||
@@ -268,7 +268,7 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
) == JOptionPane.YES_OPTION
|
) == JOptionPane.YES_OPTION
|
||||||
) {
|
) {
|
||||||
for (c in nodes) {
|
for (c in nodes) {
|
||||||
model.removeNodeFromParent(c)
|
simpleTreeModel.removeNodeFromParent(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,20 +278,20 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
val p = c.parent ?: continue
|
val p = c.parent ?: continue
|
||||||
val newNode = copyNode(c, p.host.id)
|
val newNode = copyNode(c, p.host.id)
|
||||||
// 先入 Model
|
// 先入 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 {
|
expandAll.addActionListener {
|
||||||
for (node in fullNodes) {
|
for (node in fullNodes) {
|
||||||
expandPath(TreePath(model.getPathToRoot(node)))
|
expandPath(TreePath(simpleTreeModel.getPathToRoot(node)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
colspanAll.addActionListener {
|
colspanAll.addActionListener {
|
||||||
for (node in fullNodes.reversed()) {
|
for (node in fullNodes.reversed()) {
|
||||||
collapsePath(TreePath(model.getPathToRoot(node)))
|
collapsePath(TreePath(simpleTreeModel.getPathToRoot(node)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newHost.addActionListener(object : ActionListener {
|
newHost.addActionListener(object : ActionListener {
|
||||||
@@ -306,8 +306,8 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val newNode = HostTreeNode(host)
|
val newNode = HostTreeNode(host)
|
||||||
model.insertNodeInto(newNode, lastNode, lastNode.childCount)
|
simpleTreeModel.insertNodeInto(newNode, lastNode, lastNode.childCount)
|
||||||
selectionPath = TreePath(model.getPathToRoot(newNode))
|
selectionPath = TreePath(simpleTreeModel.getPathToRoot(newNode))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
property.addActionListener(object : ActionListener {
|
property.addActionListener(object : ActionListener {
|
||||||
@@ -318,10 +318,10 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
val host = dialog.host ?: return
|
val host = dialog.host ?: return
|
||||||
lastNode.host = host
|
lastNode.host = host
|
||||||
model.nodeStructureChanged(lastNode)
|
simpleTreeModel.nodeStructureChanged(lastNode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
refresh.addActionListener { model.reload(lastNode) }
|
refresh.addActionListener { simpleTreeModel.reload(lastNode) }
|
||||||
|
|
||||||
newMenu.isEnabled = lastNode.isFolder
|
newMenu.isEnabled = lastNode.isFolder
|
||||||
remove.isEnabled = getSelectionSimpleTreeNodes().none { it.id == "0" } && hasTeamNode.not()
|
remove.isEnabled = getSelectionSimpleTreeNodes().none { it.id == "0" } && hasTeamNode.not()
|
||||||
@@ -353,7 +353,7 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
tags.add(tag.id)
|
tags.add(tag.id)
|
||||||
}
|
}
|
||||||
lastNode.host = lastHost.copy(options = lastHost.options.copy(tags = tags))
|
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) {
|
override fun onRenamed(node: SimpleTreeNode<*>, text: String) {
|
||||||
val lastNode = node as? HostTreeNode ?: return
|
val lastNode = node as? HostTreeNode ?: return
|
||||||
lastNode.host = lastNode.host.copy(name = text)
|
lastNode.host = lastNode.host.copy(name = text)
|
||||||
model.nodeStructureChanged(lastNode)
|
simpleTreeModel.nodeStructureChanged(lastNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createTreeModelListener(): TreeModelListener {
|
override fun createTreeModelListener(): TreeModelListener {
|
||||||
@@ -402,7 +402,7 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>, index: Int) {
|
override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>, index: Int) {
|
||||||
if (parent !is HostTreeNode || node !is HostTreeNode) return
|
if (parent !is HostTreeNode || node !is HostTreeNode) return
|
||||||
// 从原来的父移除
|
// 从原来的父移除
|
||||||
model.removeNodeFromParent(node)
|
simpleTreeModel.removeNodeFromParent(node)
|
||||||
|
|
||||||
node.data = node.data.copy(
|
node.data = node.data.copy(
|
||||||
id = randomUUID(),
|
id = randomUUID(),
|
||||||
@@ -411,7 +411,7 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
ownerType = parent.host.ownerType,
|
ownerType = parent.host.ownerType,
|
||||||
)
|
)
|
||||||
|
|
||||||
model.insertNodeInto(node, parent, index)
|
simpleTreeModel.insertNodeInto(node, parent, index)
|
||||||
|
|
||||||
// 子也需要变基
|
// 子也需要变基
|
||||||
for ((idx, e) in node.childrenNode().withIndex()) {
|
for ((idx, e) in node.childrenNode().withIndex()) {
|
||||||
@@ -445,7 +445,7 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
if (host.isFolder) {
|
if (host.isFolder) {
|
||||||
for (child in node.children()) {
|
for (child in node.children()) {
|
||||||
if (child is HostTreeNode) {
|
if (child is HostTreeNode) {
|
||||||
model.insertNodeInto(
|
simpleTreeModel.insertNodeInto(
|
||||||
copyNode(child, newHost.id, idGenerator, level + 1),
|
copyNode(child, newHost.id, idGenerator, level + 1),
|
||||||
newNode, node.getIndex(child)
|
newNode, node.getIndex(child)
|
||||||
)
|
)
|
||||||
@@ -650,10 +650,10 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 重新加载
|
// 重新加载
|
||||||
model.reload(folder)
|
simpleTreeModel.reload(folder)
|
||||||
|
|
||||||
// expand root
|
// expand root
|
||||||
expandPath(TreePath(model.getPathToRoot(folder)))
|
expandPath(TreePath(simpleTreeModel.getPathToRoot(folder)))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseFromWindTerm(folder: HostTreeNode, file: File): List<HostTreeNode> {
|
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.rdp.RDPProtocolProvider
|
||||||
import app.termora.plugin.internal.serial.SerialProtocolProvider
|
import app.termora.plugin.internal.serial.SerialProtocolProvider
|
||||||
import app.termora.plugin.internal.ssh.SSHProtocolProvider
|
import app.termora.plugin.internal.ssh.SSHProtocolProvider
|
||||||
|
import app.termora.plugin.internal.wsl.WSLProtocolProvider
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.Graphics2D
|
import java.awt.Graphics2D
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
@@ -32,6 +33,12 @@ class ShowMoreInfoSimpleTreeCellRendererExtension private constructor() : Simple
|
|||||||
.let { Disposer.register(this, it) }
|
.let { Disposer.register(this, it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wsl
|
||||||
|
// key: guid
|
||||||
|
// value: name
|
||||||
|
private val map = mutableMapOf<String, String?>()
|
||||||
|
|
||||||
|
@Suppress("CascadeIf")
|
||||||
override fun createAnnotations(
|
override fun createAnnotations(
|
||||||
tree: JTree,
|
tree: JTree,
|
||||||
value: Any?,
|
value: Any?,
|
||||||
@@ -58,6 +65,8 @@ class ShowMoreInfoSimpleTreeCellRendererExtension private constructor() : Simple
|
|||||||
}
|
}
|
||||||
} else if (host.protocol == SerialProtocolProvider.PROTOCOL) {
|
} else if (host.protocol == SerialProtocolProvider.PROTOCOL) {
|
||||||
text = host.options.serialComm.port
|
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() {
|
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)
|
private val editor = OutlineTextField(64)
|
||||||
protected val tree get() = this
|
protected val tree get() = this
|
||||||
|
|
||||||
@@ -123,12 +123,12 @@ open class SimpleTree : JXTree() {
|
|||||||
if (tree.canCreateTransferable(c).not()) return null
|
if (tree.canCreateTransferable(c).not()) return null
|
||||||
val nodes = getSelectionSimpleTreeNodes().toMutableList()
|
val nodes = getSelectionSimpleTreeNodes().toMutableList()
|
||||||
if (nodes.isEmpty()) return null
|
if (nodes.isEmpty()) return null
|
||||||
if (nodes.contains(model.root)) return null
|
if (nodes.contains(simpleTreeModel.root)) return null
|
||||||
|
|
||||||
val iterator = nodes.iterator()
|
val iterator = nodes.iterator()
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
val node = iterator.next()
|
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) }) {
|
if (parents.any { nodes.contains(it) }) {
|
||||||
iterator.remove()
|
iterator.remove()
|
||||||
}
|
}
|
||||||
@@ -211,11 +211,11 @@ open class SimpleTree : JXTree() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rebase(e, node, min(index, node.childCount))
|
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
|
return true
|
||||||
}
|
}
|
||||||
@@ -245,8 +245,8 @@ open class SimpleTree : JXTree() {
|
|||||||
private fun newNode(newNode: SimpleTreeNode<*>, index: Int): Boolean {
|
private fun newNode(newNode: SimpleTreeNode<*>, index: Int): Boolean {
|
||||||
val lastNode = lastSelectedPathComponent
|
val lastNode = lastSelectedPathComponent
|
||||||
if (lastNode !is SimpleTreeNode<*>) return false
|
if (lastNode !is SimpleTreeNode<*>) return false
|
||||||
model.insertNodeInto(newNode, lastNode, index)
|
simpleTreeModel.insertNodeInto(newNode, lastNode, index)
|
||||||
selectionPath = TreePath(model.getPathToRoot(newNode))
|
selectionPath = TreePath(simpleTreeModel.getPathToRoot(newNode))
|
||||||
startEditingAtPath(selectionPath)
|
startEditingAtPath(selectionPath)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -291,7 +291,7 @@ open class SimpleTree : JXTree() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected open fun isCellEditable(e: EventObject?): Boolean {
|
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) {
|
protected open fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>, index: Int) {
|
||||||
|
|||||||
@@ -87,9 +87,9 @@ open class SimpleTreeCellRenderer : DefaultTreeCellRenderer() {
|
|||||||
val icon = this.icon
|
val icon = this.icon
|
||||||
if (icon is DynamicIcon && FlatLaf.isLafDark().not()) {
|
if (icon is DynamicIcon && FlatLaf.isLafDark().not()) {
|
||||||
val oldColorFilter = icon.colorFilter
|
val oldColorFilter = icon.colorFilter
|
||||||
icon.colorFilter = colorFilter
|
// icon.colorFilter = colorFilter
|
||||||
icon.paintIcon(c, g, x, y)
|
icon.paintIcon(c, g, x, y)
|
||||||
icon.colorFilter = oldColorFilter
|
// icon.colorFilter = oldColorFilter
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,6 +212,9 @@ termora.new-host.tunneling.delete=${termora.remove}
|
|||||||
termora.new-host.rdp.desktop-placeholder=Default full screen (e.g. 1920×1080)
|
termora.new-host.rdp.desktop-placeholder=Default full screen (e.g. 1920×1080)
|
||||||
termora.new-host.rdp.resolution=Resolution
|
termora.new-host.rdp.resolution=Resolution
|
||||||
|
|
||||||
|
|
||||||
|
termora.new-host.wsl.distribution=DistroName
|
||||||
|
|
||||||
termora.new-host.test-connection=Test Connection
|
termora.new-host.test-connection=Test Connection
|
||||||
termora.new-host.test-connection-successful=Connection successful
|
termora.new-host.test-connection-successful=Connection successful
|
||||||
|
|
||||||
|
|||||||
@@ -205,6 +205,8 @@ termora.new-host.tunneling.delete=${termora.remove}
|
|||||||
termora.new-host.rdp.desktop-placeholder=默认全屏(例如:1920×1080)
|
termora.new-host.rdp.desktop-placeholder=默认全屏(例如:1920×1080)
|
||||||
termora.new-host.rdp.resolution=分辨率
|
termora.new-host.rdp.resolution=分辨率
|
||||||
|
|
||||||
|
termora.new-host.wsl.distribution=分发版
|
||||||
|
|
||||||
termora.new-host.jump-hosts=跳板机
|
termora.new-host.jump-hosts=跳板机
|
||||||
|
|
||||||
# Key manager
|
# Key manager
|
||||||
|
|||||||
@@ -204,6 +204,8 @@ termora.new-host.tunneling.delete=${termora.remove}
|
|||||||
termora.new-host.rdp.desktop-placeholder=預設全螢幕(例如:1920×1080)
|
termora.new-host.rdp.desktop-placeholder=預設全螢幕(例如:1920×1080)
|
||||||
termora.new-host.rdp.resolution=解析度
|
termora.new-host.rdp.resolution=解析度
|
||||||
|
|
||||||
|
termora.new-host.wsl.distribution=分發版
|
||||||
|
|
||||||
termora.new-host.jump-hosts=跳板機
|
termora.new-host.jump-hosts=跳板機
|
||||||
|
|
||||||
# Key manager
|
# Key manager
|
||||||
|
|||||||
1
src/main/resources/icons/almalinux.svg
Normal file
1
src/main/resources/icons/almalinux.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1750821026851" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2837" width="16" height="16"><path d="M892.32 569.44c38.624-2.976 69.824 22.272 72.8 60.896 2.976 40.128-25.28 72.8-63.872 75.776a68.032 68.032 0 0 1-72.8-62.4c-2.976-40.096 23.776-69.824 63.872-74.272z" fill="#86DA2F" p-id="2838"></path><path d="M422.848 885.888c0-38.624 29.728-68.352 65.376-68.352s69.824 32.704 69.824 69.824c0 35.68-29.696 66.88-63.872 68.352-43.072 0-71.328-26.752-71.328-69.824z" fill="#24C2FF" p-id="2839"></path><path d="M528.32 452.064c-5.92 2.976-8.896-1.472-10.4-5.92-54.944-102.528-38.624-231.776 57.952-309.024 25.28-20.8 72.8-25.28 93.6-4.48 8.928 7.456 10.4 16.352 11.872 26.752 2.976 22.304 7.424 44.576 22.304 62.4 16.32 19.328 37.12 26.752 60.896 25.28 20.8 0 41.6-2.976 54.976 20.8 7.424 13.344 4.48 65.344-7.424 75.744-5.952 4.48-10.4 1.504-14.848 0-34.176-13.376-69.824-13.376-105.504-7.424-11.872 1.472-17.824-1.472-17.824-14.848-1.472-22.304-5.92-43.104-17.824-62.4-22.272-40.128-63.872-41.6-90.624-4.48-22.272 29.76-28.224 65.376-34.176 101.056-5.92 31.2-4.448 63.872-2.976 96.544 0 0-1.472 0 0 0z" fill="#FFCB12" p-id="2840"></path><path d="M565.472 474.368c-2.976-4.48-1.472-8.928 2.976-11.904 84.672-77.248 210.976-92.096 309.024-16.32 25.248 20.8 41.6 63.872 28.224 89.12-5.952 10.4-13.376 14.88-22.304 17.824-20.8 8.928-40.096 17.856-53.472 37.152-13.376 19.328-16.32 41.6-10.4 65.376 4.48 19.296 11.904 40.096-7.424 57.92-10.4 10.4-60.928 19.328-74.272 10.4-5.952-4.448-4.48-8.896-2.976-14.848 4.448-37.12-4.48-71.296-17.824-104-4.48-11.872-2.976-17.824 8.896-20.8 20.8-5.92 40.128-16.32 54.976-31.2 32.672-31.2 25.28-71.296-17.824-89.12-34.176-14.88-69.824-11.904-104-8.928-32.672 1.504-63.872 10.4-93.6 19.328z" fill="#86DA2F" p-id="2841"></path><path d="M546.144 512.96c4.48-4.448 7.456-2.944 11.904 0 98.048 59.456 148.544 176.8 104 291.2-11.904 29.728-50.528 59.456-78.72 52-11.904-2.944-17.856-8.896-23.808-16.32-13.376-17.856-28.224-34.176-50.496-41.6-23.776-7.424-44.576-2.976-65.376 8.896-17.824 10.4-35.648 23.776-57.92 10.4-13.376-7.424-35.68-53.472-31.232-68.32 2.976-5.952 8.928-5.952 14.88-5.952 37.12-5.952 66.848-23.776 95.072-47.552 8.928-7.424 16.32-7.424 23.776 2.976 11.872 17.824 26.752 32.672 46.048 43.072 38.624 22.304 75.776 2.976 80.224-41.6 4.48-37.12-8.928-69.792-20.8-102.496a589.632 589.632 0 0 0-47.552-84.672z" fill="#24C2FF" p-id="2842"></path><path d="M498.624 521.92c-5.952 29.696-19.328 57.92-37.12 83.2-53.504 80.192-130.752 112.896-225.856 104-34.144-3.008-62.4-31.232-65.344-59.456-1.504-11.872 1.472-20.8 8.896-29.696 10.4-13.376 19.328-25.28 23.776-41.6 8.928-32.704-2.976-59.424-26.752-83.2-32.672-32.704-28.224-62.4 10.4-86.176 4.48-2.976 10.4-5.952 16.352-8.928 8.928-4.448 16.32-4.448 19.328 5.952 13.344 34.176 40.096 59.424 69.824 80.224 10.4 8.928 10.4 14.848 1.472 25.28a110.304 110.304 0 0 0-29.696 69.792c-2.976 32.704 16.32 53.504 49.024 53.504 20.8 0 40.096-7.424 57.92-16.32 46.08-23.808 81.728-57.952 115.904-93.632 4.448-1.472 5.92-4.448 11.872-2.976z" fill="#0069DA" p-id="2843"></path><path d="M254.976 209.92c2.976 0 10.4 1.472 17.824 2.976 54.976 10.4 89.152-8.928 106.976-60.928 11.872-34.176 37.12-44.576 69.824-26.752 1.472 0 1.472 1.504 2.976 1.504 34.176 19.296 34.176 22.272 13.376 52-17.824 23.776-26.752 50.496-31.2 78.72-2.976 16.352-8.928 19.328-23.776 13.376-23.776-8.896-49.024-8.896-74.304 0-28.224 8.928-40.096 34.176-31.168 62.4 11.872 37.12 44.544 53.504 72.768 72.8 28.256 19.328 60.928 29.728 92.128 43.072 4.48 1.504 11.872 1.504 10.4 8.928-1.472 4.48-7.424 4.48-13.376 4.48-66.88 2.944-130.72-7.456-182.72-52.032-49.024-40.096-84.704-89.12-78.752-157.44 4.48-22.304 20.8-38.656 49.024-43.104z" fill="#FF4649" p-id="2844"></path><path d="M133.152 627.392c-35.648 4.48-71.296-25.28-74.272-62.4-2.976-35.648 26.72-71.328 60.896-74.272 38.624-4.48 74.272 22.272 77.248 57.92 1.504 34.176-20.8 75.776-63.872 78.72z" fill="#0069DA" p-id="2845"></path><path d="M757.12 98.496c37.12-2.976 72.8 26.752 75.776 63.904 2.976 35.648-26.752 69.824-62.4 72.768-38.624 2.976-72.8-25.248-75.776-62.4-2.976-37.12 23.776-71.296 62.4-74.24z" fill="#FFCB12" p-id="2846"></path><path d="M369.376 126.72c4.48 38.624-22.304 71.296-62.4 77.248-34.176 4.48-69.824-23.776-74.272-56.448-4.48-43.072 19.296-74.272 59.424-78.72 37.12-4.48 72.768 23.744 77.248 57.92z" fill="#FF4649" p-id="2847"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.3 KiB |
1
src/main/resources/icons/debian.svg
Normal file
1
src/main/resources/icons/debian.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.8 KiB |
1
src/main/resources/icons/fedora.svg
Normal file
1
src/main/resources/icons/fedora.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1750820989771" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2335" width="16" height="16"><path d="M962 512.0140625C962 263.48046875 760.52304688 62 511.99296875 62 263.571875 62 62.17578125 263.31171875 62 511.69765625v348.23671875c0.13359375 56.39765625 45.87539063 102.05507812 102.30820312 102.05507812h347.86757813C760.625 961.89101563 962 760.48085937 962 512.0140625" fill="#294172" p-id="2336"></path><path d="M644.1171875 168.54804688c-116.39882812 0-211.09570313 94.69335938-211.09570313 211.09570312v112.04296875H321.44257813c-116.39882812 0-211.09570313 94.70039062-211.09570313 211.09921875 0 116.3953125 94.696875 211.09570313 211.09570313 211.09570313s211.09570313-94.70039062 211.09570312-211.09570313v-112.04648438h111.57890625c116.39882812 0 211.09921875-94.696875 211.09921875-211.09570312 0-116.40234375-94.70039062-211.09570313-211.09921875-211.09570313z m-210.31523438 534.23789062c0 61.95234375-50.40351563 112.35585937-112.359375 112.35585937s-112.359375-50.40351563-112.359375-112.35585937c0-61.95585938 50.40351563-112.359375 112.359375-112.359375h111.57890625v0.31289062h0.78046875v112.04648438z m210.31523438-210.7828125h-111.57890625v-0.31640625h-0.77695313v-112.04296875c0-61.95585938 50.40351563-112.359375 112.35585938-112.359375s112.359375 50.40351563 112.359375 112.359375-50.40703125 112.359375-112.359375 112.359375z" fill="#3C6EB4" p-id="2337"></path><path d="M690.77304688 174.95c-16.3828125-4.28203125-28.96171875-6.27890625-46.65585938-6.27890625-116.63789062 0-211.20117188 94.56679688-211.20117188 211.19765625v111.94453125h-88.45312499c-27.58007813 0-49.86914063 21.67382813-49.8515625 49.2046875 0 27.35859375 22.04296875 49.12382813 49.33125 49.12382813l73.23398437 0.0140625c8.69414062 0 15.74648438 7.03125 15.74648438 15.71132812v96.86601563c-0.10898438 61.49179688-49.98867188 111.30117187-111.48046876 111.30117187-20.83007813 0-25.9875-2.728125-40.20820312-2.728125-29.87226563 0-49.85859375 20.025-49.85859375 47.559375 0.00703125 22.77773438 19.52578125 42.35976563 43.4109375 48.61054688 16.3828125 4.28203125 28.96171875 6.28242187 46.65585937 6.28242187 116.63789062 0 211.20117188-94.56679688 211.20117188-211.20117187v-111.94101563h88.453125c27.58007813 0 49.86914063-21.67382813 49.8515625-49.2046875 0-27.36210938-22.04296875-49.12382813-49.33125-49.12382812l-73.23398438-0.0140625a15.73242188 15.73242188 0 0 1-15.74648437-15.71484375V379.69296875c0.10898438-61.49179688 49.98867188-111.30117187 111.48046875-111.30117187 20.83007813 0 25.9875 2.73164062 40.20820312 2.73164062 29.87226563 0 49.85859375-20.02851563 49.85859375-47.559375-0.00703125-22.78125-19.52578125-42.36328125-43.4109375-48.6140625" fill="#FFFFFF" p-id="2338"></path></svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
1
src/main/resources/icons/ubuntu.svg
Normal file
1
src/main/resources/icons/ubuntu.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1750821074847" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3059" width="16" height="16"><path d="M962 511.54954912c0 248.64864873-201.80180214 449.54954912-449.54954912 449.54955S62 760.19819785 62 511.54954912 263.80180215 62 511.54954912 62c248.64864873 0 450.45045088 201.80180214 450.45045088 449.54954912z" fill="#DD4814" p-id="3060"></path><path d="M206.14414414 452.09009023a60.36036065 60.36036065 0 1 0 60.36036065 60.36036065c0-33.33333339-27.02702724-60.36036065-60.36036065-60.36036065z m428.82882891 272.97297247c-28.82882901 16.21621583-38.73873867 53.15315273-21.62162198 81.98198261 16.21621583 28.82882901 53.15315273 38.73873867 81.98198262 21.6216211 28.82882901-16.21621583 38.73873867-53.15315273 21.6216211-81.98198174-17.1171167-28.82882901-53.15315273-38.73873867-81.98198174-21.62162198zM336.7747751 511.54954912c0-56.75675713 27.92792813-110.81081075 74.77477471-143.24324238l-44.14414483-72.97297383c-53.15315273 35.13513516-91.89189229 89.18918877-108.10810811 151.35135205 36.03603604 29.72972988 41.44144131 82.88288261 12.61261319 118.91891866-3.60360352 4.5045044-8.10810791 8.10810791-12.61261319 12.6126123 16.21621583 62.1621624 54.95495537 116.21621602 108.10810811 151.35135205l44.14414483-73.87387471c-46.84684658-33.33333339-74.77477471-86.48648613-74.77477471-144.14414414z m174.77477402-174.77477402c90.99099141 0 166.66666699 69.36936943 174.7747749 160.36035996l85.58558614-0.90090088c-4.5045044-63.96396416-31.53153164-124.32432393-77.47747735-169.36936963-43.24324307 16.21621583-91.89189229-5.40540528-109.00900898-48.64864833-1.80180176-5.40540528-3.60360352-10.81081055-4.50450528-16.21621583-62.1621624-17.1171167-127.92792832-10.81081055-185.58558545 18.01801758l41.44144131 74.77477471c24.32432461-12.61261231 49.54954922-18.01801846 74.77477471-18.01801758z m0 350.4504498c-25.22522549 0-50.4504501-5.40540528-73.87387383-16.21621582l-41.44144131 74.77477471c57.65765801 28.82882901 123.42342305 35.13513516 185.58558545 18.01801758 7.20720703-45.9459457 50.4504501-77.47747734 96.39639669-69.36936856 5.40540528 0.90090088 10.81081055 2.70270263 16.21621669 4.5045044 45.9459457-45.04504482 72.97297295-105.40540547 77.47747735-169.36936963l-85.58558614-0.90090088c-8.10810791 89.18918877-83.78378349 158.5585582-174.7747749 158.5585582z m123.42342393-388.28828759c28.82882901 16.21621583 65.76576592 6.30630615 81.98198174-21.62162198 16.21621583-28.82882901 6.30630615-65.76576592-21.6216211-81.98198174-29.72972988-17.1171167-65.76576592-7.20720703-82.8828835 21.6216211-16.21621583 28.82882901-6.30630615 65.76576592 22.52252286 81.98198262z" fill="#FFFFFF" p-id="3061"></path></svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
Reference in New Issue
Block a user