Compare commits

...

22 Commits

Author SHA1 Message Date
hstyi
932db49868 release: 1.0.13 2025-04-14 09:34:55 +08:00
hstyi
4d71c6cd05 chore: improve exit 2025-04-12 16:43:03 +08:00
hstyi
96133e5abf fix: default directory for SFTP Windows (#496) 2025-04-12 08:56:26 +08:00
hstyi
f06e5d7dc1 fix: Keymap sync override (#493) 2025-04-11 11:37:29 +08:00
dependabot[bot]
d4b96edccf chore(deps): bump org.gradle.toolchains.foojay-resolver-convention from 0.9.0 to 0.10.0 (#491)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-11 10:22:18 +08:00
dependabot[bot]
e9876d5b91 chore(deps): bump org.apache.commons:commons-text from 1.13.0 to 1.13.1 (#490)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-11 10:22:06 +08:00
hstyi
8b9a78a7bd chore: sync icon 2025-04-11 08:25:57 +08:00
hstyi
6b48f577e9 feat: macOS supports running in the background (#487) 2025-04-10 14:47:01 +08:00
hstyi
da9b6c21d6 fix: windows sftp path (#486) 2025-04-10 13:23:44 +08:00
hstyi
f1f889df14 chore: improve terminal close (#484) 2025-04-10 11:45:27 +08:00
hstyi
ed65853ebe fix: SFTP path not jumping on Windows (#483) 2025-04-10 11:08:51 +08:00
hstyi
5ffdd219d9 fix: Escape key (#482) 2025-04-10 09:37:05 +08:00
hstyi
4f84d6741c fix: JPopupMenu overlapping with background 2025-04-10 08:59:55 +08:00
hstyi
2568e7fcc8 fix: background image selection failure 2025-04-09 17:32:27 +08:00
hstyi
dddbb49084 feat: support setting background image (#475) 2025-04-09 16:03:38 +08:00
hstyi
95846ab135 fix: snippet unescape (#474) 2025-04-09 13:30:57 +08:00
dependabot[bot]
b5207e56c1 chore(deps): bump org.jetbrains.pty4j:pty4j from 0.13.2 to 0.13.3 (#471)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 10:42:50 +08:00
dependabot[bot]
160771e912 chore(deps): bump com.github.mwiede:jsch from 0.2.21 to 0.2.25 (#472)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 10:42:03 +08:00
dependabot[bot]
0fbe180f3f chore(deps): bump kotlinx-coroutines from 1.10.1 to 1.10.2 (#470)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 10:41:40 +08:00
hstyi
41a0409e9e fix: return to parent folder failure (#468) 2025-04-08 14:43:58 +08:00
hstyi
79e59143fb fix: last sync time (#467) 2025-04-08 14:40:20 +08:00
hstyi
54e0f621ce feat: support for restoring virtual windows 2025-04-07 11:51:55 +08:00
41 changed files with 608 additions and 110 deletions

View File

@@ -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()

View File

@@ -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"

View File

@@ -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"

View File

@@ -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() {

View 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)
}
}
}
}
}

View File

@@ -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)
/** /**
* 语言 * 语言
*/ */

View File

@@ -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") }

View File

@@ -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
}
} }

View File

@@ -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
}
}

View File

@@ -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) {

View File

@@ -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()
} }

View File

@@ -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()

View File

@@ -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
/** /**

View File

@@ -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)
}
}
} }

View File

@@ -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)
}
} }
} }

View File

@@ -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) {

View File

@@ -0,0 +1,9 @@
package app.termora.sftp
import org.apache.commons.vfs2.FileSystem
interface FileSystemProvider {
fun getFileSystem(): FileSystem
fun setFileSystem(fileSystem: FileSystem)
}

View File

@@ -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))
} }

View File

@@ -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()

View File

@@ -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()}'",

View File

@@ -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 {

View File

@@ -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())
} }

View File

@@ -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),

View File

@@ -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)
} }

View File

@@ -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")
} }

View File

@@ -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)

View File

@@ -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() {
}
} }

View File

@@ -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,

View File

@@ -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

View File

@@ -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()

View File

@@ -0,0 +1,3 @@
package app.termora.terminal.panel.vw
interface Resumeable

View File

@@ -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))

View File

@@ -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
} }

View File

@@ -28,4 +28,9 @@ interface VisualWindow : Disposable {
* 切换独立模式 * 切换独立模式
*/ */
fun toggleWindow() fun toggleWindow()
/**
* 同一个类,返回的相同
*/
fun getWindowName(): String
} }

View File

@@ -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)
} }

View File

@@ -374,4 +374,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
return null return null
} }
} }
override fun getWindowName(): String {
return id
}
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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=你想要終止這個終端機中正在運作的進程嗎?

View 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

View 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