Compare commits

...

12 Commits

Author SHA1 Message Date
hstyi
1c2315b5e9 fix: Linux unable to open local terminal 2025-08-12 10:22:08 +08:00
hstyi
d48e412580 release: 2.0.0-beta.12 2025-08-12 09:44:27 +08:00
dependabot[bot]
3b3fb41384 chore(deps): bump com.qcloud:cos_api from 5.6.249 to 5.6.251
Bumps [com.qcloud:cos_api](https://github.com/tencentyun/cos-java-sdk-v5) from 5.6.249 to 5.6.251.
- [Release notes](https://github.com/tencentyun/cos-java-sdk-v5/releases)
- [Changelog](https://github.com/tencentyun/cos-java-sdk-v5/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tencentyun/cos-java-sdk-v5/compare/v5.6.249...v5.6.251)

---
updated-dependencies:
- dependency-name: com.qcloud:cos_api
  dependency-version: 5.6.251
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 08:17:38 +08:00
hstyi
190ac697fb chore: turn off the ApplePressAndHoldEnabled 2025-08-09 16:24:43 +08:00
hstyi
8cdbf24cdc fix: file name containing : cannot be transferred 2025-08-09 15:56:32 +08:00
hstyi
6e182b6813 chore: remember the colspan state of the fence layout 2025-08-09 15:33:22 +08:00
hstyi
3fa4064655 feat: hyperlinks require holding down the function key to open 2025-08-09 12:45:06 +08:00
hstyi
a77a03d8b3 fix: transfer causing repositioning after refresh 2025-08-09 12:29:19 +08:00
hstyi
5f8b9d36e2 chore: Agent Forwarding 2025-08-09 11:45:37 +08:00
hstyi
1ed5e164de feat: linux window opacity 2025-08-08 18:12:00 +08:00
hstyi
c67d5b0276 feat: ssh ForwardAgent 2025-08-08 18:11:51 +08:00
hstyi
9646a98f6d feat: background image supports fill mode 2025-08-08 15:06:13 +08:00
23 changed files with 292 additions and 42 deletions

View File

@@ -1 +1 @@
2.0.0-beta.11 2.0.0-beta.12

View File

@@ -233,9 +233,10 @@ tasks.register<Copy>("copy-dependencies") {
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") } exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) { } else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
val osName = if (os.isWindows) "win32" else if (os.isMacOsX) "darwin" else "linux" val osName = if (os.isWindows) "win32" else if (os.isMacOsX) "darwin" else "linux"
val targetDir = FileUtils.getFile(dylib, pty4j.name, osName)
FileUtils.forceMkdir(targetDir)
val myArchName = if (arch.isArm) "aarch64" else "x86-64" val myArchName = if (arch.isArm) "aarch64" else "x86-64"
val targetDir = if (os.isMacOsX) FileUtils.getFile(dylib, pty4j.name, osName)
else FileUtils.getFile(dylib, pty4j.name, osName, myArchName)
FileUtils.forceMkdir(targetDir)
if (os.isWindows) { if (os.isWindows) {
// @formatter:off // @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/*win/${myArchName}/*", "-d", targetDir.absolutePath) } exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/*win/${myArchName}/*", "-d", targetDir.absolutePath) }

View File

@@ -3,7 +3,7 @@ plugins {
} }
project.version = "0.0.5" project.version = "0.0.6"

View File

@@ -18,4 +18,8 @@ object Appearance {
set(value) { set(value) {
enableManager.setFlag("Plugins.bg.interval", value) enableManager.setFlag("Plugins.bg.interval", value)
} }
var fillMode: String
get() = enableManager.getFlag("Plugins.bg.fillMode", FillMode.STRETCH.name)
set(value) = enableManager.setFlag("Plugins.bg.fillMode", value)
} }

View File

@@ -2,6 +2,8 @@ package app.termora.plugins.bg
import app.termora.GlassPaneExtension import app.termora.GlassPaneExtension
import app.termora.WindowScope import app.termora.WindowScope
import app.termora.restore
import app.termora.save
import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLaf
import java.awt.AlphaComposite import java.awt.AlphaComposite
import java.awt.Graphics2D import java.awt.Graphics2D
@@ -12,15 +14,52 @@ class BGGlassPaneExtension private constructor() : GlassPaneExtension {
val instance = BGGlassPaneExtension() val instance = BGGlassPaneExtension()
} }
override fun paint(scope: WindowScope, c: JComponent, g2d: Graphics2D) { override fun paint(scope: WindowScope, c: JComponent, g2d: Graphics2D) {
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
g2d.save()
g2d.composite = AlphaComposite.getInstance( g2d.composite = AlphaComposite.getInstance(
AlphaComposite.SRC_OVER, AlphaComposite.SRC_OVER,
if (FlatLaf.isLafDark()) 0.2f else 0.1f if (FlatLaf.isLafDark()) 0.2f else 0.1f
) )
g2d.drawImage(img, 0, 0, c.width, c.height, null)
g2d.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER) when (Appearance.fillMode) {
FillMode.STRETCH.name -> {
g2d.drawImage(img, 0, 0, c.width, c.height, null)
}
FillMode.CENTER.name -> {
val x = (c.width - img.width) / 2
val y = (c.height - img.height) / 2
g2d.drawImage(img, x, y, null)
}
FillMode.TILE.name -> {
val iw = img.width
val ih = img.height
var y = 0
while (y < c.height) {
var x = 0
while (x < c.width) {
g2d.drawImage(img, x, y, null)
x += iw
}
y += ih
}
}
FillMode.FIT.name -> {
val scale = maxOf(c.width.toDouble() / img.width, c.height.toDouble() / img.height)
val newW = (img.width * scale).toInt()
val newH = (img.height * scale).toInt()
val x = (c.width - newW) / 2
val y = (c.height - newH) / 2
g2d.drawImage(img, x, y, newW, newH, null)
}
}
g2d.restore()
} }
} }

View File

@@ -10,6 +10,8 @@ import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.exception.ExceptionUtils import org.apache.commons.lang3.exception.ExceptionUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Component
import java.awt.event.ItemEvent
import java.io.File import java.io.File
import java.nio.file.StandardCopyOption import java.nio.file.StandardCopyOption
import javax.swing.* import javax.swing.*
@@ -23,6 +25,7 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
private val owner get() = SwingUtilities.getWindowAncestor(this) private val owner get() = SwingUtilities.getWindowAncestor(this)
val backgroundImageTextField = OutlineTextField() val backgroundImageTextField = OutlineTextField()
val fillModeComboBox = OutlineComboBox<FillMode>()
val intervalSpinner = NumberSpinner(360, minimum = 30, maximum = 86400) val intervalSpinner = NumberSpinner(360, minimum = 30, maximum = 86400)
private val backgroundButton = JButton(Icons.folder) private val backgroundButton = JButton(Icons.folder)
@@ -36,6 +39,38 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
private fun initView() { private fun initView() {
fillModeComboBox.addItem(FillMode.STRETCH)
fillModeComboBox.addItem(FillMode.FIT)
fillModeComboBox.addItem(FillMode.CENTER)
fillModeComboBox.addItem(FillMode.TILE)
fillModeComboBox.selectedItem = runCatching { FillMode.valueOf(Appearance.fillMode) }
.getOrNull() ?: FillMode.STRETCH
fillModeComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component? {
var text = value?.toString()
if (value == FillMode.STRETCH) {
text = BGI18n.getString("termora.plugins.bg.fill-mode.stretch")
} else if (value == FillMode.FIT) {
text = BGI18n.getString("termora.plugins.bg.fill-mode.fit")
} else if (value == FillMode.CENTER) {
text = BGI18n.getString("termora.plugins.bg.fill-mode.center")
} else if (value == FillMode.TILE) {
text = BGI18n.getString("termora.plugins.bg.fill-mode.tile")
}
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
}
}
backgroundImageTextField.isEditable = false backgroundImageTextField.isEditable = false
backgroundImageTextField.trailingComponent = backgroundButton backgroundImageTextField.trailingComponent = backgroundButton
backgroundImageTextField.text = Appearance.backgroundImage backgroundImageTextField.text = Appearance.backgroundImage
@@ -80,6 +115,15 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
Appearance.interval = value Appearance.interval = value
} }
} }
fillModeComboBox.addItemListener {
if (it.stateChange == ItemEvent.SELECTED) {
Appearance.fillMode = fillModeComboBox.selectedItem?.toString() ?: FillMode.STRETCH.name
for (frame in TermoraFrameManager.getInstance().getWindows()) {
SwingUtilities.invokeLater { SwingUtilities.updateComponentTreeUI(frame) }
}
}
}
} }
private fun onSelectedBackgroundImage(file: File) { private fun onSelectedBackgroundImage(file: File) {
@@ -124,7 +168,7 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
private fun getFormPanel(): JPanel { private fun getFormPanel(): JPanel {
val layout = FormLayout( val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default", "left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default",
"pref, $FORM_MARGIN, pref" "pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
) )
var rows = 1 var rows = 1
@@ -138,6 +182,10 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
.add(bgClearBox).xy(5, rows) .add(bgClearBox).xy(5, rows)
.apply { rows += step } .apply { rows += step }
builder.add("${BGI18n.getString("termora.plugins.bg.fill-mode")}:").xy(1, rows)
.add(fillModeComboBox).xy(3, rows)
.apply { rows += step }
builder.add("${BGI18n.getString("termora.plugins.bg.interval")}:").xy(1, rows) builder.add("${BGI18n.getString("termora.plugins.bg.interval")}:").xy(1, rows)
.add(intervalSpinner).xy(3, rows) .add(intervalSpinner).xy(3, rows)
.apply { rows += step } .apply { rows += step }

View File

@@ -0,0 +1,8 @@
package app.termora.plugins.bg
enum class FillMode {
STRETCH, // 拉伸
FIT, // 等比例铺满
CENTER, // 居中
TILE, // 平铺
}

View File

@@ -1,2 +1,7 @@
termora.plugins.bg.interval=Interval termora.plugins.bg.interval=Interval
termora.plugins.bg.fill-mode=Fill Mode
termora.plugins.bg.fill-mode.stretch=Stretch
termora.plugins.bg.fill-mode.fit=Fit
termora.plugins.bg.fill-mode.center=Center
termora.plugins.bg.fill-mode.tile=Tile
termora.plugins.bg.background-image=Background Image termora.plugins.bg.background-image=Background Image

View File

@@ -1,2 +1,8 @@
termora.plugins.bg.background-image=背景图 termora.plugins.bg.background-image=背景图
termora.plugins.bg.interval=切换间隔 termora.plugins.bg.interval=切换间隔
termora.plugins.bg.fill-mode=填充模式
termora.plugins.bg.fill-mode.stretch=拉伸
termora.plugins.bg.fill-mode.fit=适合
termora.plugins.bg.fill-mode.center=居中
termora.plugins.bg.fill-mode.tile=平铺

View File

@@ -1,2 +1,8 @@
termora.plugins.bg.background-image=背景圖 termora.plugins.bg.background-image=背景圖
termora.plugins.bg.interval=切換間隔 termora.plugins.bg.interval=切換間隔
termora.plugins.bg.fill-mode=填充模式
termora.plugins.bg.fill-mode.stretch=拉伸
termora.plugins.bg.fill-mode.fit=適合
termora.plugins.bg.fill-mode.center=居中
termora.plugins.bg.fill-mode.tile=平鋪

View File

@@ -8,7 +8,7 @@ project.version = "0.0.4"
dependencies { dependencies {
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
implementation("com.qcloud:cos_api:5.6.249") implementation("com.qcloud:cos_api:5.6.251")
compileOnly(project(":")) compileOnly(project(":"))
} }

View File

@@ -0,0 +1,21 @@
package app.termora
import com.formdev.flatlaf.util.SystemInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
internal class ApplePressAndHoldEnabledApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
companion object {
val instance = ApplePressAndHoldEnabledApplicationRunnerExtension()
}
override fun ready() {
if (SystemInfo.isMacOS.not()) return
swingCoroutineScope.launch(Dispatchers.IO) {
Runtime.getRuntime()
.exec(arrayOf("defaults", "write", "app.termora", "ApplePressAndHoldEnabled", "-bool", "false"))
.waitFor()
}
}
}

View File

@@ -9,6 +9,7 @@ internal class FramePlugin : InternalPlugin() {
init { init {
support.addExtension(DatabasePropertiesChangedExtension::class.java) { KeymapRefresher.getInstance() } support.addExtension(DatabasePropertiesChangedExtension::class.java) { KeymapRefresher.getInstance() }
support.addExtension(DatabaseChangedExtension::class.java) { KeymapRefresher.getInstance() } support.addExtension(DatabaseChangedExtension::class.java) { KeymapRefresher.getInstance() }
support.addExtension(ApplicationRunnerExtension::class.java) { ApplePressAndHoldEnabledApplicationRunnerExtension.instance }
} }
override fun getName(): String { override fun getName(): String {

View File

@@ -21,6 +21,7 @@ import com.jgoodies.forms.layout.FormLayout
import com.jthemedetecor.OsThemeDetector import com.jthemedetecor.OsThemeDetector
import com.sun.jna.LastErrorException import com.sun.jna.LastErrorException
import com.sun.jna.Native import com.sun.jna.Native
import com.sun.jna.platform.WindowUtils
import com.sun.jna.platform.win32.Shell32 import com.sun.jna.platform.win32.Shell32
import com.sun.jna.platform.win32.ShlObj import com.sun.jna.platform.win32.ShlObj
import com.sun.jna.platform.win32.WinDef import com.sun.jna.platform.win32.WinDef
@@ -169,7 +170,8 @@ class SettingsOptionsPane : OptionsPane() {
backgroundComBoBox.isEnabled = SystemInfo.isWindows || SystemInfo.isMacOS backgroundComBoBox.isEnabled = SystemInfo.isWindows || SystemInfo.isMacOS
opacitySpinner.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows opacitySpinner.isEnabled = (SystemInfo.isMacOS || SystemInfo.isWindows)
|| (SystemInfo.isLinux && WindowUtils.isWindowAlphaSupported())
opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) { opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) {
override fun getNextValue(): Any { override fun getNextValue(): Any {
return super.getNextValue() ?: maximum return super.getNextValue() ?: maximum

View File

@@ -72,10 +72,12 @@ class TermoraFencePanel(
leftTreePanel.addComponentListener(object : ComponentAdapter() { leftTreePanel.addComponentListener(object : ComponentAdapter() {
override fun componentHidden(e: ComponentEvent) { override fun componentHidden(e: ComponentEvent) {
toolbar.isVisible = true toolbar.isVisible = true
enableManager.setFlag("Termora.Fence.colspan", true)
} }
override fun componentShown(e: ComponentEvent) { override fun componentShown(e: ComponentEvent) {
toolbar.isVisible = false toolbar.isVisible = false
enableManager.setFlag("Termora.Fence.colspan", false)
} }
}) })
@@ -86,6 +88,16 @@ class TermoraFencePanel(
toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK
), "toggle" ), "toggle"
) )
splitPane.addPropertyChangeListener("dividerLocation") {
if (leftTreePanel.isVisible)
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
}
if (enableManager.getFlag("Termora.Fence.colspan", false)) {
toggle()
}
} }
private inner class LeftTreePanel : JPanel(BorderLayout()), Disposable { private inner class LeftTreePanel : JPanel(BorderLayout()), Disposable {
@@ -144,19 +156,19 @@ class TermoraFencePanel(
} }
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
if (leftTreePanel.isVisible) dividerLocation = splitPane.dividerLocation toggle()
leftTreePanel.isVisible = leftTreePanel.isVisible.not()
if (leftTreePanel.isVisible) splitPane.dividerLocation = dividerLocation
} }
} }
} }
private fun toggle() {
override fun dispose() { if (leftTreePanel.isVisible) dividerLocation = splitPane.dividerLocation
if (leftTreePanel.isVisible) leftTreePanel.isVisible = leftTreePanel.isVisible.not()
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10)) if (leftTreePanel.isVisible) splitPane.dividerLocation = dividerLocation
mySplitPane.doLayout()
} }
fun getHostTree(): NewHostTree { fun getHostTree(): NewHostTree {
return leftTreePanel.hostTree return leftTreePanel.hostTree
} }

