mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-15 18:02:58 +08:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdf29b27e7 | ||
|
|
96da7eac41 | ||
|
|
71c0751692 | ||
|
|
442f334af2 | ||
|
|
48302a519f | ||
|
|
c00f759f15 | ||
|
|
1736dd909e | ||
|
|
1f01e368dd | ||
|
|
bfba958b7e | ||
|
|
758121b523 | ||
|
|
06e9a89e82 | ||
|
|
0ba6ac3305 | ||
|
|
993f220b8b | ||
|
|
8755c4ad23 | ||
|
|
77cb102dd6 | ||
|
|
89cfb0b451 | ||
|
|
6bdd83f208 | ||
|
|
8f86057dcc | ||
|
|
a7d7ffa2cc | ||
|
|
d51cbeee13 | ||
|
|
deb2a0151e | ||
|
|
e1c4e9312d | ||
|
|
c7233357bd | ||
|
|
eff8d565d0 | ||
|
|
932db49868 | ||
|
|
4d71c6cd05 | ||
|
|
96133e5abf | ||
|
|
f06e5d7dc1 | ||
|
|
d4b96edccf | ||
|
|
e9876d5b91 | ||
|
|
8b9a78a7bd | ||
|
|
6b48f577e9 | ||
|
|
da9b6c21d6 | ||
|
|
f1f889df14 | ||
|
|
ed65853ebe | ||
|
|
5ffdd219d9 | ||
|
|
4f84d6741c | ||
|
|
2568e7fcc8 | ||
|
|
dddbb49084 | ||
|
|
95846ab135 | ||
|
|
b5207e56c1 | ||
|
|
160771e912 | ||
|
|
0fbe180f3f | ||
|
|
41a0409e9e | ||
|
|
79e59143fb | ||
|
|
54e0f621ce |
@@ -20,7 +20,7 @@ plugins {
|
||||
|
||||
|
||||
group = "app.termora"
|
||||
version = "1.0.12"
|
||||
version = "1.0.14"
|
||||
|
||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
[versions]
|
||||
kotlin = "2.1.20"
|
||||
slf4j = "2.0.17"
|
||||
pty4j = "0.13.2"
|
||||
pty4j = "0.13.4"
|
||||
tinylog = "2.7.0"
|
||||
kotlinx-coroutines = "1.10.1"
|
||||
flatlaf = "3.5.4"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
flatlaf = "3.6"
|
||||
kotlinx-serialization-json = "1.8.1"
|
||||
commons-codec = "1.18.0"
|
||||
commons-lang3 = "3.17.0"
|
||||
commons-csv = "1.14.0"
|
||||
commons-net = "3.11.1"
|
||||
commons-text = "1.13.0"
|
||||
commons-text = "1.13.1"
|
||||
commons-compress = "1.27.1"
|
||||
commons-vfs2="2.10.0"
|
||||
swingx = "1.6.5-1"
|
||||
jgoodies-forms = "1.9.0"
|
||||
jfa = "1.2.0"
|
||||
oshi = "6.6.5"
|
||||
oshi = "6.8.1"
|
||||
versioncompare = "1.4.1"
|
||||
jna = "5.17.0"
|
||||
jSystemThemeDetector = "3.9.1"
|
||||
commons-io = "2.18.0"
|
||||
commons-io = "2.19.0"
|
||||
jbr-api = "17.1.10.1"
|
||||
hutool = "5.8.37"
|
||||
jsch = "0.2.21"
|
||||
jsch = "0.2.26"
|
||||
okhttp = "4.12.0"
|
||||
sshj = "0.39.0"
|
||||
sshd-core = "2.15.0"
|
||||
@@ -35,7 +35,7 @@ bip39 = "1.0.9"
|
||||
colorpicker = "2.0.1"
|
||||
rhino = "1.8.0"
|
||||
delight-rhino-sandbox = "0.0.17"
|
||||
testcontainers = "1.20.6"
|
||||
testcontainers = "1.21.0"
|
||||
mixpanel = "1.5.3"
|
||||
jSerialComm = "2.11.0"
|
||||
ini4j = "0.5.5-2"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0"
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0"
|
||||
}
|
||||
rootProject.name = "termora"
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.util.*;
|
||||
|
||||
@Deprecated
|
||||
public class CombinedKeyIdentityProvider implements KeyIdentityProvider {
|
||||
|
||||
private final List<KeyIdentityProvider> providers = new ArrayList<>();
|
||||
|
||||
@@ -14,6 +14,7 @@ import static com.formdev.flatlaf.util.UIScale.scale;
|
||||
/**
|
||||
* 如果要升级 FlatLaf 需要检查是否兼容
|
||||
*/
|
||||
@Deprecated
|
||||
public class MyFlatTabbedPaneUI extends FlatTabbedPaneUI {
|
||||
@Override
|
||||
protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {
|
||||
|
||||
@@ -28,8 +28,13 @@ import java.awt.MenuItem
|
||||
import java.awt.PopupMenu
|
||||
import java.awt.SystemTray
|
||||
import java.awt.TrayIcon
|
||||
import java.awt.desktop.AppReopenedEvent
|
||||
import java.awt.desktop.AppReopenedListener
|
||||
import java.awt.desktop.SystemEventListener
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.WindowEvent
|
||||
import java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.*
|
||||
import kotlin.system.exitProcess
|
||||
@@ -64,6 +69,9 @@ class ApplicationRunner {
|
||||
fileSystemManager.filesCache = WeakRefFilesCache()
|
||||
fileSystemManager.init()
|
||||
VFS.setManager(fileSystemManager)
|
||||
|
||||
// async init
|
||||
BackgroundManager.getInstance().getBackgroundImage()
|
||||
}
|
||||
|
||||
// 设置 LAF
|
||||
@@ -78,9 +86,6 @@ class ApplicationRunner {
|
||||
// 启动主窗口
|
||||
val startMainFrame = measureTimeMillis { startMainFrame() }
|
||||
|
||||
// 设置托盘
|
||||
val setupSystemTray = measureTimeMillis { SwingUtilities.invokeLater { setupSystemTray() } }
|
||||
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("printSystemInfo: {}ms", printSystemInfo)
|
||||
log.debug("openDatabase: {}ms", openDatabase)
|
||||
@@ -89,7 +94,6 @@ class ApplicationRunner {
|
||||
log.debug("setupLaf: {}ms", setupLaf)
|
||||
log.debug("openDoor: {}ms", openDoor)
|
||||
log.debug("startMainFrame: {}ms", startMainFrame)
|
||||
log.debug("setupSystemTray: {}ms", setupSystemTray)
|
||||
}
|
||||
}.let {
|
||||
if (log.isDebugEnabled) {
|
||||
@@ -119,8 +123,24 @@ class ApplicationRunner {
|
||||
|
||||
TermoraFrameManager.getInstance().createWindow().isVisible = true
|
||||
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
SwingUtilities.invokeLater { FlatDesktop.setQuitHandler { quitHandler() } }
|
||||
if (SystemInfo.isMacOS) {
|
||||
SwingUtilities.invokeLater {
|
||||
|
||||
try {
|
||||
// 设置 Dock
|
||||
setupMacOSDock()
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
// Command + Q
|
||||
FlatDesktop.setQuitHandler { quitHandler() }
|
||||
}
|
||||
} else if (SystemInfo.isWindows) {
|
||||
// 设置托盘
|
||||
SwingUtilities.invokeLater { setupSystemTray() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,9 +176,13 @@ class ApplicationRunner {
|
||||
}
|
||||
|
||||
private fun quitHandler() {
|
||||
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
||||
frame.dispose()
|
||||
val windows = TermoraFrameManager.getInstance().getWindows()
|
||||
|
||||
for (frame in windows) {
|
||||
frame.dispatchEvent(WindowEvent(frame, WindowEvent.WINDOW_CLOSED))
|
||||
}
|
||||
|
||||
Disposer.dispose(TermoraFrameManager.getInstance())
|
||||
}
|
||||
|
||||
private fun loadSettings() {
|
||||
@@ -240,7 +264,35 @@ class ApplicationRunner {
|
||||
|
||||
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
||||
|
||||
}
|
||||
|
||||
private fun setupMacOSDock() {
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
val cls = Class.forName("com.apple.eawt.Application")
|
||||
val app = cls.getMethod("getApplication").invoke(null)
|
||||
val addAppEventListener = cls.getMethod("addAppEventListener", SystemEventListener::class.java)
|
||||
|
||||
addAppEventListener.invoke(app, object : AppReopenedListener {
|
||||
override fun appReopened(e: AppReopenedEvent) {
|
||||
val manager = TermoraFrameManager.getInstance()
|
||||
if (manager.getWindows().isEmpty()) {
|
||||
manager.createWindow().isVisible = true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 当应用程序销毁时,驻守线程也可以退出了
|
||||
Disposer.register(ApplicationScope.forApplicationScope(), object : Disposable {
|
||||
override fun dispose() {
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
})
|
||||
|
||||
// 驻守线程,不然当所有窗口都关闭时,程序会自动退出
|
||||
// wait application exit
|
||||
Thread.ofPlatform().daemon(false)
|
||||
.priority(Thread.MIN_PRIORITY)
|
||||
.start { countDownLatch.await() }
|
||||
}
|
||||
|
||||
private fun printSystemInfo() {
|
||||
|
||||
88
src/main/kotlin/app/termora/BackgroundManager.kt
Normal file
88
src/main/kotlin/app/termora/BackgroundManager.kt
Normal file
@@ -0,0 +1,88 @@
|
||||
package app.termora
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.File
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
class BackgroundManager private constructor() {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(BackgroundManager::class.java)
|
||||
fun getInstance(): BackgroundManager {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(BackgroundManager::class) { BackgroundManager() }
|
||||
}
|
||||
}
|
||||
|
||||
private val appearance get() = Database.getDatabase().appearance
|
||||
private var bufferedImage: BufferedImage? = null
|
||||
private var imageFilepath = StringUtils.EMPTY
|
||||
|
||||
fun setBackgroundImage(file: File) {
|
||||
synchronized(this) {
|
||||
try {
|
||||
bufferedImage = file.inputStream().use { ImageIO.read(it) }
|
||||
imageFilepath = file.absolutePath
|
||||
appearance.backgroundImage = file.absolutePath
|
||||
|
||||
SwingUtilities.invokeLater {
|
||||
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
||||
SwingUtilities.updateComponentTreeUI(window)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getBackgroundImage(): BufferedImage? {
|
||||
val bg = doGetBackgroundImage()
|
||||
if (bg == null) {
|
||||
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||
return null
|
||||
} else {
|
||||
JPopupMenu.setDefaultLightWeightPopupEnabled(true)
|
||||
}
|
||||
} else {
|
||||
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||
JPopupMenu.setDefaultLightWeightPopupEnabled(false)
|
||||
}
|
||||
}
|
||||
return bg
|
||||
}
|
||||
|
||||
private fun doGetBackgroundImage(): BufferedImage? {
|
||||
synchronized(this) {
|
||||
if (bufferedImage == null || imageFilepath.isEmpty()) {
|
||||
if (appearance.backgroundImage.isBlank()) {
|
||||
return null
|
||||
}
|
||||
val file = File(appearance.backgroundImage)
|
||||
if (file.exists()) {
|
||||
setBackgroundImage(file)
|
||||
}
|
||||
}
|
||||
|
||||
return bufferedImage
|
||||
}
|
||||
}
|
||||
|
||||
fun clearBackgroundImage() {
|
||||
synchronized(this) {
|
||||
bufferedImage = null
|
||||
imageFilepath = StringUtils.EMPTY
|
||||
appearance.backgroundImage = StringUtils.EMPTY
|
||||
SwingUtilities.invokeLater {
|
||||
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
||||
SwingUtilities.updateComponentTreeUI(window)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -643,6 +643,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
*/
|
||||
var backgroundRunning by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 背景图片的地址
|
||||
*/
|
||||
var backgroundImage by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
|
||||
/**
|
||||
* 语言
|
||||
*/
|
||||
|
||||
@@ -25,6 +25,7 @@ enum class Protocol {
|
||||
SSH,
|
||||
Local,
|
||||
Serial,
|
||||
RDP,
|
||||
|
||||
/**
|
||||
* 交互式的 SFTP,此协议只在系统内部交互不应该暴露给用户也不应该持久化
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.apache.sshd.client.session.ClientSession
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.util.*
|
||||
import javax.swing.*
|
||||
|
||||
class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
||||
@@ -54,7 +55,8 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
||||
isEnabled = false
|
||||
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
testConnection(pane.getHost())
|
||||
// 因为测试连接的时候从数据库读取会导致失效,所以这里生成随机ID
|
||||
testConnection(pane.getHost().copy(id = UUID.randomUUID().toSimpleString()))
|
||||
withContext(Dispatchers.Swing) {
|
||||
putValue(NAME, I18n.getString("termora.new-host.test-connection"))
|
||||
isEnabled = true
|
||||
|
||||
@@ -320,6 +320,7 @@ open class HostOptionsPane : OptionsPane() {
|
||||
protocolTypeComboBox.addItem(Protocol.SSH)
|
||||
protocolTypeComboBox.addItem(Protocol.Local)
|
||||
protocolTypeComboBox.addItem(Protocol.Serial)
|
||||
protocolTypeComboBox.addItem(Protocol.RDP)
|
||||
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||
|
||||
@@ -49,6 +49,7 @@ class HostTreeNode(host: Host) : SimpleTreeNode<Host>(host) {
|
||||
return when (host.protocol) {
|
||||
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||
Protocol.Serial -> if (selected && hasFocus) Icons.plugin.dark else Icons.plugin
|
||||
Protocol.RDP -> if (selected && hasFocus) Icons.microsoftWindows.dark else Icons.microsoftWindows
|
||||
else -> if (selected && hasFocus) Icons.terminal.dark else Icons.terminal
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ object Icons {
|
||||
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
|
||||
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
|
||||
val close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") }
|
||||
val settingSync by lazy { DynamicIcon("icons/settingSync.svg", "icons/settingSync_dark.svg") }
|
||||
val openInNewWindow by lazy { DynamicIcon("icons/openInNewWindow.svg", "icons/openInNewWindow_dark.svg") }
|
||||
val openInToolWindow by lazy { DynamicIcon("icons/openInToolWindow.svg", "icons/openInToolWindow_dark.svg") }
|
||||
val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
|
||||
@@ -63,6 +64,7 @@ object Icons {
|
||||
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
|
||||
val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") }
|
||||
val microsoft by lazy { DynamicIcon("icons/microsoft.svg", "icons/microsoft_dark.svg") }
|
||||
val microsoftWindows by lazy { DynamicIcon("icons/microsoftWindows.svg", "icons/microsoftWindows_dark.svg") }
|
||||
val tencent by lazy { DynamicIcon("icons/tencent.svg") }
|
||||
val google by lazy { DynamicIcon("icons/google-small.svg") }
|
||||
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.terminal.PtyConnector
|
||||
import app.termora.terminal.PtyConnectorDelegate
|
||||
import app.termora.terminal.PtyProcessConnector
|
||||
import org.apache.commons.io.Charsets
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.charset.StandardCharsets
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
PtyHostTerminalTab(windowScope, host) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(LocalTerminalTab::class.java)
|
||||
}
|
||||
|
||||
override suspend fun openPtyConnector(): PtyConnector {
|
||||
val winSize = terminalPanel.winSize()
|
||||
val ptyConnector = PtyConnectorFactory.getInstance().createPtyConnector(
|
||||
@@ -18,4 +28,42 @@ class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
return ptyConnector
|
||||
}
|
||||
|
||||
|
||||
override fun willBeClose(): Boolean {
|
||||
val ptyProcessConnector = getPtyProcessConnector() ?: return true
|
||||
val process = ptyProcessConnector.process
|
||||
var consoleProcessCount = 0
|
||||
|
||||
try {
|
||||
val processHandle = ProcessHandle.of(process.pid()).getOrNull()
|
||||
if (processHandle != null) {
|
||||
consoleProcessCount = processHandle.children().count().toInt()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
// 没有正在运行的进程
|
||||
if (consoleProcessCount < 1) return true
|
||||
|
||||
val owner = SwingUtilities.getWindowAncestor(terminalPanel) ?: return true
|
||||
return OptionPane.showConfirmDialog(
|
||||
owner,
|
||||
I18n.getString("termora.tabbed.local-tab.close-prompt"),
|
||||
messageType = JOptionPane.INFORMATION_MESSAGE,
|
||||
optionType = JOptionPane.OK_CANCEL_OPTION
|
||||
) == JOptionPane.OK_OPTION
|
||||
}
|
||||
|
||||
|
||||
private fun getPtyProcessConnector(): PtyProcessConnector? {
|
||||
var p = getPtyConnector() as PtyConnector?
|
||||
while (p != null) {
|
||||
if (p is PtyProcessConnector) return p
|
||||
if (p is PtyConnectorDelegate) p = p.ptyConnector
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.ui.FlatRootPaneUI
|
||||
import com.formdev.flatlaf.ui.FlatTitlePane
|
||||
|
||||
class MyFlatRootPaneUI : FlatRootPaneUI() {
|
||||
|
||||
fun getTitlePane(): FlatTitlePane? {
|
||||
return super.titlePane
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import java.awt.event.*
|
||||
import java.awt.image.BufferedImage
|
||||
import java.util.*
|
||||
import javax.swing.*
|
||||
import javax.swing.plaf.TabbedPaneUI
|
||||
import kotlin.math.abs
|
||||
|
||||
class MyTabbedPane : FlatTabbedPane() {
|
||||
@@ -21,18 +20,12 @@ class MyTabbedPane : FlatTabbedPane() {
|
||||
private val owner
|
||||
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||
.getData(DataProviders.TermoraFrame) as TermoraFrame
|
||||
private val myUI = MyFlatTabbedPaneUI()
|
||||
|
||||
init {
|
||||
isFocusable = false
|
||||
super.setUI(myUI)
|
||||
initEvents()
|
||||
}
|
||||
|
||||
override fun setUI(ui: TabbedPaneUI?) {
|
||||
super.setUI(myUI)
|
||||
}
|
||||
|
||||
override fun updateUI() {
|
||||
styleMap = mapOf(
|
||||
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground"),
|
||||
|
||||
@@ -37,6 +37,7 @@ import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.xpath.XPathConstants
|
||||
import javax.xml.xpath.XPathFactory
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
class NewHostTree : SimpleTree() {
|
||||
|
||||
companion object {
|
||||
@@ -97,7 +98,7 @@ class NewHostTree : SimpleTree() {
|
||||
// 是否显示更多信息
|
||||
if (isShowMoreInfo) {
|
||||
val color = if (sel) {
|
||||
if (tree.hasFocus()) {
|
||||
if (tree.hasFocus() || isPopupMenu) {
|
||||
UIManager.getColor("textHighlightText")
|
||||
} else {
|
||||
this.foreground
|
||||
@@ -110,15 +111,15 @@ class NewHostTree : SimpleTree() {
|
||||
"""<font color=rgb(${color.red},${color.green},${color.blue})>${it}</font>"""
|
||||
}
|
||||
|
||||
if (host.protocol == Protocol.SSH) {
|
||||
text =
|
||||
"<html>${host.name} ${fontTag.apply("${host.username}@${host.host}")}</html>"
|
||||
// @formatter:off
|
||||
if (host.protocol == Protocol.SSH || host.protocol == Protocol.RDP) {
|
||||
text = "<html>${host.name} ${fontTag.apply("${host.username}@${host.host}")}</html>"
|
||||
} else if (host.protocol == Protocol.Serial) {
|
||||
text =
|
||||
"<html>${host.name} ${fontTag.apply(host.options.serialComm.port)}</html>"
|
||||
text = "<html>${host.name} ${fontTag.apply(host.options.serialComm.port)}</html>"
|
||||
} else if (host.protocol == Protocol.Folder) {
|
||||
text = "<html>${host.name}${fontTag.apply(" (${node.childCount})")}</html>"
|
||||
text = "<html>${host.name}${fontTag.apply(" (${node.getAllChildren().size})")}</html>"
|
||||
}
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus)
|
||||
@@ -134,6 +135,7 @@ class NewHostTree : SimpleTree() {
|
||||
// double click
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (getPathForLocation(e.x, e.y) == null) return
|
||||
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||
val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return
|
||||
if (lastNode.host.protocol != Protocol.Folder) {
|
||||
|
||||
@@ -52,6 +52,7 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
|
||||
init {
|
||||
terminalPanel.dropFiles = false
|
||||
terminalPanel.dataProviderSupport.addData(DataProviders.TerminalTab, this)
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
@@ -222,6 +223,11 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
}
|
||||
}
|
||||
|
||||
override fun willBeClose(): Boolean {
|
||||
// 保存窗口状态
|
||||
terminalPanel.storeVisualWindows(host.id)
|
||||
return super.willBeClose()
|
||||
}
|
||||
|
||||
private inner class MySessionListener : SessionListener, Disposable {
|
||||
override fun sessionEvent(session: Session, event: Event) {
|
||||
|
||||
@@ -151,6 +151,7 @@ class ApplicationScope private constructor() : Scope() {
|
||||
}
|
||||
|
||||
fun windowScopes(): List<WindowScope> {
|
||||
if (scopes.isEmpty()) return emptyList()
|
||||
return scopes.values.toList()
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ import kotlinx.coroutines.swing.Swing
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.*
|
||||
import org.apache.commons.codec.binary.Base64
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
@@ -57,6 +59,7 @@ import java.awt.event.ItemListener
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.*
|
||||
import java.util.function.Consumer
|
||||
import javax.swing.*
|
||||
@@ -132,8 +135,11 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
|
||||
val preferredThemeBtn = JButton(Icons.settings)
|
||||
val opacitySpinner = NumberSpinner(100, 0, 100)
|
||||
val backgroundImageTextField = OutlineTextField()
|
||||
|
||||
private val appearance get() = database.appearance
|
||||
private val backgroundButton = JButton(Icons.folder)
|
||||
private val backgroundClearButton = FlatButton()
|
||||
|
||||
init {
|
||||
initView()
|
||||
@@ -142,7 +148,21 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
private fun initView() {
|
||||
|
||||
backgroundComBoBox.isEnabled = SystemInfo.isWindows
|
||||
backgroundComBoBox.isEnabled = SystemInfo.isWindows || SystemInfo.isMacOS
|
||||
backgroundImageTextField.isEditable = false
|
||||
backgroundImageTextField.trailingComponent = backgroundButton
|
||||
backgroundImageTextField.text = FilenameUtils.getName(appearance.backgroundImage)
|
||||
backgroundImageTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||
override fun changedUpdate(e: DocumentEvent) {
|
||||
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
|
||||
}
|
||||
})
|
||||
|
||||
backgroundClearButton.isFocusable = false
|
||||
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
|
||||
backgroundClearButton.icon = Icons.delete
|
||||
backgroundClearButton.buttonType = FlatButton.ButtonType.toolBarButton
|
||||
|
||||
|
||||
opacitySpinner.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows
|
||||
opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) {
|
||||
@@ -239,6 +259,46 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
|
||||
preferredThemeBtn.addActionListener { showPreferredThemeContextmenu() }
|
||||
|
||||
backgroundButton.addActionListener {
|
||||
val chooser = FileChooser()
|
||||
chooser.osxAllowedFileTypes = listOf("png", "jpg", "jpeg")
|
||||
chooser.allowsMultiSelection = false
|
||||
chooser.win32Filters.add(Pair("Image files", listOf("png", "jpg", "jpeg")))
|
||||
chooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
||||
chooser.showOpenDialog(owner).thenAccept {
|
||||
if (it.isNotEmpty()) {
|
||||
onSelectedBackgroundImage(it.first())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
backgroundClearButton.addActionListener {
|
||||
BackgroundManager.getInstance().clearBackgroundImage()
|
||||
backgroundImageTextField.text = StringUtils.EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSelectedBackgroundImage(file: File) {
|
||||
try {
|
||||
val destFile = FileUtils.getFile(Application.getBaseDataDir(), "background", file.name)
|
||||
FileUtils.forceMkdirParent(destFile)
|
||||
FileUtils.deleteQuietly(destFile)
|
||||
FileUtils.copyFile(file, destFile, StandardCopyOption.REPLACE_EXISTING)
|
||||
backgroundImageTextField.text = destFile.name
|
||||
BackgroundManager.getInstance().setBackgroundImage(destFile)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
SwingUtilities.invokeLater {
|
||||
OptionPane.showMessageDialog(
|
||||
owner,
|
||||
ExceptionUtils.getRootCauseMessage(e),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
@@ -308,7 +368,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private fun getFormPanel(): JPanel {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow, $formMargin, default, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
val box = FlatToolBar()
|
||||
box.add(followSystemCheckBox)
|
||||
@@ -330,6 +390,13 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
})).xy(5, rows).apply { rows += step }
|
||||
|
||||
|
||||
val bgClearBox = Box.createHorizontalBox()
|
||||
bgClearBox.add(backgroundClearButton)
|
||||
builder.add("${I18n.getString("termora.settings.appearance.background-image")}:").xy(1, rows)
|
||||
.add(backgroundImageTextField).xy(3, rows)
|
||||
.add(bgClearBox).xy(5, rows)
|
||||
.apply { rows += step }
|
||||
|
||||
builder.add("${I18n.getString("termora.settings.appearance.opacity")}:").xy(1, rows)
|
||||
.add(opacitySpinner).xy(3, rows).apply { rows += step }
|
||||
|
||||
@@ -595,7 +662,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
val gistTextField = OutlineTextField(255)
|
||||
val policyComboBox = JComboBox<SyncPolicy>()
|
||||
val domainTextField = OutlineTextField(255)
|
||||
val syncConfigButton = JButton(I18n.getString("termora.settings.sync"), Icons.download)
|
||||
val syncConfigButton = JButton(I18n.getString("termora.settings.sync"), Icons.settingSync)
|
||||
val exportConfigButton = JButton(I18n.getString("termora.settings.sync.export"), Icons.export)
|
||||
val importConfigButton = JButton(I18n.getString("termora.settings.sync.import"), Icons.import)
|
||||
val lastSyncTimeLabel = JLabel()
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.apache.sshd.common.channel.ChannelFactory
|
||||
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
||||
import org.apache.sshd.common.channel.PtyChannelConfigurationHolder
|
||||
import org.apache.sshd.common.cipher.CipherNone
|
||||
import org.apache.sshd.common.compression.BuiltinCompressions
|
||||
import org.apache.sshd.common.config.keys.KeyRandomArt
|
||||
import org.apache.sshd.common.config.keys.KeyUtils
|
||||
import org.apache.sshd.common.future.CloseFuture
|
||||
@@ -47,6 +48,7 @@ import org.apache.sshd.common.kex.BuiltinDHFactories
|
||||
import org.apache.sshd.common.keyprovider.KeyIdentityProvider
|
||||
import org.apache.sshd.common.session.Session
|
||||
import org.apache.sshd.common.session.SessionListener
|
||||
import org.apache.sshd.common.signature.BuiltinSignatures
|
||||
import org.apache.sshd.common.util.net.SshdSocketAddress
|
||||
import org.apache.sshd.core.CoreModuleProperties
|
||||
import org.apache.sshd.server.forward.AcceptAllForwardingFilter
|
||||
@@ -339,6 +341,24 @@ object SshClients {
|
||||
)
|
||||
builder.keyExchangeFactories(keyExchangeFactories)
|
||||
|
||||
val compressionFactories = ClientBuilder.setUpDefaultCompressionFactories(true).toMutableList()
|
||||
for (compression in listOf(
|
||||
BuiltinCompressions.none,
|
||||
BuiltinCompressions.zlib,
|
||||
BuiltinCompressions.delayedZlib
|
||||
)) {
|
||||
if (compressionFactories.contains(compression)) continue
|
||||
compressionFactories.add(compression)
|
||||
}
|
||||
builder.compressionFactories(compressionFactories)
|
||||
|
||||
val signatureFactories = ClientBuilder.setUpDefaultSignatureFactories(true).toMutableList()
|
||||
for (signature in BuiltinSignatures.entries) {
|
||||
if (signatureFactories.contains(signature)) continue
|
||||
signatureFactories.add(signature)
|
||||
}
|
||||
builder.signatureFactories(signatureFactories)
|
||||
|
||||
if (host.tunnelings.isEmpty() && host.options.jumpHosts.isEmpty()) {
|
||||
builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE)
|
||||
} else {
|
||||
|
||||
@@ -43,6 +43,9 @@ interface TerminalTab : Disposable, DataProvider {
|
||||
*/
|
||||
fun canClose(): Boolean = true
|
||||
|
||||
/**
|
||||
* 返回 true 表示可以关闭
|
||||
*/
|
||||
fun willBeClose(): Boolean = true
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,12 +7,13 @@ import app.termora.actions.DataProviders
|
||||
import app.termora.sftp.SFTPTab
|
||||
import app.termora.terminal.DataKey
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import com.formdev.flatlaf.ui.FlatRootPaneUI
|
||||
import com.formdev.flatlaf.ui.FlatTitlePane
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jetbrains.JBR
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Insets
|
||||
import java.awt.*
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.MouseListener
|
||||
@@ -42,7 +43,6 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
private val dataProviderSupport = DataProviderSupport()
|
||||
private val welcomePanel = WelcomePanel(windowScope)
|
||||
private val sftp get() = Database.getDatabase().sftp
|
||||
private val myUI = MyFlatRootPaneUI()
|
||||
private var notifyListeners = emptyArray<NotifyListener>()
|
||||
|
||||
|
||||
@@ -63,10 +63,9 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
}
|
||||
|
||||
override fun mouseDragged(e: MouseEvent) {
|
||||
val mouseLayer = getMouseLayer() ?: return
|
||||
getMouseMotionListener()?.mouseDragged(
|
||||
MouseEvent(
|
||||
mouseLayer,
|
||||
e.component,
|
||||
e.id,
|
||||
e.`when`,
|
||||
e.modifiersEx,
|
||||
@@ -87,19 +86,19 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
return getHandler() as? MouseMotionListener
|
||||
}
|
||||
|
||||
private fun getMouseLayer(): JComponent? {
|
||||
val titlePane = myUI.getTitlePane() ?: return null
|
||||
val handlerField = titlePane.javaClass.getDeclaredField("mouseLayer") ?: return null
|
||||
handlerField.isAccessible = true
|
||||
return handlerField.get(titlePane) as? JComponent
|
||||
}
|
||||
|
||||
private fun getHandler(): Any? {
|
||||
val titlePane = myUI.getTitlePane() ?: return null
|
||||
val titlePane = getTitlePane() ?: return null
|
||||
val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null
|
||||
handlerField.isAccessible = true
|
||||
return handlerField.get(titlePane)
|
||||
}
|
||||
|
||||
private fun getTitlePane(): FlatTitlePane? {
|
||||
val ui = rootPane.ui as? FlatRootPaneUI ?: return null
|
||||
val titlePaneField = ui.javaClass.getDeclaredField("titlePane")
|
||||
titlePaneField.isAccessible = true
|
||||
return titlePaneField.get(ui) as? FlatTitlePane
|
||||
}
|
||||
}
|
||||
toolbar.getJToolBar().addMouseListener(mouseAdapter)
|
||||
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
||||
@@ -173,7 +172,6 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
// Windows 10 会有1像素误差
|
||||
tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0)
|
||||
} else if (SystemInfo.isLinux) {
|
||||
rootPane.setUI(myUI)
|
||||
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
|
||||
}
|
||||
|
||||
@@ -213,6 +211,11 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
}
|
||||
}
|
||||
|
||||
val glassPane = GlassPane()
|
||||
rootPane.glassPane = glassPane
|
||||
glassPane.isOpaque = false
|
||||
glassPane.isVisible = true
|
||||
|
||||
|
||||
Disposer.register(windowScope, terminalTabbed)
|
||||
add(terminalTabbed, BorderLayout.CENTER)
|
||||
@@ -254,4 +257,19 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
super.addNotify()
|
||||
notifyListeners.forEach { it.addNotify() }
|
||||
}
|
||||
|
||||
|
||||
private class GlassPane : JComponent() {
|
||||
override fun paintComponent(g: Graphics) {
|
||||
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
|
||||
val g2d = g as Graphics2D
|
||||
g2d.composite = AlphaComposite.getInstance(
|
||||
AlphaComposite.SRC_OVER,
|
||||
if (FlatLaf.isLafDark()) 0.2f else 0.1f
|
||||
)
|
||||
g2d.drawImage(img, 0, 0, width, height, null)
|
||||
g2d.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import java.awt.Frame
|
||||
import java.awt.Window
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.swing.JFrame
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingUtilities
|
||||
@@ -24,7 +25,7 @@ import kotlin.math.max
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
|
||||
class TermoraFrameManager {
|
||||
class TermoraFrameManager : Disposable {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(TermoraFrameManager::class.java)
|
||||
@@ -37,6 +38,7 @@ class TermoraFrameManager {
|
||||
|
||||
private val frames = mutableListOf<TermoraFrame>()
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
private val isDisposed = AtomicBoolean(false)
|
||||
private val isBackgroundRunning get() = Database.getDatabase().appearance.backgroundRunning
|
||||
|
||||
fun createWindow(): TermoraFrame {
|
||||
@@ -80,6 +82,7 @@ class TermoraFrameManager {
|
||||
|
||||
|
||||
private fun registerCloseCallback(window: TermoraFrame) {
|
||||
val manager = this
|
||||
window.addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
|
||||
@@ -95,31 +98,49 @@ class TermoraFrameManager {
|
||||
Disposer.dispose(windowScope)
|
||||
|
||||
val windowScopes = ApplicationScope.windowScopes()
|
||||
if (windowScopes.isNotEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果已经没有 Window 域了,那么就可以退出程序了
|
||||
if (windowScopes.isEmpty()) {
|
||||
this@TermoraFrameManager.dispose()
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
Disposer.dispose(manager)
|
||||
} else if (SystemInfo.isMacOS) {
|
||||
// 如果 macOS 开启了后台运行,那么尽管所有窗口都没了,也不会退出
|
||||
if (isBackgroundRunning) {
|
||||
return
|
||||
}
|
||||
Disposer.dispose(manager)
|
||||
}
|
||||
}
|
||||
|
||||
override fun windowClosing(e: WindowEvent) {
|
||||
if (ApplicationScope.windowScopes().size == 1) {
|
||||
if (SystemInfo.isWindows && isBackgroundRunning) {
|
||||
// 最小化
|
||||
window.extendedState = window.extendedState or JFrame.ICONIFIED
|
||||
// 隐藏
|
||||
window.isVisible = false
|
||||
} else {
|
||||
if (OptionPane.showConfirmDialog(
|
||||
window,
|
||||
I18n.getString("termora.quit-confirm", Application.getName()),
|
||||
optionType = JOptionPane.YES_NO_OPTION,
|
||||
) == JOptionPane.YES_OPTION
|
||||
) {
|
||||
window.dispose()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (ApplicationScope.windowScopes().size != 1) {
|
||||
window.dispose()
|
||||
return
|
||||
}
|
||||
|
||||
// 如果 Windows 开启了后台运行,那么最小化
|
||||
if (SystemInfo.isWindows && isBackgroundRunning) {
|
||||
// 最小化
|
||||
window.extendedState = window.extendedState or JFrame.ICONIFIED
|
||||
// 隐藏
|
||||
window.isVisible = false
|
||||
return
|
||||
}
|
||||
|
||||
// 如果 macOS 已经开启了后台运行,那么直接销毁,因为会有一个进程驻守
|
||||
if (SystemInfo.isMacOS && isBackgroundRunning) {
|
||||
window.dispose()
|
||||
return
|
||||
}
|
||||
|
||||
val option = OptionPane.showConfirmDialog(
|
||||
window,
|
||||
I18n.getString("termora.quit-confirm", Application.getName()),
|
||||
optionType = JOptionPane.YES_NO_OPTION,
|
||||
)
|
||||
if (option == JOptionPane.YES_OPTION) {
|
||||
window.dispose()
|
||||
}
|
||||
}
|
||||
@@ -142,14 +163,16 @@ class TermoraFrameManager {
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispose() {
|
||||
Disposer.dispose(ApplicationScope.forApplicationScope())
|
||||
override fun dispose() {
|
||||
if (isDisposed.compareAndSet(false, true)) {
|
||||
Disposer.dispose(ApplicationScope.forApplicationScope())
|
||||
|
||||
try {
|
||||
Disposer.getTree().assertIsEmpty(true)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
try {
|
||||
Disposer.getTree().assertIsEmpty(true)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.*
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import javax.swing.JOptionPane
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class OpenHostAction : AnAction() {
|
||||
companion object {
|
||||
@@ -26,10 +39,70 @@ class OpenHostAction : AnAction() {
|
||||
Protocol.SSH -> SSHTerminalTab(windowScope, evt.host)
|
||||
Protocol.Serial -> SerialTerminalTab(windowScope, evt.host)
|
||||
Protocol.SFTPPty -> SFTPPtyTerminalTab(windowScope, evt.host)
|
||||
Protocol.RDP -> openRDP(windowScope, evt.host)
|
||||
else -> LocalTerminalTab(windowScope, evt.host)
|
||||
}
|
||||
|
||||
terminalTabbedManager.addTerminalTab(tab)
|
||||
tab.start()
|
||||
if (tab is TerminalTab) {
|
||||
terminalTabbedManager.addTerminalTab(tab)
|
||||
if (tab is PtyHostTerminalTab) {
|
||||
tab.start()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun openRDP(windowScope: WindowScope, host: Host) {
|
||||
if (SystemInfo.isLinux) {
|
||||
OptionPane.showMessageDialog(
|
||||
windowScope.window,
|
||||
"Linux cannot connect to Windows Remote Server, Supported only for macOS and Windows",
|
||||
messageType = JOptionPane.WARNING_MESSAGE
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (SystemInfo.isMacOS) {
|
||||
if (!FileUtils.getFile("/Applications/Windows App.app").exists()) {
|
||||
val option = OptionPane.showConfirmDialog(
|
||||
windowScope.window,
|
||||
"If you want to connect to a Windows Remote Server, You have to install the Windows App",
|
||||
optionType = JOptionPane.OK_CANCEL_OPTION
|
||||
)
|
||||
if (option == JOptionPane.OK_OPTION) {
|
||||
Application.browse(URI.create("https://apps.apple.com/app/windows-app/id1295203466"))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val sb = StringBuilder()
|
||||
sb.append("full address:s:").append(host.host).append(':').append(host.port).appendLine()
|
||||
sb.append("username:s:").append(host.username).appendLine()
|
||||
|
||||
val file = FileUtils.getFile(Application.getTemporaryDir(), UUID.randomUUID().toSimpleString() + ".rdp")
|
||||
file.outputStream().use { IOUtils.write(sb.toString(), it, Charsets.UTF_8) }
|
||||
|
||||
if (host.authentication.type == AuthenticationType.Password) {
|
||||
val systemClipboard = windowScope.window.toolkit.systemClipboard
|
||||
val password = host.authentication.password
|
||||
systemClipboard.setContents(StringSelection(password), null)
|
||||
// clear password
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
delay(30.seconds)
|
||||
if (systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||
if (systemClipboard.getData(DataFlavor.stringFlavor) == password) {
|
||||
systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (SystemInfo.isMacOS) {
|
||||
ProcessBuilder("open", file.absolutePath).start()
|
||||
} else if (SystemInfo.isWindows) {
|
||||
ProcessBuilder("mstsc", file.absolutePath).start()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
|
||||
val customBtn = JButton("Custom")
|
||||
customBtn.addActionListener {
|
||||
val dialog = MyColorPickerDialog(this)
|
||||
dialog.setLocationRelativeTo(this)
|
||||
dialog.colorPicker.color = defaultColor
|
||||
dialog.isVisible = true
|
||||
val color = dialog.color
|
||||
|
||||
@@ -24,6 +24,11 @@ data class KeywordHighlight(
|
||||
*/
|
||||
val matchCase: Boolean = false,
|
||||
|
||||
/**
|
||||
* 是否是正则表达式
|
||||
*/
|
||||
val regex: Boolean = false,
|
||||
|
||||
/**
|
||||
* 0 是取前景色
|
||||
*/
|
||||
|
||||
@@ -20,10 +20,8 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
||||
private val model = KeywordHighlightTableModel()
|
||||
private val table = FlatTable()
|
||||
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
|
||||
private val colorPalette by lazy {
|
||||
TerminalFactory.getInstance().createTerminal().getTerminalModel()
|
||||
.getColorPalette()
|
||||
}
|
||||
private val terminal by lazy { TerminalFactory.getInstance().createTerminal() }
|
||||
private val colorPalette by lazy { terminal.getTerminalModel().getColorPalette() }
|
||||
|
||||
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
|
||||
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
|
||||
@@ -130,6 +128,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
||||
|
||||
addBtn.addActionListener {
|
||||
val dialog = NewKeywordHighlightDialog(this, colorPalette)
|
||||
dialog.setLocationRelativeTo(this)
|
||||
dialog.isVisible = true
|
||||
val keywordHighlight = dialog.keywordHighlight
|
||||
if (keywordHighlight != null) {
|
||||
@@ -143,6 +142,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
||||
if (row > -1) {
|
||||
var keywordHighlight = model.getKeywordHighlight(row)
|
||||
val dialog = NewKeywordHighlightDialog(this, colorPalette)
|
||||
dialog.setLocationRelativeTo(this)
|
||||
dialog.keywordTextField.text = keywordHighlight.keyword
|
||||
dialog.descriptionTextField.text = keywordHighlight.description
|
||||
|
||||
@@ -176,6 +176,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
||||
dialog.underlineCheckBox.isSelected = keywordHighlight.underline
|
||||
dialog.lineThroughCheckBox.isSelected = keywordHighlight.lineThrough
|
||||
dialog.matchCaseBtn.isSelected = keywordHighlight.matchCase
|
||||
dialog.regexBtn.isSelected = keywordHighlight.regex
|
||||
|
||||
dialog.isVisible = true
|
||||
|
||||
@@ -211,6 +212,12 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
||||
editBtn.isEnabled = table.selectedRowCount > 0
|
||||
deleteBtn.isEnabled = editBtn.isEnabled
|
||||
}
|
||||
|
||||
Disposer.register(disposable, object : Disposable {
|
||||
override fun dispose() {
|
||||
terminal.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
|
||||
@@ -5,6 +5,7 @@ import app.termora.terminal.*
|
||||
import app.termora.terminal.panel.TerminalDisplay
|
||||
import app.termora.terminal.panel.TerminalPaintListener
|
||||
import app.termora.terminal.panel.TerminalPanel
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Graphics
|
||||
import kotlin.math.min
|
||||
import kotlin.random.Random
|
||||
@@ -18,9 +19,10 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
|
||||
}
|
||||
|
||||
private val tag = Random.nextInt()
|
||||
private val log = LoggerFactory.getLogger(KeywordHighlightPaintListener::class.java)
|
||||
}
|
||||
|
||||
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
|
||||
private val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
||||
|
||||
override fun before(
|
||||
offset: Int,
|
||||
@@ -36,7 +38,8 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
|
||||
}
|
||||
|
||||
val document = terminal.getDocument()
|
||||
val kinds = SubstrFinder(object : Iterator<TerminalLine> {
|
||||
val kinds = mutableListOf<FindKind>()
|
||||
val iterator = object : Iterator<TerminalLine> {
|
||||
private var index = offset + 1
|
||||
private val maxCount = min(index + count, document.getLineCount())
|
||||
override fun hasNext(): Boolean {
|
||||
@@ -46,8 +49,24 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
|
||||
override fun next(): TerminalLine {
|
||||
return document.getLine(index++)
|
||||
}
|
||||
}
|
||||
|
||||
}, CharArraySubstr(highlight.keyword.toCharArray())).find(!highlight.matchCase)
|
||||
if (highlight.regex) {
|
||||
try {
|
||||
val regex = if (highlight.matchCase)
|
||||
highlight.keyword.toRegex()
|
||||
else highlight.keyword.toRegex(RegexOption.IGNORE_CASE)
|
||||
RegexFinder(regex, iterator).find()
|
||||
.apply { kinds.addAll(this) }
|
||||
} catch (e: Exception) {
|
||||
if (log.isDebugEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SubstrFinder(iterator, CharArraySubstr(highlight.keyword.toCharArray())).find(!highlight.matchCase)
|
||||
.apply { kinds.addAll(this) }
|
||||
}
|
||||
|
||||
for (kind in kinds) {
|
||||
terminal.getMarkupModel().addHighlighter(
|
||||
@@ -77,6 +96,74 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
|
||||
terminal.getMarkupModel().removeAllHighlighters(tag)
|
||||
}
|
||||
|
||||
private class RegexFinder(
|
||||
private val regex: Regex,
|
||||
private val iterator: Iterator<TerminalLine>
|
||||
) {
|
||||
private data class Coords(val row: Int, val col: Int)
|
||||
private data class MatchResultWithCoords(
|
||||
val match: String,
|
||||
val coords: List<Coords>
|
||||
)
|
||||
|
||||
fun find(): List<FindKind> {
|
||||
|
||||
val lines = mutableListOf<TerminalLine>()
|
||||
val kinds = mutableListOf<FindKind>()
|
||||
|
||||
for ((index, line) in iterator.withIndex()) {
|
||||
|
||||
lines.add(line)
|
||||
if (line.wrapped) continue
|
||||
|
||||
val data = mutableListOf<MutableList<Char>>()
|
||||
for (e in lines) {
|
||||
data.add(mutableListOf())
|
||||
for (c in e.chars()) {
|
||||
if (c.first.isNull) break
|
||||
data.last().add(c.first)
|
||||
}
|
||||
}
|
||||
|
||||
lines.clear()
|
||||
|
||||
val resultWithCoords = findMatchesWithCoords(data)
|
||||
if (resultWithCoords.isEmpty()) continue
|
||||
val offset = index - data.size + 1
|
||||
|
||||
for (e in resultWithCoords) {
|
||||
val coords = e.coords
|
||||
if (coords.isEmpty()) continue
|
||||
kinds.add(
|
||||
FindKind(
|
||||
startPosition = Position(coords.first().row + offset + 1, coords.first().col + 1),
|
||||
endPosition = Position(coords.last().row + offset + 1, coords.last().col + 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return kinds
|
||||
}
|
||||
|
||||
private fun findMatchesWithCoords(data: List<List<Char>>): List<MatchResultWithCoords> {
|
||||
val flatChars = StringBuilder()
|
||||
val indexMap = mutableListOf<Coords>()
|
||||
|
||||
// 拉平成字符串,并记录每个字符的位置
|
||||
for ((rowIndex, row) in data.withIndex()) {
|
||||
for ((colIndex, char) in row.withIndex()) {
|
||||
flatChars.append(char)
|
||||
indexMap.add(Coords(rowIndex, colIndex))
|
||||
}
|
||||
}
|
||||
|
||||
return regex.findAll(flatChars.toString())
|
||||
.map { MatchResultWithCoords(it.value, indexMap.subList(it.range.first, it.range.last + 1)) }
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class KeywordHighlightHighlighter(
|
||||
range: HighlighterRange, terminal: Terminal,
|
||||
@@ -93,4 +180,6 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package app.termora.highlight
|
||||
|
||||
import app.termora.DialogWrapper
|
||||
import app.termora.DynamicColor
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.Database
|
||||
import app.termora.*
|
||||
import app.termora.terminal.ColorPalette
|
||||
import app.termora.terminal.TerminalColor
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
@@ -46,6 +42,7 @@ class NewKeywordHighlightDialog(
|
||||
I18n.getString("termora.highlight.background-color")
|
||||
)
|
||||
val matchCaseBtn = JToggleButton(Icons.matchCase)
|
||||
val regexBtn = JToggleButton(Icons.regex)
|
||||
|
||||
|
||||
private val textColorRevert = JButton(Icons.revert)
|
||||
@@ -85,6 +82,7 @@ class NewKeywordHighlightDialog(
|
||||
|
||||
val box = FlatToolBar()
|
||||
box.add(matchCaseBtn)
|
||||
box.add(regexBtn)
|
||||
keywordTextField.trailingComponent = box
|
||||
|
||||
repaintKeywordHighlightView()
|
||||
@@ -187,6 +185,7 @@ class NewKeywordHighlightDialog(
|
||||
}
|
||||
|
||||
private fun createColorPanel(color: Color, title: String): ColorPanel {
|
||||
val owner = this
|
||||
val arc = UIManager.getInt("Component.arc")
|
||||
val lineBorder = FlatLineBorder(Insets(1, 1, 1, 1), DynamicColor.BorderColor, 1f, arc)
|
||||
val colorPanel = ColorPanel(color)
|
||||
@@ -195,7 +194,8 @@ class NewKeywordHighlightDialog(
|
||||
colorPanel.addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (SwingUtilities.isLeftMouseButton(e)) {
|
||||
val dialog = ChooseColorTemplateDialog(this@NewKeywordHighlightDialog, title)
|
||||
val dialog = ChooseColorTemplateDialog(owner, title)
|
||||
dialog.setLocationRelativeTo(owner)
|
||||
dialog.defaultColor = colorPanel.color
|
||||
dialog.isVisible = true
|
||||
colorPanel.color = dialog.color ?: return
|
||||
@@ -218,6 +218,7 @@ class NewKeywordHighlightDialog(
|
||||
keyword = keywordTextField.text,
|
||||
description = descriptionTextField.text,
|
||||
matchCase = matchCaseBtn.isSelected,
|
||||
regex = regexBtn.isSelected,
|
||||
textColor = if (textColor.colorIndex != -1) textColor.colorIndex else textColor.color.toRGB(),
|
||||
backgroundColor = if (backgroundColor.colorIndex != -1) backgroundColor.colorIndex else backgroundColor.color.toRGB(),
|
||||
bold = boldCheckBox.isSelected,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora.keymap
|
||||
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.KeyStroke
|
||||
@@ -23,7 +24,14 @@ class KeyShortcut(val keyStroke: KeyStroke) : Shortcut() {
|
||||
text = text.replace("MINUS", "-")
|
||||
}
|
||||
|
||||
return text.toCharArray().joinToString(" + ")
|
||||
text = text.toCharArray().joinToString(" + ")
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
text = text.replace("⇧", "Shift")
|
||||
text = text.replace("⌃", "Ctrl")
|
||||
text = text.replace("⌥", "Alt")
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package app.termora.keymap
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.*
|
||||
import javax.swing.KeyStroke
|
||||
|
||||
@@ -12,6 +11,10 @@ open class Keymap(
|
||||
*/
|
||||
private val parent: Keymap?,
|
||||
val isReadonly: Boolean = false,
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
var updateDate: Long = 0L,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -23,7 +26,8 @@ open class Keymap(
|
||||
val shortcuts = mutableListOf<Keymap>()
|
||||
val name = json["name"]?.jsonPrimitive?.content ?: return null
|
||||
val readonly = json["readonly"]?.jsonPrimitive?.booleanOrNull ?: return null
|
||||
val keymap = Keymap(name, null, readonly)
|
||||
val updateDate = json["updateDate"]?.jsonPrimitive?.longOrNull ?: 0
|
||||
val keymap = Keymap(name, null, readonly, updateDate)
|
||||
|
||||
for (shortcut in (json["shortcuts"]?.jsonArray ?: emptyList()).map { it.jsonObject }) {
|
||||
val keyStroke = shortcut["keyStroke"]?.jsonPrimitive?.contentOrNull ?: continue
|
||||
@@ -40,6 +44,9 @@ open class Keymap(
|
||||
}
|
||||
}
|
||||
|
||||
// 最后设置修改时间
|
||||
keymap.updateDate = updateDate
|
||||
|
||||
shortcuts.add(keymap)
|
||||
return keymap
|
||||
}
|
||||
@@ -51,6 +58,7 @@ open class Keymap(
|
||||
val actionIds = shortcuts.getOrPut(shortcut) { mutableListOf() }
|
||||
actionIds.removeIf { it == actionId }
|
||||
actionIds.add(actionId)
|
||||
updateDate = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
open fun removeAllActionShortcuts(actionId: Any) {
|
||||
@@ -62,6 +70,7 @@ open class Keymap(
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
updateDate = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
open fun getShortcut(actionId: Any): List<Shortcut> {
|
||||
@@ -102,6 +111,7 @@ open class Keymap(
|
||||
return buildJsonObject {
|
||||
put("name", name)
|
||||
put("readonly", isReadonly)
|
||||
put("updateDate", updateDate)
|
||||
parent?.let { put("parent", it.name) }
|
||||
put("shortcuts", buildJsonArray {
|
||||
for (entry in shortcuts.entries) {
|
||||
|
||||
9
src/main/kotlin/app/termora/sftp/FileSystemProvider.kt
Normal file
9
src/main/kotlin/app/termora/sftp/FileSystemProvider.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package app.termora.sftp
|
||||
|
||||
import org.apache.commons.vfs2.FileSystem
|
||||
|
||||
|
||||
interface FileSystemProvider {
|
||||
fun getFileSystem(): FileSystem
|
||||
fun setFileSystem(fileSystem: FileSystem)
|
||||
}
|
||||
@@ -27,7 +27,7 @@ import javax.swing.filechooser.FileSystemView
|
||||
import kotlin.io.path.absolutePathString
|
||||
|
||||
class FileSystemViewNav(
|
||||
private val fileSystem: org.apache.commons.vfs2.FileSystem,
|
||||
private val fileSystemProvider: FileSystemProvider,
|
||||
private val homeDirectory: FileObject
|
||||
) : JPanel(BorderLayout()) {
|
||||
|
||||
@@ -103,7 +103,7 @@ class FileSystemViewNav(
|
||||
add(layeredPane, BorderLayout.CENTER)
|
||||
|
||||
|
||||
if (SystemInfo.isWindows && fileSystem is LocalFileSystem) {
|
||||
if (SystemInfo.isWindows && fileSystemProvider.getFileSystem() is LocalFileSystem) {
|
||||
try {
|
||||
for (root in fileSystemView.roots) {
|
||||
history.add(root.absolutePath)
|
||||
@@ -174,9 +174,14 @@ class FileSystemViewNav(
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val name = textField.text.trim()
|
||||
if (name.isBlank()) return
|
||||
val fileSystem = fileSystemProvider.getFileSystem()
|
||||
try {
|
||||
if (fileSystem is LocalFileSystem && SystemUtils.IS_OS_WINDOWS) {
|
||||
changeSelectedPath(fileSystem.resolveFile("file://${name}"))
|
||||
val file = VFS.getManager().resolveFile("file://${name}")
|
||||
if (!StringUtils.equals(file.fileSystem.rootURI, fileSystemProvider.getFileSystem().rootURI)) {
|
||||
fileSystemProvider.setFileSystem(file.fileSystem)
|
||||
}
|
||||
changeSelectedPath(file)
|
||||
} else {
|
||||
changeSelectedPath(fileSystem.resolveFile(name))
|
||||
}
|
||||
@@ -192,6 +197,7 @@ class FileSystemViewNav(
|
||||
private fun showComboBoxPopup() {
|
||||
|
||||
comboBox.removeAllItems()
|
||||
val fileSystem = fileSystemProvider.getFileSystem()
|
||||
|
||||
for (text in history) {
|
||||
val path = if (SystemInfo.isWindows && fileSystem is LocalFileSystem) {
|
||||
@@ -244,6 +250,13 @@ class FileSystemViewNav(
|
||||
textField.text = formatDisplayPath(file)
|
||||
textField.putClientProperty(PATH, file)
|
||||
|
||||
val fileSystem = fileSystemProvider.getFileSystem()
|
||||
if (SystemInfo.isWindows && fileSystem is LocalFileSystem) {
|
||||
if (!StringUtils.equals(fileSystem.rootURI, file.fileSystem.rootURI)) {
|
||||
fileSystemProvider.setFileSystem(file.fileSystem)
|
||||
}
|
||||
}
|
||||
|
||||
for (listener in listenerList.getListeners(ActionListener::class.java)) {
|
||||
listener.actionPerformed(ActionEvent(this, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
||||
}
|
||||
|
||||
@@ -5,14 +5,19 @@ import app.termora.actions.DataProvider
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.vfs2.sftp.MySftpFileSystem
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import org.apache.commons.vfs2.FileObject
|
||||
import org.apache.commons.vfs2.FileSystem
|
||||
import org.apache.commons.vfs2.VFS
|
||||
import org.apache.commons.vfs2.provider.local.LocalFileSystem
|
||||
import org.jdesktop.swingx.JXBusyLabel
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.*
|
||||
@@ -22,14 +27,14 @@ import javax.swing.*
|
||||
|
||||
class FileSystemViewPanel(
|
||||
val host: Host,
|
||||
val fileSystem: org.apache.commons.vfs2.FileSystem,
|
||||
private var fileSystem: FileSystem,
|
||||
private val transportManager: TransportManager,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
) : JPanel(BorderLayout()), Disposable, DataProvider {
|
||||
) : JPanel(BorderLayout()), Disposable, DataProvider, FileSystemProvider {
|
||||
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
private val sftp get() = Database.getDatabase().sftp
|
||||
private val table = FileSystemViewTable(fileSystem, transportManager, coroutineScope)
|
||||
private val table = FileSystemViewTable(this, transportManager, coroutineScope)
|
||||
private val disposed = AtomicBoolean(false)
|
||||
private var nextReloadTicks = emptyArray<Consumer<Unit>>()
|
||||
private val isLoading = AtomicBoolean(false)
|
||||
@@ -37,7 +42,7 @@ class FileSystemViewPanel(
|
||||
private val loadingPanel = LoadingPanel()
|
||||
private val layeredPane = LayeredPane()
|
||||
private val homeDirectory = getHomeDirectory()
|
||||
private val nav = FileSystemViewNav(fileSystem, homeDirectory)
|
||||
private val nav = FileSystemViewNav(this, homeDirectory)
|
||||
private var workdir = homeDirectory
|
||||
private val model get() = table.model as FileSystemViewTableModel
|
||||
private val showHiddenFilesKey = "termora.transport.host.${host.id}.show-hidden-files"
|
||||
@@ -173,7 +178,15 @@ class FileSystemViewPanel(
|
||||
}
|
||||
bookmarkBtn.isBookmark = !bookmarkBtn.isBookmark
|
||||
} else {
|
||||
changeWorkdir(fileSystem.resolveFile(e.actionCommand))
|
||||
if (fileSystem is LocalFileSystem && SystemUtils.IS_OS_WINDOWS) {
|
||||
val file = VFS.getManager().resolveFile("file://${e.actionCommand}")
|
||||
if (!StringUtils.equals(file.fileSystem.rootURI, fileSystem.rootURI)) {
|
||||
fileSystem = file.fileSystem
|
||||
}
|
||||
changeWorkdir(file)
|
||||
} else {
|
||||
changeWorkdir(fileSystem.resolveFile(e.actionCommand))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,8 +205,7 @@ class FileSystemViewPanel(
|
||||
button.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
if (model.rowCount < 1) return
|
||||
if (model.hasParent) return
|
||||
enterTableSelectionFolder(0)
|
||||
if (model.hasParent) enterTableSelectionFolder(0)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -373,6 +385,7 @@ class FileSystemViewPanel(
|
||||
}
|
||||
|
||||
private fun getHomeDirectory(): FileObject {
|
||||
val fileSystem = this.fileSystem
|
||||
if (fileSystem is MySftpFileSystem) {
|
||||
val host = fileSystem.getClientSession().getAttribute(SshClients.HOST_KEY)
|
||||
?: return fileSystem.resolveFile(fileSystem.getDefaultDir())
|
||||
@@ -384,8 +397,13 @@ class FileSystemViewPanel(
|
||||
}
|
||||
|
||||
if (sftp.defaultDirectory.isNotBlank()) {
|
||||
val resolveFile = fileSystem.resolveFile("file://${sftp.defaultDirectory}")
|
||||
val resolveFile = if (fileSystem is LocalFileSystem && SystemInfo.isWindows) {
|
||||
VFS.getManager().resolveFile("file://${sftp.defaultDirectory}")
|
||||
} else {
|
||||
fileSystem.resolveFile("file://${sftp.defaultDirectory}")
|
||||
}
|
||||
if (resolveFile.exists()) {
|
||||
setFileSystem(resolveFile.fileSystem)
|
||||
return resolveFile
|
||||
}
|
||||
}
|
||||
@@ -430,6 +448,14 @@ class FileSystemViewPanel(
|
||||
return if (dataKey == SFTPDataProviders.FileSystemViewTable) table as T else null
|
||||
}
|
||||
|
||||
override fun getFileSystem(): FileSystem {
|
||||
return fileSystem
|
||||
}
|
||||
|
||||
override fun setFileSystem(fileSystem: FileSystem) {
|
||||
this.fileSystem = fileSystem
|
||||
}
|
||||
|
||||
private class LoadingPanel : JPanel() {
|
||||
private val busyLabel = JXBusyLabel()
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Suppress("DuplicatedCode", "CascadeIf")
|
||||
class FileSystemViewTable(
|
||||
private val fileSystem: org.apache.commons.vfs2.FileSystem,
|
||||
private val fileSystemProvider: FileSystemProvider,
|
||||
private val transportManager: TransportManager,
|
||||
private val coroutineScope: CoroutineScope
|
||||
) : JTable(), Disposable {
|
||||
@@ -184,7 +184,7 @@ class FileSystemViewTable(
|
||||
val data = support.transferable.getTransferData(FileSystemTableRowTransferable.dataFlavor)
|
||||
return data is FileSystemTableRowTransferable && data.source != table
|
||||
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||
return fileSystem !is LocalFileSystem
|
||||
return fileSystemProvider.getFileSystem() !is LocalFileSystem
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -261,6 +261,7 @@ class FileSystemViewTable(
|
||||
private fun showContextMenu(rows: IntArray, e: MouseEvent) {
|
||||
val files = rows.map { model.getFileObject(it) }
|
||||
val hasParent = rows.contains(0)
|
||||
val fileSystem = fileSystemProvider.getFileSystem()
|
||||
|
||||
val popupMenu = FlatPopupMenu()
|
||||
val newMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.new"))
|
||||
@@ -571,7 +572,7 @@ class FileSystemViewTable(
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
|
||||
runCatching {
|
||||
if (fileSystem is MySftpFileSystem) {
|
||||
if (fileSystemProvider.getFileSystem() is MySftpFileSystem) {
|
||||
deleteSftpPaths(paths, rm)
|
||||
} else {
|
||||
deleteRecursively(paths)
|
||||
@@ -594,7 +595,7 @@ class FileSystemViewTable(
|
||||
|
||||
private fun deleteSftpPaths(files: List<FileObject>, rm: Boolean = false) {
|
||||
if (rm) {
|
||||
val session = (this.fileSystem as MySftpFileSystem).getClientSession()
|
||||
val session = (this.fileSystemProvider.getFileSystem() as MySftpFileSystem).getClientSession()
|
||||
for (path in files) {
|
||||
session.executeRemoteCommand(
|
||||
"rm -rf '${path.absolutePathString()}'",
|
||||
|
||||
@@ -127,7 +127,7 @@ class SFTPPanel : JPanel(BorderLayout()), DataProvider, Disposable {
|
||||
return
|
||||
}
|
||||
|
||||
val fs = c.fileSystem
|
||||
val fs = c.getFileSystem()
|
||||
val root = transportManager.root
|
||||
|
||||
transportManager.lock.withLock {
|
||||
|
||||
@@ -6,7 +6,10 @@ import app.termora.Icons
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.terminal.ControlCharacters
|
||||
import app.termora.terminal.Null
|
||||
import app.termora.terminal.panel.TerminalWriter
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.text.StringEscapeUtils
|
||||
|
||||
class SnippetAction private constructor() : AnAction(I18n.getString("termora.snippet.title"), Icons.codeSpan) {
|
||||
companion object {
|
||||
@@ -25,18 +28,30 @@ class SnippetAction private constructor() : AnAction(I18n.getString("termora.sni
|
||||
fun runSnippet(snippet: Snippet, writer: TerminalWriter) {
|
||||
if (snippet.type != SnippetType.Snippet) return
|
||||
val map = mapOf(
|
||||
"\\r" to ControlCharacters.CR,
|
||||
"\\n" to ControlCharacters.LF,
|
||||
"\\t" to ControlCharacters.TAB,
|
||||
"\n" to ControlCharacters.LF,
|
||||
"\r" to ControlCharacters.CR,
|
||||
"\t" to ControlCharacters.TAB,
|
||||
"\b" to ControlCharacters.BS,
|
||||
"\\a" to ControlCharacters.BEL,
|
||||
"\\e" to ControlCharacters.ESC,
|
||||
"\\b" to ControlCharacters.BS,
|
||||
)
|
||||
val chars = snippet.snippet.toCharArray()
|
||||
for (i in chars.indices) {
|
||||
val c = chars[i]
|
||||
if (i == 0) continue
|
||||
if (c != '\n') continue
|
||||
if (chars[i - 1] != '\\') continue
|
||||
// 每一行的最后一个 \ 比较特殊,先转成 null 然后再去 unescapeJava
|
||||
chars[i - 1] = Char.Null
|
||||
}
|
||||
|
||||
var text = snippet.snippet
|
||||
var text = chars.joinToString(StringUtils.EMPTY)
|
||||
text = StringEscapeUtils.unescapeJava(text)
|
||||
for (e in map.entries) {
|
||||
text = text.replace(e.key, e.value.toString())
|
||||
}
|
||||
text = snippet.snippet.replace(Char.Null, '\\')
|
||||
|
||||
writer.write(TerminalWriter.WriteRequest.fromBytes(text.toByteArray(writer.getCharset())))
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ class SnippetPanel : JPanel(BorderLayout()), Disposable {
|
||||
properties.getString("SnippetPanel.LeftPanel.width", "180").toIntOrNull() ?: 180,
|
||||
-1
|
||||
)
|
||||
leftPanel.minimumSize = Dimension(leftPanel.preferredSize.width, leftPanel.preferredSize.height)
|
||||
|
||||
rightPanel.border = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 1, 0, 0, DynamicColor.BorderColor),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.I18n
|
||||
import app.termora.SimpleTreeModel
|
||||
import javax.swing.tree.MutableTreeNode
|
||||
import javax.swing.tree.TreeNode
|
||||
@@ -8,7 +9,7 @@ class SnippetTreeModel : SimpleTreeModel<Snippet>(
|
||||
SnippetTreeNode(
|
||||
Snippet(
|
||||
id = "0",
|
||||
name = "全部片段",
|
||||
name = I18n.getString("termora.snippet.title"),
|
||||
type = SnippetType.Folder
|
||||
)
|
||||
)
|
||||
|
||||
@@ -390,7 +390,15 @@ abstract class SafetySyncer : Syncer {
|
||||
|
||||
protected fun decodeKeymaps(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
||||
|
||||
for (keymap in ohMyJson.decodeFromString<List<JsonObject>>(text).mapNotNull { Keymap.fromJSON(it) }) {
|
||||
val localKeymaps = keymapManager.getKeymaps().associateBy { it.name }
|
||||
val remoteKeymaps = ohMyJson.decodeFromString<List<JsonObject>>(text).mapNotNull { Keymap.fromJSON(it) }
|
||||
for (keymap in remoteKeymaps) {
|
||||
val localKeymap = localKeymaps[keymap.name]
|
||||
if (localKeymap != null) {
|
||||
if (localKeymap.updateDate > keymap.updateDate) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
keymapManager.addKeymap(keymap)
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@ class SyncManager private constructor() : Disposable {
|
||||
|
||||
sync(config)
|
||||
|
||||
sync.lastSyncTime = System.currentTimeMillis()
|
||||
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("Automatic synchronisation end")
|
||||
}
|
||||
|
||||
@@ -399,6 +399,16 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
||||
}
|
||||
}
|
||||
|
||||
// CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).
|
||||
'Z' -> {
|
||||
val count = args.toInt(1)
|
||||
val cursorModel = terminal.getCursorModel()
|
||||
for (i in 0 until count) {
|
||||
val x = terminal.getTabulator().previousTab(cursorModel.getPosition().x - 1) + 1
|
||||
terminal.getCursorModel().move(cursorModel.getPosition().y, x)
|
||||
}
|
||||
}
|
||||
|
||||
// split
|
||||
';' -> {
|
||||
args.append(ch)
|
||||
|
||||
@@ -6,7 +6,7 @@ import java.io.InputStreamReader
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
class PtyProcessConnector(private val process: PtyProcess, private val charset: Charset = StandardCharsets.UTF_8) :
|
||||
class PtyProcessConnector(val process: PtyProcess, private val charset: Charset = StandardCharsets.UTF_8) :
|
||||
StreamPtyConnector(process.inputStream, process.outputStream) {
|
||||
|
||||
private val reader = InputStreamReader(input)
|
||||
|
||||
@@ -14,11 +14,16 @@ import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
import com.formdev.flatlaf.ui.FlatRoundBorder
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.event.ActionListener
|
||||
import java.beans.PropertyChangeEvent
|
||||
import java.beans.PropertyChangeListener
|
||||
import java.util.*
|
||||
import javax.swing.JButton
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
private val floatingToolbarEnable get() = Database.getDatabase().terminal.floatingToolbar
|
||||
private var closed = false
|
||||
private val anEvent get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -72,6 +77,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
}
|
||||
|
||||
initActions()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
override fun updateUI() {
|
||||
@@ -123,12 +129,38 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
add(initCloseActionButton())
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
// 被添加到组件后
|
||||
addPropertyChangeListener("ancestor", object : PropertyChangeListener {
|
||||
override fun propertyChange(evt: PropertyChangeEvent) {
|
||||
removePropertyChangeListener("ancestor", this)
|
||||
SwingUtilities.invokeLater { resumeVisualWindows() }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun resumeVisualWindows() {
|
||||
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
|
||||
if (tab !is SSHTerminalTab) return
|
||||
val terminalPanel = tab.getData(DataProviders.TerminalPanel) ?: return
|
||||
terminalPanel.resumeVisualWindows(tab.host.id, object : DataProvider {
|
||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||
if (dataKey == DataProviders.TerminalTab) {
|
||||
return tab as T
|
||||
}
|
||||
return super.getData(dataKey)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private fun initServerInfoActionButton(): JButton {
|
||||
val btn = JButton(Icons.infoOutline)
|
||||
btn.toolTipText = I18n.getString("termora.visual-window.system-information")
|
||||
btn.addActionListener(object : AnAction() {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
||||
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
|
||||
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
||||
|
||||
if (tab !is SSHTerminalTab) {
|
||||
@@ -156,7 +188,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
btn.toolTipText = I18n.getString("termora.snippet.title")
|
||||
btn.addActionListener(object : AnAction() {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
||||
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
|
||||
val writer = tab.getData(DataProviders.TerminalWriter) ?: return
|
||||
val dialog = SnippetTreeDialog(evt.window)
|
||||
dialog.setLocationRelativeTo(btn)
|
||||
@@ -174,7 +206,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi")
|
||||
btn.addActionListener(object : AnAction() {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
||||
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
|
||||
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
||||
|
||||
if (tab !is SSHTerminalTab) {
|
||||
@@ -233,7 +265,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
|
||||
btn.addActionListener(object : AnAction() {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
||||
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
|
||||
if (tab.canReconnect()) {
|
||||
tab.reconnect()
|
||||
}
|
||||
@@ -242,8 +274,4 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
return btn
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
package app.termora.terminal.panel
|
||||
|
||||
import app.termora.Database
|
||||
import app.termora.Disposable
|
||||
import app.termora.Disposer
|
||||
import app.termora.SSHTerminalTab
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviderSupport
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.terminal.*
|
||||
import app.termora.terminal.panel.vw.VisualWindow
|
||||
import app.termora.terminal.panel.vw.VisualWindowManager
|
||||
import app.termora.terminal.panel.vw.*
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
@@ -44,15 +45,15 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
|
||||
val SelectCopy = DataKey(Boolean::class)
|
||||
}
|
||||
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
private val terminalBlink = TerminalBlink(terminal)
|
||||
private val terminalFindPanel = TerminalFindPanel(this, terminal)
|
||||
private val floatingToolbar = FloatingToolbarPanel()
|
||||
private val terminalDisplay = TerminalDisplay(this, terminal, terminalBlink)
|
||||
private val dataProviderSupport = DataProviderSupport()
|
||||
private val layeredPane = TerminalLayeredPane()
|
||||
private var visualWindows = emptyArray<VisualWindow>()
|
||||
|
||||
val scrollBar = TerminalScrollBar(this@TerminalPanel, terminalFindPanel, terminal)
|
||||
val scrollBar = TerminalScrollBar(this, terminalFindPanel, terminal)
|
||||
var enableFloatingToolbar = true
|
||||
set(value) {
|
||||
field = value
|
||||
@@ -63,6 +64,8 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
|
||||
}
|
||||
}
|
||||
|
||||
val dataProviderSupport = DataProviderSupport()
|
||||
|
||||
|
||||
/**
|
||||
* 键盘事件
|
||||
@@ -585,6 +588,37 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
|
||||
requestFocusInWindow()
|
||||
}
|
||||
|
||||
override fun resumeVisualWindows(id: String, dataProvider: DataProvider) {
|
||||
val windows = properties.getString("VisualWindow.${id}.store") ?: return
|
||||
for (name in windows.split(",")) {
|
||||
if (name == "NVIDIA-SMI") {
|
||||
addVisualWindow(
|
||||
NvidiaSMIVisualWindow(
|
||||
dataProvider.getData(DataProviders.TerminalTab) as SSHTerminalTab,
|
||||
this
|
||||
)
|
||||
)
|
||||
} else if (name == "SystemInformation") {
|
||||
addVisualWindow(
|
||||
SystemInformationVisualWindow(
|
||||
dataProvider.getData(DataProviders.TerminalTab) as SSHTerminalTab,
|
||||
this
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun storeVisualWindows(id: String) {
|
||||
val windows = mutableListOf<String>()
|
||||
for (window in getVisualWindows()) {
|
||||
if (window is Resumeable) {
|
||||
windows.add(window.getWindowName())
|
||||
}
|
||||
}
|
||||
properties.putString("VisualWindow.${id}.store", windows.joinToString(","))
|
||||
}
|
||||
|
||||
override fun getDimension(): Dimension {
|
||||
return Dimension(
|
||||
terminalDisplay.size.width + padding.left + padding.right,
|
||||
|
||||
@@ -2,6 +2,7 @@ package app.termora.terminal.panel
|
||||
|
||||
import app.termora.keymap.KeyShortcut
|
||||
import app.termora.keymap.KeymapManager
|
||||
import app.termora.terminal.ControlCharacters
|
||||
import app.termora.terminal.Terminal
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -78,6 +79,8 @@ class TerminalPanelKeyAdapter(
|
||||
val encode = terminal.getKeyEncoder().encode(AWTTerminalKeyEvent(e))
|
||||
if (encode.isNotEmpty()) {
|
||||
writer.write(TerminalWriter.WriteRequest.fromBytes(encode.toByteArray(writer.getCharset())))
|
||||
// scroll to bottom
|
||||
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
||||
e.consume()
|
||||
}
|
||||
|
||||
@@ -90,6 +93,8 @@ class TerminalPanelKeyAdapter(
|
||||
if (isAltPressedOnly(e) && Character.isDefined(e.keyChar)) {
|
||||
val c = String(charArrayOf(ASCII_ESC, simpleMapKeyCodeToChar(e)))
|
||||
writer.write(TerminalWriter.WriteRequest.fromBytes(c.toByteArray(writer.getCharset())))
|
||||
// scroll to bottom
|
||||
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
||||
e.consume()
|
||||
return
|
||||
}
|
||||
@@ -99,11 +104,12 @@ class TerminalPanelKeyAdapter(
|
||||
return
|
||||
}
|
||||
|
||||
if (Character.isISOControl(e.keyChar)) {
|
||||
val keyChar = mapKeyChar(e)
|
||||
if (Character.isISOControl(keyChar)) {
|
||||
terminal.getSelectionModel().clearSelection()
|
||||
// 如果不为空表示已经发送过了,所以这里为空的时候再发送
|
||||
if (encode.isEmpty()) {
|
||||
writer.write(TerminalWriter.WriteRequest.fromBytes("${e.keyChar}".toByteArray(writer.getCharset())))
|
||||
writer.write(TerminalWriter.WriteRequest.fromBytes("$keyChar".toByteArray(writer.getCharset())))
|
||||
e.consume()
|
||||
}
|
||||
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
||||
@@ -111,6 +117,21 @@ class TerminalPanelKeyAdapter(
|
||||
|
||||
}
|
||||
|
||||
private fun mapKeyChar(e: KeyEvent): Char {
|
||||
if (Character.isISOControl(e.keyChar)) {
|
||||
return e.keyChar
|
||||
}
|
||||
|
||||
val isCtrlPressedOnly = isCtrlPressedOnly(e)
|
||||
|
||||
// https://github.com/TermoraDev/termora/issues/478
|
||||
if (isCtrlPressedOnly && e.keyCode == KeyEvent.VK_OPEN_BRACKET) {
|
||||
return ControlCharacters.ESC
|
||||
}
|
||||
|
||||
return e.keyChar
|
||||
}
|
||||
|
||||
private fun isCtrlPressedOnly(e: KeyEvent): Boolean {
|
||||
val modifiersEx = e.modifiersEx
|
||||
return (modifiersEx and InputEvent.ALT_DOWN_MASK) == 0
|
||||
|
||||
@@ -47,6 +47,7 @@ class NvidiaSMIVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWind
|
||||
private val percentageBtn by lazy { JButton(if (isPercentage) Icons.text else Icons.percentage) }
|
||||
|
||||
init {
|
||||
Disposer.register(tab, this)
|
||||
initViews()
|
||||
initEvents()
|
||||
initVisualWindowPanel()
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
interface Resumeable
|
||||
@@ -1,6 +1,5 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import app.termora.Disposer
|
||||
import app.termora.SSHTerminalTab
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProviders
|
||||
@@ -11,11 +10,7 @@ abstract class SSHVisualWindow(
|
||||
protected val tab: SSHTerminalTab,
|
||||
id: String,
|
||||
visualWindowManager: VisualWindowManager
|
||||
) : VisualWindowPanel(id, visualWindowManager) {
|
||||
|
||||
init {
|
||||
Disposer.register(tab, this)
|
||||
}
|
||||
) : VisualWindowPanel(id, visualWindowManager), Resumeable {
|
||||
|
||||
override fun toggleWindow() {
|
||||
val evt = AnActionEvent(tab.getJComponent(), StringUtils.EMPTY, EventObject(this))
|
||||
|
||||
@@ -25,6 +25,7 @@ class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
||||
private val systemInformationPanel by lazy { SystemInformationPanel() }
|
||||
|
||||
init {
|
||||
Disposer.register(tab, this)
|
||||
initViews()
|
||||
initEvents()
|
||||
initVisualWindowPanel()
|
||||
@@ -137,7 +138,7 @@ class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
||||
private suspend fun refreshCPUAndMem(session: ClientSession) {
|
||||
|
||||
// top
|
||||
var pair = SshClients.execChannel(session, "top -bn1")
|
||||
val pair = SshClients.execChannel(session, "top -bn1")
|
||||
if (pair.first != 0) {
|
||||
return
|
||||
}
|
||||
@@ -236,7 +237,7 @@ class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
||||
private suspend fun refreshDisk(session: ClientSession) {
|
||||
|
||||
// df -h
|
||||
var pair = SshClients.execChannel(session, "df -B1")
|
||||
val pair = SshClients.execChannel(session, "df -B1")
|
||||
if (pair.first != 0) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -28,4 +28,9 @@ interface VisualWindow : Disposable {
|
||||
* 切换独立模式
|
||||
*/
|
||||
fun toggleWindow()
|
||||
|
||||
/**
|
||||
* 同一个类,返回的相同
|
||||
*/
|
||||
fun getWindowName(): String
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import app.termora.actions.DataProvider
|
||||
import java.awt.Dimension
|
||||
|
||||
interface VisualWindowManager {
|
||||
@@ -33,4 +34,14 @@ interface VisualWindowManager {
|
||||
* 获取管理器的宽高
|
||||
*/
|
||||
fun getDimension(): Dimension
|
||||
|
||||
/**
|
||||
* 恢复所有窗口
|
||||
*/
|
||||
fun resumeVisualWindows(id: String, dataProvider: DataProvider)
|
||||
|
||||
/**
|
||||
* 存储所有窗口
|
||||
*/
|
||||
fun storeVisualWindows(id: String)
|
||||
}
|
||||
@@ -2,10 +2,12 @@ package app.termora.terminal.panel.vw
|
||||
|
||||
import app.termora.*
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import java.awt.*
|
||||
import java.awt.event.*
|
||||
import java.beans.PropertyChangeEvent
|
||||
import java.beans.PropertyChangeListener
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
@@ -333,6 +335,15 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
title = getWindowTitle()
|
||||
isAlwaysOnTop = isAlwaysTop
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
val sizes = listOf(16, 20, 24, 28, 32, 48, 64)
|
||||
val loader = TermoraFrame::class.java.classLoader
|
||||
val images = sizes.mapNotNull { e ->
|
||||
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
|
||||
}
|
||||
iconImages = images
|
||||
}
|
||||
|
||||
initEvents()
|
||||
|
||||
init()
|
||||
@@ -374,4 +385,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getWindowName(): String {
|
||||
return id
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ termora.settings.appearance.language=Language
|
||||
termora.settings.appearance.i-want-to-translate=I want to translate
|
||||
termora.settings.appearance.follow-system=Sync with OS
|
||||
termora.settings.appearance.opacity=Opacity
|
||||
termora.settings.appearance.background-image=BG Image
|
||||
termora.settings.appearance.background-running=Backgrounding
|
||||
|
||||
termora.setting.security=Security
|
||||
@@ -230,6 +231,7 @@ termora.tabbed.contextmenu.close=Close
|
||||
termora.tabbed.contextmenu.close-other-tabs=Close Other Tabs
|
||||
termora.tabbed.contextmenu.close-all-tabs=Close All Tabs
|
||||
termora.tabbed.contextmenu.reconnect=Reconnect
|
||||
termora.tabbed.local-tab.close-prompt=Do you want to terminal a running process in this terminal?
|
||||
|
||||
# Terminal logger
|
||||
termora.terminal-logger=Terminal Logger
|
||||
|
||||
@@ -52,6 +52,7 @@ termora.settings.appearance.language=语言
|
||||
termora.settings.appearance.i-want-to-translate=我想要翻译
|
||||
termora.settings.appearance.follow-system=跟随系统
|
||||
termora.settings.appearance.opacity=透明度
|
||||
termora.settings.appearance.background-image=背景图
|
||||
termora.settings.appearance.background-running=后台运行
|
||||
|
||||
termora.setting.security=安全
|
||||
@@ -219,7 +220,7 @@ termora.tabbed.contextmenu.close=关闭
|
||||
termora.tabbed.contextmenu.close-other-tabs=关闭其他标签页
|
||||
termora.tabbed.contextmenu.close-all-tabs=关闭所有标签页
|
||||
termora.tabbed.contextmenu.reconnect=重新连接
|
||||
|
||||
termora.tabbed.local-tab.close-prompt=你想要终止这个终端中正在运行的进程吗?
|
||||
|
||||
|
||||
# Terminal logger
|
||||
@@ -298,7 +299,7 @@ termora.transport.sftp.status.done=已完成
|
||||
termora.transport.sftp.status.failed=已失败
|
||||
|
||||
|
||||
termora.transport.sftp.already-exists.message1=此文件夹已包含一下名称的对象
|
||||
termora.transport.sftp.already-exists.message1=此文件夹已包含以下名称的对象
|
||||
termora.transport.sftp.already-exists.message2=请选择要执行的操作
|
||||
termora.transport.sftp.already-exists.overwrite=覆盖
|
||||
termora.transport.sftp.already-exists.append=追加
|
||||
|
||||
@@ -53,6 +53,7 @@ termora.settings.appearance.language=語言
|
||||
termora.settings.appearance.i-want-to-translate=我想要翻譯
|
||||
termora.settings.appearance.follow-system=跟隨系統
|
||||
termora.settings.appearance.opacity=透明度
|
||||
termora.settings.appearance.background-image=背景圖
|
||||
termora.settings.appearance.background-running=後台運行
|
||||
|
||||
termora.setting.security=安全
|
||||
@@ -215,6 +216,7 @@ termora.tabbed.contextmenu.close=關閉
|
||||
termora.tabbed.contextmenu.close-other-tabs=關閉其他標籤頁
|
||||
termora.tabbed.contextmenu.close-all-tabs=關閉所有標籤
|
||||
termora.tabbed.contextmenu.reconnect=重新連接
|
||||
termora.tabbed.local-tab.close-prompt=你想要終止這個終端機中正在運作的進程嗎?
|
||||
|
||||
|
||||
|
||||
@@ -292,7 +294,7 @@ termora.transport.sftp.status.waiting=等待中
|
||||
termora.transport.sftp.status.done=已完成
|
||||
termora.transport.sftp.status.failed=已失敗
|
||||
|
||||
termora.transport.sftp.already-exists.message1=此資料夾已包含一下名稱的對象
|
||||
termora.transport.sftp.already-exists.message1=此資料夾已包含以下名稱的對象
|
||||
termora.transport.sftp.already-exists.message2=請選擇要執行的操作
|
||||
termora.transport.sftp.already-exists.overwrite=覆蓋
|
||||
termora.transport.sftp.already-exists.append=追加
|
||||
|
||||
4
src/main/resources/icons/microsoftWindows.svg
Normal file
4
src/main/resources/icons/microsoftWindows.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.00098 2H7.00195L7.00098 7H2L2.00098 2ZM8.00293 2H13V7H8.00293V2ZM2 7.99902L7 8V13.001L2 13V7.99902ZM8.00195 8H12.999L12.998 13.001H8.00195" fill="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 399 B |
4
src/main/resources/icons/microsoftWindows_dark.svg
Normal file
4
src/main/resources/icons/microsoftWindows_dark.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.00098 2H7.00195L7.00098 7H2L2.00098 2ZM8.00293 2H13V7H8.00293V2ZM2 7.99902L7 8V13.001L2 13V7.99902ZM8.00195 8H12.999L12.998 13.001H8.00195" fill="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 399 B |
7
src/main/resources/icons/settingSync.svg
Normal file
7
src/main/resources/icons/settingSync.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.5 9V8C3.5 4.96243 5.96243 2.5 9 2.5C10.1068 2.5 11.1372 2.82692 12 3.38947" stroke="#6C707E" stroke-linecap="round"/>
|
||||
<path d="M6 12.6105C6.86278 13.1731 7.89321 13.5 9 13.5C12.0376 13.5 14.5 11.0376 14.5 8V7" stroke="#6C707E" stroke-linecap="round"/>
|
||||
<path d="M1.37868 7.32133L3.5 9.44265L5.62132 7.32133" stroke="#6C707E" stroke-linecap="round"/>
|
||||
<path d="M12.3787 8.67867L14.5 6.55735L16.6213 8.67867" stroke="#6C707E" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 689 B |
7
src/main/resources/icons/settingSync_dark.svg
Normal file
7
src/main/resources/icons/settingSync_dark.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.5 9V8C3.5 4.96243 5.96243 2.5 9 2.5C10.1068 2.5 11.1372 2.82692 12 3.38947" stroke="#CED0D6" stroke-linecap="round"/>
|
||||
<path d="M6 12.6105C6.86278 13.1731 7.89321 13.5 9 13.5C12.0376 13.5 14.5 11.0376 14.5 8V7" stroke="#CED0D6" stroke-linecap="round"/>
|
||||
<path d="M1.37868 7.32133L3.5 9.44265L5.62132 7.32133" stroke="#CED0D6" stroke-linecap="round"/>
|
||||
<path d="M12.3787 8.67867L14.5 6.55735L16.6213 8.67867" stroke="#CED0D6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 689 B |
@@ -1,6 +1,6 @@
|
||||
FROM linuxserver/openssh-server
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||
&& apk update && apk add wget gcc g++ git make zsh htop stress-ng inetutils-telnet xclock xcalc xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
|
||||
&& apk update && apk add wget tmux gcc g++ git make zsh htop stress-ng inetutils-telnet xclock xcalc xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
|
||||
&& tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \
|
||||
&& ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz
|
||||
RUN sed -i 's/#AllowAgentForwarding yes/AllowAgentForwarding yes/g' /etc/ssh/sshd_config
|
||||
|
||||
Reference in New Issue
Block a user