feat: quick connect

This commit is contained in:
hstyi
2025-07-08 18:04:29 +08:00
committed by hstyi
parent 5af0acb619
commit a4ae11e301
15 changed files with 226 additions and 115 deletions

View File

@@ -344,6 +344,11 @@ data class Host(
val isFolder get() = StringUtils.equalsIgnoreCase(protocol, "Folder")
/**
* 临时的 SSH 不可以保存
*/
val isTemporary get() = options.extras["Temporary"] != null
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

View File

@@ -21,9 +21,13 @@ class HostManager private constructor() : Disposable {
*/
fun addHost(host: Host, source: DatabaseChangedExtension.Source = DatabaseChangedExtension.Source.User) {
assertEventDispatchThread()
if (host.ownerType.isBlank()) {
if (host.isTemporary)
throw IllegalArgumentException("Temporary host")
if (host.ownerType.isBlank())
throw IllegalArgumentException("Owner type cannot be null")
}
databaseManager.saveAndIncrementVersion(
Data(
id = host.id,

View File

@@ -37,6 +37,8 @@ class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(own
preferredSize = size
minimumSize = size
rememberCheckBox.isVisible = host.isTemporary.not()
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
@@ -84,7 +86,7 @@ class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(own
switchPasswordComponent()
return FormBuilder.create().padding("$formMargin, $formMargin, $formMargin, $formMargin")
return FormBuilder.create().padding("1dlu, $formMargin, $formMargin, $formMargin")
.layout(layout)
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, 1)
.add(authenticationTypeComboBox).xy(3, 1)

View File

@@ -17,7 +17,6 @@ import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import org.apache.commons.lang3.StringUtils
import java.awt.*
import java.awt.event.AWTEventListener
import java.awt.event.ActionEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
@@ -32,11 +31,6 @@ class TerminalTabbed(
private val layout: TermoraLayout,
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager, DataProvider {
private val tabs = mutableListOf<TerminalTab>()
private val customizeToolBarAWTEventListener = object : AWTEventListener, Disposable {
override fun eventDispatched(event: AWTEvent?) {
}
}
private val actionManager = ActionManager.getInstance()
private val dataProviderSupport = DataProviderSupport()
private val appearance get() = DatabaseManager.getInstance().appearance
@@ -72,7 +66,6 @@ class TerminalTabbed(
private fun initEvents() {
Disposer.register(this, customizeToolBarAWTEventListener)
// 关闭 tab
tabbedPane.setTabCloseCallback { _, i -> removeTabAt(i, true) }
@@ -146,9 +139,6 @@ class TerminalTabbed(
}
}).let { Disposer.register(this, it) }
// 监听全局事件
toolkit.addAWTEventListener(customizeToolBarAWTEventListener, AWTEvent.MOUSE_EVENT_MASK)
}
private fun removeTabAt(index: Int, disposable: Boolean = true) {
@@ -301,9 +291,7 @@ class TerminalTabbed(
// 关闭
val close = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close"))
close.addActionListener {
tabbedPane.tabCloseCallback?.accept(tabbedPane, tabIndex)
}
close.addActionListener { tabbedPane.tabCloseCallback?.accept(tabbedPane, tabIndex) }
// 关闭其他标签页
popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close-other-tabs")).addActionListener {
@@ -326,7 +314,7 @@ class TerminalTabbed(
close.isEnabled = tab.canClose()
rename.isEnabled = close.isEnabled
clone.isEnabled = close.isEnabled
edit.isEnabled = tab is HostTerminalTab && tab.host.id != "local"
edit.isEnabled = tab is HostTerminalTab && tab.host.id != "local" && tab.host.isTemporary.not()
openInNewWindow.isEnabled = close.isEnabled
// 如果不允许克隆
@@ -337,12 +325,7 @@ class TerminalTabbed(
if (close.isEnabled) {
popupMenu.addSeparator()
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
reconnect.addActionListener {
if (tabIndex > 0) {
tabs[tabIndex].reconnect()
}
}
reconnect.addActionListener { tabs[tabIndex].reconnect() }
reconnect.isEnabled = tabs[tabIndex].canReconnect()
}
@@ -384,60 +367,6 @@ class TerminalTabbed(
}
}
/**
* 对着 ToolBar 右键
*/
/*private inner class CustomizeToolBarAWTEventListener : AWTEventListener, Disposable {
override fun eventDispatched(event: AWTEvent) {
if (event !is MouseEvent || event.id != MouseEvent.MOUSE_CLICKED || !SwingUtilities.isRightMouseButton(event)) return
// 如果 ToolBar 没有显示
if (!toolbar.isShowing) return
// 如果不是作用于在 ToolBar 上面
if (!Rectangle(toolbar.locationOnScreen, toolbar.size).contains(event.locationOnScreen)) return
// 显示右键菜单
showContextMenu(event)
}
private fun showContextMenu(event: MouseEvent) {
val popupMenu = FlatPopupMenu()
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
val owner = SwingUtilities.getWindowAncestor(this@TerminalTabbed)
val dialog = CustomizeToolBarDialog(owner, windowScope, termoraToolBar)
dialog.setLocationRelativeTo(owner)
if (dialog.open()) {
TermoraToolBar.rebuild()
}
}
popupMenu.show(event.component, event.x, event.y)
}
override fun dispose() {
toolkit.removeAWTEventListener(this)
}
}*/
/*private inner class CustomizeToolBarDialog(owner: Window) : DialogWrapper(owner) {
init {
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
isModal = true
title = I18n.getString("termora.setting")
setLocationRelativeTo(null)
init()
}
override fun createCenterPanel(): JComponent {
val model = DefaultListModel<String>()
val checkBoxList = CheckBoxList(model)
checkBoxList.fixedCellHeight = UIManager.getInt("Tree.rowHeight")
model.addElement("Test")
return checkBoxList
}
}*/
private inner class SwitchFindEverywhereResult(
private val title: String,
private val icon: Icon?,

View File

@@ -28,6 +28,7 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
private fun registerActions() {
addAction(NewWindowAction.NEW_WINDOW, NewWindowAction())
addAction(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
addAction(QuickConnectAction.QUICK_CONNECT, QuickConnectAction.instance)
addAction(Actions.APP_UPDATE, AppUpdateAction.getInstance())
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())

View File

@@ -38,7 +38,7 @@ class OpenHostAction : AnAction() {
if (providers.first { StringUtils.equalsIgnoreCase(it.getProtocol(), host.protocol) }
.isTransfer()) {
ActionManager.getInstance().getAction(Actions.SFTP)
.actionPerformed(TransferActionEvent(evt.source, evt.host.id, evt.event))
.actionPerformed(TransferActionEvent(evt.source, host, evt.event))
return
}

View File

@@ -0,0 +1,174 @@
package app.termora.actions
import app.termora.*
import app.termora.Application.ohMyJson
import app.termora.OptionsPane.Companion.FORM_MARGIN
import app.termora.database.DatabaseManager
import app.termora.protocol.ProtocolProvider
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import kotlinx.serialization.Serializable
import org.apache.commons.lang3.exception.ExceptionUtils
import java.awt.Dimension
import java.awt.Window
import java.net.URI
import java.util.*
import javax.swing.*
class QuickConnectAction private constructor() : AnAction(I18n.getString("termora.actions.quick-connect"), Icons.find) {
companion object {
const val QUICK_CONNECT = "QuickConnectAction"
val instance = QuickConnectAction()
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.quick-connect"))
}
override fun actionPerformed(evt: AnActionEvent) {
val scope = evt.getData(DataProviders.WindowScope) ?: return
val dialog = QuickConnectDialog(scope.window)
dialog.isVisible = true
}
private class QuickConnectDialog(owner: Window) : DialogWrapper(owner) {
private val properties get() = DatabaseManager.getInstance().properties
private val hostComboBox = OutlineComboBox<String>()
private val usernameTextField = OutlineTextField(256)
private val passwordTextField = OutlinePasswordField(256)
init {
isModal = true
title = I18n.getString("termora.actions.quick-connect")
isResizable = false
init()
pack()
size = Dimension(UIManager.getInt("Dialog.width") - 250, preferredSize.height)
setLocationRelativeTo(owner)
}
override fun createCenterPanel(): JComponent {
hostComboBox.isEditable = true
hostComboBox.placeholderText = "ssh://127.0.0.1:22"
val histories = getHistories()
for (history in histories) {
if (histories.first() == history) {
usernameTextField.text = history.host.username
passwordTextField.text = history.host.authentication.password
}
hostComboBox.addItem(history.url)
}
usernameTextField.placeholderText = I18n.getString("termora.new-host.general.username")
passwordTextField.placeholderText = I18n.getString("termora.new-host.general.password")
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
return FormBuilder.create().layout(layout)
.border(BorderFactory.createEmptyBorder(0, 8, 8, 8))
.add("${I18n.getString("termora.new-host.general.protocol")}:").xy(1, 1)
.add(hostComboBox).xy(3, 1)
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, 3)
.add(usernameTextField).xy(3, 3)
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, 5)
.add(passwordTextField).xy(3, 5)
.build()
}
override fun doOKAction() {
val host = hostComboBox.selectedItem as? String
if (host.isNullOrBlank()) {
hostComboBox.requestFocusInWindow()
return
}
val historyHost: HistoryHost
try {
historyHost = getHistoryHost(host.trim())
} catch (e: Exception) {
hostComboBox.requestFocusInWindow()
OptionPane.showMessageDialog(
this,
e.message ?: ExceptionUtils.getRootCauseMessage(e),
messageType = JOptionPane.ERROR_MESSAGE
)
return
}
val action = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
if (action is OpenHostAction) {
SwingUtilities.invokeLater {
action.actionPerformed(OpenHostActionEvent(this, historyHost.host, EventObject(this)))
}
}
super.doOKAction()
}
private fun getHistoryHost(host: String): HistoryHost {
val uri = URI.create(host)
val protocolProvider = ProtocolProvider.valueOf(uri.scheme)
if (protocolProvider == null) {
throw UnsupportedOperationException(I18n.getString("termora.protocol.not-supported", uri.scheme))
}
val historyHost = HistoryHost(
host, Host(
name = uri.host,
protocol = uri.scheme,
host = uri.host,
port = uri.port,
username = usernameTextField.text.trim(),
authentication = Authentication.No.copy(
type = AuthenticationType.Password,
password = String(passwordTextField.password)
),
options = Options.Default.copy(
extras = mutableMapOf("Temporary" to "true")
)
)
)
val histories = getHistories().toMutableList()
histories.removeIf { it.url == host }
histories.addFirst(historyHost)
if (histories.size > 20) {
histories.removeLast()
}
properties.putString("QuickConnect.historyHosts", ohMyJson.encodeToString(histories))
return historyHost
}
private fun getHistories(): List<HistoryHost> {
val text = properties.getString("QuickConnect.historyHosts", "[]")
return ohMyJson.runCatching { ohMyJson.decodeFromString<List<HistoryHost>>(text) }
.getOrNull() ?: emptyList()
}
override fun addNotify() {
super.addNotify()
controlsVisible = false
}
}
@Serializable
private data class HistoryHost(
val url: String,
val host: Host,
)
}

View File

@@ -6,6 +6,7 @@ import app.termora.Icons
import app.termora.Scope
import app.termora.actions.NewHostAction
import app.termora.actions.OpenLocalTerminalAction
import app.termora.actions.QuickConnectAction
import app.termora.snippet.SnippetAction
import com.formdev.flatlaf.FlatLaf
import org.jdesktop.swingx.action.ActionManager
@@ -19,19 +20,13 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
actionManager.let { list.add(CreateHostFindEverywhereResult()) }
// Local terminal
actionManager.getAction(OpenLocalTerminalAction.LOCAL_TERMINAL)?.let {
list.add(ActionFindEverywhereResult(it))
}
actionManager.getAction(OpenLocalTerminalAction.LOCAL_TERMINAL)?.let { list.add(ActionFindEverywhereResult(it)) }
// Snippet
actionManager.getAction(SnippetAction.SNIPPET)?.let {
list.add(ActionFindEverywhereResult(it))
}
actionManager.getAction(SnippetAction.SNIPPET)?.let { list.add(ActionFindEverywhereResult(it)) }
// SFTP
actionManager.getAction(Actions.SFTP)?.let {
list.add(ActionFindEverywhereResult(it))
}
actionManager.getAction(Actions.SFTP)?.let { list.add(ActionFindEverywhereResult(it)) }
// quick connect
actionManager.getAction(QuickConnectAction.QUICK_CONNECT)?.let { list.add(ActionFindEverywhereResult(it)) }
return list
}

View File

@@ -250,6 +250,7 @@ object SshClients {
val session = client.connect(entry).verify(timeout).session
if (host.authentication.type == AuthenticationType.Password) {
if (StringUtils.isNotBlank(host.authentication.password))
session.addPasswordIdentity(host.authentication.password)
} else if (host.authentication.type == AuthenticationType.PublicKey) {
session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password)

View File

@@ -1,11 +1,12 @@
package app.termora.transfer
import app.termora.Host
import app.termora.actions.AnActionEvent
import org.apache.commons.lang3.StringUtils
import java.util.*
class TransferActionEvent(
source: Any,
val hostId: String,
val host: Host? = null,
event: EventObject
) : AnActionEvent(source, StringUtils.EMPTY, event)

View File

@@ -1,6 +1,5 @@
package app.termora.transfer
import app.termora.HostManager
import app.termora.HostTerminalTab
import app.termora.I18n
import app.termora.Icons
@@ -8,10 +7,8 @@ import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
import app.termora.protocol.TransferProtocolProvider
import org.apache.commons.lang3.StringUtils
class TransferAnAction : AnAction(I18n.getString("termora.transport.sftp"), Icons.folder) {
private val hostManager get() = HostManager.getInstance()
override fun actionPerformed(evt: AnActionEvent) {
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
@@ -29,35 +26,32 @@ class TransferAnAction : AnAction(I18n.getString("termora.transport.sftp"), Icon
terminalTabbedManager.addTerminalTab(sftpTab, false)
}
var hostId = if (evt is TransferActionEvent) evt.hostId else StringUtils.EMPTY
var host = if (evt is TransferActionEvent) evt.host else null
// 如果不是特定事件那么尝试获取选中的Tab如果是一个 Host 并且是 SSH 协议那么直接打开
if (hostId.isBlank()) {
if (host == null) {
val tab = terminalTabbedManager.getSelectedTerminalTab()
// 如果当前选中的是 Host 主机
if (tab is HostTerminalTab) {
if (TransferProtocolProvider.valueOf(tab.host.protocol) != null) {
hostId = tab.host.id
host = tab.host
}
}
}
terminalTabbedManager.setSelectedTerminalTab(sftpTab)
if (hostId.isBlank()) return
val tabbed = sftpTab.rightTabbed
// 如果已经打开了 那么直接选中
if (host != null) {
for (i in 0 until tabbed.tabCount) {
val panel = tabbed.getTransportPanel(i) ?: continue
if (panel.host.id == hostId) {
if (panel.host.id == host.id) {
tabbed.selectedIndex = i
return
}
}
}
val host = hostManager.getHost(hostId) ?: return
var selectionPane: TransportSelectionPanel? = null
for (i in 0 until tabbed.tabCount) {
val c = tabbed.getComponentAt(i)
if (c is TransportSelectionPanel) {
@@ -72,8 +66,10 @@ class TransferAnAction : AnAction(I18n.getString("termora.transport.sftp"), Icon
selectionPane = tabbed.addSelectionTab()
}
if (host != null) {
selectionPane.connect(host)
}
terminalTabbedManager.setSelectedTerminalTab(sftpTab)
}
}

View File

@@ -481,12 +481,12 @@ class NewHostTree : SimpleTree(), Disposable {
}
private fun openWithSFTP(evt: EventObject) {
val nodes = getSelectionSimpleTreeNodes(true)
val hosts = getSelectionSimpleTreeNodes(true)
.map { it.host }.filter { TransferProtocolProvider.valueOf(it.protocol) != null }
if (nodes.isEmpty()) return
if (hosts.isEmpty()) return
for (node in nodes) {
sftpAction.actionPerformed(TransferActionEvent(this, node.id, evt))
for (host in hosts) {
sftpAction.actionPerformed(TransferActionEvent(this, host, evt))
}
}

View File

@@ -414,6 +414,7 @@ termora.actions.zoom-in-terminal=Zoom In Terminal
termora.actions.zoom-out-terminal=Zoom Out Terminal
termora.actions.zoom-reset-terminal=Reset Terminal Zoom
termora.actions.open-local-terminal=Open Local Terminal
termora.actions.quick-connect=Quick Connect
termora.actions.open-find-everywhere=Open FindEverywhere
termora.actions.open-new-window=Open new Window
termora.actions.clear-screen=Clear Terminal Screen

View File

@@ -417,6 +417,7 @@ termora.actions.zoom-in-terminal=放大终端
termora.actions.zoom-out-terminal=缩小终端
termora.actions.zoom-reset-terminal=重置终端缩放
termora.actions.open-local-terminal=打开本地终端
termora.actions.quick-connect=快速连接
termora.actions.open-find-everywhere=打开全局查找
termora.actions.open-new-window=打开新窗口
termora.actions.clear-screen=清除终端屏幕

View File

@@ -405,6 +405,7 @@ termora.actions.zoom-in-terminal=放大終端
termora.actions.zoom-out-terminal=縮小終端
termora.actions.zoom-reset-terminal=重置終端縮放
termora.actions.open-local-terminal=開啟本地終端
termora.actions.quick-connect=快速連接
termora.actions.open-find-everywhere=開啟全域搜尋
termora.actions.open-new-window=開啟新視窗
termora.actions.clear-screen=清除終端機螢幕