View File

@@ -5,6 +5,7 @@ import app.termora.plugin.ExtensionManager
import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import com.sun.jna.Pointer import com.sun.jna.Pointer
import com.sun.jna.platform.WindowUtils
import com.sun.jna.platform.win32.User32 import com.sun.jna.platform.win32.User32
import com.sun.jna.platform.win32.WinDef import com.sun.jna.platform.win32.WinDef
import com.sun.jna.platform.win32.WinUser.* import com.sun.jna.platform.win32.WinUser.*
@@ -206,7 +207,7 @@ class TermoraFrameManager : Disposable {
} }
fun setOpacity(opacity: Double) { fun setOpacity(opacity: Double) {
if (opacity < 0 || opacity > 1 || SystemInfo.isLinux) return if (opacity < 0 || opacity > 1) return
for (window in getWindows()) { for (window in getWindows()) {
setOpacity(window, opacity) setOpacity(window, opacity)
} }
@@ -227,6 +228,8 @@ class TermoraFrameManager : Disposable {
User32.INSTANCE.SetWindowLong(hwnd, GWL_EXSTYLE, exStyle or WS_EX_LAYERED) User32.INSTANCE.SetWindowLong(hwnd, GWL_EXSTYLE, exStyle or WS_EX_LAYERED)
} }
User32.INSTANCE.SetLayeredWindowAttributes(hwnd, 0, alpha, LWA_ALPHA) User32.INSTANCE.SetLayeredWindowAttributes(hwnd, 0, alpha, LWA_ALPHA)
} else if (SystemInfo.isLinux && WindowUtils.isWindowAlphaSupported()) {
WindowUtils.setWindowAlpha(window, opacity.toFloat())
} }
} }

