mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
12 Commits
2.0.0-beta
...
2.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd9b73ad6a | ||
|
|
dbea769994 | ||
|
|
9cd83c4025 | ||
|
|
d4cc080e7b | ||
|
|
a324bc3d96 | ||
|
|
36929e9ea3 | ||
|
|
dd73b933d9 | ||
|
|
117a9ea692 | ||
|
|
2f932de295 | ||
|
|
679b24a74d | ||
|
|
c6b33ea828 | ||
|
|
a4ea8f2491 |
@@ -384,6 +384,7 @@ tasks.register<Exec>("jpackage") {
|
||||
}
|
||||
|
||||
if (os.isLinux) {
|
||||
options.add("--add-opens=java.desktop/sun.awt.X11=ALL-UNNAMED")
|
||||
if (isDeb) {
|
||||
options.add("-Djpackage.app-layout=deb")
|
||||
}
|
||||
@@ -681,17 +682,24 @@ fun packOnLinux(distributionDir: Directory, finalFilenameWithoutExtension: Strin
|
||||
exec { commandLine("chmod", "+x", appimagetool.absolutePath) }
|
||||
}
|
||||
|
||||
|
||||
// Desktop file
|
||||
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
|
||||
desktopFile.writeText(
|
||||
"""[Desktop Entry]
|
||||
Type=Application
|
||||
Name=${termoraName}
|
||||
Comment=Terminal emulator and SSH client
|
||||
Icon=/lib/${termoraName}
|
||||
Icon=${termoraName}
|
||||
Categories=Development;
|
||||
StartupWMClass=${termoraName}
|
||||
Terminal=false
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
kotlin = "2.2.0"
|
||||
kotlin = "2.2.10"
|
||||
slf4j = "2.0.17"
|
||||
pty4j = "0.13.10"
|
||||
tinylog = "2.7.0"
|
||||
|
||||
@@ -9,7 +9,7 @@ dependencies {
|
||||
compileOnly(project(":"))
|
||||
implementation("com.maxmind.geoip2:geoip2:4.3.1")
|
||||
// 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")
|
||||
|
||||
@@ -8,7 +8,7 @@ project.version = "0.0.2"
|
||||
|
||||
dependencies {
|
||||
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(":"))
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.apache.commons.lang3.SystemUtils
|
||||
import org.apache.commons.lang3.math.NumberUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.tinylog.configuration.Configuration
|
||||
import java.awt.Toolkit
|
||||
import java.io.File
|
||||
import kotlin.system.exitProcess
|
||||
import kotlin.system.measureTimeMillis
|
||||
@@ -35,10 +36,20 @@ class ApplicationInitializr {
|
||||
// 检查是否单例
|
||||
checkSingleton()
|
||||
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
if (SystemInfo.isMacOS) {
|
||||
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 log = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
@@ -126,7 +126,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
|
||||
while (true) {
|
||||
val request = Request.Builder()
|
||||
.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()
|
||||
val text = AccountHttp.execute(request = request)
|
||||
val response = ohMyJson.decodeFromString<DataChangesResponse>(text)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.SettingsDialog
|
||||
import app.termora.*
|
||||
import com.formdev.flatlaf.extras.FlatDesktop
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.KeyboardFocusManager
|
||||
@@ -32,13 +29,13 @@ class SettingsAction private constructor() : AnAction(
|
||||
private val action get() = this
|
||||
|
||||
init {
|
||||
FlatDesktop.setPreferencesHandler {
|
||||
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||
// Doorman 的情况下不允许打开
|
||||
if (owner != null && ApplicationScope.windowScopes().isNotEmpty()) {
|
||||
actionPerformed(ActionEvent(owner, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
||||
}
|
||||
FlatDesktop.setPreferencesHandler(object : Runnable {
|
||||
override fun run() {
|
||||
val focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow ?: return
|
||||
if (focusedWindow !is TermoraFrame) return
|
||||
actionPerformed(ActionEvent(focusedWindow, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
|
||||
@@ -101,6 +101,16 @@ internal class KeywordHighlightPaintListener private constructor() : TerminalPai
|
||||
// -1 表示不使用高亮集
|
||||
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) {
|
||||
if (highlight.enabled.not()) continue
|
||||
if (highlight.type != KeywordHighlightType.Highlight) continue
|
||||
@@ -151,7 +161,6 @@ internal class KeywordHighlightPaintListener private constructor() : TerminalPai
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun after(
|
||||
|
||||
@@ -103,9 +103,20 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
||||
commands.add("Compression=yes")
|
||||
|
||||
// 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("HostKeyAlgorithms=${hostKeyAlgorithms}")
|
||||
commands.add("HostKeyAlgorithms=${hostKeyAlgorithms.joinToString(",")}")
|
||||
|
||||
// 不使用配置文件
|
||||
commands.add("-F")
|
||||
@@ -143,6 +154,15 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
||||
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) {
|
||||
// 如果通过公钥连接
|
||||
if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.eclipse.jgit.internal.transport.sshd.agent.connector.WinPipeConnector
|
||||
import java.awt.*
|
||||
import java.awt.event.*
|
||||
import javax.swing.*
|
||||
import javax.swing.event.DocumentEvent
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
import javax.swing.table.DefaultTableModel
|
||||
|
||||
@@ -46,6 +47,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
private val jumpHostsOption = JumpHostsOption()
|
||||
private val sftpOption = SFTPOption()
|
||||
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||
private var setHostMode = false
|
||||
|
||||
init {
|
||||
addOption(generalOption)
|
||||
@@ -135,6 +137,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
}
|
||||
|
||||
fun setHost(host: Host) {
|
||||
setHostMode = true
|
||||
generalOption.portTextField.value = host.port
|
||||
generalOption.nameTextField.text = host.name
|
||||
generalOption.usernameTextField.text = host.username
|
||||
@@ -298,6 +301,8 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
val remarkTextArea = FixedLengthTextArea(512)
|
||||
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||
|
||||
private var hostFocused = false
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
@@ -405,6 +410,26 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
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() {
|
||||
|
||||
@@ -3,6 +3,8 @@ package app.termora.tree
|
||||
import app.termora.*
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.account.AccountManager
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.OpenHostAction
|
||||
import app.termora.database.DatabaseChangedExtension
|
||||
import app.termora.database.DatabaseManager
|
||||
@@ -32,6 +34,10 @@ import org.jdesktop.swingx.action.ActionManager
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.w3c.dom.Element
|
||||
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.io.*
|
||||
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() {
|
||||
@@ -198,10 +239,12 @@ class NewHostTree : SimpleTree(), Disposable {
|
||||
val sshMenu = importMenu.add(".ssh/config")
|
||||
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 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 openInNewWindow = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open-in-new-window"))
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1075,5 +1132,23 @@ class NewHostTree : SimpleTree(), Disposable {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package app.termora.tree
|
||||
|
||||
import javax.swing.Icon
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
import javax.swing.tree.TreeNode
|
||||
|
||||
abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -35,4 +36,15 @@ abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) {
|
||||
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
|
||||
}
|
||||
}
|
||||
BIN
src/main/resources/icons/termora_256x256.png
Normal file
BIN
src/main/resources/icons/termora_256x256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Reference in New Issue
Block a user