Compare commits

...

12 Commits

Author SHA1 Message Date
hstyi
bd9b73ad6a release: 2.0.0-beta.13 2025-08-17 11:46:43 +08:00
hstyi
dbea769994 fix: sftp command may cause bad key types 2025-08-17 08:54:42 +08:00
hstyi
9cd83c4025 feat: host tree supports copy and paste 2025-08-15 16:35:43 +08:00
hstyi
d4cc080e7b chore: transfer text 2025-08-15 16:35:32 +08:00
hstyi
a324bc3d96 fix: keyword highlighting causes crash 2025-08-15 13:12:55 +08:00
dependabot[bot]
36929e9ea3 chore(deps): bump kotlin from 2.2.0 to 2.2.10
Bumps `kotlin` from 2.2.0 to 2.2.10.

Updates `org.jetbrains.kotlin.jvm` from 2.2.0 to 2.2.10
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.2.0...v2.2.10)

Updates `org.jetbrains.kotlin.plugin.serialization` from 2.2.0 to 2.2.10
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.2.0...v2.2.10)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlin.jvm
  dependency-version: 2.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.kotlin.plugin.serialization
  dependency-version: 2.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-15 11:10:11 +08:00
hstyi
dd73b933d9 fix: some data cannot be pulled 2025-08-14 17:52:33 +08:00
hstyi
117a9ea692 fix: icons not displaying on some Linux systems 2025-08-13 09:36:59 +08:00
dependabot[bot]
2f932de295 chore(deps): bump com.huaweicloud:esdk-obs-java-bundle
Bumps [com.huaweicloud:esdk-obs-java-bundle](https://github.com/huaweicloud/huaweicloud-sdk-java-obs) from 3.25.5 to 3.25.7.
- [Release notes](https://github.com/huaweicloud/huaweicloud-sdk-java-obs/releases)
- [Commits](https://github.com/huaweicloud/huaweicloud-sdk-java-obs/compare/v3.25.5...v3.25.7)

---
updated-dependencies:
- dependency-name: com.huaweicloud:esdk-obs-java-bundle
  dependency-version: 3.25.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-13 09:36:18 +08:00
hstyi
679b24a74d chore: simplify host field input 2025-08-12 18:39:54 +08:00
hstyi
c6b33ea828 chore: improve settings action 2025-08-12 18:39:45 +08:00
dependabot[bot]
a4ea8f2491 chore(deps): bump com.github.hstyi:geolite2
Bumps [com.github.hstyi:geolite2](https://github.com/hstyi/GeoLite2) from v1.0-202508040102 to v1.0-202508110059.
- [Release notes](https://github.com/hstyi/GeoLite2/releases)
- [Commits](https://github.com/hstyi/GeoLite2/commits)

---
updated-dependencies:
- dependency-name: com.github.hstyi:geolite2
  dependency-version: v1.0-202508110059
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 18:39:37 +08:00
14 changed files with 180 additions and 23 deletions

View File

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

View File

@@ -384,6 +384,7 @@ tasks.register<Exec>("jpackage") {
} }
if (os.isLinux) { if (os.isLinux) {
options.add("--add-opens=java.desktop/sun.awt.X11=ALL-UNNAMED")
if (isDeb) { if (isDeb) {
options.add("-Djpackage.app-layout=deb") options.add("-Djpackage.app-layout=deb")
} }
@@ -681,17 +682,24 @@ fun packOnLinux(distributionDir: Directory, finalFilenameWithoutExtension: Strin
exec { commandLine("chmod", "+x", appimagetool.absolutePath) } exec { commandLine("chmod", "+x", appimagetool.absolutePath) }
} }
// Desktop file // Desktop file
val termoraName = project.name.uppercaseFirstChar() val termoraName = project.name.uppercaseFirstChar()
// copy icon
FileUtils.copyFile(
File("${projectDir.absolutePath}/src/main/resources/icons/termora_256x256.png"),
distributionDir.file(termoraName + File.separator + termoraName + ".png").asFile
)
val desktopFile = distributionDir.file(termoraName + File.separator + termoraName + ".desktop").asFile val desktopFile = distributionDir.file(termoraName + File.separator + termoraName + ".desktop").asFile
desktopFile.writeText( desktopFile.writeText(
"""[Desktop Entry] """[Desktop Entry]
Type=Application Type=Application
Name=${termoraName} Name=${termoraName}
Comment=Terminal emulator and SSH client Comment=Terminal emulator and SSH client
Icon=/lib/${termoraName} Icon=${termoraName}
Categories=Development; Categories=Development;
StartupWMClass=${termoraName}
Terminal=false Terminal=false
""".trimIndent() """.trimIndent()
) )

View File

@@ -1,5 +1,5 @@
[versions] [versions]
kotlin = "2.2.0" kotlin = "2.2.10"
slf4j = "2.0.17" slf4j = "2.0.17"
pty4j = "0.13.10" pty4j = "0.13.10"
tinylog = "2.7.0" tinylog = "2.7.0"

View File

@@ -9,7 +9,7 @@ dependencies {
compileOnly(project(":")) compileOnly(project(":"))
implementation("com.maxmind.geoip2:geoip2:4.3.1") implementation("com.maxmind.geoip2:geoip2:4.3.1")
// https://github.com/hstyi/geolite2 // https://github.com/hstyi/geolite2
implementation("com.github.hstyi:geolite2:v1.0-202508040102") implementation("com.github.hstyi:geolite2:v1.0-202508110059")
} }
apply(from = "$rootDir/plugins/common.gradle.kts") apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -8,7 +8,7 @@ project.version = "0.0.2"
dependencies { dependencies {
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.5") implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.7")
compileOnly(project(":")) compileOnly(project(":"))
} }

View File

@@ -9,6 +9,7 @@ import org.apache.commons.lang3.SystemUtils
import org.apache.commons.lang3.math.NumberUtils import org.apache.commons.lang3.math.NumberUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.tinylog.configuration.Configuration import org.tinylog.configuration.Configuration
import java.awt.Toolkit
import java.io.File import java.io.File
import kotlin.system.exitProcess import kotlin.system.exitProcess
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
@@ -35,10 +36,20 @@ class ApplicationInitializr {
// 检查是否单例 // 检查是否单例
checkSingleton() checkSingleton()
if (SystemUtils.IS_OS_MAC_OSX) { if (SystemInfo.isMacOS) {
System.setProperty("apple.awt.application.name", Application.getName()) System.setProperty("apple.awt.application.name", Application.getName())
} }
if (SystemInfo.isLinux) {
// https://stackoverflow.com/questions/10593075
runCatching {
val toolkit = Toolkit.getDefaultToolkit()
val awtAppClassNameField = toolkit.javaClass.getDeclaredField("awtAppClassName")
awtAppClassNameField.setAccessible(true)
awtAppClassNameField.set(toolkit, Application.getName())
}
}
// 启动 // 启动
val runtime = measureTimeMillis { ApplicationRunner().run() } val runtime = measureTimeMillis { ApplicationRunner().run() }
val log = LoggerFactory.getLogger(javaClass) val log = LoggerFactory.getLogger(javaClass)

View File

@@ -126,7 +126,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
while (true) { while (true) {
val request = Request.Builder() val request = Request.Builder()
.get() .get()
.url("${accountManager.getServer()}/v1/data/changes?since=${since}&after=${after}&limit=${limit}") .url("${accountManager.getServer()}/v1/data/changes?since=${nextSince}&after=${after}&limit=${limit}")
.build() .build()
val text = AccountHttp.execute(request = request) val text = AccountHttp.execute(request = request)
val response = ohMyJson.decodeFromString<DataChangesResponse>(text) val response = ohMyJson.decodeFromString<DataChangesResponse>(text)

View File

@@ -1,9 +1,6 @@
package app.termora.actions package app.termora.actions
import app.termora.ApplicationScope import app.termora.*
import app.termora.I18n
import app.termora.Icons
import app.termora.SettingsDialog
import com.formdev.flatlaf.extras.FlatDesktop import com.formdev.flatlaf.extras.FlatDesktop
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import java.awt.KeyboardFocusManager import java.awt.KeyboardFocusManager
@@ -32,13 +29,13 @@ class SettingsAction private constructor() : AnAction(
private val action get() = this private val action get() = this
init { init {
FlatDesktop.setPreferencesHandler { FlatDesktop.setPreferencesHandler(object : Runnable {
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner override fun run() {
// Doorman 的情况下不允许打开 val focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow ?: return
if (owner != null && ApplicationScope.windowScopes().isNotEmpty()) { if (focusedWindow !is TermoraFrame) return
actionPerformed(ActionEvent(owner, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY)) actionPerformed(ActionEvent(focusedWindow, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
} }
} })
} }
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {

View File

@@ -101,6 +101,16 @@ internal class KeywordHighlightPaintListener private constructor() : TerminalPai
// -1 表示不使用高亮集 // -1 表示不使用高亮集
if (keywordHighlightSetId == "-1") return if (keywordHighlightSetId == "-1") return
try {
doFind(offset, count, terminal, keywordHighlightSetId)
} catch (e: Exception) {
if (log.isDebugEnabled) {
log.debug(e.message, e)
}
}
}
private fun doFind(offset: Int, count: Int, terminal: Terminal, keywordHighlightSetId: String) {
for (highlight in keywordHighlights) { for (highlight in keywordHighlights) {
if (highlight.enabled.not()) continue if (highlight.enabled.not()) continue
if (highlight.type != KeywordHighlightType.Highlight) continue if (highlight.type != KeywordHighlightType.Highlight) continue
@@ -151,7 +161,6 @@ internal class KeywordHighlightPaintListener private constructor() : TerminalPai
} }
} }
} }
override fun after( override fun after(

View File

@@ -103,9 +103,20 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
commands.add("Compression=yes") commands.add("Compression=yes")
// HostKeyAlgorithms 让 SFTP 命令的顺序和 sshd 的一致 这样可以避免 known_hosts 文件不一致问题 // HostKeyAlgorithms 让 SFTP 命令的顺序和 sshd 的一致 这样可以避免 known_hosts 文件不一致问题
val hostKeyAlgorithms = ClientBuilder.setUpDefaultSignatureFactories(true).joinToString(",") { it.name } val hostKeyAlgorithms = ClientBuilder.setUpDefaultSignatureFactories(true).map { it.name }.toMutableList()
val localHostKeyAlgorithms = getLocalSSHHostKeyAlgorithms()
// 删除本地 ssh 不存在的算法
hostKeyAlgorithms.removeIf { localHostKeyAlgorithms.contains(it).not() }
// 把本地支持的再添加进去
for (algorithm in localHostKeyAlgorithms) {
if (hostKeyAlgorithms.contains(algorithm).not()) {
hostKeyAlgorithms.add(algorithm)
}
}
commands.add("-o") commands.add("-o")
commands.add("HostKeyAlgorithms=${hostKeyAlgorithms}") commands.add("HostKeyAlgorithms=${hostKeyAlgorithms.joinToString(",")}")
// 不使用配置文件 // 不使用配置文件
commands.add("-F") commands.add("-F")
@@ -143,6 +154,15 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
return ptyConnector return ptyConnector
} }
private fun getLocalSSHHostKeyAlgorithms(): Set<String> {
val pb = ProcessBuilder("ssh", "-Q", "key")
val process = pb.start()
if (process.waitFor() != 0) {
return emptySet()
}
return String(process.inputStream.readAllBytes()).lines().filter { it.isNotBlank() }.toSet()
}
private fun setAuthentication(commands: MutableList<String>, host: Host) { private fun setAuthentication(commands: MutableList<String>, host: Host) {
// 如果通过公钥连接 // 如果通过公钥连接
if (host.authentication.type == AuthenticationType.PublicKey) { if (host.authentication.type == AuthenticationType.PublicKey) {

View File

@@ -24,6 +24,7 @@ import org.eclipse.jgit.internal.transport.sshd.agent.connector.WinPipeConnector
import java.awt.* import java.awt.*
import java.awt.event.* import java.awt.event.*
import javax.swing.* import javax.swing.*
import javax.swing.event.DocumentEvent
import javax.swing.table.DefaultTableCellRenderer import javax.swing.table.DefaultTableCellRenderer
import javax.swing.table.DefaultTableModel import javax.swing.table.DefaultTableModel
@@ -46,6 +47,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
private val jumpHostsOption = JumpHostsOption() private val jumpHostsOption = JumpHostsOption()
private val sftpOption = SFTPOption() private val sftpOption = SFTPOption()
private val owner: Window get() = SwingUtilities.getWindowAncestor(this) private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
private var setHostMode = false
init { init {
addOption(generalOption) addOption(generalOption)
@@ -135,6 +137,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
} }
fun setHost(host: Host) { fun setHost(host: Host) {
setHostMode = true
generalOption.portTextField.value = host.port generalOption.portTextField.value = host.port
generalOption.nameTextField.text = host.name generalOption.nameTextField.text = host.name
generalOption.usernameTextField.text = host.username generalOption.usernameTextField.text = host.username
@@ -298,6 +301,8 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
val remarkTextArea = FixedLengthTextArea(512) val remarkTextArea = FixedLengthTextArea(512)
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>() val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
private var hostFocused = false
init { init {
initView() initView()
initEvents() initEvents()
@@ -405,6 +410,26 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
removeComponentListener(this) removeComponentListener(this)
} }
}) })
hostTextField.addFocusListener(object : FocusAdapter() {
override fun focusGained(e: FocusEvent) {
hostTextField.removeFocusListener(this)
hostFocused = true
}
})
nameTextField.document.addDocumentListener(object : DocumentAdaptor() {
override fun changedUpdate(e: DocumentEvent) {
if (nameTextField.hasFocus().not()) return
if (hostFocused || setHostMode) {
nameTextField.document.removeDocumentListener(this)
return
}
hostTextField.text = nameTextField.text
}
})
} }
private fun chooseKeyPair() { private fun chooseKeyPair() {

View File

@@ -3,6 +3,8 @@ package app.termora.tree
import app.termora.* import app.termora.*
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.account.AccountManager import app.termora.account.AccountManager
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.actions.OpenHostAction import app.termora.actions.OpenHostAction
import app.termora.database.DatabaseChangedExtension import app.termora.database.DatabaseChangedExtension
import app.termora.database.DatabaseManager import app.termora.database.DatabaseManager
@@ -32,6 +34,10 @@ import org.jdesktop.swingx.action.ActionManager
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.NodeList import org.w3c.dom.NodeList
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
import java.awt.datatransfer.Transferable
import java.awt.datatransfer.UnsupportedFlavorException
import java.awt.event.* import java.awt.event.*
import java.io.* import java.io.*
import java.util.* import java.util.*
@@ -140,6 +146,41 @@ class NewHostTree : SimpleTree(), Disposable {
} }
}) })
actionMap.put("copy", object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
toolkit.systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
val nodes = getSelectionSimpleTreeNodes(false).toMutableList()
nodes.removeIf { e -> e.getParents().any { nodes.contains(it) } }
if (nodes.isEmpty() || nodes.any { it is TeamTreeNode }) return
if (nodes.any { it.id == "0" || it.id.isBlank() }) return
toolkit.systemClipboard.setContents(NodesTransferable(nodes), null)
}
})
actionMap.put("paste", object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
val lastNode = getLastSelectedPathNode() ?: return
val folder = if (lastNode.isFolder) lastNode.parent ?: simpleTreeModel.root
else lastNode.parent ?: return
if (toolkit.systemClipboard.isDataFlavorAvailable(NodesTransferable.FLAVOR).not()) return
val nodes = (toolkit.systemClipboard.getData(NodesTransferable.FLAVOR) as? List<*>)
?.filterIsInstance<HostTreeNode>() ?: return
for (node in nodes) {
val newNode = copyNode(node, folder.id)
// 复制的是文件夹,就在最后面
if (newNode.isFolder) {
simpleTreeModel.insertNodeInto(newNode, folder, folder.folderCount)
} else if (lastNode.isFolder) { // 用户选的节点是文件夹那就在最后一个child下面
simpleTreeModel.insertNodeInto(newNode, folder, folder.childCount)
} else { // 用户选的是主机并且复制的是主机
simpleTreeModel.insertNodeInto(newNode, folder, folder.getIndex(lastNode) + 1)
}
}
}
})
} }
fun restoreExpansions() { fun restoreExpansions() {
@@ -198,10 +239,12 @@ class NewHostTree : SimpleTree(), Disposable {
val sshMenu = importMenu.add(".ssh/config") val sshMenu = importMenu.add(".ssh/config")
val mobaXtermMenu = importMenu.add("MobaXterm") val mobaXtermMenu = importMenu.add("MobaXterm")
// 为了避免误导,如果是 SSH 右键时显示 SFTP
val sftpText = if (SSHProtocolProvider.PROTOCOL.equals(lastHost.protocol, true))
"SFTP" else I18n.getString("termora.transport.sftp")
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect")) val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect"))
val openWith = popupMenu.add(JMenu(I18n.getString("termora.welcome.contextmenu.connect-with"))) as JMenu val openWith = popupMenu.add(JMenu(I18n.getString("termora.welcome.contextmenu.connect-with"))) as JMenu
val openWithSFTP = openWith.add(I18n.getString("termora.transport.sftp")) val openWithSFTP = openWith.add(sftpText)
val openWithSFTPCommand = openWith.add(I18n.getString("termora.tabbed.contextmenu.sftp-command")) val openWithSFTPCommand = openWith.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
val openInNewWindow = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open-in-new-window")) val openInNewWindow = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open-in-new-window"))
popupMenu.addSeparator() popupMenu.addSeparator()
@@ -389,6 +432,20 @@ class NewHostTree : SimpleTree(), Disposable {
}) })
val mnemonics = mapOf(
refresh to KeyEvent.VK_R,
newMenu to KeyEvent.VK_W,
newFolder to KeyEvent.VK_F,
rename to KeyEvent.VK_M,
remove to KeyEvent.VK_D,
property to KeyEvent.VK_I,
)
for ((item, mnemonic) in mnemonics) {
item.text = "${item.text}(${KeyEvent.getKeyText(mnemonic)})"
item.setMnemonic(mnemonic)
}
popupMenu.show(this, evt.x, evt.y) popupMenu.show(this, evt.x, evt.y)
} }
@@ -1075,5 +1132,23 @@ class NewHostTree : SimpleTree(), Disposable {
electerm, electerm,
} }
private class NodesTransferable(val nodes: List<HostTreeNode>) : Transferable {
companion object {
val FLAVOR = DataFlavor("termora/host-tree", "Termora host tree transfers")
}
override fun getTransferDataFlavors(): Array<out DataFlavor> {
return arrayOf(FLAVOR)
}
override fun isDataFlavorSupported(flavor: DataFlavor?): Boolean {
return flavor == FLAVOR
}
override fun getTransferData(flavor: DataFlavor?): Any {
return if (flavor == FLAVOR) nodes else throw UnsupportedFlavorException(flavor)
}
}
} }

View File

@@ -2,6 +2,7 @@ package app.termora.tree
import javax.swing.Icon import javax.swing.Icon
import javax.swing.tree.DefaultMutableTreeNode import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.TreeNode
abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) { abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@@ -35,4 +36,15 @@ abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) {
return children return children
} }
open fun getParents(): List<SimpleTreeNode<T>> {
val parents = mutableListOf<SimpleTreeNode<T>>()
var p = parent as TreeNode?
while (p != null) {
if (p is SimpleTreeNode<T>) {
parents.add(p)
}
p = p.parent
}
return parents
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB