mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
13 Commits
2.0.0-beta
...
2.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5050aa37f5 | ||
|
|
53d3d96a06 | ||
|
|
d40b8a4c9c | ||
|
|
728671509c | ||
|
|
b7178a30fb | ||
|
|
939d6a1fd7 | ||
|
|
2986a9cc46 | ||
|
|
f36afaf5d3 | ||
|
|
8cec835583 | ||
|
|
a32838dad6 | ||
|
|
d54671757e | ||
|
|
d1dba56bcd | ||
|
|
919c06779d |
@@ -22,10 +22,7 @@ import org.apache.commons.lang3.LocaleUtils
|
|||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.MenuItem
|
import java.awt.*
|
||||||
import java.awt.PopupMenu
|
|
||||||
import java.awt.SystemTray
|
|
||||||
import java.awt.TrayIcon
|
|
||||||
import java.awt.desktop.AppReopenedEvent
|
import java.awt.desktop.AppReopenedEvent
|
||||||
import java.awt.desktop.AppReopenedListener
|
import java.awt.desktop.AppReopenedListener
|
||||||
import java.awt.desktop.SystemEventListener
|
import java.awt.desktop.SystemEventListener
|
||||||
@@ -173,7 +170,6 @@ class ApplicationRunner {
|
|||||||
private fun setupLaf() {
|
private fun setupLaf() {
|
||||||
|
|
||||||
System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux || SystemInfo.isWindows}")
|
System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux || SystemInfo.isWindows}")
|
||||||
System.setProperty(FlatSystemProperties.USE_ROUNDED_POPUP_BORDER, "false")
|
|
||||||
|
|
||||||
if (SystemInfo.isLinux) {
|
if (SystemInfo.isLinux) {
|
||||||
JFrame.setDefaultLookAndFeelDecorated(true)
|
JFrame.setDefaultLookAndFeelDecorated(true)
|
||||||
@@ -197,12 +193,13 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
themeManager.change(theme, true)
|
themeManager.change(theme, true)
|
||||||
|
|
||||||
|
if (Application.isBetaVersion()) {
|
||||||
FlatInspector.install("ctrl shift X")
|
FlatInspector.install("ctrl shift X")
|
||||||
|
}
|
||||||
|
|
||||||
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
||||||
UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false)
|
UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false)
|
||||||
UIManager.put("TitlePane.useWindowDecorations", false)
|
UIManager.put(FlatClientProperties.POPUP_FORCE_HEAVY_WEIGHT, true)
|
||||||
|
|
||||||
UIManager.put("Component.arc", 5)
|
UIManager.put("Component.arc", 5)
|
||||||
UIManager.put("TextComponent.arc", UIManager.getInt("Component.arc"))
|
UIManager.put("TextComponent.arc", UIManager.getInt("Component.arc"))
|
||||||
@@ -213,7 +210,6 @@ class ApplicationRunner {
|
|||||||
UIManager.put("Dialog.width", 650)
|
UIManager.put("Dialog.width", 650)
|
||||||
UIManager.put("Dialog.height", 550)
|
UIManager.put("Dialog.height", 550)
|
||||||
|
|
||||||
|
|
||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isMacOS) {
|
||||||
UIManager.put("TabbedPane.tabHeight", UIManager.getInt("TitleBar.height"))
|
UIManager.put("TabbedPane.tabHeight", UIManager.getInt("TitleBar.height"))
|
||||||
} else if (SystemInfo.isLinux) {
|
} else if (SystemInfo.isLinux) {
|
||||||
@@ -231,15 +227,33 @@ class ApplicationRunner {
|
|||||||
UIManager.put("Table.rowHeight", 24)
|
UIManager.put("Table.rowHeight", 24)
|
||||||
UIManager.put("Table.focusCellHighlightBorder", FlatTableCellBorder.Default())
|
UIManager.put("Table.focusCellHighlightBorder", FlatTableCellBorder.Default())
|
||||||
UIManager.put("Table.focusSelectedCellHighlightBorder", FlatTableCellBorder.Default())
|
UIManager.put("Table.focusSelectedCellHighlightBorder", FlatTableCellBorder.Default())
|
||||||
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
|
||||||
|
|
||||||
UIManager.put("Tree.rowHeight", 24)
|
UIManager.put("Tree.rowHeight", 24)
|
||||||
UIManager.put("Tree.background", DynamicColor("window"))
|
UIManager.put("Tree.background", DynamicColor("window"))
|
||||||
UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc"))
|
|
||||||
UIManager.put("Tree.showCellFocusIndicator", false)
|
UIManager.put("Tree.showCellFocusIndicator", false)
|
||||||
UIManager.put("Tree.repaintWholeRow", true)
|
UIManager.put("Tree.repaintWholeRow", true)
|
||||||
|
|
||||||
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
// Linux 更多的是尖锐风格
|
||||||
|
if (SystemInfo.isMacOS || SystemInfo.isWindows) {
|
||||||
|
val selectionInsets = Insets(0, 2, 0, 2)
|
||||||
|
UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("Tree.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
|
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("List.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
|
UIManager.put("ComboBox.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("ComboBox.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
|
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("Table.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
|
UIManager.put("MenuBar.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("MenuBar.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
|
UIManager.put("MenuItem.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("MenuItem.selectionInsets", selectionInsets)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ object Icons {
|
|||||||
val empty by lazy { DynamicIcon("icons/empty.svg") }
|
val empty by lazy { DynamicIcon("icons/empty.svg") }
|
||||||
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") }
|
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") }
|
||||||
val add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") }
|
val add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") }
|
||||||
|
val breakpoint by lazy { DynamicIcon("icons/breakpoint.svg", "icons/breakpoint_dark.svg") }
|
||||||
val softWrap by lazy { DynamicIcon("icons/softWrap.svg", "icons/softWrap_dark.svg") }
|
val softWrap by lazy { DynamicIcon("icons/softWrap.svg", "icons/softWrap_dark.svg") }
|
||||||
val scrollUp by lazy { DynamicIcon("icons/scrollUp.svg", "icons/scrollUp_dark.svg") }
|
val scrollUp by lazy { DynamicIcon("icons/scrollUp.svg", "icons/scrollUp_dark.svg") }
|
||||||
val reformatCode by lazy { DynamicIcon("icons/reformatCode.svg", "icons/reformatCode_dark.svg") }
|
val reformatCode by lazy { DynamicIcon("icons/reformatCode.svg", "icons/reformatCode_dark.svg") }
|
||||||
@@ -78,6 +79,7 @@ object Icons {
|
|||||||
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") }
|
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") }
|
||||||
val export by lazy { DynamicIcon("icons/export.svg", "icons/export_dark.svg") }
|
val export by lazy { DynamicIcon("icons/export.svg", "icons/export_dark.svg") }
|
||||||
val terminal by lazy { DynamicIcon("icons/terminal.svg", "icons/terminal_dark.svg") }
|
val terminal by lazy { DynamicIcon("icons/terminal.svg", "icons/terminal_dark.svg") }
|
||||||
|
val telnet by lazy { DynamicIcon("icons/telnet.svg", "icons/telnet_dark.svg") }
|
||||||
val ssh by lazy { DynamicIcon("icons/ssh.svg", "icons/ssh_dark.svg") }
|
val ssh by lazy { DynamicIcon("icons/ssh.svg", "icons/ssh_dark.svg") }
|
||||||
val ftp by lazy { DynamicIcon("icons/ftp.svg", "icons/ftp_dark.svg") }
|
val ftp by lazy { DynamicIcon("icons/ftp.svg", "icons/ftp_dark.svg") }
|
||||||
val minio by lazy { DynamicIcon("icons/minio.svg", "icons/minio_dark.svg") }
|
val minio by lazy { DynamicIcon("icons/minio.svg", "icons/minio_dark.svg") }
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class JSplitPaneWithZeroSizeDivider(
|
|||||||
synchronized(treeLock) {
|
synchronized(treeLock) {
|
||||||
for (c in components) {
|
for (c in components) {
|
||||||
if (c == divider) {
|
if (c == divider) {
|
||||||
|
c.isVisible = splitPane.leftComponent.isVisible
|
||||||
c.setBounds(
|
c.setBounds(
|
||||||
splitPane.dividerLocation - w,
|
splitPane.dividerLocation - w,
|
||||||
topOffset.get(),
|
topOffset.get(),
|
||||||
@@ -109,8 +110,10 @@ class JSplitPaneWithZeroSizeDivider(
|
|||||||
|
|
||||||
override fun paint(g: Graphics) {
|
override fun paint(g: Graphics) {
|
||||||
super.paint(g)
|
super.paint(g)
|
||||||
g.color = UIManager.getColor("controlShadow")
|
if (divider.isVisible) {
|
||||||
g.fillRect(splitPane.dividerLocation, 0, 1, topOffset.get())
|
g.color = UIManager.getColor("controlShadow")
|
||||||
|
g.fillRect(splitPane.dividerLocation, 0, 1, topOffset.get())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,8 @@ import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
|
|||||||
import app.termora.findeverywhere.FindEverywhereProvider
|
import app.termora.findeverywhere.FindEverywhereProvider
|
||||||
import app.termora.findeverywhere.FindEverywhereProviderExtension
|
import app.termora.findeverywhere.FindEverywhereProviderExtension
|
||||||
import app.termora.findeverywhere.FindEverywhereResult
|
import app.termora.findeverywhere.FindEverywhereResult
|
||||||
|
import app.termora.plugin.ExtensionManager
|
||||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||||
import app.termora.plugin.internal.sftppty.SFTPPtyProtocolProvider
|
|
||||||
import app.termora.plugin.internal.sftppty.SFTPPtyTerminalTab
|
|
||||||
import app.termora.plugin.internal.ssh.SSHProtocolProvider
|
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
@@ -24,7 +22,6 @@ import java.awt.event.ActionEvent
|
|||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import java.util.*
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -211,6 +208,15 @@ class TerminalTabbed(
|
|||||||
private fun showContextMenu(tabIndex: Int, e: MouseEvent) {
|
private fun showContextMenu(tabIndex: Int, e: MouseEvent) {
|
||||||
val c = tabbedPane.getComponentAt(tabIndex) as JComponent
|
val c = tabbedPane.getComponentAt(tabIndex) as JComponent
|
||||||
val tab = tabs[tabIndex]
|
val tab = tabs[tabIndex]
|
||||||
|
val extensions = ExtensionManager.getInstance().getExtensions(TerminalTabbedContextMenuExtension::class.java)
|
||||||
|
val menuItems = mutableListOf<JMenuItem>()
|
||||||
|
for (extension in extensions) {
|
||||||
|
try {
|
||||||
|
menuItems.add(extension.createJMenuItem(windowScope, tab))
|
||||||
|
} catch (_: UnsupportedOperationException) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val popupMenu = FlatPopupMenu()
|
val popupMenu = FlatPopupMenu()
|
||||||
|
|
||||||
@@ -232,7 +238,7 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 克隆
|
// 克隆
|
||||||
val clone = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.clone"))
|
val clone = popupMenu.add(I18n.getString("termora.copy"))
|
||||||
clone.addActionListener { evt ->
|
clone.addActionListener { evt ->
|
||||||
if (tab is HostTerminalTab) {
|
if (tab is HostTerminalTab) {
|
||||||
actionManager
|
actionManager
|
||||||
@@ -284,14 +290,10 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (tab is HostTerminalTab) {
|
if (menuItems.isNotEmpty()) {
|
||||||
val openHostAction = actionManager.getAction(OpenHostAction.OPEN_HOST)
|
popupMenu.addSeparator()
|
||||||
if (openHostAction != null) {
|
for (item in menuItems) {
|
||||||
if (tab.host.protocol == SSHProtocolProvider.PROTOCOL || tab.host.protocol == SFTPPtyProtocolProvider.PROTOCOL) {
|
popupMenu.add(item)
|
||||||
popupMenu.addSeparator()
|
|
||||||
val sftpCommand = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
|
|
||||||
sftpCommand.addActionListener { openSFTPPtyTab(tab, openHostAction, it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,36 +385,6 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun openSFTPPtyTab(tab: HostTerminalTab, openHostAction: Action, evt: EventObject) {
|
|
||||||
if (!SFTPPtyTerminalTab.canSupports) {
|
|
||||||
OptionPane.showMessageDialog(
|
|
||||||
SwingUtilities.getWindowAncestor(this),
|
|
||||||
I18n.getString("termora.tabbed.contextmenu.sftp-not-install"),
|
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var host = tab.host
|
|
||||||
|
|
||||||
if (host.protocol == SSHProtocolProvider.PROTOCOL) {
|
|
||||||
val envs = tab.host.options.envs().toMutableMap()
|
|
||||||
val currentDir = tab.getData(DataProviders.Terminal)?.getTerminalModel()
|
|
||||||
?.getData(DataKey.CurrentDir, StringUtils.EMPTY) ?: StringUtils.EMPTY
|
|
||||||
|
|
||||||
if (currentDir.isNotBlank()) {
|
|
||||||
envs["CurrentDir"] = currentDir
|
|
||||||
}
|
|
||||||
|
|
||||||
host = host.copy(
|
|
||||||
protocol = SFTPPtyProtocolProvider.PROTOCOL,
|
|
||||||
options = host.options.copy(env = envs.toPropertiesString())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
openHostAction.actionPerformed(OpenHostActionEvent(this, host, evt))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对着 ToolBar 右键
|
* 对着 ToolBar 右键
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import javax.swing.JMenuItem
|
||||||
|
|
||||||
|
interface TerminalTabbedContextMenuExtension : Extension {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抛出 [UnsupportedOperationException] 表示不支持
|
||||||
|
*/
|
||||||
|
fun createJMenuItem(windowScope: WindowScope, tab: TerminalTab): JMenuItem
|
||||||
|
}
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnAction
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.tree.NewHostTree
|
import app.termora.tree.NewHostTree
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Font
|
import java.awt.Font
|
||||||
|
import java.awt.event.ComponentAdapter
|
||||||
|
import java.awt.event.ComponentEvent
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
|
||||||
class TermoraFencePanel(
|
class TermoraFencePanel(
|
||||||
@@ -24,6 +31,8 @@ class TermoraFencePanel(
|
|||||||
private val leftTreePanel = LeftTreePanel()
|
private val leftTreePanel = LeftTreePanel()
|
||||||
private val mySplitPane = JSplitPaneWithZeroSizeDivider(splitPane) { tabbed.tabHeight }
|
private val mySplitPane = JSplitPaneWithZeroSizeDivider(splitPane) { tabbed.tabHeight }
|
||||||
private val enableManager get() = EnableManager.getInstance()
|
private val enableManager get() = EnableManager.getInstance()
|
||||||
|
private val toolbar = FlatToolBar().apply { isFloatable = false }
|
||||||
|
private var dividerLocation = 0
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -44,12 +53,39 @@ class TermoraFencePanel(
|
|||||||
tabbed.tabType = FlatTabbedPane.TabType.underlined
|
tabbed.tabType = FlatTabbedPane.TabType.underlined
|
||||||
tabbed.tabAreaInsets = null
|
tabbed.tabAreaInsets = null
|
||||||
|
|
||||||
|
// macOS 避开控制栏
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
toolbar.add(Box.createHorizontalStrut(76))
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbar.add(createColspanAction())
|
||||||
|
tabbed.leadingComponent = toolbar
|
||||||
|
toolbar.isVisible = false
|
||||||
|
|
||||||
add(mySplitPane, BorderLayout.CENTER)
|
add(mySplitPane, BorderLayout.CENTER)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
Disposer.register(this, leftTreePanel)
|
Disposer.register(this, leftTreePanel)
|
||||||
splitPane.addPropertyChangeListener("dividerLocation") { mySplitPane.doLayout() }
|
splitPane.addPropertyChangeListener("dividerLocation") { mySplitPane.doLayout() }
|
||||||
|
|
||||||
|
leftTreePanel.addComponentListener(object : ComponentAdapter() {
|
||||||
|
override fun componentHidden(e: ComponentEvent) {
|
||||||
|
toolbar.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun componentShown(e: ComponentEvent) {
|
||||||
|
toolbar.isVisible = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
actionMap.put("toggle", createColspanAction())
|
||||||
|
getInputMap(WHEN_IN_FOCUSED_WINDOW).put(
|
||||||
|
KeyStroke.getKeyStroke(
|
||||||
|
KeyEvent.VK_B,
|
||||||
|
toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK
|
||||||
|
), "toggle"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class LeftTreePanel : JPanel(BorderLayout()), Disposable {
|
private inner class LeftTreePanel : JPanel(BorderLayout()), Disposable {
|
||||||
@@ -70,9 +106,14 @@ class TermoraFencePanel(
|
|||||||
val label = JLabel(Application.getName())
|
val label = JLabel(Application.getName())
|
||||||
label.foreground = UIManager.getColor("textInactiveText")
|
label.foreground = UIManager.getColor("textInactiveText")
|
||||||
label.font = label.font.deriveFont(Font.BOLD)
|
label.font = label.font.deriveFont(Font.BOLD)
|
||||||
|
// 与最后一个按钮对冲,使其宽度和谐
|
||||||
|
box.add(JButton(Icons.empty))
|
||||||
box.add(Box.createHorizontalGlue())
|
box.add(Box.createHorizontalGlue())
|
||||||
if (SystemInfo.isMacOS.not()) box.add(label)
|
if (SystemInfo.isMacOS.not()) {
|
||||||
|
box.add(label)
|
||||||
|
}
|
||||||
box.add(Box.createHorizontalGlue())
|
box.add(Box.createHorizontalGlue())
|
||||||
|
box.add(createColspanAction())
|
||||||
|
|
||||||
if (SystemInfo.isMacOS || SystemInfo.isLinux) {
|
if (SystemInfo.isMacOS || SystemInfo.isLinux) {
|
||||||
box.addMouseListener(moveMouseAdapter)
|
box.addMouseListener(moveMouseAdapter)
|
||||||
@@ -95,8 +136,24 @@ class TermoraFencePanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createColspanAction(): Action {
|
||||||
|
return object : AnAction(Icons.dataColumn) {
|
||||||
|
init {
|
||||||
|
val text = I18n.getString("termora.welcome.toggle-sidebar")
|
||||||
|
putValue(SHORT_DESCRIPTION, "$text (${if (SystemInfo.isMacOS) '⌘' else "Ctrl"} + Shift + B)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
if (leftTreePanel.isVisible) dividerLocation = splitPane.dividerLocation
|
||||||
|
leftTreePanel.isVisible = leftTreePanel.isVisible.not()
|
||||||
|
if (leftTreePanel.isVisible) splitPane.dividerLocation = dividerLocation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
enableManager.setFlag("Termora.Fence.dividerLocation", splitPane.dividerLocation)
|
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getHostTree(): NewHostTree {
|
fun getHostTree(): NewHostTree {
|
||||||
|
|||||||
@@ -169,14 +169,6 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICON, false)
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICON, false)
|
||||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, height)
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, height)
|
||||||
} else if (SystemInfo.isMacOS) {
|
|
||||||
rootPane.putClientProperty("apple.awt.windowTitleVisible", false)
|
|
||||||
rootPane.putClientProperty("apple.awt.fullWindowContent", true)
|
|
||||||
rootPane.putClientProperty("apple.awt.transparentTitleBar", true)
|
|
||||||
rootPane.putClientProperty(
|
|
||||||
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING,
|
|
||||||
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING_MEDIUM
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
import javax.swing.PopupFactory
|
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
|
|
||||||
@@ -118,11 +117,7 @@ internal class ThemeManager private constructor() {
|
|||||||
|
|
||||||
private fun immediateChange(classname: String) {
|
private fun immediateChange(classname: String) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
val oldPopupFactory = PopupFactory.getSharedInstance()
|
|
||||||
UIManager.setLookAndFeel(classname)
|
UIManager.setLookAndFeel(classname)
|
||||||
PopupFactory.setSharedInstance(oldPopupFactory)
|
|
||||||
|
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
log.error(ex.message, ex)
|
log.error(ex.message, ex)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app.termora.keymgr
|
|||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
|
import app.termora.plugin.internal.ssh.SshClients
|
||||||
import app.termora.terminal.ControlCharacters
|
import app.termora.terminal.ControlCharacters
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.terminal.PtyConnectorDelegate
|
import app.termora.terminal.PtyConnectorDelegate
|
||||||
|
|||||||
@@ -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.telnet.TelnetInternalPlugin
|
||||||
import app.termora.plugin.internal.wsl.WSLInternalPlugin
|
import app.termora.plugin.internal.wsl.WSLInternalPlugin
|
||||||
import app.termora.swingCoroutineScope
|
import app.termora.swingCoroutineScope
|
||||||
import app.termora.terminal.panel.vw.FloatingToolbarPlugin
|
import app.termora.terminal.panel.vw.FloatingToolbarPlugin
|
||||||
@@ -111,12 +112,14 @@ internal class PluginManager private constructor() {
|
|||||||
|
|
||||||
// ssh plugin
|
// ssh plugin
|
||||||
plugins.add(PluginDescriptor(SSHInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
plugins.add(PluginDescriptor(SSHInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
// serial plugin
|
|
||||||
plugins.add(PluginDescriptor(SerialInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
|
||||||
// local plugin
|
// local plugin
|
||||||
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))
|
||||||
|
// telnet plugin
|
||||||
|
plugins.add(PluginDescriptor(TelnetInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
|
// serial plugin
|
||||||
|
plugins.add(PluginDescriptor(SerialInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
// wsl plugin
|
// wsl plugin
|
||||||
if (SystemUtils.IS_OS_WINDOWS) {
|
if (SystemUtils.IS_OS_WINDOWS) {
|
||||||
plugins.add(PluginDescriptor(WSLInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
plugins.add(PluginDescriptor(WSLInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import java.awt.Component
|
|||||||
import java.awt.event.ItemEvent
|
import java.awt.event.ItemEvent
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
class BasicProxyOption(private val proxyTypes: List<ProxyType> = listOf(ProxyType.HTTP, ProxyType.SOCKS5)) :
|
class BasicProxyOption(
|
||||||
|
private val proxyTypes: List<ProxyType> = listOf(ProxyType.HTTP, ProxyType.SOCKS5),
|
||||||
|
private val authenticationTypes: List<AuthenticationType> = listOf(AuthenticationType.Password),
|
||||||
|
) :
|
||||||
JPanel(BorderLayout()), Option {
|
JPanel(BorderLayout()), Option {
|
||||||
private val formMargin = "7dlu"
|
private val formMargin = "7dlu"
|
||||||
|
|
||||||
@@ -21,6 +24,10 @@ class BasicProxyOption(private val proxyTypes: List<ProxyType> = listOf(ProxyTyp
|
|||||||
val proxyPortTextField = PortSpinner(1080)
|
val proxyPortTextField = PortSpinner(1080)
|
||||||
val proxyAuthenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
val proxyAuthenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||||
|
|
||||||
|
constructor(proxyTypes: List<ProxyType> = listOf(ProxyType.HTTP, ProxyType.SOCKS5)) : this(
|
||||||
|
proxyTypes,
|
||||||
|
listOf(AuthenticationType.Password)
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -67,7 +74,9 @@ class BasicProxyOption(private val proxyTypes: List<ProxyType> = listOf(ProxyTyp
|
|||||||
}
|
}
|
||||||
|
|
||||||
proxyAuthenticationTypeComboBox.addItem(AuthenticationType.No)
|
proxyAuthenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||||
proxyAuthenticationTypeComboBox.addItem(AuthenticationType.Password)
|
for (type in authenticationTypes) {
|
||||||
|
proxyAuthenticationTypeComboBox.addItem(type)
|
||||||
|
}
|
||||||
|
|
||||||
proxyUsernameTextField.text = "root"
|
proxyUsernameTextField.text = "root"
|
||||||
|
|
||||||
|
|||||||
@@ -18,4 +18,8 @@ internal class LocalProtocolHostPanelExtension private constructor() : ProtocolH
|
|||||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return LocalProtocolHostPanel()
|
return LocalProtocolHostPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,8 @@ package app.termora.plugin.internal.local
|
|||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.protocol.GenericProtocolProvider
|
import app.termora.protocol.GenericProtocolProvider
|
||||||
import app.termora.protocol.ProtocolTestRequest
|
|
||||||
import app.termora.protocol.ProtocolTester
|
|
||||||
|
|
||||||
internal class LocalProtocolProvider private constructor() : GenericProtocolProvider, ProtocolTester {
|
internal class LocalProtocolProvider private constructor() : GenericProtocolProvider {
|
||||||
companion object {
|
companion object {
|
||||||
val instance by lazy { LocalProtocolProvider() }
|
val instance by lazy { LocalProtocolProvider() }
|
||||||
const val PROTOCOL = "local"
|
const val PROTOCOL = "local"
|
||||||
@@ -20,9 +18,6 @@ internal class LocalProtocolProvider private constructor() : GenericProtocolProv
|
|||||||
return Icons.powershell
|
return Icons.powershell
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun canTestConnection(requester: ProtocolTestRequest): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createTerminalTab(dataProvider: DataProvider, windowScope: WindowScope, host: Host): TerminalTab {
|
override fun createTerminalTab(dataProvider: DataProvider, windowScope: WindowScope, host: Host): TerminalTab {
|
||||||
return LocalTerminalTab(windowScope, host)
|
return LocalTerminalTab(windowScope, host)
|
||||||
|
|||||||
@@ -232,7 +232,8 @@ class PluginPanel(val descriptor: PluginPluginDescriptor) : JPanel(), Disposable
|
|||||||
|
|
||||||
// 当有多个插件正在安装时,那么最后一个安装成功的询问是否重启
|
// 当有多个插件正在安装时,那么最后一个安装成功的询问是否重启
|
||||||
if (installing.get() <= 1) {
|
if (installing.get() <= 1) {
|
||||||
restarter.scheduleRestart(owner)
|
// 不阻塞按钮状态变更
|
||||||
|
SwingUtilities.invokeLater { restarter.scheduleRestart(owner) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是更新,那么也需要刷新 InstalledPanel 下的按钮状态
|
// 如果是更新,那么也需要刷新 InstalledPanel 下的按钮状态
|
||||||
|
|||||||
@@ -18,4 +18,8 @@ internal class RDPProtocolHostPanelExtension private constructor() : ProtocolHos
|
|||||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return RDPProtocolHostPanel()
|
return RDPProtocolHostPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -19,4 +19,7 @@ internal class SerialProtocolHostPanelExtension private constructor() : Protocol
|
|||||||
return SerialProtocolHostPanel()
|
return SerialProtocolHostPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 5
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import app.termora.*
|
|||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
import app.termora.keymgr.KeyManager
|
import app.termora.keymgr.KeyManager
|
||||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||||
|
import app.termora.plugin.internal.ssh.SshClients
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.apache.commons.io.Charsets
|
import org.apache.commons.io.Charsets
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
import app.termora.TerminalTab
|
||||||
|
import app.termora.TerminalTabbedContextMenuExtension
|
||||||
|
import app.termora.WindowScope
|
||||||
|
import app.termora.actions.AnAction
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
|
import javax.swing.JMenuItem
|
||||||
|
|
||||||
|
class CloneSessionTerminalTabbedContextMenuExtension private constructor() : TerminalTabbedContextMenuExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = CloneSessionTerminalTabbedContextMenuExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createJMenuItem(
|
||||||
|
windowScope: WindowScope,
|
||||||
|
tab: TerminalTab
|
||||||
|
): JMenuItem {
|
||||||
|
if (tab is SSHTerminalTab) {
|
||||||
|
if (tab.host.protocol == SSHProtocolProvider.PROTOCOL) {
|
||||||
|
val cloneSession = JMenuItem(I18n.getString("termora.tabbed.contextmenu.clone-session"))
|
||||||
|
val c = tab.getData(SSHTerminalTab.MySshHandler)
|
||||||
|
cloneSession.isEnabled = c?.channel?.isOpen == true
|
||||||
|
if (c != null) {
|
||||||
|
cloneSession.addActionListener(object : AnAction() {
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||||
|
val handler = c.copy(channel = null)
|
||||||
|
val newTab = SSHTerminalTab(windowScope, tab.host, handler)
|
||||||
|
terminalTabbedManager.addTerminalTab(newTab)
|
||||||
|
newTab.start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return cloneSession
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugin.internal.ssh
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import app.termora.TerminalTabbedContextMenuExtension
|
||||||
import app.termora.plugin.Extension
|
import app.termora.plugin.Extension
|
||||||
import app.termora.plugin.InternalPlugin
|
import app.termora.plugin.InternalPlugin
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
@@ -9,6 +10,8 @@ internal class SSHInternalPlugin : InternalPlugin() {
|
|||||||
init {
|
init {
|
||||||
support.addExtension(ProtocolProviderExtension::class.java) { SSHProtocolProviderExtension.instance }
|
support.addExtension(ProtocolProviderExtension::class.java) { SSHProtocolProviderExtension.instance }
|
||||||
support.addExtension(ProtocolHostPanelExtension::class.java) { SSHProtocolHostPanelExtension.instance }
|
support.addExtension(ProtocolHostPanelExtension::class.java) { SSHProtocolHostPanelExtension.instance }
|
||||||
|
support.addExtension(TerminalTabbedContextMenuExtension::class.java) { SftpCommandTerminalTabbedContextMenuExtension.instance }
|
||||||
|
support.addExtension(TerminalTabbedContextMenuExtension::class.java) { CloneSessionTerminalTabbedContextMenuExtension.instance }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String {
|
||||||
|
|||||||
@@ -19,4 +19,8 @@ internal class SSHProtocolHostPanelExtension private constructor() : ProtocolHos
|
|||||||
return SSHProtocolHostPanel(accountOwner)
|
return SSHProtocolHostPanel(accountOwner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,41 +10,36 @@ import app.termora.keymap.KeymapManager
|
|||||||
import app.termora.terminal.ControlCharacters
|
import app.termora.terminal.ControlCharacters
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.apache.commons.io.Charsets
|
import org.apache.commons.io.Charsets
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.apache.sshd.client.SshClient
|
import org.apache.sshd.client.SshClient
|
||||||
import org.apache.sshd.client.channel.ChannelShell
|
import org.apache.sshd.client.channel.ChannelShell
|
||||||
import org.apache.sshd.client.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
import org.apache.sshd.common.SshConstants
|
import org.apache.sshd.common.future.CloseFuture
|
||||||
import org.apache.sshd.common.channel.Channel
|
import org.apache.sshd.common.future.SshFutureListener
|
||||||
import org.apache.sshd.common.channel.ChannelListener
|
|
||||||
import org.apache.sshd.common.session.Session
|
|
||||||
import org.apache.sshd.common.session.SessionListener
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
class SSHTerminalTab(
|
||||||
|
windowScope: WindowScope, host: Host,
|
||||||
|
private val handler: SshHandler = SshHandler()
|
||||||
|
) : PtyHostTerminalTab(windowScope, host) {
|
||||||
|
|
||||||
class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|
||||||
PtyHostTerminalTab(windowScope, host) {
|
|
||||||
companion object {
|
companion object {
|
||||||
val SSHSession = DataKey(ClientSession::class)
|
val SSHSession = DataKey(ClientSession::class)
|
||||||
|
internal val MySshHandler = DataKey(SshHandler::class)
|
||||||
private val log = LoggerFactory.getLogger(SSHTerminalTab::class.java)
|
private val log = LoggerFactory.getLogger(SSHTerminalTab::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
private val tab = this
|
private val owner get() = SwingUtilities.getWindowAncestor(terminalPanel)
|
||||||
|
private val tab get() = this
|
||||||
private var sshClient: SshClient? = null
|
|
||||||
private var sshSession: ClientSession? = null
|
|
||||||
private var sshChannelShell: ChannelShell? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
terminalPanel.dropFiles = false
|
terminalPanel.dropFiles = false
|
||||||
@@ -55,12 +50,10 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
return terminalPanel
|
return terminalPanel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun canReconnect(): Boolean {
|
override fun canReconnect(): Boolean {
|
||||||
return !mutex.isLocked
|
return mutex.isLocked.not()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override suspend fun openPtyConnector(): PtyConnector {
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
if (mutex.tryLock()) {
|
if (mutex.tryLock()) {
|
||||||
try {
|
try {
|
||||||
@@ -82,74 +75,32 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
// hide cursor
|
// hide cursor
|
||||||
terminalModel.setData(DataKey.Companion.ShowCursor, false)
|
terminalModel.setData(DataKey.Companion.ShowCursor, false)
|
||||||
// print
|
// print
|
||||||
terminal.write("SSH client is opening...\r\n")
|
terminal.write("Connecting to remote server ")
|
||||||
}
|
}
|
||||||
|
|
||||||
val owner = SwingUtilities.getWindowAncestor(terminalPanel)
|
val loading = coroutineScope.launch(Dispatchers.Swing) {
|
||||||
val client = SshClients.openClient(host, owner).also { sshClient = it }
|
var c = 0
|
||||||
val sessionListener = MySessionListener()
|
while (isActive) {
|
||||||
val channelListener = MyChannelListener()
|
if (++c > 6) c = 1
|
||||||
|
terminal.write("${ControlCharacters.ESC}[1;32m")
|
||||||
withContext(Dispatchers.Swing) { terminal.write("SSH client opened successfully.\r\n\r\n") }
|
terminal.write(".".repeat(c))
|
||||||
|
terminal.write(" ".repeat(6 - c))
|
||||||
client.addSessionListener(sessionListener)
|
terminal.write("${ControlCharacters.ESC}[0m")
|
||||||
client.addChannelListener(channelListener)
|
delay(350.milliseconds)
|
||||||
|
terminal.write("${ControlCharacters.BS}".repeat(6))
|
||||||
val (session, channel) = try {
|
|
||||||
val session = SshClients.openSession(host, client).also { sshSession = it }
|
|
||||||
val channel = SshClients.openShell(
|
|
||||||
host,
|
|
||||||
terminalPanel.winSize(),
|
|
||||||
session
|
|
||||||
).also { sshChannelShell = it }
|
|
||||||
Pair(session, channel)
|
|
||||||
} finally {
|
|
||||||
client.removeSessionListener(sessionListener)
|
|
||||||
client.removeChannelListener(channelListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newline
|
|
||||||
withContext(Dispatchers.Swing) {
|
|
||||||
terminal.write("\r\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
channel.addChannelListener(object : ChannelListener {
|
|
||||||
private val reconnectShortcut
|
|
||||||
get() = KeymapManager.Companion.getInstance().getActiveKeymap()
|
|
||||||
.getShortcut(TabReconnectAction.Companion.RECONNECT_TAB).firstOrNull()
|
|
||||||
|
|
||||||
override fun channelClosed(channel: Channel, reason: Throwable?) {
|
|
||||||
coroutineScope.launch(Dispatchers.Swing) {
|
|
||||||
terminal.write("\r\n\r\n${ControlCharacters.Companion.ESC}[31m")
|
|
||||||
terminal.write(I18n.getString("termora.terminal.channel-disconnected"))
|
|
||||||
if (reconnectShortcut is KeyShortcut) {
|
|
||||||
terminal.write(
|
|
||||||
I18n.getString(
|
|
||||||
"termora.terminal.channel-reconnect",
|
|
||||||
reconnectShortcut.toString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
terminal.write("\r\n")
|
|
||||||
terminal.write("${ControlCharacters.Companion.ESC}[0m")
|
|
||||||
terminalModel.setData(DataKey.Companion.ShowCursor, false)
|
|
||||||
if (DatabaseManager.getInstance().terminal.autoCloseTabWhenDisconnected) {
|
|
||||||
terminalTabbedManager?.let { manager ->
|
|
||||||
SwingUtilities.invokeLater {
|
|
||||||
manager.closeTerminalTab(tab, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop
|
|
||||||
stop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
// 打开隧道
|
val channel: ChannelShell
|
||||||
openTunnelings(session, host)
|
try {
|
||||||
|
val client = openClient()
|
||||||
|
val session = openSession(client)
|
||||||
|
channel = openChannel(session)
|
||||||
|
// 打开隧道
|
||||||
|
openTunnelings(session, host)
|
||||||
|
} finally {
|
||||||
|
loading.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
// 隐藏提示
|
// 隐藏提示
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
@@ -194,10 +145,68 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openClient(): SshClient {
|
||||||
|
val client = handler.client
|
||||||
|
if (client != null) return client
|
||||||
|
return SshClients.openClient(host, owner).also { handler.client = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openSession(client: SshClient): ClientSession {
|
||||||
|
val session = handler.session
|
||||||
|
if (session != null) return SshSessionPool.register(session, client)
|
||||||
|
return SshClients.openSession(host, client).also { handler.session = SshSessionPool.register(it, client) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openChannel(session: ClientSession): ChannelShell {
|
||||||
|
val channel = SshClients.openShell(host, terminalPanel.winSize(), session)
|
||||||
|
handler.channel = channel
|
||||||
|
|
||||||
|
channel.addCloseFutureListener(object : SshFutureListener<CloseFuture> {
|
||||||
|
private val reconnectShortcut
|
||||||
|
get() = KeymapManager.Companion.getInstance().getActiveKeymap()
|
||||||
|
.getShortcut(TabReconnectAction.Companion.RECONNECT_TAB).firstOrNull()
|
||||||
|
private val autoCloseTabWhenDisconnected get() = DatabaseManager.getInstance().terminal.autoCloseTabWhenDisconnected
|
||||||
|
|
||||||
|
override fun operationComplete(future: CloseFuture) {
|
||||||
|
coroutineScope.launch(Dispatchers.Swing) {
|
||||||
|
terminal.write("\r\n\r\n${ControlCharacters.Companion.ESC}[31m")
|
||||||
|
terminal.write(I18n.getString("termora.terminal.channel-disconnected"))
|
||||||
|
if (reconnectShortcut is KeyShortcut) {
|
||||||
|
terminal.write(
|
||||||
|
I18n.getString(
|
||||||
|
"termora.terminal.channel-reconnect",
|
||||||
|
reconnectShortcut.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
terminal.write("\r\n")
|
||||||
|
terminal.write("${ControlCharacters.Companion.ESC}[0m")
|
||||||
|
terminalModel.setData(DataKey.Companion.ShowCursor, false)
|
||||||
|
|
||||||
|
if (autoCloseTabWhenDisconnected) {
|
||||||
|
terminalTabbedManager?.let { manager ->
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
manager.closeTerminalTab(tab, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
if (dataKey == SSHSession) {
|
if (dataKey == SSHSession) {
|
||||||
return sshSession as T?
|
return handler.session as T?
|
||||||
|
}
|
||||||
|
if (dataKey == MySshHandler) {
|
||||||
|
return handler as T?
|
||||||
}
|
}
|
||||||
return super.getData(dataKey)
|
return super.getData(dataKey)
|
||||||
}
|
}
|
||||||
@@ -206,16 +215,7 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
if (mutex.tryLock()) {
|
if (mutex.tryLock()) {
|
||||||
try {
|
try {
|
||||||
super.stop()
|
super.stop()
|
||||||
|
handler.close()
|
||||||
sshChannelShell?.close(true)
|
|
||||||
sshSession?.disableSessionHeartbeat()
|
|
||||||
sshSession?.disconnect(SshConstants.SSH2_DISCONNECT_BY_APPLICATION, StringUtils.EMPTY)
|
|
||||||
sshSession?.close(true)
|
|
||||||
sshClient?.close(true)
|
|
||||||
|
|
||||||
sshChannelShell = null
|
|
||||||
sshSession = null
|
|
||||||
sshClient = null
|
|
||||||
} finally {
|
} finally {
|
||||||
mutex.unlock()
|
mutex.unlock()
|
||||||
}
|
}
|
||||||
@@ -231,36 +231,4 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
terminalPanel.storeVisualWindows(host.id)
|
terminalPanel.storeVisualWindows(host.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class MySessionListener : SessionListener, Disposable {
|
|
||||||
override fun sessionEvent(session: Session, event: SessionListener.Event) {
|
|
||||||
coroutineScope.launch {
|
|
||||||
when (event) {
|
|
||||||
SessionListener.Event.KeyEstablished -> terminal.write("Session Key exchange successful.\r\n")
|
|
||||||
SessionListener.Event.Authenticated -> terminal.write("Session authentication successful.\r\n\r\n")
|
|
||||||
SessionListener.Event.KexCompleted -> terminal.write("Session KEX negotiation successful.\r\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun sessionEstablished(session: Session) {
|
|
||||||
coroutineScope.launch { terminal.write("Session established.\r\n") }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun sessionCreated(session: Session?) {
|
|
||||||
coroutineScope.launch { terminal.write("Session created.\r\n") }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class MyChannelListener : ChannelListener, Disposable {
|
|
||||||
override fun channelOpenSuccess(channel: Channel) {
|
|
||||||
coroutineScope.launch { terminal.write("Channel shell opened successfully.\r\n") }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun channelInitialized(channel: Channel) {
|
|
||||||
coroutineScope.launch { terminal.write("Channel shell initialization successful.\r\n") }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.actions.*
|
||||||
|
import app.termora.plugin.internal.sftppty.SFTPPtyProtocolProvider
|
||||||
|
import app.termora.plugin.internal.sftppty.SFTPPtyTerminalTab
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.Action
|
||||||
|
import javax.swing.JMenuItem
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
|
||||||
|
class SftpCommandTerminalTabbedContextMenuExtension private constructor() : TerminalTabbedContextMenuExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = SftpCommandTerminalTabbedContextMenuExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val actionManager = ActionManager.getInstance()
|
||||||
|
|
||||||
|
override fun createJMenuItem(
|
||||||
|
windowScope: WindowScope,
|
||||||
|
tab: TerminalTab
|
||||||
|
): JMenuItem {
|
||||||
|
if (tab is HostTerminalTab) {
|
||||||
|
val openHostAction = actionManager.getAction(OpenHostAction.OPEN_HOST)
|
||||||
|
if (openHostAction != null) {
|
||||||
|
if (tab.host.protocol == SSHProtocolProvider.PROTOCOL || tab.host.protocol == SFTPPtyProtocolProvider.PROTOCOL) {
|
||||||
|
val sftpCommand = JMenuItem(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
|
||||||
|
sftpCommand.addActionListener(object : AnAction() {
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
openSFTPPtyTab(tab, openHostAction, evt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return sftpCommand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openSFTPPtyTab(tab: HostTerminalTab, openHostAction: Action, evt: EventObject) {
|
||||||
|
if (SFTPPtyTerminalTab.canSupports.not()) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
tab.windowScope.window,
|
||||||
|
I18n.getString("termora.tabbed.contextmenu.sftp-not-install"),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var host = tab.host
|
||||||
|
|
||||||
|
if (host.protocol == SSHProtocolProvider.PROTOCOL) {
|
||||||
|
val envs = tab.host.options.envs().toMutableMap()
|
||||||
|
val currentDir = tab.getData(DataProviders.Terminal)?.getTerminalModel()
|
||||||
|
?.getData(DataKey.CurrentDir, StringUtils.EMPTY) ?: StringUtils.EMPTY
|
||||||
|
|
||||||
|
if (currentDir.isNotBlank()) {
|
||||||
|
envs["CurrentDir"] = currentDir
|
||||||
|
}
|
||||||
|
|
||||||
|
host = host.copy(
|
||||||
|
protocol = SFTPPtyProtocolProvider.PROTOCOL,
|
||||||
|
options = host.options.copy(env = envs.toPropertiesString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
openHostAction.actionPerformed(OpenHostActionEvent(evt.source, host, evt))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||||
import app.termora.terminal.TerminalSize
|
import app.termora.terminal.TerminalSize
|
||||||
@@ -29,7 +30,6 @@ import org.apache.sshd.client.session.ClientSession
|
|||||||
import org.apache.sshd.client.session.ClientSessionImpl
|
import org.apache.sshd.client.session.ClientSessionImpl
|
||||||
import org.apache.sshd.client.session.SessionFactory
|
import org.apache.sshd.client.session.SessionFactory
|
||||||
import org.apache.sshd.common.AttributeRepository
|
import org.apache.sshd.common.AttributeRepository
|
||||||
import org.apache.sshd.common.SshConstants
|
|
||||||
import org.apache.sshd.common.SshException
|
import org.apache.sshd.common.SshException
|
||||||
import org.apache.sshd.common.channel.ChannelFactory
|
import org.apache.sshd.common.channel.ChannelFactory
|
||||||
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
||||||
@@ -63,7 +63,7 @@ import org.eclipse.jgit.internal.transport.sshd.proxy.AbstractClientProxyConnect
|
|||||||
import org.eclipse.jgit.internal.transport.sshd.proxy.HttpClientConnector
|
import org.eclipse.jgit.internal.transport.sshd.proxy.HttpClientConnector
|
||||||
import org.eclipse.jgit.internal.transport.sshd.proxy.Socks5ClientConnector
|
import org.eclipse.jgit.internal.transport.sshd.proxy.Socks5ClientConnector
|
||||||
import org.eclipse.jgit.transport.CredentialsProvider
|
import org.eclipse.jgit.transport.CredentialsProvider
|
||||||
import org.eclipse.jgit.transport.SshConstants.IDENTITY_AGENT
|
import org.eclipse.jgit.transport.SshConstants
|
||||||
import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
|
import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
|
||||||
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory
|
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -89,7 +89,7 @@ object SshClients {
|
|||||||
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
|
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
|
||||||
|
|
||||||
private val timeout = Duration.ofSeconds(30)
|
private val timeout = Duration.ofSeconds(30)
|
||||||
private val hostManager get() = HostManager.getInstance()
|
private val hostManager get() = HostManager.Companion.getInstance()
|
||||||
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
|
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,7 +166,7 @@ object SshClients {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val jumpHosts = mutableListOf<Host>()
|
val jumpHosts = mutableListOf<Host>()
|
||||||
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
val hosts = HostManager.Companion.getInstance().hosts().associateBy { it.id }
|
||||||
for (jumpHostId in h.options.jumpHosts) {
|
for (jumpHostId in h.options.jumpHosts) {
|
||||||
val e = hosts[jumpHostId]
|
val e = hosts[jumpHostId]
|
||||||
if (e == null) {
|
if (e == null) {
|
||||||
@@ -235,16 +235,16 @@ object SshClients {
|
|||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isMacOS) {
|
||||||
val file = FileUtils.getFile(Application.getBaseDataDir(), "config", "ssh-agent.sock")
|
val file = FileUtils.getFile(Application.getBaseDataDir(), "config", "ssh-agent.sock")
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
entry.setProperty(IDENTITY_AGENT, file.absolutePath)
|
entry.setProperty(SshConstants.IDENTITY_AGENT, file.absolutePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (entry.getProperty(IDENTITY_AGENT).isNullOrBlank()) {
|
if (entry.getProperty(SshConstants.IDENTITY_AGENT).isNullOrBlank()) {
|
||||||
if (host.authentication.password.isNotBlank())
|
if (host.authentication.password.isNotBlank())
|
||||||
entry.setProperty(IDENTITY_AGENT, host.authentication.password)
|
entry.setProperty(SshConstants.IDENTITY_AGENT, host.authentication.password)
|
||||||
else if (SystemInfo.isWindows)
|
else if (SystemInfo.isWindows)
|
||||||
entry.setProperty(IDENTITY_AGENT, PageantConnector.DESCRIPTOR.identityAgent)
|
entry.setProperty(SshConstants.IDENTITY_AGENT, PageantConnector.DESCRIPTOR.identityAgent)
|
||||||
else
|
else
|
||||||
entry.setProperty(IDENTITY_AGENT, UnixDomainSocketConnector.DESCRIPTOR.identityAgent)
|
entry.setProperty(SshConstants.IDENTITY_AGENT, UnixDomainSocketConnector.DESCRIPTOR.identityAgent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ object SshClients {
|
|||||||
throw SshException("Authentication failed")
|
throw SshException("Authentication failed")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e !is SshException || e.disconnectCode != SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) throw e
|
if (e !is SshException || e.disconnectCode != org.apache.sshd.common.SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) throw e
|
||||||
val owner = client.properties["owner"] as Window? ?: throw e
|
val owner = client.properties["owner"] as Window? ?: throw e
|
||||||
val askUserInfo = ask(host, entry, owner) ?: throw e
|
val askUserInfo = ask(host, entry, owner) ?: throw e
|
||||||
if (askUserInfo.authentication.type == AuthenticationType.No) throw e
|
if (askUserInfo.authentication.type == AuthenticationType.No) throw e
|
||||||
@@ -383,7 +383,7 @@ object SshClients {
|
|||||||
|
|
||||||
val channelFactories = mutableListOf<ChannelFactory>()
|
val channelFactories = mutableListOf<ChannelFactory>()
|
||||||
channelFactories.addAll(ClientBuilder.DEFAULT_CHANNEL_FACTORIES)
|
channelFactories.addAll(ClientBuilder.DEFAULT_CHANNEL_FACTORIES)
|
||||||
channelFactories.add(X11ChannelFactory.INSTANCE)
|
channelFactories.add(X11ChannelFactory.Companion.INSTANCE)
|
||||||
builder.channelFactories(channelFactories)
|
builder.channelFactories(channelFactories)
|
||||||
|
|
||||||
val sshClient = builder.build() as JGitSshClient
|
val sshClient = builder.build() as JGitSshClient
|
||||||
@@ -725,5 +725,4 @@ object SshClients {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import org.apache.sshd.client.SshClient
|
||||||
|
import org.apache.sshd.client.session.ClientSession
|
||||||
|
import org.apache.sshd.common.channel.Channel
|
||||||
|
|
||||||
|
data class SshHandler(
|
||||||
|
var client: SshClient? = null,
|
||||||
|
var session: ClientSession? = null,
|
||||||
|
var channel: Channel? = null
|
||||||
|
) : AutoCloseable {
|
||||||
|
override fun close() {
|
||||||
|
|
||||||
|
channel?.close(true)?.await()
|
||||||
|
session?.close(true)?.await()
|
||||||
|
|
||||||
|
channel = null
|
||||||
|
session = null
|
||||||
|
|
||||||
|
|
||||||
|
// client 由 SshSessionPool 负责关闭
|
||||||
|
if (client?.isClosing == true || client?.isClosed == true) {
|
||||||
|
client = null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import org.apache.sshd.client.SshClient
|
||||||
|
import org.apache.sshd.client.channel.ChannelExec
|
||||||
|
import org.apache.sshd.client.channel.ChannelShell
|
||||||
|
import org.apache.sshd.client.session.ClientSession
|
||||||
|
import org.apache.sshd.client.session.forward.DynamicPortForwardingTracker
|
||||||
|
import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker
|
||||||
|
import org.apache.sshd.common.AttributeRepository
|
||||||
|
import org.apache.sshd.common.channel.Channel
|
||||||
|
import org.apache.sshd.common.channel.PtyChannelConfigurationHolder
|
||||||
|
import org.apache.sshd.common.channel.throttle.ChannelStreamWriter
|
||||||
|
import org.apache.sshd.common.channel.throttle.ChannelStreamWriterResolver
|
||||||
|
import org.apache.sshd.common.future.CloseFuture
|
||||||
|
import org.apache.sshd.common.future.DefaultCloseFuture
|
||||||
|
import org.apache.sshd.common.io.IoWriteFuture
|
||||||
|
import org.apache.sshd.common.session.SessionHeartbeatController
|
||||||
|
import org.apache.sshd.common.util.buffer.Buffer
|
||||||
|
import org.apache.sshd.common.util.net.SshdSocketAddress
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.net.SocketAddress
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import java.util.function.Function
|
||||||
|
|
||||||
|
internal object SshSessionPool {
|
||||||
|
private val map = WeakHashMap<ClientSession, MyClientSession>()
|
||||||
|
|
||||||
|
fun register(session: ClientSession, client: SshClient): ClientSession {
|
||||||
|
if (session is MyClientSession) {
|
||||||
|
session.refCount.incrementAndGet()
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(session) {
|
||||||
|
val delegate = map[session] ?: MyClientSession(client, session)
|
||||||
|
map[session] = delegate
|
||||||
|
delegate.refCount.incrementAndGet()
|
||||||
|
return delegate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class MyClientSession(
|
||||||
|
private val client: SshClient,
|
||||||
|
private val delegate: ClientSession
|
||||||
|
) : ClientSession by delegate {
|
||||||
|
val refCount = AtomicInteger(0)
|
||||||
|
|
||||||
|
override fun createShellChannel(): ChannelShell? {
|
||||||
|
return delegate.createShellChannel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createExecChannel(command: String?): ChannelExec? {
|
||||||
|
return delegate.createExecChannel(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createExecChannel(
|
||||||
|
command: String?,
|
||||||
|
ptyConfig: PtyChannelConfigurationHolder?,
|
||||||
|
env: Map<String?, *>?
|
||||||
|
): ChannelExec? {
|
||||||
|
return delegate.createExecChannel(command, ptyConfig, env)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun executeRemoteCommand(command: String?): String? {
|
||||||
|
return delegate.executeRemoteCommand(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun executeRemoteCommand(
|
||||||
|
command: String?,
|
||||||
|
stderr: OutputStream?,
|
||||||
|
charset: Charset?
|
||||||
|
): String? {
|
||||||
|
return delegate.executeRemoteCommand(command, stderr, charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun executeRemoteCommand(
|
||||||
|
command: String?,
|
||||||
|
stdout: OutputStream?,
|
||||||
|
stderr: OutputStream?,
|
||||||
|
charset: Charset?
|
||||||
|
) {
|
||||||
|
delegate.executeRemoteCommand(command, stdout, stderr, charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createLocalPortForwardingTracker(
|
||||||
|
localPort: Int,
|
||||||
|
remote: SshdSocketAddress?
|
||||||
|
): ExplicitPortForwardingTracker? {
|
||||||
|
return delegate.createLocalPortForwardingTracker(localPort, remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createLocalPortForwardingTracker(
|
||||||
|
local: SshdSocketAddress?,
|
||||||
|
remote: SshdSocketAddress?
|
||||||
|
): ExplicitPortForwardingTracker? {
|
||||||
|
return delegate.createLocalPortForwardingTracker(local, remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createRemotePortForwardingTracker(
|
||||||
|
remote: SshdSocketAddress?,
|
||||||
|
local: SshdSocketAddress?
|
||||||
|
): ExplicitPortForwardingTracker? {
|
||||||
|
return delegate.createRemotePortForwardingTracker(remote, local)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createDynamicPortForwardingTracker(local: SshdSocketAddress?): DynamicPortForwardingTracker? {
|
||||||
|
return delegate.createDynamicPortForwardingTracker(local)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun waitFor(
|
||||||
|
mask: Collection<ClientSession.ClientSessionEvent?>?,
|
||||||
|
timeout: Duration?
|
||||||
|
): Set<ClientSession.ClientSessionEvent?>? {
|
||||||
|
return delegate.waitFor(mask, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createBuffer(cmd: Byte): Buffer? {
|
||||||
|
return delegate.createBuffer(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writePacket(
|
||||||
|
buffer: Buffer?,
|
||||||
|
timeout: Duration?
|
||||||
|
): IoWriteFuture? {
|
||||||
|
return delegate.writePacket(buffer, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writePacket(
|
||||||
|
buffer: Buffer?,
|
||||||
|
maxWaitMillis: Long
|
||||||
|
): IoWriteFuture? {
|
||||||
|
return delegate.writePacket(buffer, maxWaitMillis)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun request(
|
||||||
|
request: String?,
|
||||||
|
buffer: Buffer?,
|
||||||
|
timeout: Long,
|
||||||
|
unit: TimeUnit?
|
||||||
|
): Buffer? {
|
||||||
|
return delegate.request(request, buffer, timeout, unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun request(
|
||||||
|
request: String?,
|
||||||
|
buffer: Buffer?,
|
||||||
|
timeout: Duration?
|
||||||
|
): Buffer? {
|
||||||
|
return delegate.request(request, buffer, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLocalAddress(): SocketAddress? {
|
||||||
|
return delegate.getLocalAddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRemoteAddress(): SocketAddress? {
|
||||||
|
return delegate.getRemoteAddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : Any?> resolveAttribute(key: AttributeRepository.AttributeKey<T?>?): T? {
|
||||||
|
return delegate.resolveAttribute(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSessionHeartbeatType(): SessionHeartbeatController.HeartbeatType? {
|
||||||
|
return delegate.getSessionHeartbeatType()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSessionHeartbeatInterval(): Duration? {
|
||||||
|
return delegate.getSessionHeartbeatInterval()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disableSessionHeartbeat() {
|
||||||
|
delegate.disableSessionHeartbeat()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSessionHeartbeat(
|
||||||
|
type: SessionHeartbeatController.HeartbeatType?,
|
||||||
|
unit: TimeUnit?,
|
||||||
|
count: Long
|
||||||
|
) {
|
||||||
|
delegate.setSessionHeartbeat(type, unit, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSessionHeartbeat(
|
||||||
|
type: SessionHeartbeatController.HeartbeatType?,
|
||||||
|
interval: Duration?
|
||||||
|
) {
|
||||||
|
delegate.setSessionHeartbeat(type, interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isEmpty(): Boolean {
|
||||||
|
return delegate.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLongProperty(name: String?, def: Long): Long {
|
||||||
|
return delegate.getLongProperty(name, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLong(name: String?): Long? {
|
||||||
|
return delegate.getLong(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIntProperty(name: String?, def: Int): Int {
|
||||||
|
return delegate.getIntProperty(name, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInteger(name: String?): Int? {
|
||||||
|
return delegate.getInteger(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBooleanProperty(name: String?, def: Boolean): Boolean {
|
||||||
|
return delegate.getBooleanProperty(name, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBoolean(name: String?): Boolean? {
|
||||||
|
return delegate.getBoolean(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStringProperty(name: String?, def: String?): String? {
|
||||||
|
return delegate.getStringProperty(name, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(name: String?): String? {
|
||||||
|
return delegate.getString(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getObject(name: String?): Any? {
|
||||||
|
return delegate.getObject(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCharset(
|
||||||
|
name: String?,
|
||||||
|
defaultValue: Charset?
|
||||||
|
): Charset? {
|
||||||
|
return delegate.getCharset(name, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : Any?> computeAttributeIfAbsent(
|
||||||
|
key: AttributeRepository.AttributeKey<T?>?,
|
||||||
|
resolver: Function<in AttributeRepository.AttributeKey<T>, out T?>?
|
||||||
|
): T? {
|
||||||
|
return delegate.computeAttributeIfAbsent(key, resolver)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
close(true)?.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close(immediately: Boolean): CloseFuture? {
|
||||||
|
synchronized(delegate) {
|
||||||
|
if (refCount.decrementAndGet() < 1) {
|
||||||
|
delegate.close(immediately).await()
|
||||||
|
return client.close(immediately)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DefaultCloseFuture(this, this).apply { setClosed() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isOpen(): Boolean {
|
||||||
|
return delegate.isOpen()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCipherFactoriesNameList(): String? {
|
||||||
|
return delegate.getCipherFactoriesNameList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCipherFactoriesNames(): List<String?>? {
|
||||||
|
return delegate.getCipherFactoriesNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCipherFactoriesNameList(names: String?) {
|
||||||
|
delegate.setCipherFactoriesNameList(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCipherFactoriesNames(vararg names: String?) {
|
||||||
|
delegate.setCipherFactoriesNames(*names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCipherFactoriesNames(names: Collection<String?>?) {
|
||||||
|
delegate.setCipherFactoriesNames(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCompressionFactoriesNameList(): String? {
|
||||||
|
return delegate.getCompressionFactoriesNameList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCompressionFactoriesNames(): List<String?>? {
|
||||||
|
return delegate.getCompressionFactoriesNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCompressionFactoriesNameList(names: String?) {
|
||||||
|
delegate.setCompressionFactoriesNameList(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCompressionFactoriesNames(vararg names: String?) {
|
||||||
|
delegate.setCompressionFactoriesNames(*names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCompressionFactoriesNames(names: Collection<String?>?) {
|
||||||
|
delegate.setCompressionFactoriesNames(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMacFactoriesNameList(): String? {
|
||||||
|
return delegate.getMacFactoriesNameList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMacFactoriesNames(): List<String?>? {
|
||||||
|
return delegate.getMacFactoriesNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMacFactoriesNameList(names: String?) {
|
||||||
|
delegate.setMacFactoriesNameList(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMacFactoriesNames(vararg names: String?) {
|
||||||
|
delegate.setMacFactoriesNames(*names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMacFactoriesNames(names: Collection<String?>?) {
|
||||||
|
delegate.setMacFactoriesNames(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSignatureFactoriesNameList(names: String?) {
|
||||||
|
delegate.setSignatureFactoriesNameList(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSignatureFactoriesNames(vararg names: String?) {
|
||||||
|
delegate.setSignatureFactoriesNames(*names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSignatureFactoriesNames(names: Collection<String?>?) {
|
||||||
|
delegate.setSignatureFactoriesNames(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSignatureFactoriesNameList(): String? {
|
||||||
|
return delegate.getSignatureFactoriesNameList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSignatureFactoriesNames(): List<String?>? {
|
||||||
|
return delegate.getSignatureFactoriesNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolveChannelStreamWriterResolver(): ChannelStreamWriterResolver? {
|
||||||
|
return delegate.resolveChannelStreamWriterResolver()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolveChannelStreamWriter(
|
||||||
|
channel: Channel?,
|
||||||
|
cmd: Byte
|
||||||
|
): ChannelStreamWriter? {
|
||||||
|
return delegate.resolveChannelStreamWriter(channel, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isLocalPortForwardingStartedForPort(port: Int): Boolean {
|
||||||
|
return delegate.isLocalPortForwardingStartedForPort(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isRemotePortForwardingStartedForPort(port: Int): Boolean {
|
||||||
|
return delegate.isRemotePortForwardingStartedForPort(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setUserAuthFactoriesNames(names: Collection<String?>?) {
|
||||||
|
delegate.setUserAuthFactoriesNames(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setUserAuthFactoriesNames(vararg names: String?) {
|
||||||
|
delegate.setUserAuthFactoriesNames(*names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getUserAuthFactoriesNameList(): String? {
|
||||||
|
return delegate.getUserAuthFactoriesNameList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getUserAuthFactoriesNames(): List<String?>? {
|
||||||
|
return delegate.getUserAuthFactoriesNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setUserAuthFactoriesNameList(names: String?) {
|
||||||
|
delegate.setUserAuthFactoriesNameList(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startLocalPortForwarding(
|
||||||
|
localPort: Int,
|
||||||
|
remote: SshdSocketAddress?
|
||||||
|
): SshdSocketAddress? {
|
||||||
|
return delegate.startLocalPortForwarding(localPort, remote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,448 @@
|
|||||||
|
package app.termora.plugin.internal.telnet
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.plugin.internal.BasicProxyOption
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
|
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.*
|
||||||
|
|
||||||
|
class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() {
|
||||||
|
protected val generalOption = GeneralOption()
|
||||||
|
|
||||||
|
// telnet 不支持代理密码
|
||||||
|
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
|
||||||
|
private val terminalOption = TerminalOption()
|
||||||
|
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
|
init {
|
||||||
|
addOption(generalOption)
|
||||||
|
addOption(proxyOption)
|
||||||
|
addOption(terminalOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getHost(): Host {
|
||||||
|
val name = generalOption.nameTextField.text
|
||||||
|
val protocol = TelnetProtocolProvider.PROTOCOL
|
||||||
|
val host = generalOption.hostTextField.text
|
||||||
|
val port = (generalOption.portTextField.value ?: 23) as Int
|
||||||
|
var authentication = Authentication.No
|
||||||
|
var proxy = Proxy.Companion.No
|
||||||
|
val authenticationType = generalOption.authenticationTypeComboBox.selectedItem as AuthenticationType
|
||||||
|
|
||||||
|
if (authenticationType == AuthenticationType.Password) {
|
||||||
|
authentication = authentication.copy(
|
||||||
|
type = authenticationType,
|
||||||
|
password = String(generalOption.passwordTextField.password)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||||
|
proxy = proxy.copy(
|
||||||
|
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
|
||||||
|
host = proxyOption.proxyHostTextField.text,
|
||||||
|
username = proxyOption.proxyUsernameTextField.text,
|
||||||
|
password = String(proxyOption.proxyPasswordTextField.password),
|
||||||
|
port = proxyOption.proxyPortTextField.value as Int,
|
||||||
|
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val serialComm = SerialComm()
|
||||||
|
|
||||||
|
val options = Options.Companion.Default.copy(
|
||||||
|
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
||||||
|
env = terminalOption.environmentTextArea.text,
|
||||||
|
startupCommand = terminalOption.startupCommandTextField.text,
|
||||||
|
serialComm = serialComm,
|
||||||
|
extras = mutableMapOf("backspace" to (terminalOption.backspaceComboBox.selectedItem as Backspace).name)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Host(
|
||||||
|
name = name,
|
||||||
|
protocol = protocol,
|
||||||
|
host = host,
|
||||||
|
port = port,
|
||||||
|
username = generalOption.usernameTextField.text,
|
||||||
|
authentication = authentication,
|
||||||
|
proxy = proxy,
|
||||||
|
sort = System.currentTimeMillis(),
|
||||||
|
remark = generalOption.remarkTextArea.text,
|
||||||
|
options = options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHost(host: Host) {
|
||||||
|
generalOption.portTextField.value = host.port
|
||||||
|
generalOption.nameTextField.text = host.name
|
||||||
|
generalOption.usernameTextField.text = host.username
|
||||||
|
generalOption.hostTextField.text = host.host
|
||||||
|
generalOption.remarkTextArea.text = host.remark
|
||||||
|
generalOption.authenticationTypeComboBox.selectedItem = host.authentication.type
|
||||||
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
|
generalOption.passwordTextField.text = host.authentication.password
|
||||||
|
}
|
||||||
|
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||||
|
proxyOption.proxyHostTextField.text = host.proxy.host
|
||||||
|
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
||||||
|
proxyOption.proxyUsernameTextField.text = host.proxy.username
|
||||||
|
proxyOption.proxyPortTextField.value = host.proxy.port
|
||||||
|
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
|
||||||
|
|
||||||
|
terminalOption.charsetComboBox.selectedItem = host.options.encoding
|
||||||
|
terminalOption.environmentTextArea.text = host.options.env
|
||||||
|
terminalOption.startupCommandTextField.text = host.options.startupCommand
|
||||||
|
terminalOption.backspaceComboBox.selectedItem =
|
||||||
|
Backspace.valueOf(host.options.extras["backspace"] ?: Backspace.Delete.name)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateFields(): Boolean {
|
||||||
|
val host = getHost()
|
||||||
|
|
||||||
|
// general
|
||||||
|
if (validateField(generalOption.nameTextField)
|
||||||
|
|| validateField(generalOption.hostTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
|
if (validateField(generalOption.usernameTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (validateField(generalOption.passwordTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxy
|
||||||
|
if (host.proxy.type != ProxyType.No) {
|
||||||
|
if (validateField(proxyOption.proxyHostTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.proxy.authenticationType != AuthenticationType.No) {
|
||||||
|
if (validateField(proxyOption.proxyUsernameTextField)
|
||||||
|
|| validateField(proxyOption.proxyPasswordTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示有错误
|
||||||
|
*/
|
||||||
|
private fun validateField(textField: JTextField): Boolean {
|
||||||
|
if (textField.isEnabled && textField.text.isBlank()) {
|
||||||
|
setOutlineError(textField)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setOutlineError(textField: JTextField) {
|
||||||
|
selectOptionJComponent(textField)
|
||||||
|
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
|
textField.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 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
|
||||||
|
}
|
||||||
|
|
||||||
|
protected inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||||
|
val portTextField = PortSpinner(23)
|
||||||
|
val nameTextField = OutlineTextField(128)
|
||||||
|
val usernameTextField = OutlineTextField(128)
|
||||||
|
val hostTextField = OutlineTextField(255)
|
||||||
|
val passwordTextField = OutlinePasswordField(255)
|
||||||
|
val publicKeyComboBox = OutlineComboBox<String>()
|
||||||
|
val remarkTextArea = FixedLengthTextArea(512)
|
||||||
|
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
|
||||||
|
publicKeyComboBox.isEditable = false
|
||||||
|
|
||||||
|
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = StringUtils.EMPTY
|
||||||
|
if (value is String) {
|
||||||
|
text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
text,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = value?.toString() ?: ""
|
||||||
|
when (value) {
|
||||||
|
AuthenticationType.Password -> {
|
||||||
|
text = "Password"
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationType.PublicKey -> {
|
||||||
|
text = "Public Key"
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationType.KeyboardInteractive -> {
|
||||||
|
text = "Keyboard Interactive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
text,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||||
|
|
||||||
|
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
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, $FORM_MARGIN, pref, $FORM_MARGIN, default",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, 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)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
|
||||||
|
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
||||||
|
.add(hostTextField).xy(3, rows)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||||
|
.add(portTextField).xy(7, rows).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
||||||
|
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
|
||||||
|
.add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
||||||
|
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
||||||
|
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
||||||
|
.xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class TerminalOption : JPanel(BorderLayout()), Option {
|
||||||
|
val charsetComboBox = JComboBox<String>()
|
||||||
|
val backspaceComboBox = JComboBox<Backspace>()
|
||||||
|
val startupCommandTextField = OutlineTextField()
|
||||||
|
val environmentTextArea = FixedLengthTextArea(2048)
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
|
||||||
|
backspaceComboBox.addItem(Backspace.Delete)
|
||||||
|
backspaceComboBox.addItem(Backspace.Backspace)
|
||||||
|
backspaceComboBox.addItem(Backspace.VT220)
|
||||||
|
|
||||||
|
|
||||||
|
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, $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.backspace")}:").xy(1, rows)
|
||||||
|
.add(backspaceComboBox).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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Backspace {
|
||||||
|
/**
|
||||||
|
* 0x08
|
||||||
|
*/
|
||||||
|
Backspace,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 0x7F 默认
|
||||||
|
*/
|
||||||
|
Delete,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ESC[3~
|
||||||
|
*/
|
||||||
|
VT220, ;
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return when (this) {
|
||||||
|
Backspace -> "ASCII Backspace (0x08)"
|
||||||
|
Delete -> "ASCII Delete (0x7F)"
|
||||||
|
VT220 -> "VT220 Delete (ESC[3~)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package app.termora.plugin.internal.telnet
|
||||||
|
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.InternalPlugin
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
internal class TelnetInternalPlugin : InternalPlugin() {
|
||||||
|
init {
|
||||||
|
support.addExtension(ProtocolProviderExtension::class.java) { TelnetProtocolProviderExtension.instance }
|
||||||
|
support.addExtension(ProtocolHostPanelExtension::class.java) { TelnetProtocolHostPanelExtension.instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "Telnet Protocol"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package app.termora.plugin.internal.telnet
|
||||||
|
|
||||||
|
import app.termora.Disposer
|
||||||
|
import app.termora.Host
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
|
||||||
|
class TelnetProtocolHostPanel(accountOwner: AccountOwner) : ProtocolHostPanel() {
|
||||||
|
|
||||||
|
private val pane = TelnetHostOptionsPane(accountOwner)
|
||||||
|
|
||||||
|
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,25 @@
|
|||||||
|
package app.termora.plugin.internal.telnet
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
|
||||||
|
internal class TelnetProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = TelnetProtocolHostPanelExtension()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return TelnetProtocolProvider.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
|
return TelnetProtocolHostPanel(accountOwner)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package app.termora.plugin.internal.telnet
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.actions.DataProvider
|
||||||
|
import app.termora.protocol.GenericProtocolProvider
|
||||||
|
|
||||||
|
internal class TelnetProtocolProvider private constructor() : GenericProtocolProvider {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { TelnetProtocolProvider() }
|
||||||
|
const val PROTOCOL = "Telnet"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocol(): String {
|
||||||
|
return PROTOCOL
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createTerminalTab(dataProvider: DataProvider, windowScope: WindowScope, host: Host): TerminalTab {
|
||||||
|
return TelnetTerminalTab(windowScope, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(width: Int, height: Int): DynamicIcon {
|
||||||
|
return Icons.telnet
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ordered() = Int.MIN_VALUE
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.termora.plugin.internal.telnet
|
||||||
|
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
internal class TelnetProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = TelnetProtocolProviderExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return TelnetProtocolProvider.instance
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package app.termora.plugin.internal.telnet
|
||||||
|
|
||||||
|
import app.termora.terminal.StreamPtyConnector
|
||||||
|
import org.apache.commons.net.telnet.TelnetClient
|
||||||
|
import org.apache.commons.net.telnet.TelnetOption
|
||||||
|
import org.apache.commons.net.telnet.WindowSizeOptionHandler
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
|
class TelnetStreamPtyConnector(
|
||||||
|
private val telnet: TelnetClient,
|
||||||
|
private val charset: Charset
|
||||||
|
) :
|
||||||
|
StreamPtyConnector(telnet.inputStream, telnet.outputStream) {
|
||||||
|
private val reader = InputStreamReader(telnet.inputStream, getCharset())
|
||||||
|
|
||||||
|
override fun read(buffer: CharArray): Int {
|
||||||
|
return reader.read(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(buffer: ByteArray, offset: Int, len: Int) {
|
||||||
|
output.write(buffer, offset, len)
|
||||||
|
output.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resize(rows: Int, cols: Int) {
|
||||||
|
telnet.deleteOptionHandler(TelnetOption.WINDOW_SIZE)
|
||||||
|
telnet.addOptionHandler(WindowSizeOptionHandler(cols, rows, true, false, true, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun waitFor(): Int {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
telnet.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCharset(): Charset {
|
||||||
|
return charset
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package app.termora.plugin.internal.telnet
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.terminal.ControlCharacters
|
||||||
|
import app.termora.terminal.KeyEncoderImpl
|
||||||
|
import app.termora.terminal.PtyConnector
|
||||||
|
import app.termora.terminal.TerminalKeyEvent
|
||||||
|
import org.apache.commons.net.telnet.*
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
|
class TelnetTerminalTab(
|
||||||
|
windowScope: WindowScope, host: Host,
|
||||||
|
) : PtyHostTerminalTab(windowScope, host) {
|
||||||
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
|
val winSize = terminalPanel.winSize()
|
||||||
|
val telnet = TelnetClient()
|
||||||
|
telnet.charset = Charset.forName(host.options.encoding)
|
||||||
|
telnet.connectTimeout = 60 * 1000
|
||||||
|
|
||||||
|
if (host.proxy.type == ProxyType.HTTP) {
|
||||||
|
telnet.proxy = Proxy(
|
||||||
|
Proxy.Type.HTTP,
|
||||||
|
InetSocketAddress(host.proxy.host, host.proxy.port)
|
||||||
|
)
|
||||||
|
} else if (host.proxy.type == ProxyType.SOCKS5) {
|
||||||
|
telnet.proxy = Proxy(
|
||||||
|
Proxy.Type.SOCKS,
|
||||||
|
InetSocketAddress(host.proxy.host, host.proxy.port)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val termtype = host.options.envs()["TERM"] ?: "xterm-256color"
|
||||||
|
val ttopt = TerminalTypeOptionHandler(termtype, false, false, true, false)
|
||||||
|
val echoopt = EchoOptionHandler(false, true, false, true)
|
||||||
|
val gaopt = SuppressGAOptionHandler(true, true, true, true)
|
||||||
|
val wsopt = WindowSizeOptionHandler(winSize.cols, winSize.rows, true, false, true, false)
|
||||||
|
|
||||||
|
telnet.addOptionHandler(ttopt)
|
||||||
|
telnet.addOptionHandler(echoopt)
|
||||||
|
telnet.addOptionHandler(gaopt)
|
||||||
|
telnet.addOptionHandler(wsopt)
|
||||||
|
|
||||||
|
telnet.connect(host.host, host.port)
|
||||||
|
telnet.keepAlive = true
|
||||||
|
|
||||||
|
val encoder = terminal.getKeyEncoder()
|
||||||
|
if (encoder is KeyEncoderImpl) {
|
||||||
|
val backspace = host.options.extras["backspace"]
|
||||||
|
if (backspace == TelnetHostOptionsPane.Backspace.Backspace.name) {
|
||||||
|
encoder.putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_BACK_SPACE), String(byteArrayOf(0x08)))
|
||||||
|
} else if (backspace == TelnetHostOptionsPane.Backspace.VT220.name) {
|
||||||
|
encoder.putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_BACK_SPACE), "${ControlCharacters.ESC}[3~")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ptyConnectorFactory.decorate(TelnetStreamPtyConnector(telnet, telnet.charset))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun loginScriptsPtyConnector(host: Host, ptyConnector: PtyConnector): PtyConnector {
|
||||||
|
if (host.authentication.type != AuthenticationType.Password) {
|
||||||
|
return ptyConnector
|
||||||
|
}
|
||||||
|
|
||||||
|
val scripts = mutableListOf<LoginScript>()
|
||||||
|
scripts.add(
|
||||||
|
LoginScript(
|
||||||
|
expect = "login:",
|
||||||
|
send = host.username,
|
||||||
|
regex = false,
|
||||||
|
matchCase = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
scripts.add(
|
||||||
|
LoginScript(
|
||||||
|
expect = "password:",
|
||||||
|
send = host.authentication.password,
|
||||||
|
regex = false,
|
||||||
|
matchCase = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return super.loginScriptsPtyConnector(
|
||||||
|
host.copy(options = host.options.copy(loginScripts = scripts)),
|
||||||
|
ptyConnector
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ interface ProtocolHostPanelExtension : Extension {
|
|||||||
val extensions
|
val extensions
|
||||||
get() = ExtensionManager.getInstance()
|
get() = ExtensionManager.getInstance()
|
||||||
.getExtensions(ProtocolHostPanelExtension::class.java)
|
.getExtensions(ProtocolHostPanelExtension::class.java)
|
||||||
.sortedBy { it.getProtocolProvider().ordered() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
|||||||
|
|
||||||
configureLeftRight()
|
configureLeftRight()
|
||||||
|
|
||||||
// Ctrl + C
|
// Ctrl + C: 0x7F ASCII Delete
|
||||||
putCode(TerminalKeyEvent(keyCode = 8), String(byteArrayOf(127)))
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_BACK_SPACE), String(byteArrayOf(0x7F)))
|
||||||
|
|
||||||
// Enter
|
// Enter
|
||||||
if (terminalModel.getData(DataKey.AutoNewline, false)) {
|
if (terminalModel.getData(DataKey.AutoNewline, false)) {
|
||||||
@@ -113,7 +113,7 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
|||||||
return terminal
|
return terminal
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun putCode(event: TerminalKeyEvent, encode: String) {
|
internal fun putCode(event: TerminalKeyEvent, encode: String) {
|
||||||
mapping[event] = encode
|
mapping[event] = encode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
|||||||
|| key == KeyEvent.VK_PAGE_UP || key == KeyEvent.VK_PAGE_DOWN
|
|| key == KeyEvent.VK_PAGE_UP || key == KeyEvent.VK_PAGE_DOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
fun arrowKeysApplicationSequences() {
|
private fun arrowKeysApplicationSequences() {
|
||||||
// Up
|
// Up
|
||||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_UP), encode = "${ControlCharacters.ESC}OA")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_UP), encode = "${ControlCharacters.ESC}OA")
|
||||||
// Down
|
// Down
|
||||||
@@ -213,7 +213,7 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
|||||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_RIGHT), encode = "${ControlCharacters.ESC}OC")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_RIGHT), encode = "${ControlCharacters.ESC}OC")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun arrowKeysAnsiCursorSequences() {
|
private fun arrowKeysAnsiCursorSequences() {
|
||||||
// Up
|
// Up
|
||||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_UP), encode = "${ControlCharacters.ESC}[A")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_UP), encode = "${ControlCharacters.ESC}[A")
|
||||||
// Down
|
// Down
|
||||||
@@ -227,7 +227,7 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
|||||||
/**
|
/**
|
||||||
* Alt + Left/Right
|
* Alt + Left/Right
|
||||||
*/
|
*/
|
||||||
fun configureLeftRight() {
|
private fun configureLeftRight() {
|
||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isMacOS) {
|
||||||
putCode(
|
putCode(
|
||||||
TerminalKeyEvent(keyCode = KeyEvent.VK_LEFT, TerminalEvent.ALT_MASK),
|
TerminalKeyEvent(keyCode = KeyEvent.VK_LEFT, TerminalEvent.ALT_MASK),
|
||||||
@@ -262,7 +262,7 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun keypadApplicationSequences() {
|
private fun keypadApplicationSequences() {
|
||||||
// Up
|
// Up
|
||||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_UP), encode = "${ControlCharacters.ESC}OA")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_UP), encode = "${ControlCharacters.ESC}OA")
|
||||||
// Down
|
// Down
|
||||||
@@ -277,7 +277,7 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
|||||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_END), encode = "${ControlCharacters.ESC}OF")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_END), encode = "${ControlCharacters.ESC}OF")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun keypadAnsiSequences() {
|
private fun keypadAnsiSequences() {
|
||||||
// Up
|
// Up
|
||||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_UP), encode = "${ControlCharacters.ESC}[A")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_UP), encode = "${ControlCharacters.ESC}[A")
|
||||||
// Down
|
// Down
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package app.termora.terminal.panel.vw
|
|||||||
import app.termora.Disposer
|
import app.termora.Disposer
|
||||||
import app.termora.I18n
|
import app.termora.I18n
|
||||||
import app.termora.Icons
|
import app.termora.Icons
|
||||||
import app.termora.SshClients
|
|
||||||
import app.termora.plugin.internal.ssh.SSHTerminalTab
|
import app.termora.plugin.internal.ssh.SSHTerminalTab
|
||||||
|
import app.termora.plugin.internal.ssh.SshClients
|
||||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
@@ -28,7 +28,7 @@ import javax.xml.parsers.DocumentBuilderFactory
|
|||||||
import javax.xml.xpath.XPathFactory
|
import javax.xml.xpath.XPathFactory
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
class NvidiaSMIVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
internal class NvidiaSMIVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
||||||
SSHVisualWindow(tab, "NVIDIA-SMI", visualWindowManager) {
|
SSHVisualWindow(tab, "NVIDIA-SMI", visualWindowManager) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package app.termora.terminal.panel.vw
|
package app.termora.terminal.panel.vw
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.Disposer
|
||||||
|
import app.termora.DynamicColor
|
||||||
|
import app.termora.I18n
|
||||||
|
import app.termora.formatBytes
|
||||||
import app.termora.plugin.internal.ssh.SSHTerminalTab
|
import app.termora.plugin.internal.ssh.SSHTerminalTab
|
||||||
|
import app.termora.plugin.internal.ssh.SshClients
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -16,7 +20,7 @@ import javax.swing.table.DefaultTableCellRenderer
|
|||||||
import javax.swing.table.DefaultTableModel
|
import javax.swing.table.DefaultTableModel
|
||||||
|
|
||||||
|
|
||||||
class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
internal class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
||||||
SSHVisualWindow(tab, "SystemInformation", visualWindowManager) {
|
SSHVisualWindow(tab, "SystemInformation", visualWindowManager) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import kotlin.reflect.cast
|
|||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|
||||||
class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
||||||
SSHVisualWindow(tab, "Transfer", visualWindowManager) {
|
SSHVisualWindow(tab, "Transfer", visualWindowManager) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -99,9 +99,11 @@ class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindo
|
|||||||
if (key == DataKey.CurrentDir) {
|
if (key == DataKey.CurrentDir) {
|
||||||
val dir = DataKey.CurrentDir.clazz.cast(data)
|
val dir = DataKey.CurrentDir.clazz.cast(data)
|
||||||
val navigator = getTransportNavigator() ?: return
|
val navigator = getTransportNavigator() ?: return
|
||||||
val path = navigator.getFileSystem().getPath(dir)
|
val loader = navigator.loader
|
||||||
if (path == navigator.workdir) return
|
if (loader.isOpened().not()) return
|
||||||
navigator.navigateTo(path)
|
val fileSystem = loader.getSyncTransportSupport().getFileSystem()
|
||||||
|
val path = fileSystem.getPath(dir)
|
||||||
|
navigator.navigateTo(path.absolutePathString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -146,14 +148,25 @@ class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindo
|
|||||||
try {
|
try {
|
||||||
val session = getSession()
|
val session = getSession()
|
||||||
val fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
|
val fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
|
||||||
val support = TransportSupport(fileSystem, fileSystem.defaultDir.absolutePathString())
|
val support = DefaultTransportSupport(fileSystem, fileSystem.defaultDir)
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
val internalTransferManager = MyInternalTransferManager()
|
val internalTransferManager = MyInternalTransferManager()
|
||||||
val transportPanel = TransportPanel(
|
val transportPanel = TransportPanel(
|
||||||
internalTransferManager, tab.host,
|
internalTransferManager, tab.host,
|
||||||
TransportSupportLoader { support })
|
object : TransportSupportLoader {
|
||||||
internalTransferManager.setTransferPanel(transportPanel)
|
override suspend fun getTransportSupport(): TransportSupport {
|
||||||
|
return support
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSyncTransportSupport(): TransportSupport {
|
||||||
|
return support
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isLoaded(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
internalTransferManager.setTransferPanel(transportPanel)
|
||||||
Disposer.register(transportPanel, object : Disposable {
|
Disposer.register(transportPanel, object : Disposable {
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
panel.remove(transportPanel)
|
panel.remove(transportPanel)
|
||||||
|
|||||||
34
src/main/kotlin/app/termora/transfer/CommandTransfer.kt
Normal file
34
src/main/kotlin/app/termora/transfer/CommandTransfer.kt
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package app.termora.transfer
|
||||||
|
|
||||||
|
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
||||||
|
import org.apache.sshd.sftp.client.fs.SftpPath
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
|
||||||
|
class CommandTransfer(
|
||||||
|
parentId: String,
|
||||||
|
path: SftpPath,
|
||||||
|
isDirectory: Boolean,
|
||||||
|
private val size: Long,
|
||||||
|
val command: String,
|
||||||
|
) : AbstractTransfer(parentId, path, path, isDirectory) {
|
||||||
|
|
||||||
|
private var executed = false
|
||||||
|
|
||||||
|
override suspend fun transfer(bufferSize: Int): Long {
|
||||||
|
if (executed) return 0
|
||||||
|
val fs = source().fileSystem as SftpFileSystem
|
||||||
|
fs.session.executeRemoteCommand(command)
|
||||||
|
executed = true
|
||||||
|
return this.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun scanning(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun size(): Long {
|
||||||
|
return max(size, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import kotlinx.coroutines.swing.Swing
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.time.DateFormatUtils
|
import org.apache.commons.lang3.time.DateFormatUtils
|
||||||
|
import org.apache.sshd.sftp.client.fs.SftpPath
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
@@ -31,6 +32,7 @@ import kotlin.collections.ArrayDeque
|
|||||||
import kotlin.collections.List
|
import kotlin.collections.List
|
||||||
import kotlin.collections.Set
|
import kotlin.collections.Set
|
||||||
import kotlin.collections.isNotEmpty
|
import kotlin.collections.isNotEmpty
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
import kotlin.io.path.exists
|
import kotlin.io.path.exists
|
||||||
import kotlin.io.path.name
|
import kotlin.io.path.name
|
||||||
import kotlin.io.path.pathString
|
import kotlin.io.path.pathString
|
||||||
@@ -63,7 +65,9 @@ class DefaultInternalTransferManager(
|
|||||||
|
|
||||||
|
|
||||||
override fun canTransfer(paths: List<Path>): Boolean {
|
override fun canTransfer(paths: List<Path>): Boolean {
|
||||||
return paths.isNotEmpty() && target.getWorkdir() != null
|
val c = target.getWorkdir() ?: return false
|
||||||
|
if (c.fileSystem.isOpen.not()) return false
|
||||||
|
return paths.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addTransfer(
|
override fun addTransfer(
|
||||||
@@ -267,8 +271,9 @@ class DefaultInternalTransferManager(
|
|||||||
|
|
||||||
val isDirectory = pair.second.isDirectory
|
val isDirectory = pair.second.isDirectory
|
||||||
val path = pair.first
|
val path = pair.first
|
||||||
if (isDirectory.not()) {
|
if (isDirectory.not() || mode == TransferMode.Rmrf) {
|
||||||
val transfer = createTransfer(path, workdir.resolve(path.name), false, StringUtils.EMPTY, mode, action)
|
val transfer =
|
||||||
|
createTransfer(path, workdir.resolve(path.name), isDirectory, StringUtils.EMPTY, mode, action)
|
||||||
return if (transferManager.addTransfer(transfer)) FileVisitResult.CONTINUE else FileVisitResult.TERMINATE
|
return if (transferManager.addTransfer(transfer)) FileVisitResult.CONTINUE else FileVisitResult.TERMINATE
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,39 +368,49 @@ class DefaultInternalTransferManager(
|
|||||||
action:TransferAction,
|
action:TransferAction,
|
||||||
permissions: Set<PosixFilePermission>? = null
|
permissions: Set<PosixFilePermission>? = null
|
||||||
): Transfer {
|
): Transfer {
|
||||||
if (mode == TransferMode.Delete) {
|
when {
|
||||||
return DeleteTransfer(
|
mode == TransferMode.Delete -> {
|
||||||
parentId,
|
return DeleteTransfer(
|
||||||
source,
|
parentId,
|
||||||
isDirectory,
|
source,
|
||||||
if (isDirectory) 1 else Files.size(source)
|
isDirectory,
|
||||||
)
|
if (isDirectory) 1 else Files.size(source)
|
||||||
} else if (mode == TransferMode.ChangePermission) {
|
)
|
||||||
if (permissions == null) throw IllegalStateException()
|
}
|
||||||
return ChangePermissionTransfer(
|
mode == TransferMode.Rmrf -> {
|
||||||
parentId,
|
return CommandTransfer(
|
||||||
target,
|
parentId,
|
||||||
isDirectory = isDirectory,
|
source as SftpPath,
|
||||||
permissions = permissions,
|
isDirectory,
|
||||||
size = if (isDirectory) 1 else Files.size(target)
|
if (isDirectory) 1 else Files.size(source),
|
||||||
)
|
"rm -rf ${source.absolutePathString()}",
|
||||||
}
|
)
|
||||||
|
}
|
||||||
if (isDirectory) {
|
mode == TransferMode.ChangePermission -> {
|
||||||
return DirectoryTransfer(
|
if (permissions == null) throw IllegalStateException()
|
||||||
|
return ChangePermissionTransfer(
|
||||||
|
parentId,
|
||||||
|
target,
|
||||||
|
isDirectory = isDirectory,
|
||||||
|
permissions = permissions,
|
||||||
|
size = if (isDirectory) 1 else Files.size(target)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isDirectory -> {
|
||||||
|
return DirectoryTransfer(
|
||||||
|
parentId = parentId,
|
||||||
|
source = source,
|
||||||
|
target = target,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> return FileTransfer(
|
||||||
parentId = parentId,
|
parentId = parentId,
|
||||||
source = source,
|
source = source,
|
||||||
target = target,
|
target = target,
|
||||||
|
action = action,
|
||||||
|
size = Files.size(source)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return FileTransfer(
|
|
||||||
parentId = parentId,
|
|
||||||
source = source,
|
|
||||||
target = target,
|
|
||||||
action = action,
|
|
||||||
size = Files.size(source)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.termora.transfer
|
||||||
|
|
||||||
|
import java.nio.file.FileSystem
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
class DefaultTransportSupport(private val fileSystem: FileSystem, private val defaultPath: Path) : TransportSupport {
|
||||||
|
override fun getFileSystem(): FileSystem {
|
||||||
|
return fileSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDefaultPath(): Path {
|
||||||
|
return defaultPath
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ interface InternalTransferManager {
|
|||||||
Delete,
|
Delete,
|
||||||
Transfer,
|
Transfer,
|
||||||
ChangePermission,
|
ChangePermission,
|
||||||
|
Rmrf,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package app.termora.transfer
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.protocol.PathHandler
|
||||||
|
import app.termora.protocol.PathHandlerRequest
|
||||||
|
import app.termora.protocol.TransferProtocolProvider
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Window
|
||||||
|
import java.nio.file.FileSystem
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
|
internal class ReconnectableTransportSupportLoader(private val owner: Window, private val host: Host) :
|
||||||
|
TransportSupportLoader {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(ReconnectableTransportSupportLoader::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
private val reference = AtomicReference<MyTransportSupport>()
|
||||||
|
|
||||||
|
private var support: MyTransportSupport?
|
||||||
|
set(value) = reference.set(value)
|
||||||
|
get() = reference.get()
|
||||||
|
|
||||||
|
override suspend fun getTransportSupport(): TransportSupport {
|
||||||
|
mutex.withLock {
|
||||||
|
var c = support
|
||||||
|
if (c != null) {
|
||||||
|
if (c.getFileSystem().isOpen) {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn("Host {} has been disconnected and will reconnect soon", host.name)
|
||||||
|
}
|
||||||
|
support = null
|
||||||
|
Disposer.dispose(c)
|
||||||
|
}
|
||||||
|
c = connect().also { support = it }
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSyncTransportSupport(): TransportSupport {
|
||||||
|
assertEventDispatchThread()
|
||||||
|
val c = support
|
||||||
|
if (c == null) throw IllegalStateException("No transport support")
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isLoaded(): Boolean {
|
||||||
|
assertEventDispatchThread()
|
||||||
|
return support != null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isOpened(): Boolean {
|
||||||
|
if (isLoaded().not()) return false
|
||||||
|
val c = support ?: return false
|
||||||
|
return c.getFileSystem().isOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
val c = support
|
||||||
|
if (c != null) {
|
||||||
|
Disposer.dispose(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isOpening(): Boolean {
|
||||||
|
if (isOpened()) return false
|
||||||
|
return mutex.isLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connect(): MyTransportSupport {
|
||||||
|
val provider = TransferProtocolProvider.valueOf(host.protocol)
|
||||||
|
if (provider == null) {
|
||||||
|
throw IllegalStateException(I18n.getString("termora.protocol.not-supported", host.protocol))
|
||||||
|
}
|
||||||
|
val handler = provider.createPathHandler(PathHandlerRequest(host, owner))
|
||||||
|
return MyTransportSupport(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class MyTransportSupport(private val handler: PathHandler) : TransportSupport, Disposable {
|
||||||
|
|
||||||
|
init {
|
||||||
|
Disposer.register(this, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFileSystem(): FileSystem {
|
||||||
|
return handler.fileSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDefaultPath(): Path {
|
||||||
|
return handler.path
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -398,7 +398,9 @@ class TransferTableModel(private val coroutineScope: CoroutineScope) :
|
|||||||
if (continueTransfer(node, false)) {
|
if (continueTransfer(node, false)) {
|
||||||
doTransfer(node)
|
doTransfer(node)
|
||||||
} else {
|
} else {
|
||||||
changeState(node, State.Failed)
|
withContext(Dispatchers.Swing) {
|
||||||
|
changeState(node, State.Failed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lock.withLock { condition.signalAll() }
|
lock.withLock { condition.signalAll() }
|
||||||
|
|||||||
@@ -77,13 +77,15 @@ class TransferTreeTableNode(transfer: Transfer) : DefaultMutableTreeTableNode(tr
|
|||||||
|
|
||||||
private fun formatPath(path: Path, target: Boolean): String {
|
private fun formatPath(path: Path, target: Boolean): String {
|
||||||
if (target) {
|
if (target) {
|
||||||
if (transfer is DeleteTransfer) {
|
when (transfer) {
|
||||||
return I18n.getString("termora.transport.sftp.status.deleting")
|
is DeleteTransfer -> return I18n.getString("termora.transport.sftp.status.deleting")
|
||||||
} else if (transfer is ChangePermissionTransfer) {
|
is CommandTransfer -> return (transfer as CommandTransfer).command
|
||||||
val permissions = (transfer as ChangePermissionTransfer).permissions
|
is ChangePermissionTransfer -> {
|
||||||
// @formatter:off
|
val permissions = (transfer as ChangePermissionTransfer).permissions
|
||||||
return "${I18n.getString("termora.transport.table.permissions")} -> ${PosixFilePermissions.toString(permissions)}"
|
// @formatter:off
|
||||||
// @formatter:on
|
return "${I18n.getString("termora.transport.table.permissions")} -> ${PosixFilePermissions.toString(permissions)}"
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +97,7 @@ class TransferTreeTableNode(transfer: Transfer) : DefaultMutableTreeTableNode(tr
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun formatStatus(state: State): String {
|
private fun formatStatus(state: State): String {
|
||||||
if (transfer is DeleteTransfer && state == State.Processing) {
|
if ((transfer is DeleteTransfer || transfer is CommandTransfer) && state == State.Processing) {
|
||||||
return I18n.getString("termora.transport.sftp.status.deleting")
|
return I18n.getString("termora.transport.sftp.status.deleting")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package app.termora.transfer
|
|||||||
|
|
||||||
import app.termora.DynamicColor
|
import app.termora.DynamicColor
|
||||||
import app.termora.Icons
|
import app.termora.Icons
|
||||||
import app.termora.OptionPane
|
|
||||||
import app.termora.transfer.TransportPanel.Companion.isWindowsFileSystem
|
import app.termora.transfer.TransportPanel.Companion.isWindowsFileSystem
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
@@ -14,8 +13,6 @@ import com.formdev.flatlaf.util.SystemInfo
|
|||||||
import org.apache.commons.io.FilenameUtils
|
import org.apache.commons.io.FilenameUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.awt.CardLayout
|
import java.awt.CardLayout
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Insets
|
import java.awt.Insets
|
||||||
@@ -24,7 +21,6 @@ import java.awt.event.*
|
|||||||
import java.beans.PropertyChangeEvent
|
import java.beans.PropertyChangeEvent
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.function.Supplier
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.event.PopupMenuEvent
|
import javax.swing.event.PopupMenuEvent
|
||||||
import javax.swing.event.PopupMenuListener
|
import javax.swing.event.PopupMenuListener
|
||||||
@@ -33,13 +29,9 @@ import kotlin.io.path.name
|
|||||||
import kotlin.io.path.pathString
|
import kotlin.io.path.pathString
|
||||||
import kotlin.math.round
|
import kotlin.math.round
|
||||||
|
|
||||||
class TransportNavigationPanel(
|
internal class TransportNavigationPanel(private val navigator: TransportNavigator) : JPanel() {
|
||||||
private val support: Supplier<TransportSupport>,
|
|
||||||
private val navigator: TransportNavigator
|
|
||||||
) : JPanel() {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(TransportNavigationPanel::class.java)
|
|
||||||
private const val TEXT_FIELD = "TextField"
|
private const val TEXT_FIELD = "TextField"
|
||||||
private const val SEGMENTS = "Segments"
|
private const val SEGMENTS = "Segments"
|
||||||
|
|
||||||
@@ -50,7 +42,6 @@ class TransportNavigationPanel(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
|
||||||
private val layeredPane = LayeredPane()
|
private val layeredPane = LayeredPane()
|
||||||
private val textField = FlatTextField()
|
private val textField = FlatTextField()
|
||||||
private val downBtn = JButton(Icons.chevronDown)
|
private val downBtn = JButton(Icons.chevronDown)
|
||||||
@@ -115,8 +106,7 @@ class TransportNavigationPanel(
|
|||||||
val itemListener = object : ItemListener {
|
val itemListener = object : ItemListener {
|
||||||
override fun itemStateChanged(e: ItemEvent) {
|
override fun itemStateChanged(e: ItemEvent) {
|
||||||
val path = comboBox.selectedItem as Path? ?: return
|
val path = comboBox.selectedItem as Path? ?: return
|
||||||
if (navigator.loading) return
|
navigator.navigateTo(path.absolutePathString())
|
||||||
navigator.navigateTo(path)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,17 +169,7 @@ class TransportNavigationPanel(
|
|||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
if (navigator.loading) return
|
if (navigator.loading) return
|
||||||
if (textField.text.isBlank()) return
|
if (textField.text.isBlank()) return
|
||||||
|
navigator.navigateTo(textField.text)
|
||||||
try {
|
|
||||||
val path = support.get().fileSystem.getPath(textField.text)
|
|
||||||
navigator.navigateTo(path)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (log.isErrorEnabled) log.error(e.message, e)
|
|
||||||
OptionPane.showMessageDialog(
|
|
||||||
owner, ExceptionUtils.getRootCauseMessage(e),
|
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -257,11 +237,17 @@ class TransportNavigationPanel(
|
|||||||
button.putClientProperty(FlatClientProperties.BUTTON_TYPE, FlatClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON)
|
button.putClientProperty(FlatClientProperties.BUTTON_TYPE, FlatClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON)
|
||||||
button.addMouseListener(object : MouseAdapter() {
|
button.addMouseListener(object : MouseAdapter() {
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
if (navigator.loading) return
|
if (SwingUtilities.isLeftMouseButton(e)) {
|
||||||
if (path == navigator.workdir) {
|
if (navigator.loading) return
|
||||||
setTextFieldText(path)
|
if (path == navigator.workdir) {
|
||||||
} else {
|
setTextFieldText(path)
|
||||||
navigator.navigateTo(path)
|
} else {
|
||||||
|
if (path.fileSystem.isWindowsFileSystem() && path.pathString == path.fileSystem.separator) {
|
||||||
|
navigator.navigateTo(path.pathString)
|
||||||
|
} else {
|
||||||
|
navigator.navigateTo(path.absolutePathString())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -353,7 +339,7 @@ class TransportNavigationPanel(
|
|||||||
text = item.pathString
|
text = item.pathString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
popupMenu.add(text).addActionListener { navigator.navigateTo(item) }
|
popupMenu.add(text).addActionListener { navigator.navigateTo(item.absolutePathString()) }
|
||||||
}
|
}
|
||||||
popupMenu.show(
|
popupMenu.show(
|
||||||
button,
|
button,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ interface TransportNavigator {
|
|||||||
val loading: Boolean
|
val loading: Boolean
|
||||||
val workdir: Path?
|
val workdir: Path?
|
||||||
|
|
||||||
fun navigateTo(destination: Path): Boolean
|
fun navigateTo(destination: String): Boolean
|
||||||
|
|
||||||
fun addPropertyChangeListener(propertyName: String, listener: PropertyChangeListener)
|
fun addPropertyChangeListener(propertyName: String, listener: PropertyChangeListener)
|
||||||
fun removePropertyChangeListener(propertyName: String, listener: PropertyChangeListener)
|
fun removePropertyChangeListener(propertyName: String, listener: PropertyChangeListener)
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import org.apache.commons.lang3.ArrayUtils
|
|||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import org.apache.commons.lang3.time.DateFormatUtils
|
import org.apache.commons.lang3.time.DateFormatUtils
|
||||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
|
||||||
import org.apache.sshd.sftp.client.fs.WithFileAttributes
|
import org.apache.sshd.sftp.client.fs.WithFileAttributes
|
||||||
import org.jdesktop.swingx.JXBusyLabel
|
import org.jdesktop.swingx.JXBusyLabel
|
||||||
import org.jdesktop.swingx.JXPanel
|
import org.jdesktop.swingx.JXPanel
|
||||||
@@ -34,7 +33,6 @@ import java.awt.event.*
|
|||||||
import java.beans.PropertyChangeEvent
|
import java.beans.PropertyChangeEvent
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.OutputStream
|
|
||||||
import java.nio.file.FileSystem
|
import java.nio.file.FileSystem
|
||||||
import java.nio.file.FileSystems
|
import java.nio.file.FileSystems
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
@@ -61,9 +59,9 @@ import kotlin.io.path.*
|
|||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class TransportPanel(
|
internal class TransportPanel(
|
||||||
private val transferManager: InternalTransferManager,
|
private val transferManager: InternalTransferManager,
|
||||||
var host: Host,
|
val host: Host,
|
||||||
val loader: TransportSupportLoader,
|
val loader: TransportSupportLoader,
|
||||||
) : JPanel(BorderLayout()), DataProvider, Disposable, TransportNavigator {
|
) : JPanel(BorderLayout()), DataProvider, Disposable, TransportNavigator {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -120,9 +118,6 @@ class TransportPanel(
|
|||||||
private val disposed = AtomicBoolean(false)
|
private val disposed = AtomicBoolean(false)
|
||||||
private val futures = Collections.synchronizedSet(mutableSetOf<Future<*>>())
|
private val futures = Collections.synchronizedSet(mutableSetOf<Future<*>>())
|
||||||
|
|
||||||
private val _fileSystem by lazy { getSupport().fileSystem }
|
|
||||||
private val defaultPath by lazy { getSupport().path }
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作目录
|
* 工作目录
|
||||||
@@ -156,7 +151,7 @@ class TransportPanel(
|
|||||||
toolbar.add(prevBtn)
|
toolbar.add(prevBtn)
|
||||||
toolbar.add(homeBtn)
|
toolbar.add(homeBtn)
|
||||||
toolbar.add(nextBtn)
|
toolbar.add(nextBtn)
|
||||||
toolbar.add(TransportNavigationPanel(loader, this))
|
toolbar.add(TransportNavigationPanel(this))
|
||||||
toolbar.add(bookmarkBtn)
|
toolbar.add(bookmarkBtn)
|
||||||
toolbar.add(parentBtn)
|
toolbar.add(parentBtn)
|
||||||
toolbar.add(eyeBtn)
|
toolbar.add(eyeBtn)
|
||||||
@@ -237,7 +232,7 @@ class TransportPanel(
|
|||||||
|
|
||||||
Disposer.register(this, editTransferListener)
|
Disposer.register(this, editTransferListener)
|
||||||
|
|
||||||
refreshBtn.addActionListener { reload() }
|
refreshBtn.addActionListener { reload(requestFocus = true) }
|
||||||
|
|
||||||
prevBtn.addActionListener { navigator.back() }
|
prevBtn.addActionListener { navigator.back() }
|
||||||
nextBtn.addActionListener { navigator.forward() }
|
nextBtn.addActionListener { navigator.forward() }
|
||||||
@@ -245,7 +240,7 @@ class TransportPanel(
|
|||||||
parentBtn.addActionListener(createSmartAction(object : AbstractAction() {
|
parentBtn.addActionListener(createSmartAction(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
if (hasParent.not()) return
|
if (hasParent.not()) return
|
||||||
navigator.navigateTo(model.getPath(0))
|
reload(newPath = model.getPath(0).absolutePathString(), requestFocus = true)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -254,20 +249,23 @@ class TransportPanel(
|
|||||||
val workdir = workdir ?: return
|
val workdir = workdir ?: return
|
||||||
if (e.actionCommand.isNullOrBlank()) {
|
if (e.actionCommand.isNullOrBlank()) {
|
||||||
if (bookmarkBtn.isBookmark) {
|
if (bookmarkBtn.isBookmark) {
|
||||||
bookmarkBtn.deleteBookmark(workdir.absolutePathString())
|
bookmarkBtn.deleteBookmark(workdir.pathString)
|
||||||
} else {
|
} else {
|
||||||
bookmarkBtn.addBookmark(workdir.absolutePathString())
|
bookmarkBtn.addBookmark(workdir.pathString)
|
||||||
}
|
}
|
||||||
bookmarkBtn.isBookmark = bookmarkBtn.isBookmark.not()
|
bookmarkBtn.isBookmark = bookmarkBtn.isBookmark.not()
|
||||||
} else {
|
} else {
|
||||||
navigateTo(_fileSystem.getPath(e.actionCommand))
|
navigateTo(e.actionCommand)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
homeBtn.addActionListener(createSmartAction(object : AbstractAction() {
|
homeBtn.addActionListener(createSmartAction(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
navigator.navigateTo(_fileSystem.getPath(defaultPath))
|
if (loader.isLoaded()) {
|
||||||
|
val home = loader.getSyncTransportSupport().getDefaultPath().absolutePathString()
|
||||||
|
reload(newPath = home, requestFocus = true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -275,7 +273,7 @@ class TransportPanel(
|
|||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
showHiddenFiles = showHiddenFiles.not()
|
showHiddenFiles = showHiddenFiles.not()
|
||||||
eyeBtn.icon = if (showHiddenFiles) Icons.eye else Icons.eyeClose
|
eyeBtn.icon = if (showHiddenFiles) Icons.eye else Icons.eyeClose
|
||||||
reload()
|
reload(requestFocus = true)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -291,8 +289,11 @@ class TransportPanel(
|
|||||||
transferManager.addTransferListener(object : TransferListener {
|
transferManager.addTransferListener(object : TransferListener {
|
||||||
override fun onTransferChanged(transfer: Transfer, state: TransferTreeTableNode.State) {
|
override fun onTransferChanged(transfer: Transfer, state: TransferTreeTableNode.State) {
|
||||||
if (state != TransferTreeTableNode.State.Done && state != TransferTreeTableNode.State.Failed) return
|
if (state != TransferTreeTableNode.State.Done && state != TransferTreeTableNode.State.Failed) return
|
||||||
if (transfer.target().fileSystem != _fileSystem) return
|
val target = transfer.target()
|
||||||
if (transfer.target() == workdir || transfer.target().parent == workdir) {
|
if (loader.isLoaded()) {
|
||||||
|
if (target.fileSystem != loader.getSyncTransportSupport().getFileSystem()) return
|
||||||
|
}
|
||||||
|
if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) {
|
||||||
reload(requestFocus = false)
|
reload(requestFocus = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,7 +345,7 @@ class TransportPanel(
|
|||||||
addPropertyChangeListener("workdir") { evt ->
|
addPropertyChangeListener("workdir") { evt ->
|
||||||
val newValue = evt.newValue
|
val newValue = evt.newValue
|
||||||
if (newValue is Path) {
|
if (newValue is Path) {
|
||||||
bookmarkBtn.isBookmark = bookmarkBtn.getBookmarks().contains(newValue.absolutePathString())
|
bookmarkBtn.isBookmark = bookmarkBtn.getBookmarks().contains(newValue.pathString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,7 +365,7 @@ class TransportPanel(
|
|||||||
undoManager.addEdit(object : AbstractUndoableEdit() {
|
undoManager.addEdit(object : AbstractUndoableEdit() {
|
||||||
override fun undo() {
|
override fun undo() {
|
||||||
super.undo()
|
super.undo()
|
||||||
if (navigator.navigateTo(oldValue)) {
|
if (navigator.reload(newPath = oldValue.absolutePathString(), requestFocus = true)) {
|
||||||
undoOrRedo = true
|
undoOrRedo = true
|
||||||
undoOrRedoPath = oldValue
|
undoOrRedoPath = oldValue
|
||||||
}
|
}
|
||||||
@@ -372,7 +373,7 @@ class TransportPanel(
|
|||||||
|
|
||||||
override fun redo() {
|
override fun redo() {
|
||||||
super.redo()
|
super.redo()
|
||||||
if (navigator.navigateTo(newValue)) {
|
if (navigator.reload(newPath = newValue.absolutePathString(), requestFocus = true)) {
|
||||||
undoOrRedo = true
|
undoOrRedo = true
|
||||||
undoOrRedoPath = newValue
|
undoOrRedoPath = newValue
|
||||||
}
|
}
|
||||||
@@ -404,14 +405,6 @@ class TransportPanel(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
table.addKeyListener(object : KeyAdapter() {
|
|
||||||
override fun keyPressed(e: KeyEvent) {
|
|
||||||
if (e.keyCode == KeyEvent.VK_ENTER) {
|
|
||||||
enterSelectionFolder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// https://github.com/TermoraDev/termora/issues/401
|
// https://github.com/TermoraDev/termora/issues/401
|
||||||
table.addMouseListener(object : MouseAdapter() {
|
table.addMouseListener(object : MouseAdapter() {
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
@@ -433,10 +426,10 @@ class TransportPanel(
|
|||||||
if (attributes.isDirectory) {
|
if (attributes.isDirectory) {
|
||||||
enterSelectionFolder()
|
enterSelectionFolder()
|
||||||
} else {
|
} else {
|
||||||
transferManager.addTransfer(
|
val paths = listOf(model.getPath(row) to attributes)
|
||||||
listOf(model.getPath(row) to attributes),
|
if (loader.isOpened() && transferManager.canTransfer(paths.map { it.first })) {
|
||||||
InternalTransferManager.TransferMode.Transfer
|
transferManager.addTransfer(paths, InternalTransferManager.TransferMode.Transfer)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
} else if (SwingUtilities.isRightMouseButton(e)) {
|
} else if (SwingUtilities.isRightMouseButton(e)) {
|
||||||
val r = table.rowAtPoint(e.point)
|
val r = table.rowAtPoint(e.point)
|
||||||
@@ -467,6 +460,12 @@ class TransportPanel(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
table.actionMap.put("EnterSelectionFolder", object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
enterSelectionFolder()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 快速导航
|
// 快速导航
|
||||||
table.addKeyListener(object : KeyAdapter() {
|
table.addKeyListener(object : KeyAdapter() {
|
||||||
override fun keyPressed(e: KeyEvent) {
|
override fun keyPressed(e: KeyEvent) {
|
||||||
@@ -494,6 +493,7 @@ class TransportPanel(
|
|||||||
if (SystemInfo.isMacOS.not()) {
|
if (SystemInfo.isMacOS.not()) {
|
||||||
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), "Reload")
|
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), "Reload")
|
||||||
}
|
}
|
||||||
|
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "EnterSelectionFolder")
|
||||||
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, toolkit.menuShortcutKeyMaskEx), "Reload")
|
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, toolkit.menuShortcutKeyMaskEx), "Reload")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,7 +526,6 @@ class TransportPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getTransferData(support: TransferSupport, load: Boolean): TransferData? {
|
private fun getTransferData(support: TransferSupport, load: Boolean): TransferData? {
|
||||||
if (loader.isLoaded.not()) return null
|
|
||||||
val workdir = workdir ?: return null
|
val workdir = workdir ?: return null
|
||||||
val dropLocation = support.dropLocation as? JTable.DropLocation ?: return null
|
val dropLocation = support.dropLocation as? JTable.DropLocation ?: return null
|
||||||
val row = if (dropLocation.isInsertRow) 0 else sorter.convertRowIndexToModel(dropLocation.row)
|
val row = if (dropLocation.isInsertRow) 0 else sorter.convertRowIndexToModel(dropLocation.row)
|
||||||
@@ -542,7 +541,8 @@ class TransportPanel(
|
|||||||
if (transferTransferable.component == panel) return null
|
if (transferTransferable.component == panel) return null
|
||||||
paths.addAll(transferTransferable.files)
|
paths.addAll(transferTransferable.files)
|
||||||
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||||
if (_fileSystem.isLocallyFileSystem()) return null
|
if (loader.isLoaded() && loader.getSyncTransportSupport().getFileSystem().isLocallyFileSystem())
|
||||||
|
return null
|
||||||
if (load) {
|
if (load) {
|
||||||
val files = support.transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*>
|
val files = support.transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*>
|
||||||
if (files.isEmpty()) return null
|
if (files.isEmpty()) return null
|
||||||
@@ -579,22 +579,12 @@ class TransportPanel(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTableModel(): TransportTableModel {
|
private suspend fun getFileSystem(): FileSystem {
|
||||||
return model
|
return loader.getTransportSupport().getFileSystem()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFileSystem(): FileSystem {
|
private suspend fun getTransportSupport(): TransportSupport {
|
||||||
return _fileSystem
|
return loader.getTransportSupport()
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 不能在 EDT 线程调用
|
|
||||||
*/
|
|
||||||
private fun getSupport(): TransportSupport {
|
|
||||||
if (SwingUtilities.isEventDispatchThread()) {
|
|
||||||
throw WrongThreadException("AWT EventQueue")
|
|
||||||
}
|
|
||||||
return loader.get()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enterSelectionFolder() {
|
private fun enterSelectionFolder() {
|
||||||
@@ -611,7 +601,12 @@ class TransportPanel(
|
|||||||
if (workdir != null) registerSelectRow(workdir.name)
|
if (workdir != null) registerSelectRow(workdir.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
navigator.navigateTo(path)
|
// Windows 比较特殊,显示盘符页
|
||||||
|
if (path.fileSystem.isWindowsFileSystem() && path.pathString == path.fileSystem.separator) {
|
||||||
|
navigator.navigateTo(path.pathString)
|
||||||
|
} else {
|
||||||
|
navigator.navigateTo(path.absolutePathString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun registerSelectRow(name: String) {
|
private fun registerSelectRow(name: String) {
|
||||||
@@ -628,7 +623,11 @@ class TransportPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun reload(oldPath: Path? = workdir, newPath: Path? = workdir, requestFocus: Boolean = false): Boolean {
|
fun reload(
|
||||||
|
oldPath: String? = workdir?.absolutePathString(),
|
||||||
|
newPath: String? = workdir?.absolutePathString(),
|
||||||
|
requestFocus: Boolean = false
|
||||||
|
): Boolean {
|
||||||
assertEventDispatchThread()
|
assertEventDispatchThread()
|
||||||
|
|
||||||
if (loading) return false
|
if (loading) return false
|
||||||
@@ -664,20 +663,26 @@ class TransportPanel(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun doReload(oldPath: Path? = null, newPath: Path? = null, requestFocus: Boolean = false): Path {
|
private suspend fun doReload(
|
||||||
|
oldPath: String? = null,
|
||||||
|
newPath: String? = null,
|
||||||
|
requestFocus: Boolean = false
|
||||||
|
): Path {
|
||||||
|
|
||||||
|
val support = getTransportSupport()
|
||||||
|
val fileSystem = support.getFileSystem()
|
||||||
val workdir = newPath ?: oldPath
|
val workdir = newPath ?: oldPath
|
||||||
|
|
||||||
if (workdir == null) {
|
if (workdir == null) {
|
||||||
val path = _fileSystem.getPath(defaultPath)
|
val path = support.getDefaultPath()
|
||||||
return doReload(null, path)
|
return doReload(null, path.absolutePathString())
|
||||||
}
|
}
|
||||||
|
|
||||||
val path = workdir
|
val path = fileSystem.getPath(workdir)
|
||||||
val first = AtomicBoolean(false)
|
val first = AtomicBoolean(false)
|
||||||
var parent = path.parent
|
var parent = path.parent
|
||||||
if (parent == null && _fileSystem.isWindowsFileSystem() && workdir.pathString != _fileSystem.separator) {
|
if (parent == null && fileSystem.isWindowsFileSystem() && path.pathString != fileSystem.separator) {
|
||||||
parent = _fileSystem.getPath(_fileSystem.separator)
|
parent = fileSystem.getPath(fileSystem.separator)
|
||||||
}
|
}
|
||||||
val files = mutableListOf<Pair<Path, Attributes>>()
|
val files = mutableListOf<Pair<Path, Attributes>>()
|
||||||
if ((parent != null).also { hasParent = it }) {
|
if ((parent != null).also { hasParent = it }) {
|
||||||
@@ -698,8 +703,8 @@ class TransportPanel(
|
|||||||
files.clear()
|
files.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_fileSystem.isWindowsFileSystem() && workdir.pathString == _fileSystem.separator) {
|
if (fileSystem.isWindowsFileSystem() && path.pathString == fileSystem.separator) {
|
||||||
for (path in _fileSystem.rootDirectories) {
|
for (path in fileSystem.rootDirectories) {
|
||||||
val attributes = getAttributes(path)
|
val attributes = getAttributes(path)
|
||||||
files.add(path to attributes)
|
files.add(path to attributes)
|
||||||
}
|
}
|
||||||
@@ -718,7 +723,7 @@ class TransportPanel(
|
|||||||
if (requestFocus)
|
if (requestFocus)
|
||||||
coroutineScope.launch(Dispatchers.Swing) { table.requestFocusInWindow() }
|
coroutineScope.launch(Dispatchers.Swing) { table.requestFocusInWindow() }
|
||||||
|
|
||||||
return workdir
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun listFiles(path: Path): Stream<Pair<Path, Attributes>> {
|
private fun listFiles(path: Path): Stream<Pair<Path, Attributes>> {
|
||||||
@@ -789,21 +794,24 @@ class TransportPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun showContextmenu(rows: Array<Int>, e: MouseEvent) {
|
private fun showContextmenu(rows: Array<Int>, e: MouseEvent) {
|
||||||
val files = rows.map { model.getPath(it) to model.getAttributes(it) }
|
val files = rows.map { model.getPath(it) to model.getAttributes(it) }
|
||||||
val popupMenu = TransportPopupMenu(owner, model, transferManager, _fileSystem, files)
|
val popupMenu = TransportPopupMenu(owner, model, transferManager, loader, files)
|
||||||
popupMenu.addActionListener(PopupMenuActionListener(files))
|
popupMenu.addActionListener(PopupMenuActionListener(files))
|
||||||
popupMenu.show(table, e.x, e.y)
|
popupMenu.show(table, e.x, e.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateTo(destination: Path): Boolean {
|
|
||||||
|
override fun navigateTo(destination: String): Boolean {
|
||||||
assertEventDispatchThread()
|
assertEventDispatchThread()
|
||||||
|
|
||||||
if (loading) return false
|
if (loading) return false
|
||||||
if (workdir == destination) return false
|
|
||||||
|
|
||||||
return reload(workdir, destination)
|
if (loader.isOpened()) {
|
||||||
|
if (workdir?.absolutePathString() == destination) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return reload(newPath = destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getHistory(): List<Path> {
|
override fun getHistory(): List<Path> {
|
||||||
@@ -827,7 +835,7 @@ class TransportPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setNewWorkdir(destination: Path) {
|
private fun setNewWorkdir(destination: Path) {
|
||||||
val oldValue = workdir
|
val oldValue = if (destination.fileSystem == workdir?.fileSystem) workdir else null
|
||||||
workdir = destination
|
workdir = destination
|
||||||
firePropertyChange("workdir", oldValue, destination)
|
firePropertyChange("workdir", oldValue, destination)
|
||||||
}
|
}
|
||||||
@@ -914,8 +922,12 @@ class TransportPanel(
|
|||||||
val millis = Files.getLastModifiedTime(localPath).toMillis()
|
val millis = Files.getLastModifiedTime(localPath).toMillis()
|
||||||
if (oldMillis == millis) continue
|
if (oldMillis == millis) continue
|
||||||
|
|
||||||
|
// 正在编辑时可能会出现断线的情况 ,安全获取
|
||||||
|
val fs = getFileSystem()
|
||||||
|
if (fs.isOpen.not()) continue
|
||||||
|
|
||||||
// 发送到服务器
|
// 发送到服务器
|
||||||
transferManager.addHighTransfer(localPath, target)
|
transferManager.addHighTransfer(localPath, fs.getPath(target.absolutePathString()))
|
||||||
oldMillis = millis
|
oldMillis = millis
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1011,8 +1023,9 @@ class TransportPanel(
|
|||||||
} else if (actionCommand == TransportPopupMenu.ActionCommand.NewFolder || actionCommand == TransportPopupMenu.ActionCommand.NewFile) {
|
} else if (actionCommand == TransportPopupMenu.ActionCommand.NewFolder || actionCommand == TransportPopupMenu.ActionCommand.NewFile) {
|
||||||
val name = e.source.toString()
|
val name = e.source.toString()
|
||||||
val workdir = workdir ?: return
|
val workdir = workdir ?: return
|
||||||
val path = workdir.resolve(name)
|
|
||||||
processPath(e.source.toString()) {
|
processPath(e.source.toString()) {
|
||||||
|
// 因为此时可能已经断线,任何 Path 都不可完全相信
|
||||||
|
val path = getFileSystem().getPath(workdir.resolve(name).absolutePathString())
|
||||||
if (actionCommand == TransportPopupMenu.ActionCommand.NewFolder)
|
if (actionCommand == TransportPopupMenu.ActionCommand.NewFolder)
|
||||||
path.createDirectories()
|
path.createDirectories()
|
||||||
else
|
else
|
||||||
@@ -1023,16 +1036,10 @@ class TransportPanel(
|
|||||||
val target = source.parent.resolve(e.source.toString())
|
val target = source.parent.resolve(e.source.toString())
|
||||||
processPath(e.source.toString()) { source.moveTo(target) }
|
processPath(e.source.toString()) { source.moveTo(target) }
|
||||||
} else if (actionCommand == TransportPopupMenu.ActionCommand.Rmrf) {
|
} else if (actionCommand == TransportPopupMenu.ActionCommand.Rmrf) {
|
||||||
processPath(StringUtils.EMPTY) {
|
transferManager.addTransfer(files, InternalTransferManager.TransferMode.Rmrf)
|
||||||
val session = (_fileSystem as SftpFileSystem).clientSession
|
} else if (actionCommand == TransportPopupMenu.ActionCommand.Reconnect) {
|
||||||
for (path in files.map { it.first }) {
|
// reload now
|
||||||
session.executeRemoteCommand(
|
reload()
|
||||||
"rm -rf '${path.absolutePathString()}'",
|
|
||||||
OutputStream.nullOutputStream(),
|
|
||||||
Charsets.UTF_8
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (actionCommand == TransportPopupMenu.ActionCommand.ChangePermissions) {
|
} else if (actionCommand == TransportPopupMenu.ActionCommand.ChangePermissions) {
|
||||||
val c = e.source as TransportPopupMenu.ChangePermission
|
val c = e.source as TransportPopupMenu.ChangePermission
|
||||||
val path = files.first().first
|
val path = files.first().first
|
||||||
@@ -1115,7 +1122,7 @@ class TransportPanel(
|
|||||||
|
|
||||||
private inner class MyDefaultTableCellRenderer : DefaultTableCellRenderer() {
|
private inner class MyDefaultTableCellRenderer : DefaultTableCellRenderer() {
|
||||||
override fun getTableCellRendererComponent(
|
override fun getTableCellRendererComponent(
|
||||||
table: JTable?,
|
table: JTable,
|
||||||
value: Any?,
|
value: Any?,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
hasFocus: Boolean,
|
hasFocus: Boolean,
|
||||||
@@ -1144,12 +1151,13 @@ class TransportPanel(
|
|||||||
text = StringUtils.EMPTY
|
text = StringUtils.EMPTY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreground = null
|
||||||
val c = super.getTableCellRendererComponent(table, text, isSelected, hasFocus, row, column)
|
val c = super.getTableCellRendererComponent(table, text, isSelected, hasFocus, row, column)
|
||||||
icon = null
|
icon = null
|
||||||
|
|
||||||
if (column == TransportTableModel.COLUMN_NAME) {
|
if (column == TransportTableModel.COLUMN_NAME) {
|
||||||
if (_fileSystem.isWindowsFileSystem()) {
|
val path = model.getPath(sorter.convertRowIndexToModel(row))
|
||||||
val path = model.getPath(sorter.convertRowIndexToModel(row))
|
if (path.fileSystem.isWindowsFileSystem()) {
|
||||||
icon = if (attributes.isParent) {
|
icon = if (attributes.isParent) {
|
||||||
NativeIcons.folderIcon
|
NativeIcons.folderIcon
|
||||||
} else {
|
} else {
|
||||||
@@ -1176,6 +1184,12 @@ class TransportPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loader.isOpened().not()) {
|
||||||
|
if (isSelected.not()) {
|
||||||
|
foreground = UIManager.getColor("textInactiveText")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import app.termora.Icons
|
|||||||
import app.termora.OptionPane
|
import app.termora.OptionPane
|
||||||
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
|
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
@@ -13,7 +14,6 @@ import java.awt.datatransfer.StringSelection
|
|||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
import java.awt.event.ActionListener
|
import java.awt.event.ActionListener
|
||||||
import java.awt.event.KeyEvent
|
import java.awt.event.KeyEvent
|
||||||
import java.nio.file.FileSystem
|
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.attribute.PosixFilePermission
|
import java.nio.file.attribute.PosixFilePermission
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -26,11 +26,11 @@ import kotlin.io.path.absolutePathString
|
|||||||
import kotlin.io.path.name
|
import kotlin.io.path.name
|
||||||
|
|
||||||
|
|
||||||
class TransportPopupMenu(
|
internal class TransportPopupMenu(
|
||||||
private val owner: Window,
|
private val owner: Window,
|
||||||
private val model: TransportTableModel,
|
private val model: TransportTableModel,
|
||||||
private val transferManager: InternalTransferManager,
|
private val transferManager: InternalTransferManager,
|
||||||
private val fileSystem: FileSystem,
|
private val loader: TransportSupportLoader,
|
||||||
private val files: List<Pair<Path, TransportTableModel.Attributes>>
|
private val files: List<Pair<Path, TransportTableModel.Attributes>>
|
||||||
) : FlatPopupMenu() {
|
) : FlatPopupMenu() {
|
||||||
private val paths = files.map { it.first }
|
private val paths = files.map { it.first }
|
||||||
@@ -71,25 +71,45 @@ class TransportPopupMenu(
|
|||||||
private fun initView() {
|
private fun initView() {
|
||||||
inheritsPopupMenu = false
|
inheritsPopupMenu = false
|
||||||
|
|
||||||
|
if (loader.isOpened().not()) {
|
||||||
|
val reconnect = add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
|
||||||
|
reconnect.addActionListener { e -> fireActionPerformed(e, ActionCommand.Reconnect) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileSystem = if (loader.isLoaded()) loader.getSyncTransportSupport().getFileSystem() else null
|
||||||
|
|
||||||
add(transferMenu)
|
add(transferMenu)
|
||||||
add(editMenu)
|
add(editMenu)
|
||||||
addSeparator()
|
addSeparator()
|
||||||
add(copyPathMenu)
|
add(copyPathMenu)
|
||||||
if (fileSystem.isLocallyFileSystem()) add(openInFinderMenu)
|
if (fileSystem?.isLocallyFileSystem() == true) {
|
||||||
|
add(openInFinderMenu)
|
||||||
|
}
|
||||||
addSeparator()
|
addSeparator()
|
||||||
add(renameMenu)
|
add(renameMenu)
|
||||||
add(deleteMenu)
|
add(deleteMenu)
|
||||||
if (fileSystem is SftpFileSystem) add(rmrfMenu)
|
if (fileSystem is SftpFileSystem) {
|
||||||
|
add(rmrfMenu)
|
||||||
|
}
|
||||||
add(changePermissionsMenu)
|
add(changePermissionsMenu)
|
||||||
addSeparator()
|
addSeparator()
|
||||||
add(refreshMenu)
|
add(refreshMenu)
|
||||||
addSeparator()
|
addSeparator()
|
||||||
add(newMenu)
|
add(newMenu)
|
||||||
|
|
||||||
|
// 开发环境提供断线
|
||||||
|
if (Application.getAppPath().isBlank() && loader.isOpened()) {
|
||||||
|
addSeparator()
|
||||||
|
add("Disconnect").addActionListener {
|
||||||
|
IOUtils.closeQuietly(loader.getSyncTransportSupport().getFileSystem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
transferMenu.isEnabled = hasParent.not() && files.isNotEmpty() && transferManager.canTransfer(paths)
|
transferMenu.isEnabled = hasParent.not() && files.isNotEmpty() && transferManager.canTransfer(paths)
|
||||||
copyPathMenu.isEnabled = files.isNotEmpty()
|
copyPathMenu.isEnabled = files.isNotEmpty()
|
||||||
openInFinderMenu.isEnabled = files.isNotEmpty() && fileSystem.isLocallyFileSystem()
|
openInFinderMenu.isEnabled = files.isNotEmpty() && fileSystem?.isLocallyFileSystem() == true
|
||||||
editMenu.isEnabled = files.isNotEmpty() && fileSystem.isLocallyFileSystem().not()
|
editMenu.isEnabled = files.isNotEmpty() && fileSystem?.isLocallyFileSystem() != true
|
||||||
&& files.all { it.second.isFile && it.second.isSymbolicLink.not() }
|
&& files.all { it.second.isFile && it.second.isSymbolicLink.not() }
|
||||||
renameMenu.isEnabled = hasParent.not() && files.size == 1
|
renameMenu.isEnabled = hasParent.not() && files.size == 1
|
||||||
deleteMenu.isEnabled = hasParent.not() && files.isNotEmpty()
|
deleteMenu.isEnabled = hasParent.not() && files.isNotEmpty()
|
||||||
@@ -211,6 +231,7 @@ class TransportPopupMenu(
|
|||||||
Refresh,
|
Refresh,
|
||||||
ChangePermissions,
|
ChangePermissions,
|
||||||
Rmrf,
|
Rmrf,
|
||||||
|
Reconnect,
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ChangePermission(val permissions: Set<PosixFilePermission>, val includeSubFolder: Boolean)
|
data class ChangePermission(val permissions: Set<PosixFilePermission>, val includeSubFolder: Boolean)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package app.termora.transfer
|
|||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
import app.termora.protocol.PathHandlerRequest
|
|
||||||
import app.termora.protocol.TransferProtocolProvider
|
import app.termora.protocol.TransferProtocolProvider
|
||||||
import app.termora.tree.*
|
import app.termora.tree.*
|
||||||
import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon
|
import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon
|
||||||
@@ -24,9 +23,8 @@ import java.util.concurrent.Executors
|
|||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.event.TreeExpansionEvent
|
import javax.swing.event.TreeExpansionEvent
|
||||||
import javax.swing.event.TreeExpansionListener
|
import javax.swing.event.TreeExpansionListener
|
||||||
import kotlin.io.path.absolutePathString
|
|
||||||
|
|
||||||
class TransportSelectionPanel(
|
internal class TransportSelectionPanel(
|
||||||
private val tabbed: TransportTabbed,
|
private val tabbed: TransportTabbed,
|
||||||
private val transferManager: InternalTransferManager,
|
private val transferManager: InternalTransferManager,
|
||||||
) : JPanel(BorderLayout()), Disposable {
|
) : JPanel(BorderLayout()), Disposable {
|
||||||
@@ -99,24 +97,21 @@ class TransportSelectionPanel(
|
|||||||
|
|
||||||
private suspend fun doConnect(host: Host) {
|
private suspend fun doConnect(host: Host) {
|
||||||
|
|
||||||
val provider = TransferProtocolProvider.valueOf(host.protocol)
|
val loader = ReconnectableTransportSupportLoader(owner, host)
|
||||||
if (provider == null) {
|
|
||||||
throw IllegalStateException(I18n.getString("termora.protocol.not-supported", host.protocol))
|
|
||||||
}
|
|
||||||
|
|
||||||
val handler = provider.createPathHandler(PathHandlerRequest(host, owner))
|
// try load
|
||||||
val support = TransportSupport(handler.fileSystem, handler.path.absolutePathString())
|
loader.getTransportSupport()
|
||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
val panel = TransportPanel(transferManager, host, TransportSupportLoader { support })
|
val panel = TransportPanel(transferManager, host, loader)
|
||||||
Disposer.register(panel, object : Disposable {
|
Disposer.register(panel, object : Disposable {
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
Disposer.dispose(handler)
|
Disposer.dispose(loader)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
swingCoroutineScope.launch {
|
swingCoroutineScope.launch {
|
||||||
tabbed.remove(that)
|
tabbed.remove(that)
|
||||||
tabbed.addTab(host.name, panel)
|
tabbed.addTab(host.name, TransportViewer.MyIcon.Success, panel)
|
||||||
tabbed.selectedIndex = tabbed.tabCount - 1
|
tabbed.selectedIndex = tabbed.tabCount - 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package app.termora.transfer
|
package app.termora.transfer
|
||||||
|
|
||||||
import java.nio.file.FileSystem
|
import java.nio.file.FileSystem
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
|
||||||
class TransportSupport(
|
internal interface TransportSupport {
|
||||||
val fileSystem: FileSystem,
|
fun getFileSystem(): FileSystem
|
||||||
val path: String
|
fun getDefaultPath(): Path
|
||||||
)
|
}
|
||||||
@@ -1,52 +1,31 @@
|
|||||||
package app.termora.transfer
|
package app.termora.transfer
|
||||||
|
|
||||||
import okio.withLock
|
import app.termora.Disposable
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
|
||||||
import java.util.function.Supplier
|
|
||||||
|
|
||||||
class TransportSupportLoader(private val support: Supplier<TransportSupport>) : Supplier<TransportSupport> {
|
internal interface TransportSupportLoader : Disposable {
|
||||||
private val loading = AtomicBoolean(false)
|
|
||||||
private lateinit var mySupport: TransportSupport
|
|
||||||
private val lock = ReentrantLock()
|
|
||||||
private val condition = lock.newCondition()
|
|
||||||
private val exceptionReference = AtomicReference<Exception>(null)
|
|
||||||
|
|
||||||
val isLoaded get() = ::mySupport.isInitialized
|
/**
|
||||||
|
* 获取传输支持
|
||||||
|
*/
|
||||||
|
suspend fun getTransportSupport(): TransportSupport
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只有当 [isLoaded] 返回 true 时才能调用,为了不出现问题,只有 EDT 线程才能调用
|
||||||
|
*/
|
||||||
|
fun getSyncTransportSupport(): TransportSupport
|
||||||
|
|
||||||
override fun get(): TransportSupport {
|
/**
|
||||||
if (isLoaded) return mySupport
|
* 是否已经加载,已经加载不表示可以正常使用,它仅证明已经加载可以同步调用
|
||||||
|
*/
|
||||||
if (loading.compareAndSet(false, true)) {
|
fun isLoaded(): Boolean
|
||||||
try {
|
|
||||||
mySupport = support.get()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
exceptionReference.set(e)
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
lock.withLock {
|
|
||||||
loading.set(false)
|
|
||||||
condition.signalAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lock.lock()
|
|
||||||
try {
|
|
||||||
condition.await()
|
|
||||||
} finally {
|
|
||||||
lock.unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val exception = exceptionReference.get()
|
|
||||||
if (exception != null) {
|
|
||||||
throw exception
|
|
||||||
}
|
|
||||||
|
|
||||||
return get()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速检查是否已经成功打开
|
||||||
|
*/
|
||||||
|
fun isOpened(): Boolean = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否正在打开中,也有可能是加载中
|
||||||
|
*/
|
||||||
|
fun isOpening(): Boolean = false
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ import javax.swing.JToolBar
|
|||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
@Suppress("DuplicatedCode")
|
@Suppress("DuplicatedCode")
|
||||||
class TransportTabbed(
|
internal class TransportTabbed(
|
||||||
private val transferManager: TransferManager,
|
private val transferManager: TransferManager,
|
||||||
) : FlatTabbedPane(), Disposable {
|
) : FlatTabbedPane(), Disposable {
|
||||||
private val addBtn = JButton(Icons.add)
|
private val addBtn = JButton(Icons.add)
|
||||||
@@ -64,14 +64,15 @@ class TransportTabbed(
|
|||||||
// 右键菜单
|
// 右键菜单
|
||||||
addMouseListener(object : MouseAdapter() {
|
addMouseListener(object : MouseAdapter() {
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
if (!SwingUtilities.isRightMouseButton(e)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val index = indexAtLocation(e.x, e.y)
|
val index = indexAtLocation(e.x, e.y)
|
||||||
if (index < 0) return
|
if (index < 0) return
|
||||||
|
if (SwingUtilities.isRightMouseButton(e)) {
|
||||||
showContextMenu(index, e)
|
showContextMenu(index, e)
|
||||||
|
} else if (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||||
|
val tab = getTransportPanel(index) ?: return
|
||||||
|
if (tab.loader.isOpening() || tab.loader.isOpened()) return
|
||||||
|
tab.reload()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -105,8 +106,9 @@ class TransportTabbed(
|
|||||||
|
|
||||||
private fun tabClose(c: TransportPanel): Boolean {
|
private fun tabClose(c: TransportPanel): Boolean {
|
||||||
if (transferManager.getTransferCount() < 1) return true
|
if (transferManager.getTransferCount() < 1) return true
|
||||||
if (c.loader.isLoaded.not()) return false
|
val loader = c.loader
|
||||||
val fileSystem = c.getFileSystem()
|
if (loader.isLoaded().not()) return false
|
||||||
|
val fileSystem = loader.getSyncTransportSupport()
|
||||||
val transfers = transferManager.getTransfers()
|
val transfers = transferManager.getTransfers()
|
||||||
.filter { it.source().fileSystem == fileSystem || it.target().fileSystem == fileSystem }
|
.filter { it.source().fileSystem == fileSystem || it.target().fileSystem == fileSystem }
|
||||||
if (transfers.isEmpty()) return true
|
if (transfers.isEmpty()) return true
|
||||||
@@ -136,10 +138,24 @@ class TransportTabbed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addLocalTab() {
|
fun addLocalTab() {
|
||||||
val host = Host(name = "Local", protocol = LocalProtocolProvider.PROTOCOL)
|
// local id 必须固定,不然书签无法使用
|
||||||
val support = TransportSupport(FileSystems.getDefault(), getDefaultLocalPath())
|
val host = Host(id = "local", name = "Local", protocol = LocalProtocolProvider.PROTOCOL)
|
||||||
val panel = TransportPanel(internalTransferManager, host, TransportSupportLoader { support })
|
val fs = FileSystems.getDefault()
|
||||||
addTab(I18n.getString("termora.transport.local"), panel)
|
val support = DefaultTransportSupport(fs, fs.getPath(getDefaultLocalPath()))
|
||||||
|
val panel = TransportPanel(internalTransferManager, host, object : TransportSupportLoader {
|
||||||
|
override suspend fun getTransportSupport(): TransportSupport {
|
||||||
|
return support
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSyncTransportSupport(): TransportSupport {
|
||||||
|
return support
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isLoaded(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
addTab(I18n.getString("termora.transport.local"), TransportViewer.MyIcon.Success, panel)
|
||||||
super.setTabClosable(0, false)
|
super.setTabClosable(0, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +170,7 @@ class TransportTabbed(
|
|||||||
val popupMenu = FlatPopupMenu()
|
val popupMenu = FlatPopupMenu()
|
||||||
|
|
||||||
// 克隆
|
// 克隆
|
||||||
val clone = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.clone"))
|
val clone = popupMenu.add(I18n.getString("termora.copy"))
|
||||||
clone.addActionListener(object : AnAction() {
|
clone.addActionListener(object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val c = addSelectionTab()
|
val c = addSelectionTab()
|
||||||
@@ -165,20 +181,30 @@ class TransportTabbed(
|
|||||||
// 编辑
|
// 编辑
|
||||||
val edit = popupMenu.add(I18n.getString("termora.keymgr.edit"))
|
val edit = popupMenu.add(I18n.getString("termora.keymgr.edit"))
|
||||||
edit.addActionListener(object : AnAction() {
|
edit.addActionListener(object : AnAction() {
|
||||||
|
private val hostManager get() = HostManager.getInstance()
|
||||||
private val accountManager get() = AccountManager.getInstance()
|
private val accountManager get() = AccountManager.getInstance()
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val window = evt.window
|
val window = evt.window
|
||||||
val dialog = NewHostDialogV2(
|
val dialog = NewHostDialogV2(
|
||||||
window,
|
window,
|
||||||
panel.host,
|
getHost(panel),
|
||||||
accountOwner = accountManager.getOwners().first { it.id == panel.host.ownerId })
|
accountOwner = accountManager.getOwners().first { it.id == panel.host.ownerId })
|
||||||
dialog.setLocationRelativeTo(window)
|
dialog.setLocationRelativeTo(window)
|
||||||
dialog.title = panel.host.name
|
dialog.title = panel.host.name
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
val host = dialog.host ?: return
|
val host = dialog.host ?: return
|
||||||
HostManager.getInstance().addHost(host, DatabaseChangedExtension.Source.Sync)
|
hostManager.addHost(host, DatabaseChangedExtension.Source.User)
|
||||||
setTitleAt(tabIndex, host.name)
|
setTitleAt(tabIndex, host.name)
|
||||||
panel.host = host
|
setHost(panel, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun getHost(panel: TransportPanel): Host {
|
||||||
|
return panel.getClientProperty("EditHost") as Host? ?: panel.host
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setHost(panel: TransportPanel, host: Host) {
|
||||||
|
panel.putClientProperty("EditHost", host)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ package app.termora.transfer
|
|||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
|
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import java.nio.file.FileSystems
|
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.JOptionPane
|
import javax.swing.JOptionPane
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
class TransportTerminalTab : RememberFocusTerminalTab() {
|
internal class TransportTerminalTab : RememberFocusTerminalTab() {
|
||||||
private val transportViewer = TransportViewer()
|
private val transportViewer = TransportViewer()
|
||||||
private val sftp get() = DatabaseManager.getInstance().sftp
|
private val sftp get() = DatabaseManager.getInstance().sftp
|
||||||
private val transferManager get() = transportViewer.getTransferManager()
|
private val transferManager get() = transportViewer.getTransferManager()
|
||||||
val leftTabbed get() = transportViewer.getLeftTabbed()
|
private val leftTabbed get() = transportViewer.getLeftTabbed()
|
||||||
val rightTabbed get() = transportViewer.getRightTabbed()
|
val rightTabbed get() = transportViewer.getRightTabbed()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -66,8 +66,8 @@ class TransportTerminalTab : RememberFocusTerminalTab() {
|
|||||||
private fun hasActiveTab(tabbed: TransportTabbed): Boolean {
|
private fun hasActiveTab(tabbed: TransportTabbed): Boolean {
|
||||||
for (i in 0 until tabbed.tabCount) {
|
for (i in 0 until tabbed.tabCount) {
|
||||||
val c = tabbed.getComponentAt(i) ?: continue
|
val c = tabbed.getComponentAt(i) ?: continue
|
||||||
if (c is TransportPanel && c.loader.isLoaded) {
|
if (c is TransportPanel && c.loader.isOpened()) {
|
||||||
if (c.getFileSystem() != FileSystems.getDefault()) {
|
if (c.loader.getSyncTransportSupport().getFileSystem().isLocallyFileSystem().not()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,21 +4,17 @@ import app.termora.Disposable
|
|||||||
import app.termora.Disposer
|
import app.termora.Disposer
|
||||||
import app.termora.DynamicColor
|
import app.termora.DynamicColor
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import java.awt.*
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.awt.BorderLayout
|
|
||||||
import java.awt.event.ComponentAdapter
|
import java.awt.event.ComponentAdapter
|
||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|
||||||
class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
||||||
companion object {
|
|
||||||
private val log = LoggerFactory.getLogger(TransportViewer::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
private val splitPane = JSplitPane()
|
private val splitPane = JSplitPane()
|
||||||
@@ -78,10 +74,39 @@ class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
coroutineScope.launch(Dispatchers.Swing) {
|
||||||
|
while (isActive) {
|
||||||
|
checkDisconnected(leftTabbed)
|
||||||
|
checkDisconnected(rightTabbed)
|
||||||
|
delay(250.milliseconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Disposer.register(this, leftTabbed)
|
Disposer.register(this, leftTabbed)
|
||||||
Disposer.register(this, rightTabbed)
|
Disposer.register(this, rightTabbed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun checkDisconnected(tabbed: TransportTabbed) {
|
||||||
|
for (i in 0 until tabbed.tabCount) {
|
||||||
|
val tab = tabbed.getTransportPanel(i) ?: continue
|
||||||
|
val icon = tabbed.getIconAt(i)
|
||||||
|
if (tab.loader.isOpened()) {
|
||||||
|
if (icon != MyIcon.Success) {
|
||||||
|
tabbed.setIconAt(i, MyIcon.Success)
|
||||||
|
}
|
||||||
|
} else if (tab.loader.isOpening()) {
|
||||||
|
if (icon != MyIcon.Warning) {
|
||||||
|
tabbed.setIconAt(i, MyIcon.Warning)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (icon != MyIcon.Error) {
|
||||||
|
tabbed.setIconAt(i, MyIcon.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createInternalTransferManager(
|
private fun createInternalTransferManager(
|
||||||
source: TransportTabbed,
|
source: TransportTabbed,
|
||||||
target: TransportTabbed
|
target: TransportTabbed
|
||||||
@@ -118,4 +143,38 @@ class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
return rightTabbed
|
return rightTabbed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
coroutineScope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MyIcon(private val color: Color) : Icon {
|
||||||
|
private val size = 10
|
||||||
|
|
||||||
|
|
||||||
|
// https://plugins.jetbrains.com/docs/intellij/icons-style.html#action-icons
|
||||||
|
companion object {
|
||||||
|
val Success = MyIcon(DynamicColor(Color(89, 168, 105), Color(73, 156, 84)))
|
||||||
|
val Error = MyIcon(DynamicColor(Color(219, 88, 96), Color(199, 84, 80)))
|
||||||
|
val Warning = MyIcon(DynamicColor(Color(237, 162, 0), Color(240, 167, 50)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun paintIcon(c: Component, g: Graphics, x: Int, y: Int) {
|
||||||
|
if (g is Graphics2D) {
|
||||||
|
g.color = color
|
||||||
|
val centerX = x + (iconWidth - size) / 2
|
||||||
|
val centerY = y + (iconHeight - size) / 2
|
||||||
|
g.fillRoundRect(centerX, centerY, size, size, size, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIconWidth(): Int {
|
||||||
|
return 16
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIconHeight(): Int {
|
||||||
|
return 16
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package app.termora.transfer.internal.sftp
|
package app.termora.transfer.internal.sftp
|
||||||
|
|
||||||
import app.termora.SshClients
|
import app.termora.plugin.internal.ssh.SshClients
|
||||||
import app.termora.protocol.PathHandler
|
import app.termora.protocol.PathHandler
|
||||||
import app.termora.protocol.PathHandlerRequest
|
import app.termora.protocol.PathHandlerRequest
|
||||||
import app.termora.protocol.TransferProtocolProvider
|
import app.termora.protocol.TransferProtocolProvider
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ open class S3FileSystem(provider: S3FileSystemProvider) : BaseFileSystem<S3Path>
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
isOpen.compareAndSet(false, true)
|
isOpen.compareAndSet(true, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getRootDirectories(): Iterable<Path> {
|
override fun getRootDirectories(): Iterable<Path> {
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ termora.find-everywhere.quick-command.local-terminal=Local Terminal
|
|||||||
|
|
||||||
# Welcome
|
# Welcome
|
||||||
termora.welcome.my-hosts=My hosts
|
termora.welcome.my-hosts=My hosts
|
||||||
|
termora.welcome.toggle-sidebar=Toggle Sidebar
|
||||||
termora.welcome.contextmenu.connect=Connect
|
termora.welcome.contextmenu.connect=Connect
|
||||||
termora.welcome.contextmenu.connect-with=Connect with
|
termora.welcome.contextmenu.connect-with=Connect with
|
||||||
termora.welcome.contextmenu.open-in-new-window=${termora.tabbed.contextmenu.open-in-new-window}
|
termora.welcome.contextmenu.open-in-new-window=${termora.tabbed.contextmenu.open-in-new-window}
|
||||||
@@ -196,6 +197,7 @@ termora.new-host.proxy=Proxy
|
|||||||
|
|
||||||
termora.new-host.terminal=${termora.settings.terminal}
|
termora.new-host.terminal=${termora.settings.terminal}
|
||||||
termora.new-host.terminal.encoding=Encoding
|
termora.new-host.terminal.encoding=Encoding
|
||||||
|
termora.new-host.terminal.backspace=Backspace
|
||||||
termora.new-host.terminal.heartbeat-interval=Heartbeat Interval
|
termora.new-host.terminal.heartbeat-interval=Heartbeat Interval
|
||||||
termora.new-host.terminal.startup-commands=Startup Command
|
termora.new-host.terminal.startup-commands=Startup Command
|
||||||
termora.new-host.terminal.env=Environment
|
termora.new-host.terminal.env=Environment
|
||||||
@@ -257,6 +259,7 @@ termora.tabbed.contextmenu.rename=Rename
|
|||||||
termora.tabbed.contextmenu.sftp-command=SFTP Command
|
termora.tabbed.contextmenu.sftp-command=SFTP Command
|
||||||
termora.tabbed.contextmenu.sftp-not-install=SFTP programme not found, please install and try again
|
termora.tabbed.contextmenu.sftp-not-install=SFTP programme not found, please install and try again
|
||||||
termora.tabbed.contextmenu.clone=Clone
|
termora.tabbed.contextmenu.clone=Clone
|
||||||
|
termora.tabbed.contextmenu.clone-session=Clone Session
|
||||||
termora.tabbed.contextmenu.open-in-new-window=Open in New Window
|
termora.tabbed.contextmenu.open-in-new-window=Open in New Window
|
||||||
termora.tabbed.contextmenu.close=Close
|
termora.tabbed.contextmenu.close=Close
|
||||||
termora.tabbed.contextmenu.close-other-tabs=Close Other Tabs
|
termora.tabbed.contextmenu.close-other-tabs=Close Other Tabs
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ termora.settings.sftp.preserve-time=保留原始文件修改时间
|
|||||||
|
|
||||||
# Welcome
|
# Welcome
|
||||||
termora.welcome.my-hosts=我的主机
|
termora.welcome.my-hosts=我的主机
|
||||||
|
termora.welcome.toggle-sidebar=显示/隐藏侧边栏
|
||||||
termora.welcome.contextmenu.connect=连接
|
termora.welcome.contextmenu.connect=连接
|
||||||
termora.welcome.contextmenu.connect-with=连接到
|
termora.welcome.contextmenu.connect-with=连接到
|
||||||
termora.welcome.contextmenu.copy=${termora.copy}
|
termora.welcome.contextmenu.copy=${termora.copy}
|
||||||
@@ -187,6 +188,7 @@ termora.new-host.proxy=代理
|
|||||||
|
|
||||||
termora.new-host.terminal=${termora.settings.terminal}
|
termora.new-host.terminal=${termora.settings.terminal}
|
||||||
termora.new-host.terminal.encoding=编码
|
termora.new-host.terminal.encoding=编码
|
||||||
|
termora.new-host.terminal.backspace=退格键
|
||||||
termora.new-host.terminal.heartbeat-interval=心跳间隔
|
termora.new-host.terminal.heartbeat-interval=心跳间隔
|
||||||
termora.new-host.terminal.startup-commands=启动命令
|
termora.new-host.terminal.startup-commands=启动命令
|
||||||
termora.new-host.terminal.env=环境
|
termora.new-host.terminal.env=环境
|
||||||
@@ -252,6 +254,7 @@ termora.tabbed.contextmenu.rename=重命名
|
|||||||
termora.tabbed.contextmenu.sftp-command=SFTP 终端
|
termora.tabbed.contextmenu.sftp-command=SFTP 终端
|
||||||
termora.tabbed.contextmenu.sftp-not-install=没有找到 SFTP 程序,请安装后重试
|
termora.tabbed.contextmenu.sftp-not-install=没有找到 SFTP 程序,请安装后重试
|
||||||
termora.tabbed.contextmenu.clone=克隆
|
termora.tabbed.contextmenu.clone=克隆
|
||||||
|
termora.tabbed.contextmenu.clone-session=克隆会话
|
||||||
termora.tabbed.contextmenu.open-in-new-window=在新窗口打开
|
termora.tabbed.contextmenu.open-in-new-window=在新窗口打开
|
||||||
termora.tabbed.contextmenu.close=关闭
|
termora.tabbed.contextmenu.close=关闭
|
||||||
termora.tabbed.contextmenu.close-other-tabs=关闭其他标签页
|
termora.tabbed.contextmenu.close-other-tabs=关闭其他标签页
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ termora.settings.account.login-failed=登入失敗,請稍後再試
|
|||||||
|
|
||||||
# Welcome
|
# Welcome
|
||||||
termora.welcome.my-hosts=我的主機
|
termora.welcome.my-hosts=我的主機
|
||||||
|
termora.welcome.toggle-sidebar=顯示/隱藏側邊欄
|
||||||
termora.welcome.contextmenu.connect=連接
|
termora.welcome.contextmenu.connect=連接
|
||||||
termora.welcome.contextmenu.connect-with=連接到
|
termora.welcome.contextmenu.connect-with=連接到
|
||||||
termora.welcome.contextmenu.copy=複製
|
termora.welcome.contextmenu.copy=複製
|
||||||
@@ -186,6 +187,7 @@ termora.new-host.proxy=代理
|
|||||||
|
|
||||||
termora.new-host.terminal=${termora.settings.terminal}
|
termora.new-host.terminal=${termora.settings.terminal}
|
||||||
termora.new-host.terminal.encoding=編碼
|
termora.new-host.terminal.encoding=編碼
|
||||||
|
termora.new-host.terminal.backspace=退格鍵
|
||||||
termora.new-host.terminal.startup-commands=啟動命令
|
termora.new-host.terminal.startup-commands=啟動命令
|
||||||
termora.new-host.terminal.heartbeat-interval=心跳間隔
|
termora.new-host.terminal.heartbeat-interval=心跳間隔
|
||||||
termora.new-host.terminal.env=環境
|
termora.new-host.terminal.env=環境
|
||||||
@@ -247,6 +249,7 @@ termora.tabbed.contextmenu.rename=重新命名
|
|||||||
termora.tabbed.contextmenu.sftp-command=SFTP 終端
|
termora.tabbed.contextmenu.sftp-command=SFTP 終端
|
||||||
termora.tabbed.contextmenu.sftp-not-install=沒有找到 SFTP 程序,請安裝後重試
|
termora.tabbed.contextmenu.sftp-not-install=沒有找到 SFTP 程序,請安裝後重試
|
||||||
termora.tabbed.contextmenu.clone=克隆
|
termora.tabbed.contextmenu.clone=克隆
|
||||||
|
termora.tabbed.contextmenu.clone-session=克隆會話
|
||||||
termora.tabbed.contextmenu.open-in-new-window=在新視窗打開
|
termora.tabbed.contextmenu.open-in-new-window=在新視窗打開
|
||||||
termora.tabbed.contextmenu.close=關閉
|
termora.tabbed.contextmenu.close=關閉
|
||||||
termora.tabbed.contextmenu.close-other-tabs=關閉其他標籤頁
|
termora.tabbed.contextmenu.close-other-tabs=關閉其他標籤頁
|
||||||
|
|||||||
4
src/main/resources/icons/breakpoint.svg
Normal file
4
src/main/resources/icons/breakpoint.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14Z" fill="#E55765"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 402 B |
4
src/main/resources/icons/breakpoint_dark.svg
Normal file
4
src/main/resources/icons/breakpoint_dark.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14Z" fill="#DB5C5C"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 402 B |
1
src/main/resources/icons/telnet.svg
Normal file
1
src/main/resources/icons/telnet.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1751694328543" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M889.5623525 160.37019636v579.50117625H134.4376475V160.31372511h755.124705z m-755.124705-56.47058813a56.47058813 56.47058813 0 0 0-56.47058906 56.47058813v579.50117625a56.47058813 56.47058813 0 0 0 56.47058906 56.47058812h755.124705a56.47058813 56.47058813 0 0 0 56.47058906-56.47058812V160.31372511a56.47058813 56.47058813 0 0 0-56.47058906-56.47058813H134.4376475zM733.53411781 915.26901917H290.46588219v-56.47058813h443.06823562v56.47058813z" p-id="1613" fill="#6C707E"></path><path d="M672.65882375 589.60313698v-278.96470593h43.87764656v241.46823562h118.08v37.49647031h-161.95764656zM445.70352969 589.60313698v-278.96470593h164.66823469v37.10117718H489.6376475v77.59058813h102.21176438V462.43137261h-102.21176438v89.67529406h124.85647031v37.49647031H445.70352969zM270.7576475 589.60313698V347.73960823H189.38352969v-37.10117719h207.81176437v37.10117719H315.36941187v241.86352875h-44.66823562z" p-id="1614" fill="#6C707E"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
1
src/main/resources/icons/telnet_dark.svg
Normal file
1
src/main/resources/icons/telnet_dark.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1751694328543" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M889.5623525 160.37019636v579.50117625H134.4376475V160.31372511h755.124705z m-755.124705-56.47058813a56.47058813 56.47058813 0 0 0-56.47058906 56.47058813v579.50117625a56.47058813 56.47058813 0 0 0 56.47058906 56.47058812h755.124705a56.47058813 56.47058813 0 0 0 56.47058906-56.47058812V160.31372511a56.47058813 56.47058813 0 0 0-56.47058906-56.47058813H134.4376475zM733.53411781 915.26901917H290.46588219v-56.47058813h443.06823562v56.47058813z" p-id="1613" fill="#CED0D6"></path><path d="M672.65882375 589.60313698v-278.96470593h43.87764656v241.46823562h118.08v37.49647031h-161.95764656zM445.70352969 589.60313698v-278.96470593h164.66823469v37.10117718H489.6376475v77.59058813h102.21176438V462.43137261h-102.21176438v89.67529406h124.85647031v37.49647031H445.70352969zM270.7576475 589.60313698V347.73960823H189.38352969v-37.10117719h207.81176437v37.10117719H315.36941187v241.86352875h-44.66823562z" p-id="1614" fill="#CED0D6"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.plugin.internal.ssh.SshClients
|
||||||
import org.apache.sshd.client.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
import org.testcontainers.containers.GenericContainer
|
import org.testcontainers.containers.GenericContainer
|
||||||
import kotlin.test.AfterTest
|
import kotlin.test.AfterTest
|
||||||
|
|||||||
Reference in New Issue
Block a user