mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 18:32:58 +08:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
343d11482d | ||
|
|
7ef81a0116 | ||
|
|
5df62d5d3e | ||
|
|
7db650d69f | ||
|
|
8d80d38d63 | ||
|
|
48f05d4cff | ||
|
|
9a1cf387c0 | ||
|
|
8b7efefbdb | ||
|
|
75f21db325 | ||
|
|
b094c9d4ff | ||
|
|
0da3c95759 | ||
|
|
fa79473ece | ||
|
|
86ccb5e0cc | ||
|
|
f385f4b277 | ||
|
|
3d0ef2a331 | ||
|
|
96999205a8 | ||
|
|
ee7f3871eb | ||
|
|
df2e9b0743 | ||
|
|
7964950149 | ||
|
|
e2d77fe881 | ||
|
|
f5783c8587 | ||
|
|
346044b1ba | ||
|
|
aa6ec8dd43 | ||
|
|
e0e6a85a81 | ||
|
|
56ba107c87 |
@@ -14,7 +14,7 @@ plugins {
|
|||||||
|
|
||||||
|
|
||||||
group = "app.termora"
|
group = "app.termora"
|
||||||
version = "1.0.3"
|
version = "1.0.5"
|
||||||
|
|
||||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||||
val arch: Architecture = DefaultNativePlatform.getCurrentArchitecture()
|
val arch: Architecture = DefaultNativePlatform.getCurrentArchitecture()
|
||||||
@@ -383,6 +383,14 @@ tasks.register("dist") {
|
|||||||
"--wait",
|
"--wait",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 绑定公证信息
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"/usr/bin/xcrun",
|
||||||
|
"stapler", "staple", macOSFinalFilePath,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,6 +415,18 @@ tasks.register("check-license") {
|
|||||||
thirdParty[nameWithVersion.replace(StringUtils.SPACE, "-")] = license
|
thirdParty[nameWithVersion.replace(StringUtils.SPACE, "-")] = license
|
||||||
thirdPartyNames.add(nameWithVersion.split(StringUtils.SPACE).first())
|
thirdPartyNames.add(nameWithVersion.split(StringUtils.SPACE).first())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (file in configurations.runtimeClasspath.get()) {
|
||||||
|
val name = file.nameWithoutExtension
|
||||||
|
if (!thirdParty.containsKey(name)) {
|
||||||
|
if (logger.isWarnEnabled) {
|
||||||
|
logger.warn("$name does not exist in third-party")
|
||||||
|
}
|
||||||
|
if (!thirdPartyNames.contains(name)) {
|
||||||
|
throw GradleException("$name No license found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ import com.jthemedetecor.OsThemeDetector
|
|||||||
import com.mixpanel.mixpanelapi.ClientDelivery
|
import com.mixpanel.mixpanelapi.ClientDelivery
|
||||||
import com.mixpanel.mixpanelapi.MessageBuilder
|
import com.mixpanel.mixpanelapi.MessageBuilder
|
||||||
import com.mixpanel.mixpanelapi.MixpanelAPI
|
import com.mixpanel.mixpanelapi.MixpanelAPI
|
||||||
import com.sun.jna.platform.WindowUtils
|
|
||||||
import com.sun.jna.platform.win32.User32
|
|
||||||
import com.sun.jna.ptr.IntByReference
|
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -20,25 +17,27 @@ import kotlinx.coroutines.launch
|
|||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.apache.commons.lang3.LocaleUtils
|
import org.apache.commons.lang3.LocaleUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.apache.commons.lang3.math.NumberUtils
|
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.tinylog.configuration.Configuration
|
import org.tinylog.configuration.Configuration
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.RandomAccessFile
|
import java.nio.channels.FileChannel
|
||||||
import java.nio.channels.FileLock
|
import java.nio.channels.FileLock
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
class ApplicationRunner {
|
class ApplicationRunner {
|
||||||
|
private lateinit var singletonChannel: FileChannel
|
||||||
private lateinit var singletonLock: FileLock
|
private lateinit var singletonLock: FileLock
|
||||||
private val log by lazy {
|
private val log by lazy {
|
||||||
if (!::singletonLock.isInitialized) {
|
if (!::singletonLock.isInitialized) {
|
||||||
throw UnsupportedOperationException("Singleton lock is not initialized")
|
throw UnsupportedOperationException("Singleton lock is not initialized")
|
||||||
}
|
}
|
||||||
LoggerFactory.getLogger("Main")
|
LoggerFactory.getLogger(ApplicationRunner::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun run() {
|
fun run() {
|
||||||
@@ -224,36 +223,14 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
|
|
||||||
private fun checkSingleton() {
|
private fun checkSingleton() {
|
||||||
val file = File(Application.getBaseDataDir(), "lock")
|
singletonChannel = FileChannel.open(
|
||||||
val pidFile = File(Application.getBaseDataDir(), "pid")
|
Paths.get(Application.getBaseDataDir().absolutePath, "lock"),
|
||||||
|
StandardOpenOption.CREATE,
|
||||||
|
StandardOpenOption.WRITE,
|
||||||
val raf = RandomAccessFile(file, "rw")
|
)
|
||||||
val lock = raf.channel.tryLock()
|
|
||||||
|
|
||||||
if (lock != null) {
|
|
||||||
pidFile.writeText(ProcessHandle.current().pid().toString())
|
|
||||||
pidFile.deleteOnExit()
|
|
||||||
file.deleteOnExit()
|
|
||||||
} else {
|
|
||||||
if (SystemInfo.isWindows && pidFile.exists()) {
|
|
||||||
val pid = NumberUtils.toLong(pidFile.readText())
|
|
||||||
for (window in WindowUtils.getAllWindows(false)) {
|
|
||||||
if (pid > 0) {
|
|
||||||
val processId = IntByReference()
|
|
||||||
User32.INSTANCE.GetWindowThreadProcessId(window.hwnd, processId)
|
|
||||||
if (processId.value.toLong() != pid) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else if (window.title != Application.getName() || window.filePath.endsWith("explorer.exe")) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
User32.INSTANCE.ShowWindow(window.hwnd, User32.SW_SHOWNOACTIVATE)
|
|
||||||
User32.INSTANCE.SetForegroundWindow(window.hwnd)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
val lock = singletonChannel.tryLock()
|
||||||
|
if (lock == null) {
|
||||||
System.err.println("Program is already running")
|
System.err.println("Program is already running")
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package app.termora
|
|||||||
|
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.swing.Swing
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import org.apache.sshd.client.SshClient
|
import org.apache.sshd.client.SshClient
|
||||||
import org.apache.sshd.client.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
@@ -47,19 +50,27 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
putValue(NAME, "${I18n.getString("termora.new-host.test-connection")}...")
|
putValue(NAME, "${I18n.getString("termora.new-host.test-connection")}...")
|
||||||
SwingUtilities.invokeLater {
|
isEnabled = false
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
testConnection(pane.getHost())
|
testConnection(pane.getHost())
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
putValue(NAME, I18n.getString("termora.new-host.test-connection"))
|
putValue(NAME, I18n.getString("termora.new-host.test-connection"))
|
||||||
|
isEnabled = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun testConnection(host: Host) {
|
private suspend fun testConnection(host: Host) {
|
||||||
|
val owner = this
|
||||||
if (host.protocol != Protocol.SSH) {
|
if (host.protocol != Protocol.SSH) {
|
||||||
OptionPane.showMessageDialog(this, I18n.getString("termora.new-host.test-connection-successful"))
|
withContext(Dispatchers.Swing) {
|
||||||
|
OptionPane.showMessageDialog(owner, I18n.getString("termora.new-host.test-connection-successful"))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,13 +78,21 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
|||||||
var session: ClientSession? = null
|
var session: ClientSession? = null
|
||||||
try {
|
try {
|
||||||
client = SshClients.openClient(host)
|
client = SshClients.openClient(host)
|
||||||
|
client.userInteraction = TerminalUserInteraction(owner)
|
||||||
session = SshClients.openSession(host, client)
|
session = SshClients.openSession(host, client)
|
||||||
OptionPane.showMessageDialog(this, I18n.getString("termora.new-host.test-connection-successful"))
|
withContext(Dispatchers.Swing) {
|
||||||
} catch (e: Exception) {
|
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
this, ExceptionUtils.getRootCauseMessage(e),
|
owner,
|
||||||
|
I18n.getString("termora.new-host.test-connection-successful")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, ExceptionUtils.getRootCauseMessage(e),
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
)
|
)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
session?.close()
|
session?.close()
|
||||||
client?.close()
|
client?.close()
|
||||||
|
|||||||
@@ -1006,7 +1006,8 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
val rows = table.selectedRows.sortedDescending()
|
val rows = table.selectedRows.sortedDescending()
|
||||||
if (rows.isEmpty()) return
|
if (rows.isEmpty()) return
|
||||||
for (row in rows) {
|
for (row in rows) {
|
||||||
model.removeRow(row)
|
jumpHosts.removeAt(row)
|
||||||
|
model.fireTableRowsDeleted(row, row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -318,6 +318,7 @@ class HostTree : JTree(), Disposable {
|
|||||||
val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host"))
|
val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host"))
|
||||||
|
|
||||||
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open"))
|
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open"))
|
||||||
|
val openInNewWindow = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open-in-new-window"))
|
||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
val copy = popupMenu.add(I18n.getString("termora.welcome.contextmenu.copy"))
|
val copy = popupMenu.add(I18n.getString("termora.welcome.contextmenu.copy"))
|
||||||
val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove"))
|
val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove"))
|
||||||
@@ -330,15 +331,8 @@ class HostTree : JTree(), Disposable {
|
|||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
|
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
|
||||||
|
|
||||||
open.addActionListener { evt ->
|
open.addActionListener { openHosts(it, false) }
|
||||||
getSelectionNodes()
|
openInNewWindow.addActionListener { openHosts(it, true) }
|
||||||
.filter { it.protocol != Protocol.Folder }
|
|
||||||
.forEach {
|
|
||||||
ActionManager.getInstance()
|
|
||||||
.getAction(OpenHostAction.OPEN_HOST)
|
|
||||||
?.actionPerformed(OpenHostActionEvent(evt.source, it, evt))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rename.addActionListener {
|
rename.addActionListener {
|
||||||
startEditingAtPath(TreePath(model.getPathToRoot(lastHost)))
|
startEditingAtPath(TreePath(model.getPathToRoot(lastHost)))
|
||||||
@@ -454,6 +448,17 @@ class HostTree : JTree(), Disposable {
|
|||||||
popupMenu.show(this, event.x, event.y)
|
popupMenu.show(this, event.x, event.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openHosts(evt: EventObject, openInNewWindow: Boolean) {
|
||||||
|
assertEventDispatchThread()
|
||||||
|
val nodes = getSelectionNodes().filter { it.protocol != Protocol.Folder }
|
||||||
|
if (nodes.isEmpty()) return
|
||||||
|
val openHostAction = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST) ?: return
|
||||||
|
val source = if (openInNewWindow)
|
||||||
|
TermoraFrameManager.getInstance().createWindow().apply { isVisible = true }
|
||||||
|
else evt.source
|
||||||
|
|
||||||
|
nodes.forEach { openHostAction.actionPerformed(OpenHostActionEvent(source, it, evt)) }
|
||||||
|
}
|
||||||
|
|
||||||
fun expandNode(node: Host, including: Boolean = false) {
|
fun expandNode(node: Host, including: Boolean = false) {
|
||||||
expandPath(TreePath(model.getPathToRoot(node)))
|
expandPath(TreePath(model.getPathToRoot(node)))
|
||||||
|
|||||||
@@ -1,12 +1,190 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.*
|
||||||
|
import java.awt.event.*
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.*
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
class MyTabbedPane : FlatTabbedPane() {
|
class MyTabbedPane : FlatTabbedPane() {
|
||||||
|
|
||||||
|
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
private val dragMouseAdaptor = DragMouseAdaptor()
|
||||||
|
private val terminalTabbedManager
|
||||||
|
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||||
|
.getData(DataProviders.TerminalTabbedManager)
|
||||||
|
|
||||||
|
init {
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateUI() {
|
||||||
|
styleMap = mapOf(
|
||||||
|
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground"),
|
||||||
|
"hoverColor" to UIManager.getColor("TabbedPane.background"),
|
||||||
|
)
|
||||||
|
super.updateUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
addMouseListener(dragMouseAdaptor)
|
||||||
|
addMouseMotionListener(dragMouseAdaptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun processMouseEvent(e: MouseEvent) {
|
||||||
|
// Shift + Click ===> close tab
|
||||||
|
if (e.id == MouseEvent.MOUSE_CLICKED && SwingUtilities.isLeftMouseButton(e) && isShiftPressedOnly(e.modifiersEx)) {
|
||||||
|
val index = indexAtLocation(e.x, e.y)
|
||||||
|
if (index >= 0) {
|
||||||
|
tabCloseCallback?.accept(this, index)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (e.id == MouseEvent.MOUSE_PRESSED && isShiftPressedOnly(e.modifiersEx)) {
|
||||||
|
val index = indexAtLocation(e.x, e.y)
|
||||||
|
if (index >= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.processMouseEvent(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isShiftPressedOnly(modifiersEx: Int): Boolean {
|
||||||
|
return (modifiersEx and InputEvent.ALT_DOWN_MASK) == 0
|
||||||
|
&& (modifiersEx and InputEvent.ALT_GRAPH_DOWN_MASK) == 0
|
||||||
|
&& (modifiersEx and InputEvent.CTRL_DOWN_MASK) == 0
|
||||||
|
&& (modifiersEx and InputEvent.SHIFT_DOWN_MASK) != 0
|
||||||
|
}
|
||||||
|
|
||||||
override fun setSelectedIndex(index: Int) {
|
override fun setSelectedIndex(index: Int) {
|
||||||
val oldIndex = selectedIndex
|
val oldIndex = selectedIndex
|
||||||
super.setSelectedIndex(index)
|
super.setSelectedIndex(index)
|
||||||
firePropertyChange("selectedIndex", oldIndex, index)
|
firePropertyChange("selectedIndex", oldIndex, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class DragMouseAdaptor : MouseAdapter(), KeyEventDispatcher {
|
||||||
|
private var mousePressedPoint = Point()
|
||||||
|
private var tabIndex = 0 - 1
|
||||||
|
private var cancelled = false
|
||||||
|
private var window: Window? = null
|
||||||
|
private var terminalTab: TerminalTab? = null
|
||||||
|
private var isDragging = false
|
||||||
|
private var lastVisitTabIndex = -1
|
||||||
|
|
||||||
|
override fun mousePressed(e: MouseEvent) {
|
||||||
|
val index = indexAtLocation(e.x, e.y)
|
||||||
|
if (index < 0 || !isTabClosable(index)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tabIndex = index
|
||||||
|
mousePressedPoint = e.point
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseDragged(e: MouseEvent) {
|
||||||
|
// 如果正在拖拽中,那么修改 Window 的位置
|
||||||
|
if (isDragging) {
|
||||||
|
window?.location = e.locationOnScreen
|
||||||
|
lastVisitTabIndex = indexAtLocation(e.x, e.y)
|
||||||
|
} else if (tabIndex >= 0) { // 这里之所以判断是确保在 mousePressed 时已经确定了 Tab
|
||||||
|
// 有的时候会太灵敏,这里容错一下
|
||||||
|
val diff = 5
|
||||||
|
if (abs(mousePressedPoint.y - e.y) >= diff || abs(mousePressedPoint.x - e.x) >= diff) {
|
||||||
|
startDrag(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startDrag(e: MouseEvent) {
|
||||||
|
if (isDragging) return
|
||||||
|
val terminalTabbedManager = terminalTabbedManager ?: return
|
||||||
|
val window = JDialog(owner).also { this.window = it }
|
||||||
|
window.isUndecorated = true
|
||||||
|
val image = createTabImage(tabIndex)
|
||||||
|
window.size = Dimension(image.width, image.height)
|
||||||
|
window.add(JLabel(ImageIcon(image)))
|
||||||
|
window.location = e.locationOnScreen
|
||||||
|
window.addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowClosed(e: WindowEvent) {
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.removeKeyEventDispatcher(this@DragMouseAdaptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun windowOpened(e: WindowEvent) {
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.addKeyEventDispatcher(this@DragMouseAdaptor)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 暂时关闭 Tab
|
||||||
|
terminalTabbedManager.closeTerminalTab(terminalTabbedManager.getTerminalTabs()[tabIndex].also {
|
||||||
|
terminalTab = it
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
window.isVisible = true
|
||||||
|
|
||||||
|
isDragging = true
|
||||||
|
cancelled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopDrag() {
|
||||||
|
if (!isDragging) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val tab = this.terminalTab
|
||||||
|
val terminalTabbedManager = terminalTabbedManager
|
||||||
|
|
||||||
|
if (tab != null && terminalTabbedManager != null) {
|
||||||
|
// 如果是手动取消
|
||||||
|
if (cancelled) {
|
||||||
|
terminalTabbedManager.addTerminalTab(tabIndex, tab)
|
||||||
|
} else if (lastVisitTabIndex > 0) {
|
||||||
|
terminalTabbedManager.addTerminalTab(lastVisitTabIndex, tab)
|
||||||
|
} else if (lastVisitTabIndex == 0) {
|
||||||
|
terminalTabbedManager.addTerminalTab(1, tab)
|
||||||
|
} else {
|
||||||
|
terminalTabbedManager.addTerminalTab(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset
|
||||||
|
window?.dispose()
|
||||||
|
isDragging = false
|
||||||
|
tabIndex = -1
|
||||||
|
cancelled = false
|
||||||
|
lastVisitTabIndex = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseReleased(e: MouseEvent) {
|
||||||
|
stopDrag()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTabImage(index: Int): BufferedImage {
|
||||||
|
val tabBounds = getBoundsAt(index)
|
||||||
|
val image = BufferedImage(tabBounds.width, tabBounds.height, BufferedImage.TYPE_INT_ARGB)
|
||||||
|
val g2 = image.createGraphics()
|
||||||
|
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||||
|
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
|
||||||
|
g2.translate(-tabBounds.x, -tabBounds.y)
|
||||||
|
paint(g2)
|
||||||
|
g2.dispose()
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
|
||||||
|
if (e.keyCode == KeyEvent.VK_ESCAPE) {
|
||||||
|
cancelled = true
|
||||||
|
stopDrag()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -38,6 +38,8 @@ class PtyConnectorFactory : Disposable {
|
|||||||
val locale = Locale.getDefault()
|
val locale = Locale.getDefault()
|
||||||
if (StringUtils.isNoneBlank(locale.language, locale.country)) {
|
if (StringUtils.isNoneBlank(locale.language, locale.country)) {
|
||||||
envs["LANG"] = "${locale.language}_${locale.country}.${Charset.defaultCharset().name()}"
|
envs["LANG"] = "${locale.language}_${locale.country}.${Charset.defaultCharset().name()}"
|
||||||
|
} else {
|
||||||
|
envs["LANG"] = "en_US.UTF-8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,14 @@ import app.termora.AES.encodeBase64String
|
|||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.highlight.KeywordHighlight
|
||||||
import app.termora.highlight.KeywordHighlightManager
|
import app.termora.highlight.KeywordHighlightManager
|
||||||
|
import app.termora.keymap.Keymap
|
||||||
|
import app.termora.keymap.KeymapManager
|
||||||
import app.termora.keymap.KeymapPanel
|
import app.termora.keymap.KeymapPanel
|
||||||
import app.termora.keymgr.KeyManager
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.keymgr.OhKeyPair
|
||||||
|
import app.termora.macro.Macro
|
||||||
import app.termora.macro.MacroManager
|
import app.termora.macro.MacroManager
|
||||||
import app.termora.native.FileChooser
|
import app.termora.native.FileChooser
|
||||||
import app.termora.sync.SyncConfig
|
import app.termora.sync.SyncConfig
|
||||||
@@ -19,6 +24,7 @@ import app.termora.terminal.panel.TerminalPanel
|
|||||||
import cash.z.ecc.android.bip39.Mnemonics
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
import com.formdev.flatlaf.extras.components.*
|
import com.formdev.flatlaf.extras.components.*
|
||||||
|
import com.formdev.flatlaf.util.FontUtils
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
@@ -27,12 +33,11 @@ import com.sun.jna.LastErrorException
|
|||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.*
|
||||||
import kotlinx.serialization.json.encodeToJsonElement
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
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
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import org.apache.commons.lang3.time.DateFormatUtils
|
import org.apache.commons.lang3.time.DateFormatUtils
|
||||||
import org.jdesktop.swingx.JXEditorPane
|
import org.jdesktop.swingx.JXEditorPane
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -54,6 +59,11 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||||||
class SettingsOptionsPane : OptionsPane() {
|
class SettingsOptionsPane : OptionsPane() {
|
||||||
private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane)
|
private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane)
|
||||||
private val database get() = Database.getDatabase()
|
private val database get() = Database.getDatabase()
|
||||||
|
private val hostManager get() = HostManager.getInstance()
|
||||||
|
private val keymapManager get() = KeymapManager.getInstance()
|
||||||
|
private val macroManager get() = MacroManager.getInstance()
|
||||||
|
private val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
||||||
|
private val keyManager get() = KeyManager.getInstance()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java)
|
private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java)
|
||||||
@@ -379,6 +389,28 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fontComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
if (value is String) {
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
"<html><font face='$value'>$value</font></html>",
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fontComboBox.maximumSize = fontComboBox.preferredSize
|
||||||
|
|
||||||
cursorStyleComboBox.addItem(CursorStyle.Block)
|
cursorStyleComboBox.addItem(CursorStyle.Block)
|
||||||
cursorStyleComboBox.addItem(CursorStyle.Bar)
|
cursorStyleComboBox.addItem(CursorStyle.Bar)
|
||||||
cursorStyleComboBox.addItem(CursorStyle.Underline)
|
cursorStyleComboBox.addItem(CursorStyle.Underline)
|
||||||
@@ -391,8 +423,33 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
shellComboBox.selectedItem = terminalSetting.localShell
|
shellComboBox.selectedItem = terminalSetting.localShell
|
||||||
|
|
||||||
fontComboBox.addItem("JetBrains Mono")
|
val fonts = linkedSetOf(
|
||||||
fontComboBox.addItem("Source Code Pro")
|
"JetBrains Mono",
|
||||||
|
"Source Code Pro",
|
||||||
|
"Monospaced",
|
||||||
|
"Andale Mono",
|
||||||
|
"Ayuthaya",
|
||||||
|
"Courier New",
|
||||||
|
"Droid Sans Mono",
|
||||||
|
"Fira Code",
|
||||||
|
"PCMyungjo",
|
||||||
|
"Menlo",
|
||||||
|
"Monaco",
|
||||||
|
"Osaka",
|
||||||
|
"PT Mono",
|
||||||
|
"SimSong",
|
||||||
|
)
|
||||||
|
|
||||||
|
for (font in FontUtils.getAllFonts()) {
|
||||||
|
if (fonts.contains(font.family)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fonts.remove(font.family)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (font in fonts) {
|
||||||
|
fontComboBox.addItem(font)
|
||||||
|
}
|
||||||
|
|
||||||
fontComboBox.selectedItem = terminalSetting.font
|
fontComboBox.selectedItem = terminalSetting.font
|
||||||
debugComboBox.selectedItem = terminalSetting.debug
|
debugComboBox.selectedItem = terminalSetting.debug
|
||||||
@@ -451,6 +508,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
val domainTextField = OutlineTextField(255)
|
val domainTextField = OutlineTextField(255)
|
||||||
val uploadConfigButton = JButton(I18n.getString("termora.settings.sync.push"), Icons.upload)
|
val uploadConfigButton = JButton(I18n.getString("termora.settings.sync.push"), Icons.upload)
|
||||||
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 downloadConfigButton = JButton(I18n.getString("termora.settings.sync.pull"), Icons.download)
|
val downloadConfigButton = JButton(I18n.getString("termora.settings.sync.pull"), Icons.download)
|
||||||
val lastSyncTimeLabel = JLabel()
|
val lastSyncTimeLabel = JLabel()
|
||||||
val sync get() = database.sync
|
val sync get() = database.sync
|
||||||
@@ -562,6 +620,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exportConfigButton.addActionListener { export() }
|
exportConfigButton.addActionListener { export() }
|
||||||
|
importConfigButton.addActionListener { import() }
|
||||||
|
|
||||||
keysCheckBox.addActionListener { refreshButtons() }
|
keysCheckBox.addActionListener { refreshButtons() }
|
||||||
hostsCheckBox.addActionListener { refreshButtons() }
|
hostsCheckBox.addActionListener { refreshButtons() }
|
||||||
@@ -578,6 +637,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|| keywordHighlightsCheckBox.isSelected
|
|| keywordHighlightsCheckBox.isSelected
|
||||||
uploadConfigButton.isEnabled = downloadConfigButton.isEnabled
|
uploadConfigButton.isEnabled = downloadConfigButton.isEnabled
|
||||||
exportConfigButton.isEnabled = downloadConfigButton.isEnabled
|
exportConfigButton.isEnabled = downloadConfigButton.isEnabled
|
||||||
|
importConfigButton.isEnabled = downloadConfigButton.isEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun export() {
|
private fun export() {
|
||||||
@@ -593,6 +653,109 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun import() {
|
||||||
|
val fileChooser = FileChooser()
|
||||||
|
fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
||||||
|
fileChooser.osxAllowedFileTypes = listOf("json")
|
||||||
|
fileChooser.win32Filters.add(Pair("JSON files", listOf("json")))
|
||||||
|
fileChooser.showOpenDialog(owner).thenAccept { files ->
|
||||||
|
if (files.isNotEmpty()) {
|
||||||
|
SwingUtilities.invokeLater { importFromFile(files.first()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun importFromFile(file: File) {
|
||||||
|
if (!file.exists()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val ranges = getSyncConfig().ranges
|
||||||
|
if (ranges.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最大 100MB
|
||||||
|
if (file.length() >= 1024 * 1024 * 100) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, I18n.getString("termora.settings.sync.import.file-too-large"),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val text = file.readText()
|
||||||
|
val jsonResult = ohMyJson.runCatching { decodeFromString<JsonObject>(text) }
|
||||||
|
if (jsonResult.isFailure) {
|
||||||
|
val e = jsonResult.exceptionOrNull() ?: return
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, ExceptionUtils.getRootCauseMessage(e),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val json = jsonResult.getOrNull() ?: return
|
||||||
|
if (ranges.contains(SyncRange.Hosts)) {
|
||||||
|
val hosts = json["hosts"]
|
||||||
|
if (hosts is JsonArray) {
|
||||||
|
ohMyJson.runCatching { decodeFromJsonElement<List<Host>>(hosts.jsonArray) }.onSuccess {
|
||||||
|
for (host in it) {
|
||||||
|
hostManager.addHost(host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ranges.contains(SyncRange.KeyPairs)) {
|
||||||
|
val keyPairs = json["keyPairs"]
|
||||||
|
if (keyPairs is JsonArray) {
|
||||||
|
ohMyJson.runCatching { decodeFromJsonElement<List<OhKeyPair>>(keyPairs.jsonArray) }.onSuccess {
|
||||||
|
for (keyPair in it) {
|
||||||
|
keyManager.addOhKeyPair(keyPair)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ranges.contains(SyncRange.KeywordHighlights)) {
|
||||||
|
val keywordHighlights = json["keywordHighlights"]
|
||||||
|
if (keywordHighlights is JsonArray) {
|
||||||
|
ohMyJson.runCatching { decodeFromJsonElement<List<KeywordHighlight>>(keywordHighlights.jsonArray) }
|
||||||
|
.onSuccess {
|
||||||
|
for (keyPair in it) {
|
||||||
|
keywordHighlightManager.addKeywordHighlight(keyPair)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ranges.contains(SyncRange.Macros)) {
|
||||||
|
val macros = json["macros"]
|
||||||
|
if (macros is JsonArray) {
|
||||||
|
ohMyJson.runCatching { decodeFromJsonElement<List<Macro>>(macros.jsonArray) }.onSuccess {
|
||||||
|
for (macro in it) {
|
||||||
|
macroManager.addMacro(macro)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ranges.contains(SyncRange.Keymap)) {
|
||||||
|
val keymaps = json["keymaps"]
|
||||||
|
if (keymaps is JsonArray) {
|
||||||
|
for (keymap in keymaps.jsonArray.mapNotNull { Keymap.fromJSON(it.jsonObject) }) {
|
||||||
|
keymapManager.addKeymap(keymap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, I18n.getString("termora.settings.sync.import.successful"),
|
||||||
|
messageType = JOptionPane.INFORMATION_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun exportText(file: File) {
|
private fun exportText(file: File) {
|
||||||
val syncConfig = getSyncConfig()
|
val syncConfig = getSyncConfig()
|
||||||
val text = ohMyJson.encodeToString(buildJsonObject {
|
val text = ohMyJson.encodeToString(buildJsonObject {
|
||||||
@@ -603,21 +766,29 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
put("os", SystemUtils.OS_NAME)
|
put("os", SystemUtils.OS_NAME)
|
||||||
put("exportDateHuman", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(Date(now)))
|
put("exportDateHuman", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(Date(now)))
|
||||||
if (syncConfig.ranges.contains(SyncRange.Hosts)) {
|
if (syncConfig.ranges.contains(SyncRange.Hosts)) {
|
||||||
put("hosts", ohMyJson.encodeToJsonElement(HostManager.getInstance().hosts()))
|
put("hosts", ohMyJson.encodeToJsonElement(hostManager.hosts()))
|
||||||
}
|
}
|
||||||
if (syncConfig.ranges.contains(SyncRange.KeyPairs)) {
|
if (syncConfig.ranges.contains(SyncRange.KeyPairs)) {
|
||||||
put("keyPairs", ohMyJson.encodeToJsonElement(KeyManager.getInstance().getOhKeyPairs()))
|
put("keyPairs", ohMyJson.encodeToJsonElement(keyManager.getOhKeyPairs()))
|
||||||
}
|
}
|
||||||
if (syncConfig.ranges.contains(SyncRange.KeywordHighlights)) {
|
if (syncConfig.ranges.contains(SyncRange.KeywordHighlights)) {
|
||||||
put(
|
put(
|
||||||
"keywordHighlights",
|
"keywordHighlights",
|
||||||
ohMyJson.encodeToJsonElement(KeywordHighlightManager.getInstance().getKeywordHighlights())
|
ohMyJson.encodeToJsonElement(keywordHighlightManager.getKeywordHighlights())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (syncConfig.ranges.contains(SyncRange.Macros)) {
|
if (syncConfig.ranges.contains(SyncRange.Macros)) {
|
||||||
put(
|
put(
|
||||||
"macros",
|
"macros",
|
||||||
ohMyJson.encodeToJsonElement(MacroManager.getInstance().getMacros())
|
ohMyJson.encodeToJsonElement(macroManager.getMacros())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (syncConfig.ranges.contains(SyncRange.Keymap)) {
|
||||||
|
val keymaps = keymapManager.getKeymaps().filter { !it.isReadonly }
|
||||||
|
.map { it.toJSONObject() }
|
||||||
|
put(
|
||||||
|
"keymaps",
|
||||||
|
ohMyJson.encodeToJsonElement(keymaps)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
put("settings", buildJsonObject {
|
put("settings", buildJsonObject {
|
||||||
@@ -662,6 +833,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DuplicatedCode")
|
||||||
private suspend fun pushOrPull(push: Boolean) {
|
private suspend fun pushOrPull(push: Boolean) {
|
||||||
|
|
||||||
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
||||||
@@ -717,6 +889,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
exportConfigButton.isEnabled = false
|
exportConfigButton.isEnabled = false
|
||||||
|
importConfigButton.isEnabled = false
|
||||||
downloadConfigButton.isEnabled = false
|
downloadConfigButton.isEnabled = false
|
||||||
uploadConfigButton.isEnabled = false
|
uploadConfigButton.isEnabled = false
|
||||||
typeComboBox.isEnabled = false
|
typeComboBox.isEnabled = false
|
||||||
@@ -752,6 +925,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
downloadConfigButton.isEnabled = true
|
downloadConfigButton.isEnabled = true
|
||||||
exportConfigButton.isEnabled = true
|
exportConfigButton.isEnabled = true
|
||||||
|
importConfigButton.isEnabled = true
|
||||||
uploadConfigButton.isEnabled = true
|
uploadConfigButton.isEnabled = true
|
||||||
keysCheckBox.isEnabled = true
|
keysCheckBox.isEnabled = true
|
||||||
hostsCheckBox.isEnabled = true
|
hostsCheckBox.isEnabled = true
|
||||||
@@ -892,7 +1066,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
var rows = 1
|
var rows = 1
|
||||||
val step = 2
|
val step = 2
|
||||||
val builder = FormBuilder.create().layout(layout).debug(false);
|
val builder = FormBuilder.create().layout(layout).debug(false)
|
||||||
val box = Box.createHorizontalBox()
|
val box = Box.createHorizontalBox()
|
||||||
box.add(typeComboBox)
|
box.add(typeComboBox)
|
||||||
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
||||||
@@ -911,10 +1085,11 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
// Sync buttons
|
// Sync buttons
|
||||||
.add(
|
.add(
|
||||||
FormBuilder.create()
|
FormBuilder.create()
|
||||||
.layout(FormLayout("left:pref, $formMargin, left:pref, $formMargin, left:pref", "pref"))
|
.layout(FormLayout("pref, 2dlu, pref, 2dlu, pref, 2dlu, pref", "pref"))
|
||||||
.add(uploadConfigButton).xy(1, 1)
|
.add(uploadConfigButton).xy(1, 1)
|
||||||
.add(downloadConfigButton).xy(3, 1)
|
.add(downloadConfigButton).xy(3, 1)
|
||||||
.add(exportConfigButton).xy(5, 1)
|
.add(exportConfigButton).xy(5, 1)
|
||||||
|
.add(importConfigButton).xy(7, 1)
|
||||||
.build()
|
.build()
|
||||||
).xy(3, rows, "center, fill").apply { rows += step }
|
).xy(3, rows, "center, fill").apply { rows += step }
|
||||||
.add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step }
|
.add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step }
|
||||||
@@ -1009,8 +1184,6 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private val tip = FlatLabel()
|
private val tip = FlatLabel()
|
||||||
private val safeBtn = FlatButton()
|
private val safeBtn = FlatButton()
|
||||||
private val doorman get() = Doorman.getInstance()
|
private val doorman get() = Doorman.getInstance()
|
||||||
private val hostManager get() = HostManager.getInstance()
|
|
||||||
private val keyManager get() = KeyManager.getInstance()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import org.apache.sshd.client.ClientBuilder
|
|||||||
import org.apache.sshd.client.SshClient
|
import org.apache.sshd.client.SshClient
|
||||||
import org.apache.sshd.client.channel.ChannelShell
|
import org.apache.sshd.client.channel.ChannelShell
|
||||||
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver
|
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver
|
||||||
|
import org.apache.sshd.client.kex.DHGClient
|
||||||
import org.apache.sshd.client.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
import org.apache.sshd.common.SshException
|
import org.apache.sshd.common.SshException
|
||||||
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
||||||
import org.apache.sshd.common.global.KeepAliveHandler
|
import org.apache.sshd.common.global.KeepAliveHandler
|
||||||
|
import org.apache.sshd.common.kex.BuiltinDHFactories
|
||||||
import org.apache.sshd.common.util.net.SshdSocketAddress
|
import org.apache.sshd.common.util.net.SshdSocketAddress
|
||||||
import org.apache.sshd.core.CoreModuleProperties
|
import org.apache.sshd.core.CoreModuleProperties
|
||||||
import org.apache.sshd.server.forward.AcceptAllForwardingFilter
|
import org.apache.sshd.server.forward.AcceptAllForwardingFilter
|
||||||
@@ -133,6 +135,18 @@ object SshClients {
|
|||||||
builder.globalRequestHandlers(listOf(KeepAliveHandler.INSTANCE))
|
builder.globalRequestHandlers(listOf(KeepAliveHandler.INSTANCE))
|
||||||
.factory { JGitSshClient() }
|
.factory { JGitSshClient() }
|
||||||
|
|
||||||
|
val keyExchangeFactories = ClientBuilder.setUpDefaultKeyExchanges(true).toMutableList()
|
||||||
|
|
||||||
|
// https://github.com/TermoraDev/termora/issues/123
|
||||||
|
keyExchangeFactories.addAll(
|
||||||
|
listOf(
|
||||||
|
DHGClient.newFactory(BuiltinDHFactories.dhg1),
|
||||||
|
DHGClient.newFactory(BuiltinDHFactories.dhg14),
|
||||||
|
DHGClient.newFactory(BuiltinDHFactories.dhgex),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
builder.keyExchangeFactories(keyExchangeFactories)
|
||||||
|
|
||||||
if (host.tunnelings.isEmpty() && host.options.jumpHosts.isEmpty()) {
|
if (host.tunnelings.isEmpty() && host.options.jumpHosts.isEmpty()) {
|
||||||
builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE)
|
builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE)
|
||||||
} else {
|
} else {
|
||||||
@@ -144,6 +158,8 @@ object SshClients {
|
|||||||
val sshClient = builder.build() as JGitSshClient
|
val sshClient = builder.build() as JGitSshClient
|
||||||
val heartbeatInterval = max(host.options.heartbeatInterval, 3)
|
val heartbeatInterval = max(host.options.heartbeatInterval, 3)
|
||||||
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
|
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
|
||||||
|
CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true)
|
||||||
|
|
||||||
sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) }
|
sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) }
|
||||||
|
|
||||||
if (host.proxy.type != ProxyType.No) {
|
if (host.proxy.type != ProxyType.No) {
|
||||||
|
|||||||
@@ -10,14 +10,19 @@ import app.termora.transport.TransportPanel
|
|||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
import java.awt.event.AWTEventListener
|
import java.awt.event.AWTEventListener
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import javax.swing.*
|
import java.util.*
|
||||||
|
import javax.swing.Icon
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.JPanel
|
||||||
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class TerminalTabbed(
|
class TerminalTabbed(
|
||||||
@@ -30,7 +35,7 @@ class TerminalTabbed(
|
|||||||
private val toolbar = termoraToolBar.getJToolBar()
|
private val toolbar = termoraToolBar.getJToolBar()
|
||||||
private val actionManager = ActionManager.getInstance()
|
private val actionManager = ActionManager.getInstance()
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
|
private val titleProperty = UUID.randomUUID().toSimpleString()
|
||||||
private val iconListener = PropertyChangeListener { e ->
|
private val iconListener = PropertyChangeListener { e ->
|
||||||
val source = e.source
|
val source = e.source
|
||||||
if (e.propertyName == "icon" && source is TerminalTab) {
|
if (e.propertyName == "icon" && source is TerminalTab) {
|
||||||
@@ -52,9 +57,6 @@ class TerminalTabbed(
|
|||||||
tabbedPane.isTabsClosable = true
|
tabbedPane.isTabsClosable = true
|
||||||
tabbedPane.tabType = FlatTabbedPane.TabType.card
|
tabbedPane.tabType = FlatTabbedPane.TabType.card
|
||||||
|
|
||||||
tabbedPane.styleMap = mapOf(
|
|
||||||
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground")
|
|
||||||
)
|
|
||||||
tabbedPane.trailingComponent = toolbar
|
tabbedPane.trailingComponent = toolbar
|
||||||
|
|
||||||
add(tabbedPane, BorderLayout.CENTER)
|
add(tabbedPane, BorderLayout.CENTER)
|
||||||
@@ -190,16 +192,16 @@ class TerminalTabbed(
|
|||||||
// 修改名称
|
// 修改名称
|
||||||
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
|
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
|
||||||
rename.addActionListener {
|
rename.addActionListener {
|
||||||
val index = tabbedPane.selectedIndex
|
if (tabIndex > 0) {
|
||||||
if (index > 0) {
|
|
||||||
val dialog = InputDialog(
|
val dialog = InputDialog(
|
||||||
SwingUtilities.getWindowAncestor(this),
|
SwingUtilities.getWindowAncestor(this),
|
||||||
title = rename.text,
|
title = rename.text,
|
||||||
text = tabbedPane.getTitleAt(index),
|
text = tabbedPane.getTitleAt(tabIndex),
|
||||||
)
|
)
|
||||||
val text = dialog.getText()
|
val text = dialog.getText()
|
||||||
if (!text.isNullOrBlank()) {
|
if (!text.isNullOrBlank()) {
|
||||||
tabbedPane.setTitleAt(index, text)
|
tabbedPane.setTitleAt(tabIndex, text)
|
||||||
|
c.putClientProperty(titleProperty, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,9 +278,8 @@ class TerminalTabbed(
|
|||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
|
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
|
||||||
reconnect.addActionListener {
|
reconnect.addActionListener {
|
||||||
val index = tabbedPane.selectedIndex
|
if (tabIndex > 0) {
|
||||||
if (index > 0) {
|
tabs[tabIndex].reconnect()
|
||||||
tabs[index].reconnect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,18 +290,24 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun addTab(tab: TerminalTab) {
|
private fun addTab(index: Int, tab: TerminalTab) {
|
||||||
tabbedPane.addTab(
|
val c = tab.getJComponent()
|
||||||
tab.getTitle(),
|
val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
|
||||||
|
|
||||||
|
tabbedPane.insertTab(
|
||||||
|
title,
|
||||||
tab.getIcon(),
|
tab.getIcon(),
|
||||||
tab.getJComponent()
|
c,
|
||||||
|
StringUtils.EMPTY,
|
||||||
|
index
|
||||||
)
|
)
|
||||||
|
c.putClientProperty(titleProperty, title)
|
||||||
|
|
||||||
// 监听 icons 变化
|
// 监听 icons 变化
|
||||||
tab.addPropertyChangeListener(iconListener)
|
tab.addPropertyChangeListener(iconListener)
|
||||||
|
|
||||||
tabs.add(tab)
|
tabs.add(index, tab)
|
||||||
tabbedPane.selectedIndex = tabbedPane.tabCount - 1
|
tabbedPane.selectedIndex = index
|
||||||
Disposer.register(this, tab)
|
Disposer.register(this, tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +400,11 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun addTerminalTab(tab: TerminalTab) {
|
override fun addTerminalTab(tab: TerminalTab) {
|
||||||
addTab(tab)
|
addTab(tabs.size, tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addTerminalTab(index: Int, tab: TerminalTab) {
|
||||||
|
addTab(index, tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSelectedTerminalTab(): TerminalTab? {
|
override fun getSelectedTerminalTab(): TerminalTab? {
|
||||||
@@ -418,10 +429,10 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun closeTerminalTab(tab: TerminalTab) {
|
override fun closeTerminalTab(tab: TerminalTab, disposable: Boolean) {
|
||||||
for (i in 0 until tabs.size) {
|
for (i in 0 until tabs.size) {
|
||||||
if (tabs[i] == tab) {
|
if (tabs[i] == tab) {
|
||||||
removeTabAt(i, true)
|
removeTabAt(i, disposable)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package app.termora
|
|||||||
|
|
||||||
interface TerminalTabbedManager {
|
interface TerminalTabbedManager {
|
||||||
fun addTerminalTab(tab: TerminalTab)
|
fun addTerminalTab(tab: TerminalTab)
|
||||||
|
fun addTerminalTab(index: Int, tab: TerminalTab)
|
||||||
fun getSelectedTerminalTab(): TerminalTab?
|
fun getSelectedTerminalTab(): TerminalTab?
|
||||||
fun getTerminalTabs(): List<TerminalTab>
|
fun getTerminalTabs(): List<TerminalTab>
|
||||||
fun setSelectedTerminalTab(tab: TerminalTab)
|
fun setSelectedTerminalTab(tab: TerminalTab)
|
||||||
fun closeTerminalTab(tab: TerminalTab)
|
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true)
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
minimumSize = Dimension(640, 400)
|
minimumSize = Dimension(640, 400)
|
||||||
terminalTabbed.addTab(welcomePanel)
|
terminalTabbed.addTerminalTab(welcomePanel)
|
||||||
|
|
||||||
// macOS 要避开左边的控制栏
|
// macOS 要避开左边的控制栏
|
||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isMacOS) {
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import org.apache.commons.lang3.StringUtils
|
|||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.KeyEventDispatcher
|
import java.awt.KeyEventDispatcher
|
||||||
import java.awt.KeyEventPostProcessor
|
|
||||||
import java.awt.KeyboardFocusManager
|
import java.awt.KeyboardFocusManager
|
||||||
import java.awt.event.KeyEvent
|
import java.awt.event.KeyEvent
|
||||||
|
import javax.swing.JComponent
|
||||||
import javax.swing.JDialog
|
import javax.swing.JDialog
|
||||||
import javax.swing.KeyStroke
|
import javax.swing.KeyStroke
|
||||||
|
|
||||||
@@ -23,15 +23,13 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(KeymapManager::class.java)
|
private val log = LoggerFactory.getLogger(KeymapManager::class.java)
|
||||||
|
|
||||||
const val PROCESS_GLOBAL_KEYMAP = "PROCESS_GLOBAL_KEYMAP"
|
|
||||||
|
|
||||||
fun getInstance(): KeymapManager {
|
fun getInstance(): KeymapManager {
|
||||||
return ApplicationScope.forApplicationScope()
|
return ApplicationScope.forApplicationScope()
|
||||||
.getOrCreate(KeymapManager::class) { KeymapManager() }
|
.getOrCreate(KeymapManager::class) { KeymapManager() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val myKeyEventPostProcessor = MyKeyEventPostProcessor()
|
private val keymapKeyEventDispatcher = KeymapKeyEventDispatcher()
|
||||||
private val myKeyEventDispatcher = MyKeyEventDispatcher()
|
private val myKeyEventDispatcher = MyKeyEventDispatcher()
|
||||||
private val database get() = Database.getDatabase()
|
private val database get() = Database.getDatabase()
|
||||||
private val keymaps = linkedMapOf<String, Keymap>()
|
private val keymaps = linkedMapOf<String, Keymap>()
|
||||||
@@ -39,7 +37,7 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
|
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
keyboardFocusManager.addKeyEventPostProcessor(myKeyEventPostProcessor)
|
keyboardFocusManager.addKeyEventDispatcher(keymapKeyEventDispatcher)
|
||||||
keyboardFocusManager.addKeyEventDispatcher(myKeyEventDispatcher)
|
keyboardFocusManager.addKeyEventDispatcher(myKeyEventDispatcher)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -97,12 +95,26 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
database.removeKeymap(name)
|
database.removeKeymap(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class MyKeyEventPostProcessor : KeyEventPostProcessor {
|
private inner class KeymapKeyEventDispatcher : KeyEventDispatcher {
|
||||||
override fun postProcessKeyEvent(e: KeyEvent): Boolean {
|
|
||||||
// 只处理 PRESSED 和 带有 modifiers 键的事件
|
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
|
||||||
if (!e.isConsumed && e.id == KeyEvent.KEY_PRESSED && e.modifiersEx != 0) {
|
if (e.isConsumed || e.id != KeyEvent.KEY_PRESSED || e.modifiersEx == 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyStroke = KeyStroke.getKeyStrokeForEvent(e)
|
||||||
|
val component = e.source
|
||||||
|
|
||||||
|
if (component is JComponent) {
|
||||||
|
// 如果这个键已经被组件注册了,那么忽略
|
||||||
|
if (component.getConditionForKeyStroke(keyStroke) != JComponent.UNDEFINED_CONDITION) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val shortcuts = getActiveKeymap()
|
val shortcuts = getActiveKeymap()
|
||||||
val actionIds = shortcuts.getActionIds(KeyShortcut(KeyStroke.getKeyStrokeForEvent(e)))
|
val actionIds = shortcuts.getActionIds(KeyShortcut(keyStroke))
|
||||||
if (actionIds.isEmpty()) {
|
if (actionIds.isEmpty()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -128,7 +140,6 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -163,7 +174,7 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
|
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
keyboardFocusManager.removeKeyEventPostProcessor(myKeyEventPostProcessor)
|
keyboardFocusManager.removeKeyEventDispatcher(keymapKeyEventDispatcher)
|
||||||
keyboardFocusManager.removeKeyEventDispatcher(myKeyEventDispatcher)
|
keyboardFocusManager.removeKeyEventDispatcher(myKeyEventDispatcher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,7 @@ class KeyManager private constructor() {
|
|||||||
if (keyPair == OhKeyPair.empty) {
|
if (keyPair == OhKeyPair.empty) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
keyPairs.remove(keyPair)
|
||||||
keyPairs.add(keyPair)
|
keyPairs.add(keyPair)
|
||||||
database.addKeyPair(keyPair)
|
database.addKeyPair(keyPair)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ class FileChooser {
|
|||||||
var allowsOtherFileTypes = true
|
var allowsOtherFileTypes = true
|
||||||
var canCreateDirectories = true
|
var canCreateDirectories = true
|
||||||
var win32Filters = mutableListOf<Pair<String, List<String>>>()
|
var win32Filters = mutableListOf<Pair<String, List<String>>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* e.g. listOf("json")
|
||||||
|
*/
|
||||||
var osxAllowedFileTypes = emptyList<String>()
|
var osxAllowedFileTypes = emptyList<String>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package app.termora.terminal
|
|
||||||
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
class DeviceControlProcessor(private val terminal: Terminal) : Processor {
|
|
||||||
private val args = StringBuilder()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val log = LoggerFactory.getLogger(DeviceControlProcessor::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun process(ch: Char): ProcessorState {
|
|
||||||
val state = when (ch) {
|
|
||||||
ControlCharacters.ST -> {
|
|
||||||
if (log.isWarnEnabled) {
|
|
||||||
log.warn("Ignore DCS: {}", args)
|
|
||||||
}
|
|
||||||
TerminalState.READY
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
args.append(ch)
|
|
||||||
TerminalState.DCS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == TerminalState.READY) {
|
|
||||||
args.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
return state
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package app.termora.terminal
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
class DeviceControlStringProcessor(terminal: Terminal, reader: TerminalReader) : AbstractProcessor(terminal, reader) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(DeviceControlStringProcessor::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val systemCommandSequence = SystemCommandSequence()
|
||||||
|
|
||||||
|
override fun process(ch: Char): ProcessorState {
|
||||||
|
// 回退回去,然后重新读取出来
|
||||||
|
reader.addFirst(ch)
|
||||||
|
|
||||||
|
do {
|
||||||
|
|
||||||
|
if (systemCommandSequence.process(reader.read())) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有检测到结束,那么退出重新来
|
||||||
|
if (reader.isEmpty()) {
|
||||||
|
return TerminalState.DCS
|
||||||
|
}
|
||||||
|
|
||||||
|
} while (reader.isNotEmpty())
|
||||||
|
|
||||||
|
processCommand(systemCommandSequence.getCommand())
|
||||||
|
|
||||||
|
systemCommandSequence.reset()
|
||||||
|
|
||||||
|
return TerminalState.READY
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun processCommand(command: String) {
|
||||||
|
if (command.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn("Cannot process command: {}", command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -128,9 +128,9 @@ class EscapeSequenceProcessor(terminal: Terminal, reader: TerminalReader) : Abst
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Device Control String (DCS is 0x90).
|
// Device Control String (DCS is 0x90).
|
||||||
'P' -> {
|
'P' -> {
|
||||||
|
state = TerminalState.DCS
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start of Guarded Area (SPA is 0x96).
|
// Start of Guarded Area (SPA is 0x96).
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package app.termora.terminal
|
package app.termora.terminal
|
||||||
|
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.event.KeyEvent
|
import java.awt.event.KeyEvent
|
||||||
|
|
||||||
@Suppress("MemberVisibilityCanBePrivate")
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataListener {
|
open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataListener {
|
||||||
|
|
||||||
private val mapping = mutableMapOf<TerminalKeyEvent, String>()
|
private val mapping = mutableMapOf<TerminalKeyEvent, String>()
|
||||||
private val nothing = String()
|
private val nothing = StringUtils.EMPTY
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
|||||||
|
|
||||||
configureLeftRight()
|
configureLeftRight()
|
||||||
|
|
||||||
|
// Ctrl + C
|
||||||
putCode(TerminalKeyEvent(keyCode = 8), String(byteArrayOf(127)))
|
putCode(TerminalKeyEvent(keyCode = 8), String(byteArrayOf(127)))
|
||||||
|
|
||||||
// Enter
|
// Enter
|
||||||
@@ -38,15 +40,15 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
|||||||
|
|
||||||
|
|
||||||
// Page Up
|
// Page Up
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x21), encode = "${ControlCharacters.ESC}[5~")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_PAGE_UP), encode = "${ControlCharacters.ESC}[5~")
|
||||||
// Page Down
|
// Page Down
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x22), encode = "${ControlCharacters.ESC}[6~")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_PAGE_DOWN), encode = "${ControlCharacters.ESC}[6~")
|
||||||
|
|
||||||
|
|
||||||
// Insert
|
// Insert
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x9B), encode = "${ControlCharacters.ESC}[2~")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_INSERT), encode = "${ControlCharacters.ESC}[2~")
|
||||||
// Delete
|
// Delete
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x7F), encode = "${ControlCharacters.ESC}[3~")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_DELETE), encode = "${ControlCharacters.ESC}[3~")
|
||||||
|
|
||||||
// Function Keys
|
// Function Keys
|
||||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F1), encode = "${ControlCharacters.ESC}OP")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F1), encode = "${ControlCharacters.ESC}OP")
|
||||||
@@ -84,26 +86,29 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
|||||||
|
|
||||||
fun arrowKeysApplicationSequences() {
|
fun arrowKeysApplicationSequences() {
|
||||||
// Up
|
// Up
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x26), encode = "${ControlCharacters.ESC}OA")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_UP), encode = "${ControlCharacters.ESC}OA")
|
||||||
// Down
|
// Down
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x28), encode = "${ControlCharacters.ESC}OB")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_DOWN), encode = "${ControlCharacters.ESC}OB")
|
||||||
// Left
|
// Left
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x25), encode = "${ControlCharacters.ESC}OD")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_LEFT), encode = "${ControlCharacters.ESC}OD")
|
||||||
// Right
|
// Right
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x27), encode = "${ControlCharacters.ESC}OC")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_RIGHT), encode = "${ControlCharacters.ESC}OC")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun arrowKeysAnsiCursorSequences() {
|
fun arrowKeysAnsiCursorSequences() {
|
||||||
// Up
|
// Up
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x26), encode = "${ControlCharacters.ESC}[A")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_UP), encode = "${ControlCharacters.ESC}[A")
|
||||||
// Down
|
// Down
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x28), encode = "${ControlCharacters.ESC}[B")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_DOWN), encode = "${ControlCharacters.ESC}[B")
|
||||||
// Left
|
// Left
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x25), encode = "${ControlCharacters.ESC}[D")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_LEFT), encode = "${ControlCharacters.ESC}[D")
|
||||||
// Right
|
// Right
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x27), encode = "${ControlCharacters.ESC}[C")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_RIGHT), encode = "${ControlCharacters.ESC}[C")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alt + Left/Right
|
||||||
|
*/
|
||||||
fun configureLeftRight() {
|
fun configureLeftRight() {
|
||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isMacOS) {
|
||||||
putCode(
|
putCode(
|
||||||
@@ -141,32 +146,32 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
|||||||
|
|
||||||
fun keypadApplicationSequences() {
|
fun keypadApplicationSequences() {
|
||||||
// Up
|
// Up
|
||||||
putCode(TerminalKeyEvent(keyCode = 0xE0), encode = "${ControlCharacters.ESC}OA")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_UP), encode = "${ControlCharacters.ESC}OA")
|
||||||
// Down
|
// Down
|
||||||
putCode(TerminalKeyEvent(keyCode = 0xE1), encode = "${ControlCharacters.ESC}OB")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_DOWN), encode = "${ControlCharacters.ESC}OB")
|
||||||
// Left
|
// Left
|
||||||
putCode(TerminalKeyEvent(keyCode = 0xE2), encode = "${ControlCharacters.ESC}OD")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_LEFT), encode = "${ControlCharacters.ESC}OD")
|
||||||
// Right
|
// Right
|
||||||
putCode(TerminalKeyEvent(keyCode = 0xE3), encode = "${ControlCharacters.ESC}OC")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_RIGHT), encode = "${ControlCharacters.ESC}OC")
|
||||||
// Home
|
// Home
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x24), encode = "${ControlCharacters.ESC}OH")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_HOME), encode = "${ControlCharacters.ESC}OH")
|
||||||
// End
|
// End
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x23), encode = "${ControlCharacters.ESC}OF")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_END), encode = "${ControlCharacters.ESC}OF")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun keypadAnsiSequences() {
|
fun keypadAnsiSequences() {
|
||||||
// Up
|
// Up
|
||||||
putCode(TerminalKeyEvent(keyCode = 0xE0), encode = "${ControlCharacters.ESC}[A")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_UP), encode = "${ControlCharacters.ESC}[A")
|
||||||
// Down
|
// Down
|
||||||
putCode(TerminalKeyEvent(keyCode = 0xE1), encode = "${ControlCharacters.ESC}[B")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_DOWN), encode = "${ControlCharacters.ESC}[B")
|
||||||
// Left
|
// Left
|
||||||
putCode(TerminalKeyEvent(keyCode = 0xE2), encode = "${ControlCharacters.ESC}[D")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_LEFT), encode = "${ControlCharacters.ESC}[D")
|
||||||
// Right
|
// Right
|
||||||
putCode(TerminalKeyEvent(keyCode = 0xE3), encode = "${ControlCharacters.ESC}[C")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_RIGHT), encode = "${ControlCharacters.ESC}[C")
|
||||||
// Home
|
// Home
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x24), encode = "${ControlCharacters.ESC}[H")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_HOME), encode = "${ControlCharacters.ESC}[H")
|
||||||
// End
|
// End
|
||||||
putCode(TerminalKeyEvent(keyCode = 0x23), encode = "${ControlCharacters.ESC}[F")
|
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_END), encode = "${ControlCharacters.ESC}[F")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onChanged(key: DataKey<*>, data: Any) {
|
override fun onChanged(key: DataKey<*>, data: Any) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import java.awt.datatransfer.StringSelection
|
|||||||
|
|
||||||
class OperatingSystemCommandProcessor(terminal: Terminal, reader: TerminalReader) :
|
class OperatingSystemCommandProcessor(terminal: Terminal, reader: TerminalReader) :
|
||||||
AbstractProcessor(terminal, reader) {
|
AbstractProcessor(terminal, reader) {
|
||||||
private val args = StringBuilder()
|
private val systemCommandSequence = SystemCommandSequence()
|
||||||
private val colorPalette get() = terminal.getTerminalModel().getColorPalette()
|
private val colorPalette get() = terminal.getTerminalModel().getColorPalette()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -20,14 +20,7 @@ class OperatingSystemCommandProcessor(terminal: Terminal, reader: TerminalReader
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
|
|
||||||
val c = reader.read()
|
if (systemCommandSequence.process(reader.read())) {
|
||||||
args.append(c)
|
|
||||||
if (c == ControlCharacters.BEL || c == ControlCharacters.ST) {
|
|
||||||
args.deleteAt(args.lastIndex)
|
|
||||||
break
|
|
||||||
} else if (c == '\\' && args.length >= 2 && args[args.length - 2] == ControlCharacters.ESC) {
|
|
||||||
args.deleteAt(args.lastIndex)
|
|
||||||
args.deleteAt(args.lastIndex)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +35,7 @@ class OperatingSystemCommandProcessor(terminal: Terminal, reader: TerminalReader
|
|||||||
// process osc
|
// process osc
|
||||||
processOperatingSystemCommandProcessor()
|
processOperatingSystemCommandProcessor()
|
||||||
|
|
||||||
args.clear()
|
systemCommandSequence.reset()
|
||||||
|
|
||||||
return TerminalState.READY
|
return TerminalState.READY
|
||||||
}
|
}
|
||||||
@@ -52,6 +45,7 @@ class OperatingSystemCommandProcessor(terminal: Terminal, reader: TerminalReader
|
|||||||
* https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
* https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
||||||
*/
|
*/
|
||||||
private fun processOperatingSystemCommandProcessor() {
|
private fun processOperatingSystemCommandProcessor() {
|
||||||
|
val args = systemCommandSequence.getCommand()
|
||||||
val idx = args.indexOfFirst { it == ';' }
|
val idx = args.indexOfFirst { it == ';' }
|
||||||
if (idx == -1) {
|
if (idx == -1) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package app.termora.terminal
|
||||||
|
|
||||||
|
class SystemCommandSequence {
|
||||||
|
private var isTerminated = false
|
||||||
|
private val command = StringBuilder()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return 返回 true 表示处理完毕
|
||||||
|
*/
|
||||||
|
fun process(c: Char): Boolean {
|
||||||
|
|
||||||
|
if (isTerminated) {
|
||||||
|
throw UnsupportedOperationException("Cannot be processed, call the reset method")
|
||||||
|
}
|
||||||
|
|
||||||
|
command.append(c)
|
||||||
|
if (c == ControlCharacters.BEL || c == ControlCharacters.ST) {
|
||||||
|
command.deleteAt(command.lastIndex)
|
||||||
|
isTerminated = true
|
||||||
|
} else if (c == '\\' && command.length >= 2 && command[command.length - 2] == ControlCharacters.ESC) {
|
||||||
|
command.deleteAt(command.lastIndex)
|
||||||
|
command.deleteAt(command.lastIndex)
|
||||||
|
isTerminated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return isTerminated
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCommand(): String {
|
||||||
|
return command.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
isTerminated = false
|
||||||
|
command.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.terminal
|
package app.termora.terminal
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.Toolkit
|
import java.awt.Toolkit
|
||||||
import kotlin.reflect.cast
|
import kotlin.reflect.cast
|
||||||
@@ -8,7 +9,7 @@ open class TerminalModelImpl(private val terminal: Terminal) : TerminalModel {
|
|||||||
private var rows: Int = 27
|
private var rows: Int = 27
|
||||||
private var cols: Int = 80
|
private var cols: Int = 80
|
||||||
private val data = mutableMapOf<DataKey<*>, Any>()
|
private val data = mutableMapOf<DataKey<*>, Any>()
|
||||||
private val listeners = mutableListOf<DataListener>()
|
private var listeners = emptyArray<DataListener>()
|
||||||
private val colorPalette = ColorPaletteImpl(terminal)
|
private val colorPalette = ColorPaletteImpl(terminal)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -92,11 +93,11 @@ open class TerminalModelImpl(private val terminal: Terminal) : TerminalModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun addDataListener(listener: DataListener) {
|
override fun addDataListener(listener: DataListener) {
|
||||||
listeners.add(listener)
|
listeners = ArrayUtils.add(listeners, listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeDataListener(listener: DataListener) {
|
override fun removeDataListener(listener: DataListener) {
|
||||||
listeners.remove(listener)
|
listeners = ArrayUtils.removeElement(listeners, listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bell() {
|
override fun bell() {
|
||||||
@@ -129,9 +130,8 @@ open class TerminalModelImpl(private val terminal: Terminal) : TerminalModel {
|
|||||||
|
|
||||||
@Suppress("MemberVisibilityCanBePrivate")
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
protected fun <T : Any> fireDataChanged(key: DataKey<T>, data: T) {
|
protected fun <T : Any> fireDataChanged(key: DataKey<T>, data: T) {
|
||||||
val size = listeners.size
|
for (listener in listeners) {
|
||||||
for (i in 0 until size) {
|
listener.onChanged(key, data)
|
||||||
listeners.getOrNull(i)?.onChanged(key, data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ private class MyProcessor(private val terminal: Terminal, reader: TerminalReader
|
|||||||
TerminalState.CSI to ControlSequenceIntroducerProcessor(terminal, reader),
|
TerminalState.CSI to ControlSequenceIntroducerProcessor(terminal, reader),
|
||||||
TerminalState.OSC to OperatingSystemCommandProcessor(terminal, reader),
|
TerminalState.OSC to OperatingSystemCommandProcessor(terminal, reader),
|
||||||
TerminalState.ESC_LPAREN to EscapeDesignateCharacterSetProcessor(terminal, reader),
|
TerminalState.ESC_LPAREN to EscapeDesignateCharacterSetProcessor(terminal, reader),
|
||||||
TerminalState.DCS to DeviceControlProcessor(terminal),
|
TerminalState.DCS to DeviceControlStringProcessor(terminal, reader),
|
||||||
TerminalState.Text to TextProcessor(terminal, reader),
|
TerminalState.Text to TextProcessor(terminal, reader),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.termora.terminal.panel
|
package app.termora.terminal.panel
|
||||||
|
|
||||||
|
import app.termora.keymap.KeyShortcut
|
||||||
|
import app.termora.keymap.KeymapManager
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
import app.termora.terminal.Terminal
|
import app.termora.terminal.Terminal
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
@@ -12,8 +14,9 @@ class TerminalPanelKeyAdapter(
|
|||||||
private val terminalPanel: TerminalPanel,
|
private val terminalPanel: TerminalPanel,
|
||||||
private val terminal: Terminal,
|
private val terminal: Terminal,
|
||||||
private val ptyConnector: PtyConnector
|
private val ptyConnector: PtyConnector
|
||||||
) :
|
) : KeyAdapter() {
|
||||||
KeyAdapter() {
|
|
||||||
|
private val activeKeymap get() = KeymapManager.getInstance().getActiveKeymap()
|
||||||
|
|
||||||
override fun keyTyped(e: KeyEvent) {
|
override fun keyTyped(e: KeyEvent) {
|
||||||
if (Character.isISOControl(e.keyChar)) {
|
if (Character.isISOControl(e.keyChar)) {
|
||||||
@@ -52,7 +55,12 @@ class TerminalPanelKeyAdapter(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Character.isISOControl(e.keyChar) && isCtrlPressedOnly(e)) {
|
// 如果命中了全局快捷键,那么不处理
|
||||||
|
if (keyStroke.modifiers != 0 && activeKeymap.getActionIds(KeyShortcut(keyStroke)).isNotEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Character.isISOControl(e.keyChar)) {
|
||||||
terminal.getSelectionModel().clearSelection()
|
terminal.getSelectionModel().clearSelection()
|
||||||
// 如果不为空表示已经发送过了,所以这里为空的时候再发送
|
// 如果不为空表示已经发送过了,所以这里为空的时候再发送
|
||||||
if (encode.isEmpty()) {
|
if (encode.isEmpty()) {
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ class FileSystemTableModel(private val fileSystem: FileSystem) : DefaultTableMod
|
|||||||
when (column) {
|
when (column) {
|
||||||
COLUMN_NAME -> path
|
COLUMN_NAME -> path
|
||||||
COLUMN_FILE_SIZE -> if (path.isDirectory) StringUtils.EMPTY else formatBytes(path.fileSize)
|
COLUMN_FILE_SIZE -> if (path.isDirectory) StringUtils.EMPTY else formatBytes(path.fileSize)
|
||||||
COLUMN_TYPE -> if (path.isDirectory) I18n.getString("termora.transport.table.type.folder") else path.extension
|
COLUMN_TYPE -> if (path.isDirectory) I18n.getString("termora.transport.table.type.folder")
|
||||||
|
else if (path.isSymbolicLink) I18n.getString("termora.transport.table.type.symbolic-link")
|
||||||
|
else path.extension
|
||||||
|
|
||||||
COLUMN_LAST_MODIFIED_TIME -> DateFormatUtils.format(Date(path.lastModifiedTime), "yyyy/MM/dd HH:mm")
|
COLUMN_LAST_MODIFIED_TIME -> DateFormatUtils.format(Date(path.lastModifiedTime), "yyyy/MM/dd HH:mm")
|
||||||
|
|
||||||
// 如果是本地的并且还是Windows系统
|
// 如果是本地的并且还是Windows系统
|
||||||
@@ -173,6 +176,7 @@ class FileSystemTableModel(private val fileSystem: FileSystem) : DefaultTableMod
|
|||||||
val extension by lazy { path.extension }
|
val extension by lazy { path.extension }
|
||||||
|
|
||||||
open val isDirectory by lazy { path.isDirectory() }
|
open val isDirectory by lazy { path.isDirectory() }
|
||||||
|
open val isSymbolicLink by lazy { path.isSymbolicLink() }
|
||||||
open val isHidden by lazy { fileName != ".." && path.isHidden() }
|
open val isHidden by lazy { fileName != ".." && path.isHidden() }
|
||||||
open val fileSize by lazy { path.fileSize() }
|
open val fileSize by lazy { path.fileSize() }
|
||||||
open val lastModifiedTime by lazy { Files.getLastModifiedTime(path).toMillis() }
|
open val lastModifiedTime by lazy { Files.getLastModifiedTime(path).toMillis() }
|
||||||
@@ -227,8 +231,10 @@ class FileSystemTableModel(private val fileSystem: FileSystem) : DefaultTableMod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val isDirectory: Boolean
|
override val isDirectory by lazy { attributes.isDirectory || (isSymbolicLink && Files.isDirectory(path)) }
|
||||||
get() = attributes.isDirectory
|
|
||||||
|
override val isSymbolicLink: Boolean
|
||||||
|
get() = attributes.isSymbolicLink
|
||||||
|
|
||||||
override val isHidden: Boolean
|
override val isHidden: Boolean
|
||||||
get() = fileName != ".." && fileName.startsWith(".")
|
get() = fileName != ".." && fileName.startsWith(".")
|
||||||
|
|||||||
@@ -70,7 +70,10 @@ termora.settings.sync.push=Push
|
|||||||
termora.settings.sync.push-warning=Pushing will overwrite the configuration. It is recommended to pull before pushing
|
termora.settings.sync.push-warning=Pushing will overwrite the configuration. It is recommended to pull before pushing
|
||||||
termora.settings.sync.pull=Pull
|
termora.settings.sync.pull=Pull
|
||||||
termora.settings.sync.done=Synchronized data successfully
|
termora.settings.sync.done=Synchronized data successfully
|
||||||
termora.settings.sync.export=Export
|
termora.settings.sync.export=${termora.keymgr.export}
|
||||||
|
termora.settings.sync.import=${termora.keymgr.import}
|
||||||
|
termora.settings.sync.import.file-too-large=The file is too large
|
||||||
|
termora.settings.sync.import.successful=Import data successfully
|
||||||
termora.settings.sync.export-done=The export was successful
|
termora.settings.sync.export-done=The export was successful
|
||||||
termora.settings.sync.export-done-open-folder=The export was successful. Do you want to open the folder?
|
termora.settings.sync.export-done-open-folder=The export was successful. Do you want to open the folder?
|
||||||
termora.settings.sync.range=Range
|
termora.settings.sync.range=Range
|
||||||
@@ -110,6 +113,7 @@ termora.find-everywhere.quick-command.local-terminal=Local Terminal
|
|||||||
# Welcome
|
# Welcome
|
||||||
termora.welcome.my-hosts=My hosts
|
termora.welcome.my-hosts=My hosts
|
||||||
termora.welcome.contextmenu.open=Open
|
termora.welcome.contextmenu.open=Open
|
||||||
|
termora.welcome.contextmenu.open-in-new-window=${termora.tabbed.contextmenu.open-in-new-window}
|
||||||
termora.welcome.contextmenu.copy=${termora.copy}
|
termora.welcome.contextmenu.copy=${termora.copy}
|
||||||
termora.welcome.contextmenu.remove=${termora.remove}
|
termora.welcome.contextmenu.remove=${termora.remove}
|
||||||
termora.welcome.contextmenu.rename=Rename
|
termora.welcome.contextmenu.rename=Rename
|
||||||
@@ -221,6 +225,7 @@ termora.transport.bookmarks.down=Down
|
|||||||
termora.transport.table.filename=Filename
|
termora.transport.table.filename=Filename
|
||||||
termora.transport.table.type=Type
|
termora.transport.table.type=Type
|
||||||
termora.transport.table.type.folder=${termora.welcome.contextmenu.new.folder}
|
termora.transport.table.type.folder=${termora.welcome.contextmenu.new.folder}
|
||||||
|
termora.transport.table.type.symbolic-link=Symbolic Link
|
||||||
termora.transport.table.size=Size
|
termora.transport.table.size=Size
|
||||||
termora.transport.table.modified-time=Modified
|
termora.transport.table.modified-time=Modified
|
||||||
termora.transport.table.permissions=Permissions
|
termora.transport.table.permissions=Permissions
|
||||||
|
|||||||
@@ -74,13 +74,14 @@ termora.settings.sync=同步
|
|||||||
termora.settings.sync.push=推送
|
termora.settings.sync.push=推送
|
||||||
termora.settings.sync.push-warning=推送将覆盖已有配置,建议先拉取再推送
|
termora.settings.sync.push-warning=推送将覆盖已有配置,建议先拉取再推送
|
||||||
termora.settings.sync.pull=拉取
|
termora.settings.sync.pull=拉取
|
||||||
termora.settings.sync.export=导出
|
|
||||||
termora.settings.sync.export-done=导出成功
|
termora.settings.sync.export-done=导出成功
|
||||||
termora.settings.sync.export-done-open-folder=导出成功,是否需要打开所在文件夹?
|
termora.settings.sync.export-done-open-folder=导出成功,是否需要打开所在文件夹?
|
||||||
termora.settings.sync.range=范围
|
termora.settings.sync.range=范围
|
||||||
termora.settings.sync.range.keys=我的密钥
|
termora.settings.sync.range.keys=我的密钥
|
||||||
termora.settings.sync.last-sync-time=最后同步时间
|
termora.settings.sync.last-sync-time=最后同步时间
|
||||||
termora.settings.sync.done=同步数据成功
|
termora.settings.sync.done=同步数据成功
|
||||||
|
termora.settings.sync.import.file-too-large=文件太大
|
||||||
|
termora.settings.sync.import.successful=导入数据成功
|
||||||
termora.settings.sync.gist=片段
|
termora.settings.sync.gist=片段
|
||||||
termora.settings.sync.token=令牌
|
termora.settings.sync.token=令牌
|
||||||
termora.settings.sync.type=类型
|
termora.settings.sync.type=类型
|
||||||
@@ -217,6 +218,7 @@ termora.transport.bookmarks.down=下移
|
|||||||
termora.transport.table.filename=文件名
|
termora.transport.table.filename=文件名
|
||||||
termora.transport.table.type=类型
|
termora.transport.table.type=类型
|
||||||
termora.transport.table.size=大小
|
termora.transport.table.size=大小
|
||||||
|
termora.transport.table.type.symbolic-link=软链接
|
||||||
termora.transport.table.modified-time=修改时间
|
termora.transport.table.modified-time=修改时间
|
||||||
termora.transport.table.permissions=权限
|
termora.transport.table.permissions=权限
|
||||||
termora.transport.table.owner=所有者
|
termora.transport.table.owner=所有者
|
||||||
|
|||||||
@@ -78,13 +78,14 @@ termora.settings.sync=同步
|
|||||||
termora.settings.sync.push=推送
|
termora.settings.sync.push=推送
|
||||||
termora.settings.sync.push-warning=推送將覆蓋先前的配置,建議先拉取再推送
|
termora.settings.sync.push-warning=推送將覆蓋先前的配置,建議先拉取再推送
|
||||||
termora.settings.sync.pull=拉取
|
termora.settings.sync.pull=拉取
|
||||||
termora.settings.sync.export=匯出
|
|
||||||
termora.settings.sync.export-done=匯出成功
|
termora.settings.sync.export-done=匯出成功
|
||||||
termora.settings.sync.export-done-open-folder=匯出成功,是否需要打開所在資料夾?
|
termora.settings.sync.export-done-open-folder=匯出成功,是否需要打開所在資料夾?
|
||||||
termora.settings.sync.range=範圍
|
termora.settings.sync.range=範圍
|
||||||
termora.settings.sync.range.keys=我的密鑰
|
termora.settings.sync.range.keys=我的密鑰
|
||||||
termora.settings.sync.last-sync-time=最後同步時間
|
termora.settings.sync.last-sync-time=最後同步時間
|
||||||
termora.settings.sync.done=同步資料成功
|
termora.settings.sync.done=同步資料成功
|
||||||
|
termora.settings.sync.import.file-too-large=檔案太大
|
||||||
|
termora.settings.sync.import.successful=導入資料成功
|
||||||
termora.settings.sync.gist=片段
|
termora.settings.sync.gist=片段
|
||||||
termora.settings.sync.token=令牌
|
termora.settings.sync.token=令牌
|
||||||
termora.settings.sync.type=類型
|
termora.settings.sync.type=類型
|
||||||
@@ -211,6 +212,7 @@ termora.transport.bookmarks.down=下移
|
|||||||
termora.transport.table.filename=檔名
|
termora.transport.table.filename=檔名
|
||||||
termora.transport.table.type=類型
|
termora.transport.table.type=類型
|
||||||
termora.transport.table.size=大小
|
termora.transport.table.size=大小
|
||||||
|
termora.transport.table.type.symbolic-link=軟連結
|
||||||
termora.transport.table.modified-time=修改時間
|
termora.transport.table.modified-time=修改時間
|
||||||
termora.transport.table.permissions=權限
|
termora.transport.table.permissions=權限
|
||||||
termora.transport.table.owner=所有者
|
termora.transport.table.owner=所有者
|
||||||
|
|||||||
Reference in New Issue
Block a user