mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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"
|
group = "app.termora"
|
||||||
version = "1.0.12"
|
version = "1.0.13"
|
||||||
|
|
||||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "2.1.20"
|
kotlin = "2.1.20"
|
||||||
slf4j = "2.0.17"
|
slf4j = "2.0.17"
|
||||||
pty4j = "0.13.2"
|
pty4j = "0.13.3"
|
||||||
tinylog = "2.7.0"
|
tinylog = "2.7.0"
|
||||||
kotlinx-coroutines = "1.10.1"
|
kotlinx-coroutines = "1.10.2"
|
||||||
flatlaf = "3.5.4"
|
flatlaf = "3.5.4"
|
||||||
kotlinx-serialization-json = "1.8.1"
|
kotlinx-serialization-json = "1.8.1"
|
||||||
commons-codec = "1.18.0"
|
commons-codec = "1.18.0"
|
||||||
commons-lang3 = "3.17.0"
|
commons-lang3 = "3.17.0"
|
||||||
commons-csv = "1.14.0"
|
commons-csv = "1.14.0"
|
||||||
commons-net = "3.11.1"
|
commons-net = "3.11.1"
|
||||||
commons-text = "1.13.0"
|
commons-text = "1.13.1"
|
||||||
commons-compress = "1.27.1"
|
commons-compress = "1.27.1"
|
||||||
commons-vfs2="2.10.0"
|
commons-vfs2="2.10.0"
|
||||||
swingx = "1.6.5-1"
|
swingx = "1.6.5-1"
|
||||||
@@ -23,7 +23,7 @@ jSystemThemeDetector = "3.9.1"
|
|||||||
commons-io = "2.18.0"
|
commons-io = "2.18.0"
|
||||||
jbr-api = "17.1.10.1"
|
jbr-api = "17.1.10.1"
|
||||||
hutool = "5.8.37"
|
hutool = "5.8.37"
|
||||||
jsch = "0.2.21"
|
jsch = "0.2.25"
|
||||||
okhttp = "4.12.0"
|
okhttp = "4.12.0"
|
||||||
sshj = "0.39.0"
|
sshj = "0.39.0"
|
||||||
sshd-core = "2.15.0"
|
sshd-core = "2.15.0"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
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"
|
rootProject.name = "termora"
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,13 @@ import java.awt.MenuItem
|
|||||||
import java.awt.PopupMenu
|
import java.awt.PopupMenu
|
||||||
import java.awt.SystemTray
|
import java.awt.SystemTray
|
||||||
import java.awt.TrayIcon
|
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.ActionEvent
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
import javax.imageio.ImageIO
|
import javax.imageio.ImageIO
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
@@ -64,6 +69,9 @@ class ApplicationRunner {
|
|||||||
fileSystemManager.filesCache = WeakRefFilesCache()
|
fileSystemManager.filesCache = WeakRefFilesCache()
|
||||||
fileSystemManager.init()
|
fileSystemManager.init()
|
||||||
VFS.setManager(fileSystemManager)
|
VFS.setManager(fileSystemManager)
|
||||||
|
|
||||||
|
// async init
|
||||||
|
BackgroundManager.getInstance().getBackgroundImage()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置 LAF
|
// 设置 LAF
|
||||||
@@ -78,9 +86,6 @@ class ApplicationRunner {
|
|||||||
// 启动主窗口
|
// 启动主窗口
|
||||||
val startMainFrame = measureTimeMillis { startMainFrame() }
|
val startMainFrame = measureTimeMillis { startMainFrame() }
|
||||||
|
|
||||||
// 设置托盘
|
|
||||||
val setupSystemTray = measureTimeMillis { SwingUtilities.invokeLater { setupSystemTray() } }
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("printSystemInfo: {}ms", printSystemInfo)
|
log.debug("printSystemInfo: {}ms", printSystemInfo)
|
||||||
log.debug("openDatabase: {}ms", openDatabase)
|
log.debug("openDatabase: {}ms", openDatabase)
|
||||||
@@ -89,7 +94,6 @@ class ApplicationRunner {
|
|||||||
log.debug("setupLaf: {}ms", setupLaf)
|
log.debug("setupLaf: {}ms", setupLaf)
|
||||||
log.debug("openDoor: {}ms", openDoor)
|
log.debug("openDoor: {}ms", openDoor)
|
||||||
log.debug("startMainFrame: {}ms", startMainFrame)
|
log.debug("startMainFrame: {}ms", startMainFrame)
|
||||||
log.debug("setupSystemTray: {}ms", setupSystemTray)
|
|
||||||
}
|
}
|
||||||
}.let {
|
}.let {
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
@@ -119,8 +123,24 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
TermoraFrameManager.getInstance().createWindow().isVisible = true
|
TermoraFrameManager.getInstance().createWindow().isVisible = true
|
||||||
|
|
||||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
if (SystemInfo.isMacOS) {
|
||||||
SwingUtilities.invokeLater { FlatDesktop.setQuitHandler { quitHandler() } }
|
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() {
|
private fun quitHandler() {
|
||||||
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
val windows = TermoraFrameManager.getInstance().getWindows()
|
||||||
frame.dispose()
|
|
||||||
|
for (frame in windows) {
|
||||||
|
frame.dispatchEvent(WindowEvent(frame, WindowEvent.WINDOW_CLOSED))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Disposer.dispose(TermoraFrameManager.getInstance())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadSettings() {
|
private fun loadSettings() {
|
||||||
@@ -240,7 +264,35 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
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() {
|
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 backgroundRunning by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 背景图片的地址
|
||||||
|
*/
|
||||||
|
var backgroundImage by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 语言
|
* 语言
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ object Icons {
|
|||||||
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
|
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 moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
|
||||||
val close by lazy { DynamicIcon("icons/close.svg", "icons/close_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 openInNewWindow by lazy { DynamicIcon("icons/openInNewWindow.svg", "icons/openInNewWindow_dark.svg") }
|
||||||
val openInToolWindow by lazy { DynamicIcon("icons/openInToolWindow.svg", "icons/openInToolWindow_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") }
|
val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
|
import app.termora.terminal.PtyConnectorDelegate
|
||||||
|
import app.termora.terminal.PtyProcessConnector
|
||||||
import org.apache.commons.io.Charsets
|
import org.apache.commons.io.Charsets
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
||||||
class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
||||||
PtyHostTerminalTab(windowScope, host) {
|
PtyHostTerminalTab(windowScope, host) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(LocalTerminalTab::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun openPtyConnector(): PtyConnector {
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
val winSize = terminalPanel.winSize()
|
val winSize = terminalPanel.winSize()
|
||||||
val ptyConnector = PtyConnectorFactory.getInstance().createPtyConnector(
|
val ptyConnector = PtyConnectorFactory.getInstance().createPtyConnector(
|
||||||
@@ -18,4 +28,42 @@ class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
return ptyConnector
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -52,6 +52,7 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
terminalPanel.dropFiles = false
|
terminalPanel.dropFiles = false
|
||||||
|
terminalPanel.dataProviderSupport.addData(DataProviders.TerminalTab, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getJComponent(): JComponent {
|
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 {
|
private inner class MySessionListener : SessionListener, Disposable {
|
||||||
override fun sessionEvent(session: Session, event: Event) {
|
override fun sessionEvent(session: Session, event: Event) {
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ class ApplicationScope private constructor() : Scope() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun windowScopes(): List<WindowScope> {
|
fun windowScopes(): List<WindowScope> {
|
||||||
|
if (scopes.isEmpty()) return emptyList()
|
||||||
return scopes.values.toList()
|
return scopes.values.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ import kotlinx.coroutines.swing.Swing
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import org.apache.commons.codec.binary.Base64
|
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.io.IOUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
@@ -57,6 +59,7 @@ import java.awt.event.ItemListener
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
@@ -132,8 +135,11 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
|
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
|
||||||
val preferredThemeBtn = JButton(Icons.settings)
|
val preferredThemeBtn = JButton(Icons.settings)
|
||||||
val opacitySpinner = NumberSpinner(100, 0, 100)
|
val opacitySpinner = NumberSpinner(100, 0, 100)
|
||||||
|
val backgroundImageTextField = OutlineTextField()
|
||||||
|
|
||||||
private val appearance get() = database.appearance
|
private val appearance get() = database.appearance
|
||||||
|
private val backgroundButton = JButton(Icons.folder)
|
||||||
|
private val backgroundClearButton = FlatButton()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -142,7 +148,21 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
private fun initView() {
|
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.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows
|
||||||
opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) {
|
opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) {
|
||||||
@@ -239,6 +259,46 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preferredThemeBtn.addActionListener { showPreferredThemeContextmenu() }
|
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 {
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
@@ -308,7 +368,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private fun getFormPanel(): JPanel {
|
private fun getFormPanel(): JPanel {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $formMargin, default:grow, $formMargin, default, default:grow",
|
"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()
|
val box = FlatToolBar()
|
||||||
box.add(followSystemCheckBox)
|
box.add(followSystemCheckBox)
|
||||||
@@ -330,6 +390,13 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
})).xy(5, rows).apply { rows += step }
|
})).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)
|
builder.add("${I18n.getString("termora.settings.appearance.opacity")}:").xy(1, rows)
|
||||||
.add(opacitySpinner).xy(3, rows).apply { rows += step }
|
.add(opacitySpinner).xy(3, rows).apply { rows += step }
|
||||||
|
|
||||||
@@ -595,7 +662,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
val gistTextField = OutlineTextField(255)
|
val gistTextField = OutlineTextField(255)
|
||||||
val policyComboBox = JComboBox<SyncPolicy>()
|
val policyComboBox = JComboBox<SyncPolicy>()
|
||||||
val domainTextField = OutlineTextField(255)
|
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 exportConfigButton = JButton(I18n.getString("termora.settings.sync.export"), Icons.export)
|
||||||
val importConfigButton = JButton(I18n.getString("termora.settings.sync.import"), Icons.import)
|
val importConfigButton = JButton(I18n.getString("termora.settings.sync.import"), Icons.import)
|
||||||
val lastSyncTimeLabel = JLabel()
|
val lastSyncTimeLabel = JLabel()
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ interface TerminalTab : Disposable, DataProvider {
|
|||||||
*/
|
*/
|
||||||
fun canClose(): Boolean = true
|
fun canClose(): Boolean = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示可以关闭
|
||||||
|
*/
|
||||||
fun willBeClose(): Boolean = true
|
fun willBeClose(): Boolean = true
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import app.termora.actions.DataProviders
|
|||||||
import app.termora.sftp.SFTPTab
|
import app.termora.sftp.SFTPTab
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
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.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jetbrains.JBR
|
import com.jetbrains.JBR
|
||||||
import org.apache.commons.lang3.ArrayUtils
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
import java.awt.BorderLayout
|
import java.awt.*
|
||||||
import java.awt.Dimension
|
|
||||||
import java.awt.Insets
|
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.awt.event.MouseListener
|
import java.awt.event.MouseListener
|
||||||
@@ -42,7 +43,6 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
private val dataProviderSupport = DataProviderSupport()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
private val welcomePanel = WelcomePanel(windowScope)
|
private val welcomePanel = WelcomePanel(windowScope)
|
||||||
private val sftp get() = Database.getDatabase().sftp
|
private val sftp get() = Database.getDatabase().sftp
|
||||||
private val myUI = MyFlatRootPaneUI()
|
|
||||||
private var notifyListeners = emptyArray<NotifyListener>()
|
private var notifyListeners = emptyArray<NotifyListener>()
|
||||||
|
|
||||||
|
|
||||||
@@ -88,18 +88,25 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getMouseLayer(): JComponent? {
|
private fun getMouseLayer(): JComponent? {
|
||||||
val titlePane = myUI.getTitlePane() ?: return null
|
val titlePane = getTitlePane() ?: return null
|
||||||
val handlerField = titlePane.javaClass.getDeclaredField("mouseLayer") ?: return null
|
val handlerField = titlePane.javaClass.getDeclaredField("mouseLayer") ?: return null
|
||||||
handlerField.isAccessible = true
|
handlerField.isAccessible = true
|
||||||
return handlerField.get(titlePane) as? JComponent
|
return handlerField.get(titlePane) as? JComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getHandler(): Any? {
|
private fun getHandler(): Any? {
|
||||||
val titlePane = myUI.getTitlePane() ?: return null
|
val titlePane = getTitlePane() ?: return null
|
||||||
val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null
|
val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null
|
||||||
handlerField.isAccessible = true
|
handlerField.isAccessible = true
|
||||||
return handlerField.get(titlePane)
|
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().addMouseListener(mouseAdapter)
|
||||||
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
||||||
@@ -173,7 +180,6 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
// Windows 10 会有1像素误差
|
// Windows 10 会有1像素误差
|
||||||
tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0)
|
tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0)
|
||||||
} else if (SystemInfo.isLinux) {
|
} else if (SystemInfo.isLinux) {
|
||||||
rootPane.setUI(myUI)
|
|
||||||
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
|
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +219,11 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val glassPane = GlassPane()
|
||||||
|
rootPane.glassPane = glassPane
|
||||||
|
glassPane.isOpaque = false
|
||||||
|
glassPane.isVisible = true
|
||||||
|
|
||||||
|
|
||||||
Disposer.register(windowScope, terminalTabbed)
|
Disposer.register(windowScope, terminalTabbed)
|
||||||
add(terminalTabbed, BorderLayout.CENTER)
|
add(terminalTabbed, BorderLayout.CENTER)
|
||||||
@@ -254,4 +265,19 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
super.addNotify()
|
super.addNotify()
|
||||||
notifyListeners.forEach { it.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.Window
|
||||||
import java.awt.event.WindowAdapter
|
import java.awt.event.WindowAdapter
|
||||||
import java.awt.event.WindowEvent
|
import java.awt.event.WindowEvent
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.swing.JFrame
|
import javax.swing.JFrame
|
||||||
import javax.swing.JOptionPane
|
import javax.swing.JOptionPane
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
@@ -24,7 +25,7 @@ import kotlin.math.max
|
|||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
|
||||||
class TermoraFrameManager {
|
class TermoraFrameManager : Disposable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(TermoraFrameManager::class.java)
|
private val log = LoggerFactory.getLogger(TermoraFrameManager::class.java)
|
||||||
@@ -37,6 +38,7 @@ class TermoraFrameManager {
|
|||||||
|
|
||||||
private val frames = mutableListOf<TermoraFrame>()
|
private val frames = mutableListOf<TermoraFrame>()
|
||||||
private val properties get() = Database.getDatabase().properties
|
private val properties get() = Database.getDatabase().properties
|
||||||
|
private val isDisposed = AtomicBoolean(false)
|
||||||
private val isBackgroundRunning get() = Database.getDatabase().appearance.backgroundRunning
|
private val isBackgroundRunning get() = Database.getDatabase().appearance.backgroundRunning
|
||||||
|
|
||||||
fun createWindow(): TermoraFrame {
|
fun createWindow(): TermoraFrame {
|
||||||
@@ -80,6 +82,7 @@ class TermoraFrameManager {
|
|||||||
|
|
||||||
|
|
||||||
private fun registerCloseCallback(window: TermoraFrame) {
|
private fun registerCloseCallback(window: TermoraFrame) {
|
||||||
|
val manager = this
|
||||||
window.addWindowListener(object : WindowAdapter() {
|
window.addWindowListener(object : WindowAdapter() {
|
||||||
override fun windowClosed(e: WindowEvent) {
|
override fun windowClosed(e: WindowEvent) {
|
||||||
|
|
||||||
@@ -95,31 +98,49 @@ class TermoraFrameManager {
|
|||||||
Disposer.dispose(windowScope)
|
Disposer.dispose(windowScope)
|
||||||
|
|
||||||
val windowScopes = ApplicationScope.windowScopes()
|
val windowScopes = ApplicationScope.windowScopes()
|
||||||
|
if (windowScopes.isNotEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 如果已经没有 Window 域了,那么就可以退出程序了
|
// 如果已经没有 Window 域了,那么就可以退出程序了
|
||||||
if (windowScopes.isEmpty()) {
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
this@TermoraFrameManager.dispose()
|
Disposer.dispose(manager)
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
// 如果 macOS 开启了后台运行,那么尽管所有窗口都没了,也不会退出
|
||||||
|
if (isBackgroundRunning) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Disposer.dispose(manager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun windowClosing(e: WindowEvent) {
|
override fun windowClosing(e: WindowEvent) {
|
||||||
if (ApplicationScope.windowScopes().size == 1) {
|
if (ApplicationScope.windowScopes().size != 1) {
|
||||||
if (SystemInfo.isWindows && isBackgroundRunning) {
|
window.dispose()
|
||||||
// 最小化
|
return
|
||||||
window.extendedState = window.extendedState or JFrame.ICONIFIED
|
}
|
||||||
// 隐藏
|
|
||||||
window.isVisible = false
|
// 如果 Windows 开启了后台运行,那么最小化
|
||||||
} else {
|
if (SystemInfo.isWindows && isBackgroundRunning) {
|
||||||
if (OptionPane.showConfirmDialog(
|
// 最小化
|
||||||
window,
|
window.extendedState = window.extendedState or JFrame.ICONIFIED
|
||||||
I18n.getString("termora.quit-confirm", Application.getName()),
|
// 隐藏
|
||||||
optionType = JOptionPane.YES_NO_OPTION,
|
window.isVisible = false
|
||||||
) == JOptionPane.YES_OPTION
|
return
|
||||||
) {
|
}
|
||||||
window.dispose()
|
|
||||||
}
|
// 如果 macOS 已经开启了后台运行,那么直接销毁,因为会有一个进程驻守
|
||||||
}
|
if (SystemInfo.isMacOS && isBackgroundRunning) {
|
||||||
} else {
|
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()
|
window.dispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,14 +163,16 @@ class TermoraFrameManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dispose() {
|
override fun dispose() {
|
||||||
Disposer.dispose(ApplicationScope.forApplicationScope())
|
if (isDisposed.compareAndSet(false, true)) {
|
||||||
|
Disposer.dispose(ApplicationScope.forApplicationScope())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Disposer.getTree().assertIsEmpty(true)
|
Disposer.getTree().assertIsEmpty(true)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (log.isErrorEnabled) {
|
if (log.isErrorEnabled) {
|
||||||
log.error(e.message, e)
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.termora.keymap
|
package app.termora.keymap
|
||||||
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import javax.swing.KeyStroke
|
import javax.swing.KeyStroke
|
||||||
|
|
||||||
@@ -12,6 +11,10 @@ open class Keymap(
|
|||||||
*/
|
*/
|
||||||
private val parent: Keymap?,
|
private val parent: Keymap?,
|
||||||
val isReadonly: Boolean = false,
|
val isReadonly: Boolean = false,
|
||||||
|
/**
|
||||||
|
* 修改时间
|
||||||
|
*/
|
||||||
|
var updateDate: Long = 0L,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -23,7 +26,8 @@ open class Keymap(
|
|||||||
val shortcuts = mutableListOf<Keymap>()
|
val shortcuts = mutableListOf<Keymap>()
|
||||||
val name = json["name"]?.jsonPrimitive?.content ?: return null
|
val name = json["name"]?.jsonPrimitive?.content ?: return null
|
||||||
val readonly = json["readonly"]?.jsonPrimitive?.booleanOrNull ?: 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 }) {
|
for (shortcut in (json["shortcuts"]?.jsonArray ?: emptyList()).map { it.jsonObject }) {
|
||||||
val keyStroke = shortcut["keyStroke"]?.jsonPrimitive?.contentOrNull ?: continue
|
val keyStroke = shortcut["keyStroke"]?.jsonPrimitive?.contentOrNull ?: continue
|
||||||
@@ -40,6 +44,9 @@ open class Keymap(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 最后设置修改时间
|
||||||
|
keymap.updateDate = updateDate
|
||||||
|
|
||||||
shortcuts.add(keymap)
|
shortcuts.add(keymap)
|
||||||
return keymap
|
return keymap
|
||||||
}
|
}
|
||||||
@@ -51,6 +58,7 @@ open class Keymap(
|
|||||||
val actionIds = shortcuts.getOrPut(shortcut) { mutableListOf() }
|
val actionIds = shortcuts.getOrPut(shortcut) { mutableListOf() }
|
||||||
actionIds.removeIf { it == actionId }
|
actionIds.removeIf { it == actionId }
|
||||||
actionIds.add(actionId)
|
actionIds.add(actionId)
|
||||||
|
updateDate = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun removeAllActionShortcuts(actionId: Any) {
|
open fun removeAllActionShortcuts(actionId: Any) {
|
||||||
@@ -62,6 +70,7 @@ open class Keymap(
|
|||||||
iterator.remove()
|
iterator.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateDate = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getShortcut(actionId: Any): List<Shortcut> {
|
open fun getShortcut(actionId: Any): List<Shortcut> {
|
||||||
@@ -102,6 +111,7 @@ open class Keymap(
|
|||||||
return buildJsonObject {
|
return buildJsonObject {
|
||||||
put("name", name)
|
put("name", name)
|
||||||
put("readonly", isReadonly)
|
put("readonly", isReadonly)
|
||||||
|
put("updateDate", updateDate)
|
||||||
parent?.let { put("parent", it.name) }
|
parent?.let { put("parent", it.name) }
|
||||||
put("shortcuts", buildJsonArray {
|
put("shortcuts", buildJsonArray {
|
||||||
for (entry in shortcuts.entries) {
|
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
|
import kotlin.io.path.absolutePathString
|
||||||
|
|
||||||
class FileSystemViewNav(
|
class FileSystemViewNav(
|
||||||
private val fileSystem: org.apache.commons.vfs2.FileSystem,
|
private val fileSystemProvider: FileSystemProvider,
|
||||||
private val homeDirectory: FileObject
|
private val homeDirectory: FileObject
|
||||||
) : JPanel(BorderLayout()) {
|
) : JPanel(BorderLayout()) {
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ class FileSystemViewNav(
|
|||||||
add(layeredPane, BorderLayout.CENTER)
|
add(layeredPane, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
|
||||||
if (SystemInfo.isWindows && fileSystem is LocalFileSystem) {
|
if (SystemInfo.isWindows && fileSystemProvider.getFileSystem() is LocalFileSystem) {
|
||||||
try {
|
try {
|
||||||
for (root in fileSystemView.roots) {
|
for (root in fileSystemView.roots) {
|
||||||
history.add(root.absolutePath)
|
history.add(root.absolutePath)
|
||||||
@@ -174,9 +174,14 @@ class FileSystemViewNav(
|
|||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
val name = textField.text.trim()
|
val name = textField.text.trim()
|
||||||
if (name.isBlank()) return
|
if (name.isBlank()) return
|
||||||
|
val fileSystem = fileSystemProvider.getFileSystem()
|
||||||
try {
|
try {
|
||||||
if (fileSystem is LocalFileSystem && SystemUtils.IS_OS_WINDOWS) {
|
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 {
|
} else {
|
||||||
changeSelectedPath(fileSystem.resolveFile(name))
|
changeSelectedPath(fileSystem.resolveFile(name))
|
||||||
}
|
}
|
||||||
@@ -192,6 +197,7 @@ class FileSystemViewNav(
|
|||||||
private fun showComboBoxPopup() {
|
private fun showComboBoxPopup() {
|
||||||
|
|
||||||
comboBox.removeAllItems()
|
comboBox.removeAllItems()
|
||||||
|
val fileSystem = fileSystemProvider.getFileSystem()
|
||||||
|
|
||||||
for (text in history) {
|
for (text in history) {
|
||||||
val path = if (SystemInfo.isWindows && fileSystem is LocalFileSystem) {
|
val path = if (SystemInfo.isWindows && fileSystem is LocalFileSystem) {
|
||||||
@@ -244,6 +250,13 @@ class FileSystemViewNav(
|
|||||||
textField.text = formatDisplayPath(file)
|
textField.text = formatDisplayPath(file)
|
||||||
textField.putClientProperty(PATH, 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)) {
|
for (listener in listenerList.getListeners(ActionListener::class.java)) {
|
||||||
listener.actionPerformed(ActionEvent(this, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
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.terminal.DataKey
|
||||||
import app.termora.vfs2.sftp.MySftpFileSystem
|
import app.termora.vfs2.sftp.MySftpFileSystem
|
||||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import org.apache.commons.vfs2.FileObject
|
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 org.jdesktop.swingx.JXBusyLabel
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.event.*
|
import java.awt.event.*
|
||||||
@@ -22,14 +27,14 @@ import javax.swing.*
|
|||||||
|
|
||||||
class FileSystemViewPanel(
|
class FileSystemViewPanel(
|
||||||
val host: Host,
|
val host: Host,
|
||||||
val fileSystem: org.apache.commons.vfs2.FileSystem,
|
private var fileSystem: FileSystem,
|
||||||
private val transportManager: TransportManager,
|
private val transportManager: TransportManager,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
) : JPanel(BorderLayout()), Disposable, DataProvider {
|
) : JPanel(BorderLayout()), Disposable, DataProvider, FileSystemProvider {
|
||||||
|
|
||||||
private val properties get() = Database.getDatabase().properties
|
private val properties get() = Database.getDatabase().properties
|
||||||
private val sftp get() = Database.getDatabase().sftp
|
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 val disposed = AtomicBoolean(false)
|
||||||
private var nextReloadTicks = emptyArray<Consumer<Unit>>()
|
private var nextReloadTicks = emptyArray<Consumer<Unit>>()
|
||||||
private val isLoading = AtomicBoolean(false)
|
private val isLoading = AtomicBoolean(false)
|
||||||
@@ -37,7 +42,7 @@ class FileSystemViewPanel(
|
|||||||
private val loadingPanel = LoadingPanel()
|
private val loadingPanel = LoadingPanel()
|
||||||
private val layeredPane = LayeredPane()
|
private val layeredPane = LayeredPane()
|
||||||
private val homeDirectory = getHomeDirectory()
|
private val homeDirectory = getHomeDirectory()
|
||||||
private val nav = FileSystemViewNav(fileSystem, homeDirectory)
|
private val nav = FileSystemViewNav(this, homeDirectory)
|
||||||
private var workdir = homeDirectory
|
private var workdir = homeDirectory
|
||||||
private val model get() = table.model as FileSystemViewTableModel
|
private val model get() = table.model as FileSystemViewTableModel
|
||||||
private val showHiddenFilesKey = "termora.transport.host.${host.id}.show-hidden-files"
|
private val showHiddenFilesKey = "termora.transport.host.${host.id}.show-hidden-files"
|
||||||
@@ -173,7 +178,15 @@ class FileSystemViewPanel(
|
|||||||
}
|
}
|
||||||
bookmarkBtn.isBookmark = !bookmarkBtn.isBookmark
|
bookmarkBtn.isBookmark = !bookmarkBtn.isBookmark
|
||||||
} else {
|
} 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() {
|
button.addActionListener(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
if (model.rowCount < 1) return
|
if (model.rowCount < 1) return
|
||||||
if (model.hasParent) return
|
if (model.hasParent) enterTableSelectionFolder(0)
|
||||||
enterTableSelectionFolder(0)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -373,6 +385,7 @@ class FileSystemViewPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getHomeDirectory(): FileObject {
|
private fun getHomeDirectory(): FileObject {
|
||||||
|
val fileSystem = this.fileSystem
|
||||||
if (fileSystem is MySftpFileSystem) {
|
if (fileSystem is MySftpFileSystem) {
|
||||||
val host = fileSystem.getClientSession().getAttribute(SshClients.HOST_KEY)
|
val host = fileSystem.getClientSession().getAttribute(SshClients.HOST_KEY)
|
||||||
?: return fileSystem.resolveFile(fileSystem.getDefaultDir())
|
?: return fileSystem.resolveFile(fileSystem.getDefaultDir())
|
||||||
@@ -384,8 +397,13 @@ class FileSystemViewPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sftp.defaultDirectory.isNotBlank()) {
|
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()) {
|
if (resolveFile.exists()) {
|
||||||
|
setFileSystem(resolveFile.fileSystem)
|
||||||
return resolveFile
|
return resolveFile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,6 +448,14 @@ class FileSystemViewPanel(
|
|||||||
return if (dataKey == SFTPDataProviders.FileSystemViewTable) table as T else null
|
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 class LoadingPanel : JPanel() {
|
||||||
private val busyLabel = JXBusyLabel()
|
private val busyLabel = JXBusyLabel()
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||||||
|
|
||||||
@Suppress("DuplicatedCode", "CascadeIf")
|
@Suppress("DuplicatedCode", "CascadeIf")
|
||||||
class FileSystemViewTable(
|
class FileSystemViewTable(
|
||||||
private val fileSystem: org.apache.commons.vfs2.FileSystem,
|
private val fileSystemProvider: FileSystemProvider,
|
||||||
private val transportManager: TransportManager,
|
private val transportManager: TransportManager,
|
||||||
private val coroutineScope: CoroutineScope
|
private val coroutineScope: CoroutineScope
|
||||||
) : JTable(), Disposable {
|
) : JTable(), Disposable {
|
||||||
@@ -184,7 +184,7 @@ class FileSystemViewTable(
|
|||||||
val data = support.transferable.getTransferData(FileSystemTableRowTransferable.dataFlavor)
|
val data = support.transferable.getTransferData(FileSystemTableRowTransferable.dataFlavor)
|
||||||
return data is FileSystemTableRowTransferable && data.source != table
|
return data is FileSystemTableRowTransferable && data.source != table
|
||||||
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||||
return fileSystem !is LocalFileSystem
|
return fileSystemProvider.getFileSystem() !is LocalFileSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -261,6 +261,7 @@ class FileSystemViewTable(
|
|||||||
private fun showContextMenu(rows: IntArray, e: MouseEvent) {
|
private fun showContextMenu(rows: IntArray, e: MouseEvent) {
|
||||||
val files = rows.map { model.getFileObject(it) }
|
val files = rows.map { model.getFileObject(it) }
|
||||||
val hasParent = rows.contains(0)
|
val hasParent = rows.contains(0)
|
||||||
|
val fileSystem = fileSystemProvider.getFileSystem()
|
||||||
|
|
||||||
val popupMenu = FlatPopupMenu()
|
val popupMenu = FlatPopupMenu()
|
||||||
val newMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.new"))
|
val newMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.new"))
|
||||||
@@ -571,7 +572,7 @@ class FileSystemViewTable(
|
|||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
if (fileSystem is MySftpFileSystem) {
|
if (fileSystemProvider.getFileSystem() is MySftpFileSystem) {
|
||||||
deleteSftpPaths(paths, rm)
|
deleteSftpPaths(paths, rm)
|
||||||
} else {
|
} else {
|
||||||
deleteRecursively(paths)
|
deleteRecursively(paths)
|
||||||
@@ -594,7 +595,7 @@ class FileSystemViewTable(
|
|||||||
|
|
||||||
private fun deleteSftpPaths(files: List<FileObject>, rm: Boolean = false) {
|
private fun deleteSftpPaths(files: List<FileObject>, rm: Boolean = false) {
|
||||||
if (rm) {
|
if (rm) {
|
||||||
val session = (this.fileSystem as MySftpFileSystem).getClientSession()
|
val session = (this.fileSystemProvider.getFileSystem() as MySftpFileSystem).getClientSession()
|
||||||
for (path in files) {
|
for (path in files) {
|
||||||
session.executeRemoteCommand(
|
session.executeRemoteCommand(
|
||||||
"rm -rf '${path.absolutePathString()}'",
|
"rm -rf '${path.absolutePathString()}'",
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class SFTPPanel : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val fs = c.fileSystem
|
val fs = c.getFileSystem()
|
||||||
val root = transportManager.root
|
val root = transportManager.root
|
||||||
|
|
||||||
transportManager.lock.withLock {
|
transportManager.lock.withLock {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import app.termora.actions.AnAction
|
|||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.terminal.ControlCharacters
|
import app.termora.terminal.ControlCharacters
|
||||||
import app.termora.terminal.panel.TerminalWriter
|
import app.termora.terminal.panel.TerminalWriter
|
||||||
|
import org.apache.commons.text.StringEscapeUtils
|
||||||
|
|
||||||
class SnippetAction private constructor() : AnAction(I18n.getString("termora.snippet.title"), Icons.codeSpan) {
|
class SnippetAction private constructor() : AnAction(I18n.getString("termora.snippet.title"), Icons.codeSpan) {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -25,15 +26,15 @@ class SnippetAction private constructor() : AnAction(I18n.getString("termora.sni
|
|||||||
fun runSnippet(snippet: Snippet, writer: TerminalWriter) {
|
fun runSnippet(snippet: Snippet, writer: TerminalWriter) {
|
||||||
if (snippet.type != SnippetType.Snippet) return
|
if (snippet.type != SnippetType.Snippet) return
|
||||||
val map = mapOf(
|
val map = mapOf(
|
||||||
"\\r" to ControlCharacters.CR,
|
"\n" to ControlCharacters.LF,
|
||||||
"\\n" to ControlCharacters.LF,
|
"\r" to ControlCharacters.CR,
|
||||||
"\\t" to ControlCharacters.TAB,
|
"\t" to ControlCharacters.TAB,
|
||||||
|
"\b" to ControlCharacters.BS,
|
||||||
"\\a" to ControlCharacters.BEL,
|
"\\a" to ControlCharacters.BEL,
|
||||||
"\\e" to ControlCharacters.ESC,
|
"\\e" to ControlCharacters.ESC,
|
||||||
"\\b" to ControlCharacters.BS,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var text = snippet.snippet
|
var text = StringEscapeUtils.unescapeJava(snippet.snippet)
|
||||||
for (e in map.entries) {
|
for (e in map.entries) {
|
||||||
text = text.replace(e.key, e.value.toString())
|
text = text.replace(e.key, e.value.toString())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ class SnippetPanel : JPanel(BorderLayout()), Disposable {
|
|||||||
properties.getString("SnippetPanel.LeftPanel.width", "180").toIntOrNull() ?: 180,
|
properties.getString("SnippetPanel.LeftPanel.width", "180").toIntOrNull() ?: 180,
|
||||||
-1
|
-1
|
||||||
)
|
)
|
||||||
|
leftPanel.minimumSize = Dimension(leftPanel.preferredSize.width, leftPanel.preferredSize.height)
|
||||||
|
|
||||||
rightPanel.border = BorderFactory.createCompoundBorder(
|
rightPanel.border = BorderFactory.createCompoundBorder(
|
||||||
BorderFactory.createMatteBorder(0, 1, 0, 0, DynamicColor.BorderColor),
|
BorderFactory.createMatteBorder(0, 1, 0, 0, DynamicColor.BorderColor),
|
||||||
|
|||||||
@@ -390,7 +390,15 @@ abstract class SafetySyncer : Syncer {
|
|||||||
|
|
||||||
protected fun decodeKeymaps(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
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)
|
keymapManager.addKeymap(keymap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ class SyncManager private constructor() : Disposable {
|
|||||||
|
|
||||||
sync(config)
|
sync(config)
|
||||||
|
|
||||||
|
sync.lastSyncTime = System.currentTimeMillis()
|
||||||
|
|
||||||
if (log.isInfoEnabled) {
|
if (log.isInfoEnabled) {
|
||||||
log.info("Automatic synchronisation end")
|
log.info("Automatic synchronisation end")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import java.io.InputStreamReader
|
|||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.nio.charset.StandardCharsets
|
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) {
|
StreamPtyConnector(process.inputStream, process.outputStream) {
|
||||||
|
|
||||||
private val reader = InputStreamReader(input)
|
private val reader = InputStreamReader(input)
|
||||||
|
|||||||
@@ -14,11 +14,16 @@ import com.formdev.flatlaf.extras.components.FlatToolBar
|
|||||||
import com.formdev.flatlaf.ui.FlatRoundBorder
|
import com.formdev.flatlaf.ui.FlatRoundBorder
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.event.ActionListener
|
import java.awt.event.ActionListener
|
||||||
|
import java.beans.PropertyChangeEvent
|
||||||
|
import java.beans.PropertyChangeListener
|
||||||
|
import java.util.*
|
||||||
import javax.swing.JButton
|
import javax.swing.JButton
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||||
private val floatingToolbarEnable get() = Database.getDatabase().terminal.floatingToolbar
|
private val floatingToolbarEnable get() = Database.getDatabase().terminal.floatingToolbar
|
||||||
private var closed = false
|
private var closed = false
|
||||||
|
private val anEvent get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@@ -72,6 +77,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initActions()
|
initActions()
|
||||||
|
initEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateUI() {
|
override fun updateUI() {
|
||||||
@@ -123,12 +129,38 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
add(initCloseActionButton())
|
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 {
|
private fun initServerInfoActionButton(): JButton {
|
||||||
val btn = JButton(Icons.infoOutline)
|
val btn = JButton(Icons.infoOutline)
|
||||||
btn.toolTipText = I18n.getString("termora.visual-window.system-information")
|
btn.toolTipText = I18n.getString("termora.visual-window.system-information")
|
||||||
btn.addActionListener(object : AnAction() {
|
btn.addActionListener(object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
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
|
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
||||||
|
|
||||||
if (tab !is SSHTerminalTab) {
|
if (tab !is SSHTerminalTab) {
|
||||||
@@ -156,7 +188,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
btn.toolTipText = I18n.getString("termora.snippet.title")
|
btn.toolTipText = I18n.getString("termora.snippet.title")
|
||||||
btn.addActionListener(object : AnAction() {
|
btn.addActionListener(object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
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 writer = tab.getData(DataProviders.TerminalWriter) ?: return
|
||||||
val dialog = SnippetTreeDialog(evt.window)
|
val dialog = SnippetTreeDialog(evt.window)
|
||||||
dialog.setLocationRelativeTo(btn)
|
dialog.setLocationRelativeTo(btn)
|
||||||
@@ -174,7 +206,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi")
|
btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi")
|
||||||
btn.addActionListener(object : AnAction() {
|
btn.addActionListener(object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
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
|
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
||||||
|
|
||||||
if (tab !is SSHTerminalTab) {
|
if (tab !is SSHTerminalTab) {
|
||||||
@@ -233,7 +265,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
|
|
||||||
btn.addActionListener(object : AnAction() {
|
btn.addActionListener(object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
|
||||||
if (tab.canReconnect()) {
|
if (tab.canReconnect()) {
|
||||||
tab.reconnect()
|
tab.reconnect()
|
||||||
}
|
}
|
||||||
@@ -242,8 +274,4 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
|||||||
return btn
|
return btn
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
package app.termora.terminal.panel
|
package app.termora.terminal.panel
|
||||||
|
|
||||||
|
import app.termora.Database
|
||||||
import app.termora.Disposable
|
import app.termora.Disposable
|
||||||
import app.termora.Disposer
|
import app.termora.Disposer
|
||||||
|
import app.termora.SSHTerminalTab
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.actions.DataProviderSupport
|
import app.termora.actions.DataProviderSupport
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import app.termora.terminal.panel.vw.VisualWindow
|
import app.termora.terminal.panel.vw.*
|
||||||
import app.termora.terminal.panel.vw.VisualWindowManager
|
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.apache.commons.lang3.ArrayUtils
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
@@ -44,15 +45,15 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
|
|||||||
val SelectCopy = DataKey(Boolean::class)
|
val SelectCopy = DataKey(Boolean::class)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val properties get() = Database.getDatabase().properties
|
||||||
private val terminalBlink = TerminalBlink(terminal)
|
private val terminalBlink = TerminalBlink(terminal)
|
||||||
private val terminalFindPanel = TerminalFindPanel(this, terminal)
|
private val terminalFindPanel = TerminalFindPanel(this, terminal)
|
||||||
private val floatingToolbar = FloatingToolbarPanel()
|
private val floatingToolbar = FloatingToolbarPanel()
|
||||||
private val terminalDisplay = TerminalDisplay(this, terminal, terminalBlink)
|
private val terminalDisplay = TerminalDisplay(this, terminal, terminalBlink)
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
|
||||||
private val layeredPane = TerminalLayeredPane()
|
private val layeredPane = TerminalLayeredPane()
|
||||||
private var visualWindows = emptyArray<VisualWindow>()
|
private var visualWindows = emptyArray<VisualWindow>()
|
||||||
|
|
||||||
val scrollBar = TerminalScrollBar(this@TerminalPanel, terminalFindPanel, terminal)
|
val scrollBar = TerminalScrollBar(this, terminalFindPanel, terminal)
|
||||||
var enableFloatingToolbar = true
|
var enableFloatingToolbar = true
|
||||||
set(value) {
|
set(value) {
|
||||||
field = 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()
|
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 {
|
override fun getDimension(): Dimension {
|
||||||
return Dimension(
|
return Dimension(
|
||||||
terminalDisplay.size.width + padding.left + padding.right,
|
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.KeyShortcut
|
||||||
import app.termora.keymap.KeymapManager
|
import app.termora.keymap.KeymapManager
|
||||||
|
import app.termora.terminal.ControlCharacters
|
||||||
import app.termora.terminal.Terminal
|
import app.termora.terminal.Terminal
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -99,11 +100,12 @@ class TerminalPanelKeyAdapter(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Character.isISOControl(e.keyChar)) {
|
val keyChar = mapKeyChar(e)
|
||||||
|
if (Character.isISOControl(keyChar)) {
|
||||||
terminal.getSelectionModel().clearSelection()
|
terminal.getSelectionModel().clearSelection()
|
||||||
// 如果不为空表示已经发送过了,所以这里为空的时候再发送
|
// 如果不为空表示已经发送过了,所以这里为空的时候再发送
|
||||||
if (encode.isEmpty()) {
|
if (encode.isEmpty()) {
|
||||||
writer.write(TerminalWriter.WriteRequest.fromBytes("${e.keyChar}".toByteArray(writer.getCharset())))
|
writer.write(TerminalWriter.WriteRequest.fromBytes("$keyChar".toByteArray(writer.getCharset())))
|
||||||
e.consume()
|
e.consume()
|
||||||
}
|
}
|
||||||
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
||||||
@@ -111,6 +113,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 {
|
private fun isCtrlPressedOnly(e: KeyEvent): Boolean {
|
||||||
val modifiersEx = e.modifiersEx
|
val modifiersEx = e.modifiersEx
|
||||||
return (modifiersEx and InputEvent.ALT_DOWN_MASK) == 0
|
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) }
|
private val percentageBtn by lazy { JButton(if (isPercentage) Icons.text else Icons.percentage) }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
Disposer.register(tab, this)
|
||||||
initViews()
|
initViews()
|
||||||
initEvents()
|
initEvents()
|
||||||
initVisualWindowPanel()
|
initVisualWindowPanel()
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package app.termora.terminal.panel.vw
|
||||||
|
|
||||||
|
interface Resumeable
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package app.termora.terminal.panel.vw
|
package app.termora.terminal.panel.vw
|
||||||
|
|
||||||
import app.termora.Disposer
|
|
||||||
import app.termora.SSHTerminalTab
|
import app.termora.SSHTerminalTab
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
@@ -11,11 +10,7 @@ abstract class SSHVisualWindow(
|
|||||||
protected val tab: SSHTerminalTab,
|
protected val tab: SSHTerminalTab,
|
||||||
id: String,
|
id: String,
|
||||||
visualWindowManager: VisualWindowManager
|
visualWindowManager: VisualWindowManager
|
||||||
) : VisualWindowPanel(id, visualWindowManager) {
|
) : VisualWindowPanel(id, visualWindowManager), Resumeable {
|
||||||
|
|
||||||
init {
|
|
||||||
Disposer.register(tab, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toggleWindow() {
|
override fun toggleWindow() {
|
||||||
val evt = AnActionEvent(tab.getJComponent(), StringUtils.EMPTY, EventObject(this))
|
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() }
|
private val systemInformationPanel by lazy { SystemInformationPanel() }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
Disposer.register(tab, this)
|
||||||
initViews()
|
initViews()
|
||||||
initEvents()
|
initEvents()
|
||||||
initVisualWindowPanel()
|
initVisualWindowPanel()
|
||||||
@@ -137,7 +138,7 @@ class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
|||||||
private suspend fun refreshCPUAndMem(session: ClientSession) {
|
private suspend fun refreshCPUAndMem(session: ClientSession) {
|
||||||
|
|
||||||
// top
|
// top
|
||||||
var pair = SshClients.execChannel(session, "top -bn1")
|
val pair = SshClients.execChannel(session, "top -bn1")
|
||||||
if (pair.first != 0) {
|
if (pair.first != 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -236,7 +237,7 @@ class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
|||||||
private suspend fun refreshDisk(session: ClientSession) {
|
private suspend fun refreshDisk(session: ClientSession) {
|
||||||
|
|
||||||
// df -h
|
// df -h
|
||||||
var pair = SshClients.execChannel(session, "df -B1")
|
val pair = SshClients.execChannel(session, "df -B1")
|
||||||
if (pair.first != 0) {
|
if (pair.first != 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,4 +28,9 @@ interface VisualWindow : Disposable {
|
|||||||
* 切换独立模式
|
* 切换独立模式
|
||||||
*/
|
*/
|
||||||
fun toggleWindow()
|
fun toggleWindow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同一个类,返回的相同
|
||||||
|
*/
|
||||||
|
fun getWindowName(): String
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.terminal.panel.vw
|
package app.termora.terminal.panel.vw
|
||||||
|
|
||||||
|
import app.termora.actions.DataProvider
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
|
|
||||||
interface VisualWindowManager {
|
interface VisualWindowManager {
|
||||||
@@ -33,4 +34,14 @@ interface VisualWindowManager {
|
|||||||
* 获取管理器的宽高
|
* 获取管理器的宽高
|
||||||
*/
|
*/
|
||||||
fun getDimension(): Dimension
|
fun getDimension(): Dimension
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复所有窗口
|
||||||
|
*/
|
||||||
|
fun resumeVisualWindows(id: String, dataProvider: DataProvider)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储所有窗口
|
||||||
|
*/
|
||||||
|
fun storeVisualWindows(id: String)
|
||||||
}
|
}
|
||||||
@@ -374,4 +374,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
|||||||
return null
|
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.i-want-to-translate=I want to translate
|
||||||
termora.settings.appearance.follow-system=Sync with OS
|
termora.settings.appearance.follow-system=Sync with OS
|
||||||
termora.settings.appearance.opacity=Opacity
|
termora.settings.appearance.opacity=Opacity
|
||||||
|
termora.settings.appearance.background-image=BG Image
|
||||||
termora.settings.appearance.background-running=Backgrounding
|
termora.settings.appearance.background-running=Backgrounding
|
||||||
|
|
||||||
termora.setting.security=Security
|
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-other-tabs=Close Other Tabs
|
||||||
termora.tabbed.contextmenu.close-all-tabs=Close All Tabs
|
termora.tabbed.contextmenu.close-all-tabs=Close All Tabs
|
||||||
termora.tabbed.contextmenu.reconnect=Reconnect
|
termora.tabbed.contextmenu.reconnect=Reconnect
|
||||||
|
termora.tabbed.local-tab.close-prompt=Do you want to terminal a running process in this terminal?
|
||||||
|
|
||||||
# Terminal logger
|
# Terminal logger
|
||||||
termora.terminal-logger=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.i-want-to-translate=我想要翻译
|
||||||
termora.settings.appearance.follow-system=跟随系统
|
termora.settings.appearance.follow-system=跟随系统
|
||||||
termora.settings.appearance.opacity=透明度
|
termora.settings.appearance.opacity=透明度
|
||||||
|
termora.settings.appearance.background-image=背景图
|
||||||
termora.settings.appearance.background-running=后台运行
|
termora.settings.appearance.background-running=后台运行
|
||||||
|
|
||||||
termora.setting.security=安全
|
termora.setting.security=安全
|
||||||
@@ -219,7 +220,7 @@ termora.tabbed.contextmenu.close=关闭
|
|||||||
termora.tabbed.contextmenu.close-other-tabs=关闭其他标签页
|
termora.tabbed.contextmenu.close-other-tabs=关闭其他标签页
|
||||||
termora.tabbed.contextmenu.close-all-tabs=关闭所有标签页
|
termora.tabbed.contextmenu.close-all-tabs=关闭所有标签页
|
||||||
termora.tabbed.contextmenu.reconnect=重新连接
|
termora.tabbed.contextmenu.reconnect=重新连接
|
||||||
|
termora.tabbed.local-tab.close-prompt=你想要终止这个终端中正在运行的进程吗?
|
||||||
|
|
||||||
|
|
||||||
# Terminal logger
|
# Terminal logger
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ termora.settings.appearance.language=語言
|
|||||||
termora.settings.appearance.i-want-to-translate=我想要翻譯
|
termora.settings.appearance.i-want-to-translate=我想要翻譯
|
||||||
termora.settings.appearance.follow-system=跟隨系統
|
termora.settings.appearance.follow-system=跟隨系統
|
||||||
termora.settings.appearance.opacity=透明度
|
termora.settings.appearance.opacity=透明度
|
||||||
|
termora.settings.appearance.background-image=背景圖
|
||||||
termora.settings.appearance.background-running=後台運行
|
termora.settings.appearance.background-running=後台運行
|
||||||
|
|
||||||
termora.setting.security=安全
|
termora.setting.security=安全
|
||||||
@@ -215,6 +216,7 @@ termora.tabbed.contextmenu.close=關閉
|
|||||||
termora.tabbed.contextmenu.close-other-tabs=關閉其他標籤頁
|
termora.tabbed.contextmenu.close-other-tabs=關閉其他標籤頁
|
||||||
termora.tabbed.contextmenu.close-all-tabs=關閉所有標籤
|
termora.tabbed.contextmenu.close-all-tabs=關閉所有標籤
|
||||||
termora.tabbed.contextmenu.reconnect=重新連接
|
termora.tabbed.contextmenu.reconnect=重新連接
|
||||||
|
termora.tabbed.local-tab.close-prompt=你想要終止這個終端機中正在運作的進程嗎?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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 |
Reference in New Issue
Block a user