View File

@@ -114,7 +114,8 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
?: AltKeyModifier.EightBit.name), ?: AltKeyModifier.EightBit.name),
"keywordHighlightSetId" to ((terminalOption.highlightSetComboBox.selectedItem as? KeywordHighlight)?.id "keywordHighlightSetId" to ((terminalOption.highlightSetComboBox.selectedItem as? KeywordHighlight)?.id
?: "-1"), ?: "-1"),
"timeout" to (terminalOption.timeoutTextField.value ?: 60).toString() "timeout" to (terminalOption.timeoutTextField.value ?: 60).toString(),
"forwardAgent" to tunnelingOption.forwardAgentCheckBox.isSelected.toString(),
) )
) )
@@ -182,6 +183,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
tunnelingOption.tunnelings.addAll(host.tunnelings) tunnelingOption.tunnelings.addAll(host.tunnelings)
tunnelingOption.x11ForwardingCheckBox.isSelected = host.options.enableX11Forwarding tunnelingOption.x11ForwardingCheckBox.isSelected = host.options.enableX11Forwarding
tunnelingOption.x11ServerTextField.text = StringUtils.defaultIfBlank(host.options.x11Forwarding, "localhost:0") tunnelingOption.x11ServerTextField.text = StringUtils.defaultIfBlank(host.options.x11Forwarding, "localhost:0")
tunnelingOption.forwardAgentCheckBox.isSelected = host.options.extras["forwardAgent"]?.toBoolean() ?: false
if (host.options.jumpHosts.isNotEmpty()) { if (host.options.jumpHosts.isNotEmpty()) {
val hosts = HostManager.getInstance().hosts().associateBy { it.id } val hosts = HostManager.getInstance().hosts().associateBy { it.id }
@@ -570,9 +572,10 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
} }
} }
protected inner class TunnelingOption : JPanel(BorderLayout()), Option { private inner class TunnelingOption : JPanel(BorderLayout()), Option {
val tunnelings = mutableListOf<Tunneling>() val tunnelings = mutableListOf<Tunneling>()
val x11ForwardingCheckBox = JCheckBox("X DISPLAY:") val x11ForwardingCheckBox = JCheckBox("X DISPLAY:")
val forwardAgentCheckBox = JCheckBox("Enable ForwardAgent")
val x11ServerTextField = OutlineTextField(255) val x11ServerTextField = OutlineTextField(255)
private val model = object : DefaultTableModel() { private val model = object : DefaultTableModel() {
@@ -649,6 +652,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
box.add(deleteBtn) box.add(deleteBtn)
x11ForwardingCheckBox.isFocusable = false x11ForwardingCheckBox.isFocusable = false
forwardAgentCheckBox.isFocusable = false
if (x11ServerTextField.text.isBlank()) { if (x11ServerTextField.text.isBlank()) {
x11ServerTextField.text = "localhost:0" x11ServerTextField.text = "localhost:0"
@@ -662,6 +666,13 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
x11Forwarding.add(x11ForwardingCheckBox) x11Forwarding.add(x11ForwardingCheckBox)
x11Forwarding.add(x11ServerTextField) x11Forwarding.add(x11ServerTextField)
val forwardAgent = Box.createHorizontalBox()
forwardAgent.border = BorderFactory.createCompoundBorder(
BorderFactory.createTitledBorder("Agent Forwarding"),
BorderFactory.createEmptyBorder(4, 4, 4, 4)
)
forwardAgent.add(forwardAgentCheckBox)
x11ServerTextField.isEnabled = x11ForwardingCheckBox.isSelected x11ServerTextField.isEnabled = x11ForwardingCheckBox.isSelected
val panel = JPanel(BorderLayout()) val panel = JPanel(BorderLayout())
@@ -670,8 +681,13 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
panel.add(box, BorderLayout.SOUTH) panel.add(box, BorderLayout.SOUTH)
panel.border = BorderFactory.createEmptyBorder(0, 0, 8, 0) panel.border = BorderFactory.createEmptyBorder(0, 0, 8, 0)
val forwardingBox = Box.createHorizontalBox()
forwardingBox.add(x11Forwarding)
forwardingBox.add(Box.createHorizontalStrut(4))
forwardingBox.add(forwardAgent)
add(panel, BorderLayout.CENTER) add(panel, BorderLayout.CENTER)
add(x11Forwarding, BorderLayout.SOUTH) add(forwardingBox, BorderLayout.SOUTH)
} }

View File

@@ -0,0 +1,14 @@
package app.termora.plugin.internal.ssh
import org.apache.sshd.agent.local.ChannelAgentForwardingFactory
import org.apache.sshd.common.FactoryManager
import org.apache.sshd.common.channel.ChannelFactory
import org.eclipse.jgit.internal.transport.sshd.agent.JGitSshAgentFactory
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory
import java.io.File
internal class SshAgentFactory(factory: ConnectorFactory, homeDir: File?) : JGitSshAgentFactory(factory, homeDir) {
override fun getChannelForwardingFactories(manager: FactoryManager?): List<ChannelFactory> {
return listOf(ChannelAgentForwardingFactory.OPENSSH, ChannelAgentForwardingFactory.IETF)
}
}

View File

@@ -56,7 +56,6 @@ import org.apache.sshd.server.forward.AcceptAllForwardingFilter
import org.apache.sshd.server.forward.RejectAllForwardingFilter import org.apache.sshd.server.forward.RejectAllForwardingFilter
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession import org.eclipse.jgit.internal.transport.sshd.JGitClientSession
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient import org.eclipse.jgit.internal.transport.sshd.JGitSshClient
import org.eclipse.jgit.internal.transport.sshd.agent.JGitSshAgentFactory
import org.eclipse.jgit.internal.transport.sshd.agent.connector.PageantConnector import org.eclipse.jgit.internal.transport.sshd.agent.connector.PageantConnector
import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocketConnector import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocketConnector
import org.eclipse.jgit.internal.transport.sshd.proxy.AbstractClientProxyConnector import org.eclipse.jgit.internal.transport.sshd.proxy.AbstractClientProxyConnector
@@ -112,6 +111,8 @@ object SshClients {
env.putAll(host.options.envs()) env.putAll(host.options.envs())
val channel = session.createShellChannel(configuration, env) val channel = session.createShellChannel(configuration, env)
channel.isAgentForwarding = host.options.extras["forwardAgent"]?.toBoolean() == true
if (host.options.enableX11Forwarding) { if (host.options.enableX11Forwarding) {
if (channel is app.termora.x11.ChannelShell) { if (channel is app.termora.x11.ChannelShell) {
channel.xForwarding = true channel.xForwarding = true
@@ -386,7 +387,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.Companion.INSTANCE) channelFactories.add(X11ChannelFactory.INSTANCE)
builder.channelFactories(channelFactories) builder.channelFactories(channelFactories)
val sshClient = builder.build() as JGitSshClient val sshClient = builder.build() as JGitSshClient
@@ -395,12 +396,14 @@ object SshClients {
// JGit 会尝试读取本地的私钥或缓存的私钥 // JGit 会尝试读取本地的私钥或缓存的私钥
sshClient.keyIdentityProvider = KeyIdentityProvider { mutableListOf() } sshClient.keyIdentityProvider = KeyIdentityProvider { mutableListOf() }
// https://github.com/TermoraDev/termora/issues/1001
if (host.authentication.type == AuthenticationType.SSHAgent || host.options.extras["forwardAgent"]?.toBoolean() == true) {
// ssh-agent
sshClient.agentFactory = SshAgentFactory(ConnectorFactory.getDefault(), null)
}
// 设置优先级 // 设置优先级
if (host.authentication.type == AuthenticationType.PublicKey || host.authentication.type == AuthenticationType.SSHAgent) { if (host.authentication.type == AuthenticationType.PublicKey || host.authentication.type == AuthenticationType.SSHAgent) {
if (host.authentication.type == AuthenticationType.SSHAgent) {
// ssh-agent
sshClient.agentFactory = JGitSshAgentFactory(ConnectorFactory.getDefault(), null)
}
CoreModuleProperties.PREFERRED_AUTHS.set( CoreModuleProperties.PREFERRED_AUTHS.set(
sshClient, sshClient,
listOf( listOf(

View File

@@ -185,8 +185,9 @@ class TerminalPanel(val tab: TerminalTab?, val terminal: Terminal, private val w
this.addMouseMotionListener(mouseAdapter) this.addMouseMotionListener(mouseAdapter)
// 超链接 // 超链接
val hyperlinkAdapter = TerminalPanelMouseHyperlinkAdapter(this, terminal) val hyperlinkAdapter = TerminalPanelMouseHyperlinkAdapter(this, terminalDisplay, terminal)
this.addMouseListener(hyperlinkAdapter) this.addMouseListener(hyperlinkAdapter)
this.addMouseMotionListener(hyperlinkAdapter)
// 鼠标跟踪 // 鼠标跟踪
val trackingAdapter = TerminalPanelMouseTrackingAdapter(this, terminal, writer) val trackingAdapter = TerminalPanelMouseTrackingAdapter(this, terminal, writer)

View File

@@ -2,6 +2,8 @@ package app.termora.terminal.panel
import app.termora.terminal.ClickableHighlighter import app.termora.terminal.ClickableHighlighter
import app.termora.terminal.Terminal import app.termora.terminal.Terminal
import com.formdev.flatlaf.util.SystemInfo
import java.awt.Cursor
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
@@ -11,19 +13,42 @@ import javax.swing.SwingUtilities
*/ */
class TerminalPanelMouseHyperlinkAdapter( class TerminalPanelMouseHyperlinkAdapter(
private val terminalPanel: TerminalPanel, private val terminalPanel: TerminalPanel,
private val terminalDisplay: TerminalDisplay,
private val terminal: Terminal, private val terminal: Terminal,
) : MouseAdapter() { ) : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { override fun mouseClicked(e: MouseEvent) {
if (SwingUtilities.isLeftMouseButton(e)) { if (SwingUtilities.isLeftMouseButton(e).not()) {
val position = terminalPanel.pointToPosition(e.point) return
for (highlighter in terminal.getMarkupModel().getHighlighters(position)) { }
if (highlighter is ClickableHighlighter) {
highlighter.onClicked(position) if (SystemInfo.isMacOS) {
} if (e.isMetaDown.not())
return
} else if (e.isControlDown.not()) {
return
}
val position = terminalPanel.pointToPosition(e.point)
for (highlighter in terminal.getMarkupModel().getHighlighters(position)) {
if (highlighter is ClickableHighlighter) {
highlighter.onClicked(position)
} }
} }
} }
override fun mouseMoved(e: MouseEvent) {
val position = terminalPanel.pointToPosition(e.point)
var cursor = Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)
for (highlighter in terminal.getMarkupModel().getHighlighters(position)) {
if (highlighter is ClickableHighlighter) {
cursor = if (SystemInfo.isMacOS) Cursor.getDefaultCursor()
else Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
break
}
}
terminalDisplay.cursor = cursor
}
} }

View File

@@ -2,6 +2,7 @@ package app.termora.transfer
import app.termora.* import app.termora.*
import app.termora.transfer.InternalTransferManager.TransferMode import app.termora.transfer.InternalTransferManager.TransferMode
import app.termora.transfer.TransportPanel.Companion.isWindowsFileSystem
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.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -95,8 +96,12 @@ internal class DefaultInternalTransferManager(
val context = AskTransferContext(TransferAction.Overwrite, false) val context = AskTransferContext(TransferAction.Overwrite, false)
for (pair in paths) { for (pair in paths) {
if (mode == TransferMode.Transfer && context.applyAll.not()) { if (mode == TransferMode.Transfer && context.applyAll.not()) {
var name = pair.first.name
if (targetWorkdir.fileSystem.isWindowsFileSystem()) {
name = name.replace(":", "-")
}
val action = withContext(Dispatchers.Swing) { val action = withContext(Dispatchers.Swing) {
getTransferAction(context, targetWorkdir.resolve(pair.first.name), pair.second) getTransferAction(context, targetWorkdir.resolve(name), pair.second)
} }
if (action == null) { if (action == null) {
break break
@@ -272,8 +277,11 @@ internal class DefaultInternalTransferManager(
val isDirectory = pair.second.isDirectory val isDirectory = pair.second.isDirectory
val path = pair.first val path = pair.first
if (isDirectory.not() || mode == TransferMode.Rmrf) { if (isDirectory.not() || mode == TransferMode.Rmrf) {
val transfer = var name = path.name
createTransfer(path, workdir.resolve(path.name), isDirectory, StringUtils.EMPTY, mode, action) if (workdir.fileSystem.isWindowsFileSystem()) {
name = name.replace(":", "-")
}
val transfer = createTransfer(path, workdir.resolve(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
} }

View File

@@ -106,6 +106,7 @@ internal class TransportPanel(
private val loadingPanel = LoadingPanel() private val loadingPanel = LoadingPanel()
private val model = TransportTableModel() private val model = TransportTableModel()
private val table = JTable(model) private val table = JTable(model)
private val tableScrollPane = JScrollPane(table)
private val sorter = TableRowSorter(table.model) private val sorter = TableRowSorter(table.model)
private var hasParent = false private var hasParent = false
private val panel get() = this private val panel get() = this
@@ -211,7 +212,7 @@ internal class TransportPanel(
table.setDefaultRenderer(Any::class.java, MyDefaultTableCellRenderer()) table.setDefaultRenderer(Any::class.java, MyDefaultTableCellRenderer())
val scrollPane = JScrollPane(table) val scrollPane = tableScrollPane
scrollPane.apply { border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor) } scrollPane.apply { border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor) }
layeredPane.add(scrollPane, JLayeredPane.DEFAULT_LAYER as Any) layeredPane.add(scrollPane, JLayeredPane.DEFAULT_LAYER as Any)
@@ -241,7 +242,11 @@ internal class TransportPanel(
Disposer.register(this, editTransferListener) Disposer.register(this, editTransferListener)
refreshBtn.addActionListener { reload(requestFocus = true) } refreshBtn.addActionListener {
val filename = getSelectFilename()
if (filename != null) registerSelectRow(filename)
reload(requestFocus = true)
}
prevBtn.addActionListener { navigator.back() } prevBtn.addActionListener { navigator.back() }
nextBtn.addActionListener { navigator.forward() } nextBtn.addActionListener { navigator.forward() }
@@ -303,11 +308,16 @@ internal class TransportPanel(
if (target.fileSystem != loader.getSyncTransportSupport().getFileSystem()) return if (target.fileSystem != loader.getSyncTransportSupport().getFileSystem()) return
} }
if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) { if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) {
if (loading) { val c = {
registerNextReloadCallback { reload(requestFocus = false) } val filename = getSelectFilename()
} else { if (filename != null) registerSelectRow(filename)
reload(requestFocus = false) reload(requestFocus = false)
} }
if (loading) {
registerNextReloadCallback { c.invoke() }
} else {
c.invoke()
}
} }
} }
}).let { Disposer.register(this, it) } }).let { Disposer.register(this, it) }
@@ -658,6 +668,9 @@ internal class TransportPanel(
} }
fun registerSelectRow(name: String) { fun registerSelectRow(name: String) {
val verticalValue = tableScrollPane.verticalScrollBar.value
val horizontalValue = tableScrollPane.horizontalScrollBar.value
registerNextReloadCallback { registerNextReloadCallback {
for (i in 0 until model.rowCount) { for (i in 0 until model.rowCount) {
if (model.getAttributes(i).name == name) { if (model.getAttributes(i).name == name) {
@@ -665,12 +678,22 @@ internal class TransportPanel(
table.clearSelection() table.clearSelection()
table.setRowSelectionInterval(c, c) table.setRowSelectionInterval(c, c)
table.scrollRectToVisible(table.getCellRect(c, TransportTableModel.COLUMN_NAME, true)) table.scrollRectToVisible(table.getCellRect(c, TransportTableModel.COLUMN_NAME, true))
tableScrollPane.verticalScrollBar.value = verticalValue
tableScrollPane.horizontalScrollBar.value = horizontalValue
break break
} }
} }
} }
} }
fun getSelectFilename(): String? {
val row = table.selectedRow
if (row < 0) return null
val c = sorter.convertRowIndexToModel(row)
if (c < 0) return null
return model.getAttributes(c).name
}
private fun registerNextReloadCallback(block: () -> Unit) { private fun registerNextReloadCallback(block: () -> Unit) {
nextReloadCallbacks.computeIfAbsent(mod.get()) { mutableListOf() } nextReloadCallbacks.computeIfAbsent(mod.get()) { mutableListOf() }
.add(block) .add(block)
@@ -1083,6 +1106,8 @@ internal class TransportPanel(
} else if (actionCommand == TransportPopupMenu.ActionCommand.Delete) { } else if (actionCommand == TransportPopupMenu.ActionCommand.Delete) {
transfer(InternalTransferManager.TransferMode.Delete) transfer(InternalTransferManager.TransferMode.Delete)
} else if (actionCommand == TransportPopupMenu.ActionCommand.Refresh) { } else if (actionCommand == TransportPopupMenu.ActionCommand.Refresh) {
val filename = getSelectFilename()
if (filename != null) registerSelectRow(filename)
reload(requestFocus = true) reload(requestFocus = true)
} else if (actionCommand == TransportPopupMenu.ActionCommand.Edit) { } else if (actionCommand == TransportPopupMenu.ActionCommand.Edit) {
edit() edit()
@@ -1139,7 +1164,9 @@ internal class TransportPanel(
private fun edit() { private fun edit() {
for (path in files.map { it.first }) { for (path in files.map { it.first }) {
val target = Application.createSubTemporaryDir().resolve(path.name) var name = path.name
if (SystemInfo.isWindows) name = name.replace(":", "-")
val target = Application.createSubTemporaryDir().resolve(name)
val transferId = internalTransferManager.addHighTransfer(path, target) val transferId = internalTransferManager.addHighTransfer(path, target)
editTransferListener.addListenTransfer(transferId) editTransferListener.addListenTransfer(transferId)
} }