mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
37 Commits
2.0.0-beta
...
2.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
613a1ca78a | ||
|
|
bf9e3ea2e2 | ||
|
|
a4390c4c6d | ||
|
|
9cf317e245 | ||
|
|
d000d73122 | ||
|
|
88613ed2f6 | ||
|
|
2fc381caa5 | ||
|
|
30e245f7a3 | ||
|
|
35cf92e685 | ||
|
|
522ee44ca2 | ||
|
|
5cf03e1f1f | ||
|
|
afca4ddf0e | ||
|
|
ca757f975a | ||
|
|
79c304ae3d | ||
|
|
1848c869e7 | ||
|
|
029e570551 | ||
|
|
905c570e4c | ||
|
|
a3069229b8 | ||
|
|
1e930d61c9 | ||
|
|
0015c3a7fb | ||
|
|
4bfb87e5c7 | ||
|
|
4fbb626c42 | ||
|
|
35b175d944 | ||
|
|
5939297550 | ||
|
|
e6e5867742 | ||
|
|
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")
|
||||
}
|
||||
@@ -403,18 +404,6 @@ tasks.register<Exec>("jpackage") {
|
||||
arguments.addAll(listOf("--copyright", "TermoraDev"))
|
||||
arguments.addAll(listOf("--app-content", "$buildDir/plugins"))
|
||||
|
||||
if (os.isWindows) {
|
||||
arguments.addAll(
|
||||
listOf(
|
||||
"--description",
|
||||
"${project.name.uppercaseFirstChar()}: A terminal emulator and SSH client"
|
||||
)
|
||||
)
|
||||
} else {
|
||||
arguments.addAll(listOf("--description", "A terminal emulator and SSH client."))
|
||||
}
|
||||
|
||||
|
||||
if (os.isMacOsX) {
|
||||
arguments.addAll(listOf("--mac-package-name", project.name.uppercaseFirstChar()))
|
||||
arguments.addAll(listOf("--mac-app-category", "developer-tools"))
|
||||
@@ -681,17 +670,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.20"
|
||||
slf4j = "2.0.17"
|
||||
pty4j = "0.13.10"
|
||||
tinylog = "2.7.0"
|
||||
@@ -22,13 +22,13 @@ jna = "5.17.0"
|
||||
jSystemThemeDetector = "3.9.1"
|
||||
commons-io = "2.20.0"
|
||||
jbr-api = "17.1.10.1"
|
||||
hutool = "5.8.39"
|
||||
jsch = "2.27.2"
|
||||
hutool = "5.8.40"
|
||||
jsch = "2.27.3"
|
||||
okhttp = "5.1.0"
|
||||
sshj = "0.39.0"
|
||||
sshd-core = "2.15.0"
|
||||
jgit = "7.2.0.202503040940-r"
|
||||
commonmark = "0.25.1"
|
||||
commonmark = "0.26.0"
|
||||
jnafilechooser = "1.1.2"
|
||||
xodus = "2.0.1"
|
||||
bip39 = "1.0.9"
|
||||
@@ -41,7 +41,7 @@ jSerialComm = "2.11.2"
|
||||
ini4j = "0.5.5-2"
|
||||
restart4j = "0.0.1"
|
||||
eddsa = "0.3.0"
|
||||
exposed = "1.0.0-beta-5"
|
||||
exposed = "1.0.0-rc-1"
|
||||
h2 = "2.3.232"
|
||||
sqlite = "3.50.3.0"
|
||||
jug = "5.1.0"
|
||||
@@ -106,7 +106,7 @@ eddsa = { module = "net.i2p.crypto:eddsa", version.ref = "eddsa" }
|
||||
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
|
||||
exposed-crypt = { module = "org.jetbrains.exposed:exposed-crypt", version.ref = "exposed" }
|
||||
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
||||
exposed-migration = { module = "org.jetbrains.exposed:exposed-migration", version.ref = "exposed" }
|
||||
exposed-migration = { module = "org.jetbrains.exposed:exposed-migration-core", version.ref = "exposed" }
|
||||
h2 = { module = "com.h2database:h2", version.ref = "h2" }
|
||||
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
|
||||
jug = { module = "com.fasterxml.uuid:java-uuid-generator", version.ref = "jug" }
|
||||
|
||||
@@ -8,7 +8,7 @@ project.version = "0.0.4"
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
implementation("com.qcloud:cos_api:5.6.251")
|
||||
implementation("com.qcloud:cos_api:5.6.255")
|
||||
compileOnly(project(":"))
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ project.version = "0.0.8"
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
implementation("com.maxmind.geoip2:geoip2:4.3.1")
|
||||
implementation("com.maxmind.geoip2:geoip2:4.4.0")
|
||||
// https://github.com/hstyi/geolite2
|
||||
implementation("com.github.hstyi:geolite2:v1.0-202508040102")
|
||||
implementation("com.github.hstyi:geolite2:v1.0-202508180058")
|
||||
}
|
||||
|
||||
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(":"))
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,6 @@ termora.plugins.sync.disabled-sync=You are already logged in and cannot use this
|
||||
|
||||
termora.settings.sync=Sync
|
||||
termora.settings.sync.done=Synchronized data successfully
|
||||
termora.settings.sync.export=${termora.keymgr.export}
|
||||
termora.settings.sync.import=${termora.keymgr.import}
|
||||
termora.settings.sync.import.file-too-large=The file is too large
|
||||
termora.settings.sync.import.successful=Import data successfully
|
||||
termora.settings.sync.export-done=The export was successful
|
||||
termora.settings.sync.export-encrypt=Enter password to encrypt file (optional)
|
||||
termora.settings.sync.export-done-open-folder=The export was successful. Do you want to open the folder?
|
||||
termora.settings.sync.range=Range
|
||||
termora.settings.sync.range.keys=My keys
|
||||
termora.settings.sync.range.keyword-highlights=${termora.highlight}
|
||||
|
||||
@@ -2,15 +2,10 @@ termora.plugins.sync.disabled-sync=你已登录,无法使用此功能
|
||||
|
||||
|
||||
termora.settings.sync=同步
|
||||
termora.settings.sync.export-done=导出成功
|
||||
termora.settings.sync.export-encrypt=输入密码加密文件 (可选)
|
||||
termora.settings.sync.export-done-open-folder=导出成功,是否需要打开所在文件夹?
|
||||
termora.settings.sync.range=范围
|
||||
termora.settings.sync.range.keys=我的密钥
|
||||
termora.settings.sync.last-sync-time=最后同步时间
|
||||
termora.settings.sync.done=同步数据成功
|
||||
termora.settings.sync.import.file-too-large=文件太大
|
||||
termora.settings.sync.import.successful=导入数据成功
|
||||
termora.settings.sync.gist=片段
|
||||
termora.settings.sync.token=令牌
|
||||
termora.settings.sync.type=类型
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
termora.plugins.sync.disabled-sync=你已登錄,無法使用此功能
|
||||
|
||||
termora.settings.sync=同步
|
||||
termora.settings.sync.export-done=匯出成功
|
||||
termora.settings.sync.export-encrypt=輸入密碼加密檔案 (可選)
|
||||
termora.settings.sync.export-done-open-folder=匯出成功,是否需要打開所在資料夾?
|
||||
termora.settings.sync.range=範圍
|
||||
termora.settings.sync.range.keys=我的密鑰
|
||||
termora.settings.sync.last-sync-time=最後同步時間
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.database.DatabaseManager
|
||||
import com.formdev.flatlaf.util.UIScale
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
@@ -14,7 +15,10 @@ internal class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
||||
private val properties get() = DatabaseManager.getInstance().properties
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||
size = Dimension(
|
||||
UIScale.scale(UIManager.getInt("Dialog.width")),
|
||||
UIScale.scale(UIManager.getInt("Dialog.height"))
|
||||
)
|
||||
isModal = true
|
||||
title = I18n.getString("termora.setting")
|
||||
setLocationRelativeTo(null)
|
||||
|
||||
@@ -2,6 +2,7 @@ package app.termora
|
||||
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||
import app.termora.tree.NewHostTree
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
@@ -9,15 +10,14 @@ import com.formdev.flatlaf.util.SystemInfo
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
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.*
|
||||
import javax.swing.*
|
||||
import javax.swing.tree.TreePath
|
||||
import kotlin.math.max
|
||||
|
||||
|
||||
class TermoraFencePanel(
|
||||
private val ws: WindowScope,
|
||||
private val terminalTabbed: TerminalTabbed,
|
||||
private val tabbed: FlatTabbedPane,
|
||||
private val moveMouseAdapter: MouseAdapter,
|
||||
@@ -98,6 +98,40 @@ class TermoraFencePanel(
|
||||
toggle()
|
||||
}
|
||||
|
||||
|
||||
DynamicExtensionHandler.getInstance()
|
||||
.register(TerminalTabbedContextMenuExtension::class.java, object : TerminalTabbedContextMenuExtension {
|
||||
override fun createJMenuItem(
|
||||
windowScope: WindowScope,
|
||||
tab: TerminalTab
|
||||
): JMenuItem {
|
||||
if (windowScope != ws) throw UnsupportedOperationException()
|
||||
if (tab !is HostTerminalTab) throw UnsupportedOperationException()
|
||||
if (tab.host.isTemporary) throw UnsupportedOperationException()
|
||||
if (tab.host.id == "local") throw UnsupportedOperationException()
|
||||
|
||||
val item = JMenuItem(I18n.getString("termora.tabbed.contextmenu.select-host"))
|
||||
item.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val tree = getHostTree()
|
||||
for (node in tree.simpleTreeModel.root.getAllChildren()) {
|
||||
if (node.id == tab.host.id) {
|
||||
tree.selectionPath = TreePath(tree.simpleTreeModel.getPathToRoot(node))
|
||||
tree.requestFocusInWindow()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
override fun ordered(): Long {
|
||||
return Long.MAX_VALUE
|
||||
}
|
||||
|
||||
}).let { Disposer.register(this, it) }
|
||||
}
|
||||
|
||||
private inner class LeftTreePanel : JPanel(BorderLayout()), Disposable {
|
||||
|
||||
@@ -212,7 +212,7 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
}
|
||||
|
||||
if (layout == TermoraLayout.Fence) {
|
||||
val fencePanel = TermoraFencePanel(terminalTabbed, tabbedPane, moveMouseAdapter)
|
||||
val fencePanel = TermoraFencePanel(windowScope, terminalTabbed, tabbedPane, moveMouseAdapter)
|
||||
add(fencePanel, BorderLayout.CENTER)
|
||||
dataProviderSupport.addData(DataProviders.Welcome.HostTree, fencePanel.getHostTree())
|
||||
Disposer.register(windowScope, fencePanel)
|
||||
|
||||
@@ -139,6 +139,7 @@ object AccountHttp {
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
if (cidr == "localhost" || cidr == "127.0.0.1") continue
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug(e.message, e)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.apache.commons.codec.binary.Base64
|
||||
import org.apache.commons.codec.digest.DigestUtils
|
||||
import org.apache.commons.lang3.ObjectUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -16,8 +16,8 @@ import app.termora.snippet.SnippetManager
|
||||
import app.termora.terminal.CursorStyle
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jetbrains.exposed.v1.core.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.statements.StatementType
|
||||
import org.jetbrains.exposed.v1.jdbc.*
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -262,8 +262,8 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
|
||||
|
||||
OptionPane.openFileInFolder(
|
||||
SwingUtilities.getWindowAncestor(this),
|
||||
file, I18n.getString("termora.settings.sync.export-done-open-folder"),
|
||||
I18n.getString("termora.settings.sync.export-done")
|
||||
file, I18n.getString("termora.keymgr.export-done-open-folder"),
|
||||
I18n.getString("termora.keymgr.export-done")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class MacroManager private constructor() {
|
||||
|
||||
val accountId = AccountManager.getInstance().getAccountId()
|
||||
|
||||
database.save(
|
||||
database.saveAndIncrementVersion(
|
||||
Data(
|
||||
id = macro.id,
|
||||
ownerId = accountId,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import app.termora.keymgr.KeyManagerDialog
|
||||
import app.termora.plugin.internal.AltKeyModifier
|
||||
import app.termora.plugin.internal.BasicProxyOption
|
||||
import app.termora.plugin.internal.BasicTerminalOption
|
||||
import app.termora.plugin.internal.telnet.TelnetHostOptionsPane.Backspace
|
||||
import app.termora.tree.Filter
|
||||
import app.termora.tree.HostTreeNode
|
||||
import app.termora.tree.NewHostTreeDialog
|
||||
@@ -24,6 +25,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
|
||||
|
||||
@@ -35,6 +37,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
private val terminalOption = BasicTerminalOption().apply {
|
||||
showCharsetComboBox = true
|
||||
showLoginScripts = true
|
||||
showBackspaceComboBox = true
|
||||
showEnvironmentTextArea = true
|
||||
showStartupCommandTextField = true
|
||||
showHeartbeatIntervalTextField = true
|
||||
@@ -46,6 +49,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)
|
||||
@@ -110,6 +114,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
x11Forwarding = tunnelingOption.x11ServerTextField.text,
|
||||
loginScripts = terminalOption.loginScripts,
|
||||
extras = mutableMapOf(
|
||||
"backspace" to (terminalOption.backspaceComboBox.selectedItem as Backspace).name,
|
||||
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
|
||||
?: AltKeyModifier.EightBit.name),
|
||||
"keywordHighlightSetId" to ((terminalOption.highlightSetComboBox.selectedItem as? KeywordHighlight)?.id
|
||||
@@ -135,6 +140,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
|
||||
@@ -166,6 +172,9 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
.getOrNull() ?: AltKeyModifier.EightBit
|
||||
|
||||
|
||||
terminalOption.backspaceComboBox.selectedItem =
|
||||
Backspace.valueOf(host.options.extras["backspace"] ?: Backspace.Delete.name)
|
||||
|
||||
val timeout = host.options.extras["timeout"] ?: "60"
|
||||
terminalOption.timeoutTextField.value = timeout.toIntOrNull() ?: 60
|
||||
|
||||
@@ -298,6 +307,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 +416,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() {
|
||||
|
||||
@@ -7,9 +7,8 @@ import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.keymap.KeyShortcut
|
||||
import app.termora.keymap.KeymapManager
|
||||
import app.termora.terminal.ControlCharacters
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.PtyConnector
|
||||
import app.termora.plugin.internal.telnet.TelnetHostOptionsPane
|
||||
import app.termora.terminal.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@@ -20,6 +19,7 @@ import org.apache.sshd.client.session.ClientSession
|
||||
import org.apache.sshd.common.future.CloseFuture
|
||||
import org.apache.sshd.common.future.SshFutureListener
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.event.KeyEvent
|
||||
import java.nio.charset.StandardCharsets
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComponent
|
||||
@@ -110,7 +110,18 @@ class SSHTerminalTab(
|
||||
// clear screen
|
||||
terminal.clearScreen()
|
||||
// show cursor
|
||||
terminalModel.setData(DataKey.Companion.ShowCursor, true)
|
||||
terminalModel.setData(DataKey.ShowCursor, 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(
|
||||
|
||||
@@ -69,4 +69,7 @@ class SftpCommandTerminalTabbedContextMenuExtension private constructor() : Term
|
||||
openHostAction.actionPerformed(OpenHostActionEvent(evt.source, host, evt))
|
||||
}
|
||||
|
||||
override fun ordered(): Long {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
@@ -425,8 +425,11 @@ object SshClients {
|
||||
|
||||
|
||||
val heartbeatInterval = max(host.options.heartbeatInterval, 3)
|
||||
val timeout = Duration.ofSeconds(host.options.extras["timeout"]?.toLongOrNull() ?: 60)
|
||||
|
||||
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
|
||||
CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true)
|
||||
CoreModuleProperties.IO_CONNECT_TIMEOUT.set(sshClient, timeout)
|
||||
|
||||
sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) }
|
||||
|
||||
|
||||
@@ -520,9 +520,13 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
||||
|
||||
val writer = terminalModel.getData(DataKey.TerminalWriter)
|
||||
|
||||
// VT102_RESPONSE
|
||||
val bytes = "${ControlCharacters.ESC}[?6c".toByteArray(writer.getCharset())
|
||||
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
|
||||
if (args.startsWith('>')) {
|
||||
val bytes = "${ControlCharacters.ESC}[>0;276;0c".toByteArray(writer.getCharset())
|
||||
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
|
||||
} else {
|
||||
val bytes = "${ControlCharacters.ESC}[?1;2c".toByteArray(writer.getCharset())
|
||||
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ class TerminalFindPanel(
|
||||
}
|
||||
} else {
|
||||
if (index - 1 <= 0) {
|
||||
index = 0
|
||||
index = kinds.size - 1
|
||||
} else {
|
||||
index--
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ import java.nio.file.Path
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.Executors
|
||||
import javax.swing.*
|
||||
import javax.swing.event.PopupMenuEvent
|
||||
import javax.swing.event.PopupMenuListener
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.math.max
|
||||
import kotlin.reflect.cast
|
||||
@@ -58,12 +60,24 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
||||
private val connectFailedPanel = ConnectFailedPanel()
|
||||
private val transferManager = TransferTableModel(coroutineScope)
|
||||
private val disposable = Disposer.newDisposable()
|
||||
private val focusedWindow get() = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||
private val questionBtn = JButton(Icons.questionMark)
|
||||
private val downloadBtn = JButton(Icons.download)
|
||||
private val badgePresentation = Badge.getInstance(tab.windowScope)
|
||||
.addBadge(downloadBtn).apply { visible = false }
|
||||
private val support = DataProviderSupport()
|
||||
private var isShowPopupMenu = false
|
||||
|
||||
override var isStickHover: Boolean
|
||||
get() = super.isStickHover
|
||||
set(value) {
|
||||
if (isShowPopupMenu || owner != focusedWindow) {
|
||||
super.isStickHover = true
|
||||
} else {
|
||||
super.isStickHover = value
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
initViews()
|
||||
@@ -135,6 +149,8 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
||||
}
|
||||
})
|
||||
|
||||
questionBtn.toolTipText = I18n.getString("termora.visual-window.transport.question")
|
||||
|
||||
// 立即连接
|
||||
connect()
|
||||
}
|
||||
@@ -151,7 +167,7 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
||||
val support = DefaultTransportSupport(fileSystem, fileSystem.defaultDir)
|
||||
withContext(Dispatchers.Swing) {
|
||||
val internalTransferManager = MyInternalTransferManager()
|
||||
val transportPanel = TransportPanel(
|
||||
val transportPanel = object : TransportPanel(
|
||||
internalTransferManager, tab.host,
|
||||
object : TransportSupportLoader {
|
||||
override suspend fun getTransportSupport(): TransportSupport {
|
||||
@@ -165,7 +181,27 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
||||
override fun isLoaded(): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
}) {
|
||||
override fun customizeContextmenu(
|
||||
rows: Array<Int>,
|
||||
e: MouseEvent,
|
||||
popupMenu: TransportPopupMenu
|
||||
) {
|
||||
popupMenu.addPopupMenuListener(object : PopupMenuListener {
|
||||
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent?) {
|
||||
isShowPopupMenu = true
|
||||
}
|
||||
|
||||
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent?) {
|
||||
isShowPopupMenu = false
|
||||
}
|
||||
|
||||
override fun popupMenuCanceled(e: PopupMenuEvent?) {
|
||||
isShowPopupMenu = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
internalTransferManager.setTransferPanel(transportPanel)
|
||||
Disposer.register(transportPanel, object : Disposable {
|
||||
override fun dispose() {
|
||||
@@ -240,6 +276,10 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
||||
super.dispose()
|
||||
}
|
||||
|
||||
override fun reassemble() {
|
||||
super.reassemble()
|
||||
}
|
||||
|
||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||
return support.getData(dataKey)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
private var dialog: VisualWindowDialog? = null
|
||||
private var oldBounds = Rectangle()
|
||||
private var toggleWindowBtn = JButton(Icons.openInNewWindow)
|
||||
private val closeBtn = JButton(Icons.close)
|
||||
private var isAlwaysTop
|
||||
get() = properties.getString("VisualWindow.${id}.dialog.isAlwaysTop", "false").toBoolean()
|
||||
set(value) = properties.putString("VisualWindow.${id}.dialog.isAlwaysTop", value.toString())
|
||||
@@ -47,8 +48,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
}
|
||||
}
|
||||
|
||||
protected var isStickHover = false
|
||||
private set(value) {
|
||||
protected open var isStickHover = false
|
||||
set(value) {
|
||||
if (value == field) return
|
||||
field = value
|
||||
reassemble()
|
||||
@@ -92,6 +93,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
oldBounds = bounds
|
||||
alwaysTopBtn.isSelected = isAlwaysTop
|
||||
alwaysTopBtn.isVisible = false
|
||||
|
||||
closeBtn.toolTipText = I18n.getString("termora.tabbed.contextmenu.close")
|
||||
}
|
||||
|
||||
protected open fun toolbarButtons(): List<Pair<JButton, Position>> {
|
||||
@@ -134,6 +137,7 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
addMouseListener(object : MouseAdapter() {})
|
||||
|
||||
toggleWindowBtn.addActionListener { toggleWindow() }
|
||||
toggleWindowBtn.toolTipText = I18n.getString("termora.visual-window.toggle-window")
|
||||
|
||||
addPropertyChangeListener("isWindow") {
|
||||
if (isWindow) {
|
||||
@@ -165,6 +169,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
dialog?.isAlwaysOnTop = isAlwaysTop
|
||||
}
|
||||
}
|
||||
|
||||
closeBtn.addActionListener { if (beforeClose()) Disposer.dispose(visualWindow) }
|
||||
}
|
||||
|
||||
private fun initToolBar() {
|
||||
@@ -180,7 +186,7 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
buttons.filter { it.second == Position.Right }.forEach { toolbar.add(it.first) }
|
||||
|
||||
toolbar.add(toggleWindowBtn)
|
||||
toolbar.add(JButton(Icons.close).apply { addActionListener { if (beforeClose()) Disposer.dispose(visualWindow) } })
|
||||
toolbar.add(closeBtn)
|
||||
toolbar.border = BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor)
|
||||
add(toolbar, BorderLayout.NORTH)
|
||||
}
|
||||
|
||||
@@ -66,6 +66,8 @@ class BookmarkButton : JButton(Icons.bookmarks) {
|
||||
})
|
||||
|
||||
isBookmark = false
|
||||
|
||||
toolTipText = I18n.getString("termora.transport.bookmarks")
|
||||
}
|
||||
|
||||
private fun showBookmarks(e: MouseEvent) {
|
||||
|
||||
@@ -63,7 +63,7 @@ import kotlin.io.path.*
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
internal class TransportPanel(
|
||||
internal open class TransportPanel(
|
||||
private val internalTransferManager: InternalTransferManager,
|
||||
val host: Host,
|
||||
val loader: TransportSupportLoader,
|
||||
@@ -131,10 +131,10 @@ internal class TransportPanel(
|
||||
* 工作目录
|
||||
*/
|
||||
override var workdir: Path? = null
|
||||
private set
|
||||
protected set
|
||||
|
||||
override var loading = false
|
||||
private set(value) {
|
||||
protected set(value) {
|
||||
val oldValue = field
|
||||
field = value
|
||||
if (oldValue != value) {
|
||||
@@ -165,6 +165,14 @@ internal class TransportPanel(
|
||||
toolbar.add(eyeBtn)
|
||||
toolbar.add(refreshBtn)
|
||||
|
||||
prevBtn.toolTipText = I18n.getString("termora.transport.toolbar.prev")
|
||||
homeBtn.toolTipText = I18n.getString("termora.transport.toolbar.home")
|
||||
nextBtn.toolTipText = I18n.getString("termora.transport.toolbar.next")
|
||||
|
||||
parentBtn.toolTipText = I18n.getString("termora.transport.toolbar.parent")
|
||||
eyeBtn.toolTipText = I18n.getString("termora.transport.toolbar.show-hide")
|
||||
refreshBtn.toolTipText = I18n.getString("termora.transport.toolbar.refresh")
|
||||
|
||||
sorter.maxSortKeys = 1
|
||||
table.setRowSorter(sorter)
|
||||
table.setAutoCreateRowSorter(false)
|
||||
@@ -411,7 +419,7 @@ internal class TransportPanel(
|
||||
}
|
||||
})
|
||||
|
||||
addPropertyChangeListener("workdir") { evt -> reload() }
|
||||
addPropertyChangeListener("workdir") { _ -> reload() }
|
||||
|
||||
reload()
|
||||
}
|
||||
@@ -507,6 +515,29 @@ internal class TransportPanel(
|
||||
}
|
||||
})
|
||||
|
||||
table.actionMap.put("Delete", object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val rows = table.selectedRows.map { sorter.convertRowIndexToModel(it) }.toTypedArray()
|
||||
val files = rows.map { model.getPath(it) to model.getAttributes(it) }
|
||||
// 排除父目录
|
||||
val validFiles = files.filter { !it.second.isParent }
|
||||
if (validFiles.isNotEmpty()) {
|
||||
// 显示删除确认对话框
|
||||
if (OptionPane.showConfirmDialog(
|
||||
owner,
|
||||
I18n.getString("termora.keymgr.delete-warning"),
|
||||
messageType = JOptionPane.WARNING_MESSAGE
|
||||
) == JOptionPane.YES_OPTION
|
||||
) {
|
||||
// 直接执行删除操作
|
||||
val future =
|
||||
internalTransferManager.addTransfer(validFiles, InternalTransferManager.TransferMode.Delete)
|
||||
mountFuture(future)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 快速导航
|
||||
table.addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
@@ -530,12 +561,25 @@ internal class TransportPanel(
|
||||
}
|
||||
})
|
||||
|
||||
// 重写全选行为,排除".."父目录
|
||||
table.actionMap.put("selectAll", object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
table.clearSelection()
|
||||
val startRow = if (hasParent) 1 else 0 // 跳过".."行
|
||||
if (startRow < table.rowCount) {
|
||||
table.setRowSelectionInterval(startRow, table.rowCount - 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val inputMap = table.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
|
||||
if (SystemInfo.isMacOS.not()) {
|
||||
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_DELETE, 0), "Delete")
|
||||
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), "Delete")
|
||||
}
|
||||
|
||||
private fun initTransferHandler() {
|
||||
@@ -881,13 +925,18 @@ internal class TransportPanel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun showContextmenu(rows: Array<Int>, e: MouseEvent) {
|
||||
protected open fun showContextmenu(rows: Array<Int>, e: MouseEvent) {
|
||||
val files = rows.map { model.getPath(it) to model.getAttributes(it) }
|
||||
val popupMenu = TransportPopupMenu(owner, model, internalTransferManager, loader, files)
|
||||
popupMenu.addActionListener(PopupMenuActionListener(files))
|
||||
customizeContextmenu(rows, e, popupMenu)
|
||||
popupMenu.show(table, e.x, e.y)
|
||||
}
|
||||
|
||||
protected open fun customizeContextmenu(rows: Array<Int>, e: MouseEvent, popupMenu: TransportPopupMenu) {
|
||||
|
||||
}
|
||||
|
||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||
return support.getData(dataKey)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import app.termora.*
|
||||
import app.termora.plugin.ExtensionManager
|
||||
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import kotlinx.coroutines.launch
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
@@ -41,7 +42,14 @@ internal class TransportPopupMenu(
|
||||
private val copyPathMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.copy-path"))
|
||||
private val copyMenu = JMenuItem(I18n.getString("termora.copy"))
|
||||
private val pasteMenu = JMenuItem(I18n.getString("termora.paste"))
|
||||
private val openInFinderMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.open-in-folder"))
|
||||
private val openInFinderMenu = JMenuItem(
|
||||
I18n.getString(
|
||||
"termora.transport.table.contextmenu.open-in-folder",
|
||||
if (SystemInfo.isMacOS) I18n.getString("termora.finder")
|
||||
else if (SystemInfo.isWindows) I18n.getString("termora.explorer")
|
||||
else I18n.getString("termora.folder")
|
||||
)
|
||||
)
|
||||
private val renameMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.rename"))
|
||||
private val deleteMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.delete"))
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.sshd.client.SshClient
|
||||
import org.apache.sshd.client.session.ClientSession
|
||||
import org.apache.sshd.core.CoreModuleProperties
|
||||
import org.apache.sshd.sftp.SftpModuleProperties
|
||||
import org.apache.sshd.sftp.client.SftpClientFactory
|
||||
|
||||
internal class SFTPTransferProtocolProvider : TransferProtocolProvider {
|
||||
@@ -32,6 +34,11 @@ internal class SFTPTransferProtocolProvider : TransferProtocolProvider {
|
||||
client = if (owner == null) SshClients.openClient(requester.host)
|
||||
else SshClients.openClient(requester.host, owner)
|
||||
session = SshClients.openSession(requester.host, client)
|
||||
|
||||
CoreModuleProperties.IO_CONNECT_TIMEOUT.get(client).ifPresent { e ->
|
||||
SftpModuleProperties.SFTP_CHANNEL_OPEN_TIMEOUT.set(session, e)
|
||||
}
|
||||
|
||||
val fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
|
||||
|
||||
val host = requester.host
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -239,6 +239,8 @@ termora.keymgr.table.name=Name
|
||||
termora.keymgr.table.type=Type
|
||||
termora.keymgr.table.length=Length
|
||||
termora.keymgr.table.remark=Description
|
||||
termora.keymgr.export-done=The export was successful
|
||||
termora.keymgr.export-done-open-folder=The export was successful. Do you want to open the folder?
|
||||
|
||||
termora.keymgr.ssh-copy-id.number=Number of hosts [{0}] Number of public keys [{1}]
|
||||
termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied}
|
||||
@@ -248,6 +250,7 @@ termora.keymgr.ssh-copy-id.end=End of public key copying
|
||||
|
||||
# Tabbed
|
||||
termora.tabbed.contextmenu.rename=Rename
|
||||
termora.tabbed.contextmenu.select-host=Select Host
|
||||
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.clone=Clone
|
||||
@@ -308,6 +311,14 @@ termora.tools.multiple=Send command to the current window sessions
|
||||
termora.transport.local=Local
|
||||
termora.transport.file-already-exists=The file {0} already exists
|
||||
|
||||
|
||||
termora.transport.toolbar.prev=Backward
|
||||
termora.transport.toolbar.home=Home Folder
|
||||
termora.transport.toolbar.next=Forward
|
||||
termora.transport.toolbar.parent=Parent Folder
|
||||
termora.transport.toolbar.show-hide=Show/Hide Folders
|
||||
termora.transport.toolbar.refresh=Refresh Folder
|
||||
|
||||
termora.transport.bookmarks=Bookmarks Manager
|
||||
termora.transport.bookmarks.up=Up
|
||||
termora.transport.bookmarks.down=Down
|
||||
@@ -324,7 +335,7 @@ termora.transport.table.owner=Owner
|
||||
termora.transport.table.contextmenu.transfer=Transfer
|
||||
termora.transport.table.contextmenu.edit=${termora.keymgr.edit}
|
||||
termora.transport.table.contextmenu.copy-path=Copy Path
|
||||
termora.transport.table.contextmenu.open-in-folder=Open in ${termora.finder}
|
||||
termora.transport.table.contextmenu.open-in-folder=Open in {0}
|
||||
termora.transport.table.contextmenu.rename=${termora.welcome.contextmenu.rename}
|
||||
termora.transport.table.contextmenu.delete=${termora.remove}
|
||||
termora.transport.table.contextmenu.rm-warning=Using the rm -rf command to delete a file is very dangerous
|
||||
@@ -430,6 +441,8 @@ termora.visual-window.system-information.mem=Mem
|
||||
termora.visual-window.system-information.swap=Swap
|
||||
termora.visual-window.system-information.filesystem=Filesystem
|
||||
termora.visual-window.system-information.used-total=Used / Total
|
||||
termora.visual-window.toggle-window=Toggle window
|
||||
termora.visual-window.transport.question=More Features
|
||||
|
||||
|
||||
termora.visual-window.nvidia-smi=NVIDIA SMI
|
||||
|
||||
@@ -69,27 +69,6 @@ termora.settings.terminal.floating-toolbar=Плавающая панель
|
||||
termora.settings.terminal.auto-close-tab=Автозакрытие вкладки
|
||||
termora.settings.terminal.auto-close-tab-description=Автоматически закрывать вкладку при обычном отключении терминала
|
||||
|
||||
termora.settings.sync=Синхронизация
|
||||
termora.settings.sync.done=Синхронизация успешна
|
||||
termora.settings.sync.export=${termora.keymgr.export}
|
||||
termora.settings.sync.import=${termora.keymgr.import}
|
||||
termora.settings.sync.import.file-too-large=Файл слишком большой
|
||||
termora.settings.sync.import.successful=Импортировано успешно
|
||||
termora.settings.sync.export-done=Экспортировано успешно
|
||||
termora.settings.sync.export-encrypt=Введите пароль для расшифровки файла (выборочно)
|
||||
termora.settings.sync.export-done-open-folder=Экспорт прошел успешно. Открыть папку?
|
||||
termora.settings.sync.range=Диапазон
|
||||
termora.settings.sync.range.keys=Мои ключи
|
||||
termora.settings.sync.range.keyword-highlights=${termora.highlight}
|
||||
termora.settings.sync.last-sync-time=Последняя синхронизация
|
||||
termora.settings.sync.gist=Gist
|
||||
termora.settings.sync.token=Токен
|
||||
termora.settings.sync.type=Сервис
|
||||
termora.settings.sync.webdav.help=WebDAV адрес, https://yourhost/webdav/termora.json
|
||||
termora.settings.sync.policy=Тип синхронизации
|
||||
termora.settings.sync.policy.manual=Вручную
|
||||
termora.settings.sync.policy.on-change=При изменениях
|
||||
|
||||
termora.settings.about=О программе
|
||||
termora.settings.about.author=Автор
|
||||
termora.settings.about.source=Ссылка
|
||||
@@ -204,6 +183,8 @@ termora.keymgr.table.name=Название
|
||||
termora.keymgr.table.type=Тип
|
||||
termora.keymgr.table.length=Длина
|
||||
termora.keymgr.table.remark=Описание
|
||||
termora.keymgr.export-done=Экспорт выполнен успешно
|
||||
termora.keymgr.export-done-open-folder=Экспорт выполнен успешно. Открыть папку?
|
||||
|
||||
termora.keymgr.ssh-copy-id.number=Кол-во хостов [{0}] Кол-во публичных ключей [{1}]
|
||||
termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied}
|
||||
@@ -212,6 +193,7 @@ termora.keymgr.ssh-copy-id.end=Копирования открытого клю
|
||||
|
||||
# Tabbed
|
||||
termora.tabbed.contextmenu.rename=Переименовать
|
||||
termora.tabbed.contextmenu.select-host=Выбрать хост
|
||||
termora.tabbed.contextmenu.sftp-command=SFTP Команда
|
||||
termora.tabbed.contextmenu.sftp-not-install=Программа SFTP не найдена, пожалуйста, установите и повторите попытку.
|
||||
termora.tabbed.contextmenu.clone=Дублировать
|
||||
@@ -269,6 +251,14 @@ termora.transport.bookmarks=Менеджер закладок
|
||||
termora.transport.bookmarks.up=Вверх
|
||||
termora.transport.bookmarks.down=Вниз
|
||||
|
||||
termora.transport.toolbar.prev=Назад
|
||||
termora.transport.toolbar.home=Домашняя папка
|
||||
termora.transport.toolbar.next=Вперёд
|
||||
termora.transport.toolbar.parent=Родительская папка
|
||||
termora.transport.toolbar.show-hide=Показать/Скрыть папки
|
||||
termora.transport.toolbar.refresh=Обновить
|
||||
|
||||
|
||||
termora.transport.table.filename=Имя файла
|
||||
termora.transport.table.type=Тип
|
||||
termora.transport.table.type.symbolic-link=Символьная Ссылка
|
||||
@@ -376,6 +366,8 @@ termora.visual-window.system-information.mem=Память
|
||||
termora.visual-window.system-information.swap=Подкачка
|
||||
termora.visual-window.system-information.filesystem=Файловая система
|
||||
termora.visual-window.system-information.used-total=Использовано / Всего
|
||||
termora.visual-window.toggle-window=Переключить окно
|
||||
termora.visual-window.transport.question=Больше возможностей
|
||||
|
||||
|
||||
termora.visual-window.nvidia-smi=NVIDIA SMI
|
||||
|
||||
@@ -232,6 +232,8 @@ termora.keymgr.table.name=名称
|
||||
termora.keymgr.table.type=类型
|
||||
termora.keymgr.table.length=长度
|
||||
termora.keymgr.table.remark=备注
|
||||
termora.keymgr.export-done=导出成功
|
||||
termora.keymgr.export-done-open-folder=导出成功,是否需要打开所在文件夹?
|
||||
|
||||
termora.keymgr.ssh-copy-id.number=主机数量 [{0}] 公钥数量 [{1}]
|
||||
termora.keymgr.ssh-copy-id.failed=复制失败
|
||||
@@ -244,6 +246,7 @@ termora.tools.multiple=将命令发送到当前窗口会话
|
||||
|
||||
# Tabbed
|
||||
termora.tabbed.contextmenu.rename=重命名
|
||||
termora.tabbed.contextmenu.select-host=选中主机
|
||||
termora.tabbed.contextmenu.sftp-command=SFTP 终端
|
||||
termora.tabbed.contextmenu.sftp-not-install=没有找到 SFTP 程序,请安装后重试
|
||||
termora.tabbed.contextmenu.clone=克隆
|
||||
@@ -309,6 +312,15 @@ termora.transport.bookmarks=书签管理
|
||||
termora.transport.bookmarks.up=上移
|
||||
termora.transport.bookmarks.down=下移
|
||||
|
||||
|
||||
termora.transport.toolbar.prev=返回
|
||||
termora.transport.toolbar.home=默认目录
|
||||
termora.transport.toolbar.next=前进
|
||||
termora.transport.toolbar.parent=父目录
|
||||
termora.transport.toolbar.show-hide=显示/隐藏目录
|
||||
termora.transport.toolbar.refresh=刷新
|
||||
|
||||
|
||||
termora.transport.table.filename=文件名
|
||||
termora.transport.table.type=类型
|
||||
termora.transport.table.size=大小
|
||||
@@ -320,7 +332,7 @@ termora.transport.table.owner=所有者
|
||||
# contextmenu
|
||||
termora.transport.table.contextmenu.transfer=传输
|
||||
termora.transport.table.contextmenu.copy-path=复制路径
|
||||
termora.transport.table.contextmenu.open-in-folder=在${termora.finder}中打开
|
||||
termora.transport.table.contextmenu.open-in-folder=在{0}中打开
|
||||
termora.transport.table.contextmenu.change-permissions=更改权限...
|
||||
termora.transport.table.contextmenu.refresh=刷新
|
||||
termora.transport.table.contextmenu.compress=压缩
|
||||
@@ -426,6 +438,8 @@ termora.visual-window.system-information.mem=内存
|
||||
termora.visual-window.system-information.swap=交换
|
||||
termora.visual-window.system-information.filesystem=文件系统
|
||||
termora.visual-window.system-information.used-total=使用 / 大小
|
||||
termora.visual-window.toggle-window=切换窗口
|
||||
termora.visual-window.transport.question=更多功能
|
||||
|
||||
termora.floating-toolbar.close-in-current-tab=在当前标签页关闭
|
||||
|
||||
|
||||
@@ -228,6 +228,8 @@ termora.keymgr.table.name=名稱
|
||||
termora.keymgr.table.type=型別
|
||||
termora.keymgr.table.length=長度
|
||||
termora.keymgr.table.remark=備註
|
||||
termora.keymgr.export-done=匯出成功
|
||||
termora.keymgr.export-done-open-folder=匯出成功,是否需要打開所在資料夾?
|
||||
|
||||
termora.keymgr.ssh-copy-id.number=主機數量 [{0}] 公鑰數量 [{1}]
|
||||
termora.keymgr.ssh-copy-id.failed=複製失敗
|
||||
@@ -239,6 +241,7 @@ termora.tools.multiple=將命令傳送到目前視窗會話
|
||||
|
||||
# Tabbed
|
||||
termora.tabbed.contextmenu.rename=重新命名
|
||||
termora.tabbed.contextmenu.select-host=選取主機
|
||||
termora.tabbed.contextmenu.sftp-command=SFTP 終端
|
||||
termora.tabbed.contextmenu.sftp-not-install=沒有找到 SFTP 程序,請安裝後重試
|
||||
termora.tabbed.contextmenu.clone=克隆
|
||||
@@ -304,6 +307,13 @@ termora.transport.bookmarks=書籤管理
|
||||
termora.transport.bookmarks.up=上移
|
||||
termora.transport.bookmarks.down=下移
|
||||
|
||||
termora.transport.toolbar.prev=返回
|
||||
termora.transport.toolbar.home=預設目錄
|
||||
termora.transport.toolbar.next=前進
|
||||
termora.transport.toolbar.parent=父目錄
|
||||
termora.transport.toolbar.show-hide=顯示/隱藏目錄
|
||||
termora.transport.toolbar.refresh=重新整理
|
||||
|
||||
termora.transport.table.filename=檔名
|
||||
termora.transport.table.type=類型
|
||||
termora.transport.table.size=大小
|
||||
@@ -315,7 +325,7 @@ termora.transport.table.owner=所有者
|
||||
# contextmenu
|
||||
termora.transport.table.contextmenu.transfer=傳輸
|
||||
termora.transport.table.contextmenu.copy-path=複製路徑
|
||||
termora.transport.table.contextmenu.open-in-folder=在${termora.finder}中打開
|
||||
termora.transport.table.contextmenu.open-in-folder=在{0}中打開
|
||||
termora.transport.table.contextmenu.change-permissions=更改權限...
|
||||
termora.transport.table.contextmenu.refresh=刷新
|
||||
termora.transport.table.contextmenu.compress=壓縮
|
||||
@@ -413,6 +423,8 @@ termora.visual-window.system-information.mem=內存
|
||||
termora.visual-window.system-information.swap=交換
|
||||
termora.visual-window.system-information.filesystem=檔案系統
|
||||
termora.visual-window.system-information.used-total=使用 / 大小
|
||||
termora.visual-window.toggle-window=切換視窗
|
||||
termora.visual-window.transport.question=更多功能
|
||||
|
||||
termora.floating-toolbar.close-in-current-tab=在目前標籤頁關閉
|
||||
|
||||
|
||||
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 |
@@ -34,6 +34,7 @@ DisableProgramGroupPage=yes
|
||||
;PrivilegesRequired=lowest
|
||||
OutputDir={#MyOutputDir}
|
||||
OutputBaseFilename={#MyAppName}-{#MyAppVersion}
|
||||
Compression=lzma2/max
|
||||
SolidCompression=yes
|
||||
WizardStyle=classic
|
||||
;WizardStyle=modern
|
||||
|
||||
12
src/test/resources/issue-1055/Dockerfile
Normal file
12
src/test/resources/issue-1055/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM debian:bookworm
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
RUN sed -i 's|http://deb.debian.org/debian|http://mirrors.aliyun.com/debian|g' /etc/apt/sources.list.d/debian.sources \
|
||||
&& sed -i 's|http://security.debian.org/debian-security|http://mirrors.aliyun.com/debian-security|g' /etc/apt/sources.list.d/debian.sources
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates autoconf libevent-dev bison automake libtool pkg-config build-essential libncurses-dev
|
||||
|
||||
RUN git clone https://github.com/tmux/tmux.git && cd tmux && sh autogen.sh && ./configure && make && make install
|
||||
|
||||
|
||||
Reference in New Issue
Block a user