Compare commits

..

23 Commits
1.0.4 ... 1.0.5

Author SHA1 Message Date
hstyi
343d11482d release: 1.0.5 2025-01-26 20:35:18 +08:00
hstyi
7ef81a0116 feat: xterm DCS 2025-01-26 14:42:59 +08:00
hstyi
5df62d5d3e fix: possible invalid window creation 2025-01-26 10:24:55 +08:00
hstyi
7db650d69f feat: open in new window 2025-01-26 10:20:26 +08:00
hstyi
8d80d38d63 fix: missing exchange algorithms 2025-01-26 08:44:00 +08:00
hstyi
48f05d4cff feat: ssh insecure key exchange algorithms 2025-01-26 08:44:00 +08:00
hstyi
9a1cf387c0 fix: check-license 2025-01-25 21:20:08 +08:00
hstyi
8b7efefbdb fix: shift to close tabs causes switching 2025-01-25 21:11:54 +08:00
hstyi
75f21db325 fix: theAwtToolkitWindow 2025-01-25 18:06:01 +08:00
hstyi
b094c9d4ff chore: remove tabbed hover background 2025-01-25 17:03:06 +08:00
hstyi
0da3c95759 feat: press and hold Shift to close Tab (#131) 2025-01-25 16:24:36 +08:00
hstyi
fa79473ece chore: optimize key encoder 2025-01-25 15:03:52 +08:00
hstyi
86ccb5e0cc chore: LANG=en_US.UTF-8 2025-01-24 17:27:47 +08:00
hstyi
f385f4b277 feat: support import (#127) 2025-01-24 16:45:36 +08:00
hstyi
3d0ef2a331 feat: shortcut key prediction (#126) 2025-01-24 15:40:14 +08:00
hstyi
96999205a8 fix: host test connection 2025-01-24 10:55:42 +08:00
hstyi
ee7f3871eb fix: sftp symbolic link (#120) 2025-01-24 10:27:15 +08:00
hstyi
df2e9b0743 feat: support drag and drop sorting 2025-01-23 16:23:16 +08:00
hstyi
7964950149 fix: #112 2025-01-23 14:47:39 +08:00
hstyi
e2d77fe881 fix: key manager 2025-01-23 14:43:48 +08:00
hstyi
f5783c8587 feat: support more monospaced fonts 2025-01-23 11:26:24 +08:00
hstyi
346044b1ba fix: shortcut keys lead to terminal input 2025-01-23 11:26:12 +08:00
hstyi
aa6ec8dd43 feat: xcrun stapler staple 2025-01-23 10:17:18 +08:00
28 changed files with 698 additions and 210 deletions

View File

@@ -14,7 +14,7 @@ plugins {
group = "app.termora"
version = "1.0.4"
version = "1.0.5"
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
val arch: Architecture = DefaultNativePlatform.getCurrentArchitecture()
@@ -383,6 +383,14 @@ tasks.register("dist") {
"--wait",
)
}
// 绑定公证信息
exec {
commandLine(
"/usr/bin/xcrun",
"stapler", "staple", macOSFinalFilePath,
)
}
}
}
}
@@ -407,6 +415,18 @@ tasks.register("check-license") {
thirdParty[nameWithVersion.replace(StringUtils.SPACE, "-")] = license
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")
}
}
}
}
}

View File

@@ -10,9 +10,6 @@ import com.jthemedetecor.OsThemeDetector
import com.mixpanel.mixpanelapi.ClientDelivery
import com.mixpanel.mixpanelapi.MessageBuilder
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.Dispatchers
import kotlinx.coroutines.GlobalScope
@@ -20,25 +17,27 @@ import kotlinx.coroutines.launch
import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.LocaleUtils
import org.apache.commons.lang3.SystemUtils
import org.apache.commons.lang3.math.NumberUtils
import org.json.JSONObject
import org.slf4j.LoggerFactory
import org.tinylog.configuration.Configuration
import java.io.File
import java.io.RandomAccessFile
import java.nio.channels.FileChannel
import java.nio.channels.FileLock
import java.nio.file.Paths
import java.nio.file.StandardOpenOption
import java.util.*
import javax.swing.*
import kotlin.system.exitProcess
import kotlin.system.measureTimeMillis
class ApplicationRunner {
private lateinit var singletonChannel: FileChannel
private lateinit var singletonLock: FileLock
private val log by lazy {
if (!::singletonLock.isInitialized) {
throw UnsupportedOperationException("Singleton lock is not initialized")
}
LoggerFactory.getLogger("Main")
LoggerFactory.getLogger(ApplicationRunner::class.java)
}
fun run() {
@@ -224,36 +223,14 @@ class ApplicationRunner {
private fun checkSingleton() {
val file = File(Application.getBaseDataDir(), "lock")
val pidFile = File(Application.getBaseDataDir(), "pid")
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
}
}
singletonChannel = FileChannel.open(
Paths.get(Application.getBaseDataDir().absolutePath, "lock"),
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
)
val lock = singletonChannel.tryLock()
if (lock == null) {
System.err.println("Program is already running")
exitProcess(1)
}

View File

@@ -2,6 +2,9 @@ package app.termora
import app.termora.actions.AnAction
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.sshd.client.SshClient
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")}...")
SwingUtilities.invokeLater {
isEnabled = false
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) {
testConnection(pane.getHost())
withContext(Dispatchers.Swing) {
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) {
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
}
@@ -67,13 +78,21 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
var session: ClientSession? = null
try {
client = SshClients.openClient(host)
client.userInteraction = TerminalUserInteraction(owner)
session = SshClients.openSession(host, client)
OptionPane.showMessageDialog(this, I18n.getString("termora.new-host.test-connection-successful"))
} catch (e: Exception) {
withContext(Dispatchers.Swing) {
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
)
}
} finally {
session?.close()
client?.close()

View File

@@ -1006,7 +1006,8 @@ open class HostOptionsPane : OptionsPane() {
val rows = table.selectedRows.sortedDescending()
if (rows.isEmpty()) return
for (row in rows) {
model.removeRow(row)
jumpHosts.removeAt(row)
model.fireTableRowsDeleted(row, row)
}
}
})

View File

@@ -318,6 +318,7 @@ class HostTree : JTree(), Disposable {
val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host"))
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()
val copy = popupMenu.add(I18n.getString("termora.welcome.contextmenu.copy"))
val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove"))
@@ -330,15 +331,8 @@ class HostTree : JTree(), Disposable {
popupMenu.addSeparator()
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
open.addActionListener { evt ->
getSelectionNodes()
.filter { it.protocol != Protocol.Folder }
.forEach {
ActionManager.getInstance()
.getAction(OpenHostAction.OPEN_HOST)
?.actionPerformed(OpenHostActionEvent(evt.source, it, evt))
}
}
open.addActionListener { openHosts(it, false) }
openInNewWindow.addActionListener { openHosts(it, true) }
rename.addActionListener {
startEditingAtPath(TreePath(model.getPathToRoot(lastHost)))
@@ -454,6 +448,17 @@ class HostTree : JTree(), Disposable {
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) {
expandPath(TreePath(model.getPathToRoot(node)))

View File

@@ -1,12 +1,190 @@
package app.termora
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
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() {
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) {
val oldIndex = selectedIndex
super.setSelectedIndex(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
}
}
}

View File

@@ -38,6 +38,8 @@ class PtyConnectorFactory : Disposable {
val locale = Locale.getDefault()
if (StringUtils.isNoneBlank(locale.language, locale.country)) {
envs["LANG"] = "${locale.language}_${locale.country}.${Charset.defaultCharset().name()}"
} else {
envs["LANG"] = "en_US.UTF-8"
}
}
}

View File

@@ -4,9 +4,14 @@ import app.termora.AES.encodeBase64String
import app.termora.Application.ohMyJson
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.highlight.KeywordHighlight
import app.termora.highlight.KeywordHighlightManager
import app.termora.keymap.Keymap
import app.termora.keymap.KeymapManager
import app.termora.keymap.KeymapPanel
import app.termora.keymgr.KeyManager
import app.termora.keymgr.OhKeyPair
import app.termora.macro.Macro
import app.termora.macro.MacroManager
import app.termora.native.FileChooser
import app.termora.sync.SyncConfig
@@ -19,6 +24,7 @@ import app.termora.terminal.panel.TerminalPanel
import cash.z.ecc.android.bip39.Mnemonics
import com.formdev.flatlaf.extras.FlatSVGIcon
import com.formdev.flatlaf.extras.components.*
import com.formdev.flatlaf.util.FontUtils
import com.formdev.flatlaf.util.SystemInfo
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
@@ -27,12 +33,11 @@ import com.sun.jna.LastErrorException
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.put
import kotlinx.serialization.json.*
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.SystemUtils
import org.apache.commons.lang3.exception.ExceptionUtils
import org.apache.commons.lang3.time.DateFormatUtils
import org.jdesktop.swingx.JXEditorPane
import org.slf4j.LoggerFactory
@@ -54,6 +59,11 @@ import kotlin.time.Duration.Companion.milliseconds
class SettingsOptionsPane : OptionsPane() {
private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane)
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 {
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.Bar)
cursorStyleComboBox.addItem(CursorStyle.Underline)
@@ -391,8 +423,33 @@ class SettingsOptionsPane : OptionsPane() {
shellComboBox.selectedItem = terminalSetting.localShell
fontComboBox.addItem("JetBrains Mono")
fontComboBox.addItem("Source Code Pro")
val fonts = linkedSetOf(
"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
debugComboBox.selectedItem = terminalSetting.debug
@@ -451,6 +508,7 @@ class SettingsOptionsPane : OptionsPane() {
val domainTextField = OutlineTextField(255)
val uploadConfigButton = JButton(I18n.getString("termora.settings.sync.push"), Icons.upload)
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 lastSyncTimeLabel = JLabel()
val sync get() = database.sync
@@ -562,6 +620,7 @@ class SettingsOptionsPane : OptionsPane() {
}
exportConfigButton.addActionListener { export() }
importConfigButton.addActionListener { import() }
keysCheckBox.addActionListener { refreshButtons() }
hostsCheckBox.addActionListener { refreshButtons() }
@@ -578,6 +637,7 @@ class SettingsOptionsPane : OptionsPane() {
|| keywordHighlightsCheckBox.isSelected
uploadConfigButton.isEnabled = downloadConfigButton.isEnabled
exportConfigButton.isEnabled = downloadConfigButton.isEnabled
importConfigButton.isEnabled = downloadConfigButton.isEnabled
}
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) {
val syncConfig = getSyncConfig()
val text = ohMyJson.encodeToString(buildJsonObject {
@@ -603,21 +766,29 @@ class SettingsOptionsPane : OptionsPane() {
put("os", SystemUtils.OS_NAME)
put("exportDateHuman", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(Date(now)))
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)) {
put("keyPairs", ohMyJson.encodeToJsonElement(KeyManager.getInstance().getOhKeyPairs()))
put("keyPairs", ohMyJson.encodeToJsonElement(keyManager.getOhKeyPairs()))
}
if (syncConfig.ranges.contains(SyncRange.KeywordHighlights)) {
put(
"keywordHighlights",
ohMyJson.encodeToJsonElement(KeywordHighlightManager.getInstance().getKeywordHighlights())
ohMyJson.encodeToJsonElement(keywordHighlightManager.getKeywordHighlights())
)
}
if (syncConfig.ranges.contains(SyncRange.Macros)) {
put(
"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 {
@@ -662,6 +833,7 @@ class SettingsOptionsPane : OptionsPane() {
)
}
@Suppress("DuplicatedCode")
private suspend fun pushOrPull(push: Boolean) {
if (typeComboBox.selectedItem == SyncType.GitLab) {
@@ -717,6 +889,7 @@ class SettingsOptionsPane : OptionsPane() {
withContext(Dispatchers.Swing) {
exportConfigButton.isEnabled = false
importConfigButton.isEnabled = false
downloadConfigButton.isEnabled = false
uploadConfigButton.isEnabled = false
typeComboBox.isEnabled = false
@@ -752,6 +925,7 @@ class SettingsOptionsPane : OptionsPane() {
withContext(Dispatchers.Swing) {
downloadConfigButton.isEnabled = true
exportConfigButton.isEnabled = true
importConfigButton.isEnabled = true
uploadConfigButton.isEnabled = true
keysCheckBox.isEnabled = true
hostsCheckBox.isEnabled = true
@@ -892,7 +1066,7 @@ class SettingsOptionsPane : OptionsPane() {
var rows = 1
val step = 2
val builder = FormBuilder.create().layout(layout).debug(false);
val builder = FormBuilder.create().layout(layout).debug(false)
val box = Box.createHorizontalBox()
box.add(typeComboBox)
if (typeComboBox.selectedItem == SyncType.GitLab) {
@@ -911,10 +1085,11 @@ class SettingsOptionsPane : OptionsPane() {
// Sync buttons
.add(
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(downloadConfigButton).xy(3, 1)
.add(exportConfigButton).xy(5, 1)
.add(importConfigButton).xy(7, 1)
.build()
).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 safeBtn = FlatButton()
private val doorman get() = Doorman.getInstance()
private val hostManager get() = HostManager.getInstance()
private val keyManager get() = KeyManager.getInstance()
init {
initView()

View File

@@ -6,10 +6,12 @@ import org.apache.sshd.client.ClientBuilder
import org.apache.sshd.client.SshClient
import org.apache.sshd.client.channel.ChannelShell
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.common.SshException
import org.apache.sshd.common.channel.PtyChannelConfiguration
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.core.CoreModuleProperties
import org.apache.sshd.server.forward.AcceptAllForwardingFilter
@@ -133,6 +135,18 @@ object SshClients {
builder.globalRequestHandlers(listOf(KeepAliveHandler.INSTANCE))
.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()) {
builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE)
} else {
@@ -144,6 +158,8 @@ object SshClients {
val sshClient = builder.build() as JGitSshClient
val heartbeatInterval = max(host.options.heartbeatInterval, 3)
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true)
sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) }
if (host.proxy.type != ProxyType.No) {

View File

@@ -10,14 +10,19 @@ import app.termora.transport.TransportPanel
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import org.apache.commons.lang3.StringUtils
import java.awt.*
import java.awt.event.AWTEventListener
import java.awt.event.ActionEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
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.SwingUtilities
import kotlin.math.min
class TerminalTabbed(
@@ -30,7 +35,7 @@ class TerminalTabbed(
private val toolbar = termoraToolBar.getJToolBar()
private val actionManager = ActionManager.getInstance()
private val dataProviderSupport = DataProviderSupport()
private val titleProperty = UUID.randomUUID().toSimpleString()
private val iconListener = PropertyChangeListener { e ->
val source = e.source
if (e.propertyName == "icon" && source is TerminalTab) {
@@ -52,9 +57,6 @@ class TerminalTabbed(
tabbedPane.isTabsClosable = true
tabbedPane.tabType = FlatTabbedPane.TabType.card
tabbedPane.styleMap = mapOf(
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground")
)
tabbedPane.trailingComponent = toolbar
add(tabbedPane, BorderLayout.CENTER)
@@ -190,16 +192,16 @@ class TerminalTabbed(
// 修改名称
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
rename.addActionListener {
val index = tabbedPane.selectedIndex
if (index > 0) {
if (tabIndex > 0) {
val dialog = InputDialog(
SwingUtilities.getWindowAncestor(this),
title = rename.text,
text = tabbedPane.getTitleAt(index),
text = tabbedPane.getTitleAt(tabIndex),
)
val text = dialog.getText()
if (!text.isNullOrBlank()) {
tabbedPane.setTitleAt(index, text)
tabbedPane.setTitleAt(tabIndex, text)
c.putClientProperty(titleProperty, text)
}
}
@@ -276,9 +278,8 @@ class TerminalTabbed(
popupMenu.addSeparator()
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
reconnect.addActionListener {
val index = tabbedPane.selectedIndex
if (index > 0) {
tabs[index].reconnect()
if (tabIndex > 0) {
tabs[tabIndex].reconnect()
}
}
@@ -289,18 +290,24 @@ class TerminalTabbed(
}
fun addTab(tab: TerminalTab) {
tabbedPane.addTab(
tab.getTitle(),
private fun addTab(index: Int, tab: TerminalTab) {
val c = tab.getJComponent()
val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
tabbedPane.insertTab(
title,
tab.getIcon(),
tab.getJComponent()
c,
StringUtils.EMPTY,
index
)
c.putClientProperty(titleProperty, title)
// 监听 icons 变化
tab.addPropertyChangeListener(iconListener)
tabs.add(tab)
tabbedPane.selectedIndex = tabbedPane.tabCount - 1
tabs.add(index, tab)
tabbedPane.selectedIndex = index
Disposer.register(this, tab)
}
@@ -393,7 +400,11 @@ class TerminalTabbed(
}
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? {
@@ -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) {
if (tabs[i] == tab) {
removeTabAt(i, true)
removeTabAt(i, disposable)
break
}
}

View File

@@ -2,8 +2,9 @@ package app.termora
interface TerminalTabbedManager {
fun addTerminalTab(tab: TerminalTab)
fun addTerminalTab(index: Int, tab: TerminalTab)
fun getSelectedTerminalTab(): TerminalTab?
fun getTerminalTabs(): List<TerminalTab>
fun setSelectedTerminalTab(tab: TerminalTab)
fun closeTerminalTab(tab: TerminalTab)
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true)
}

View File

@@ -101,7 +101,7 @@ class TermoraFrame : JFrame(), DataProvider {
}
minimumSize = Dimension(640, 400)
terminalTabbed.addTab(welcomePanel)
terminalTabbed.addTerminalTab(welcomePanel)
// macOS 要避开左边的控制栏
if (SystemInfo.isMacOS) {

View File

@@ -12,9 +12,9 @@ import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionManager
import org.slf4j.LoggerFactory
import java.awt.KeyEventDispatcher
import java.awt.KeyEventPostProcessor
import java.awt.KeyboardFocusManager
import java.awt.event.KeyEvent
import javax.swing.JComponent
import javax.swing.JDialog
import javax.swing.KeyStroke
@@ -23,15 +23,13 @@ class KeymapManager private constructor() : Disposable {
companion object {
private val log = LoggerFactory.getLogger(KeymapManager::class.java)
const val PROCESS_GLOBAL_KEYMAP = "PROCESS_GLOBAL_KEYMAP"
fun getInstance(): KeymapManager {
return ApplicationScope.forApplicationScope()
.getOrCreate(KeymapManager::class) { KeymapManager() }
}
}
private val myKeyEventPostProcessor = MyKeyEventPostProcessor()
private val keymapKeyEventDispatcher = KeymapKeyEventDispatcher()
private val myKeyEventDispatcher = MyKeyEventDispatcher()
private val database get() = Database.getDatabase()
private val keymaps = linkedMapOf<String, Keymap>()
@@ -39,7 +37,7 @@ class KeymapManager private constructor() : Disposable {
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
init {
keyboardFocusManager.addKeyEventPostProcessor(myKeyEventPostProcessor)
keyboardFocusManager.addKeyEventDispatcher(keymapKeyEventDispatcher)
keyboardFocusManager.addKeyEventDispatcher(myKeyEventDispatcher)
try {
@@ -97,12 +95,26 @@ class KeymapManager private constructor() : Disposable {
database.removeKeymap(name)
}
private inner class MyKeyEventPostProcessor : KeyEventPostProcessor {
override fun postProcessKeyEvent(e: KeyEvent): Boolean {
// 只处理 PRESSED 和 带有 modifiers 键的事件
if (!e.isConsumed && e.id == KeyEvent.KEY_PRESSED && e.modifiersEx != 0) {
private inner class KeymapKeyEventDispatcher : KeyEventDispatcher {
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
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 actionIds = shortcuts.getActionIds(KeyShortcut(KeyStroke.getKeyStrokeForEvent(e)))
val actionIds = shortcuts.getActionIds(KeyShortcut(keyStroke))
if (actionIds.isEmpty()) {
return false
}
@@ -128,7 +140,6 @@ class KeymapManager private constructor() : Disposable {
return true
}
}
}
return false
}
@@ -163,7 +174,7 @@ class KeymapManager private constructor() : Disposable {
override fun dispose() {
keyboardFocusManager.removeKeyEventPostProcessor(myKeyEventPostProcessor)
keyboardFocusManager.removeKeyEventDispatcher(keymapKeyEventDispatcher)
keyboardFocusManager.removeKeyEventDispatcher(myKeyEventDispatcher)
}
}

View File

@@ -21,6 +21,7 @@ class KeyManager private constructor() {
if (keyPair == OhKeyPair.empty) {
return
}
keyPairs.remove(keyPair)
keyPairs.add(keyPair)
database.addKeyPair(keyPair)
}

View File

@@ -17,6 +17,10 @@ class FileChooser {
var allowsOtherFileTypes = true
var canCreateDirectories = true
var win32Filters = mutableListOf<Pair<String, List<String>>>()
/**
* e.g. listOf("json")
*/
var osxAllowedFileTypes = emptyList<String>()
/**

View File

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

View File

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

View File

@@ -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' -> {
state = TerminalState.DCS
}
// Start of Guarded Area (SPA is 0x96).

View File

@@ -1,13 +1,14 @@
package app.termora.terminal
import com.formdev.flatlaf.util.SystemInfo
import org.apache.commons.lang3.StringUtils
import java.awt.event.KeyEvent
@Suppress("MemberVisibilityCanBePrivate")
open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataListener {
private val mapping = mutableMapOf<TerminalKeyEvent, String>()
private val nothing = String()
private val nothing = StringUtils.EMPTY
init {
@@ -27,6 +28,7 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
configureLeftRight()
// Ctrl + C
putCode(TerminalKeyEvent(keyCode = 8), String(byteArrayOf(127)))
// Enter
@@ -38,15 +40,15 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
// Page Up
putCode(TerminalKeyEvent(keyCode = 0x21), encode = "${ControlCharacters.ESC}[5~")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_PAGE_UP), encode = "${ControlCharacters.ESC}[5~")
// Page Down
putCode(TerminalKeyEvent(keyCode = 0x22), encode = "${ControlCharacters.ESC}[6~")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_PAGE_DOWN), encode = "${ControlCharacters.ESC}[6~")
// Insert
putCode(TerminalKeyEvent(keyCode = 0x9B), encode = "${ControlCharacters.ESC}[2~")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_INSERT), encode = "${ControlCharacters.ESC}[2~")
// Delete
putCode(TerminalKeyEvent(keyCode = 0x7F), encode = "${ControlCharacters.ESC}[3~")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_DELETE), encode = "${ControlCharacters.ESC}[3~")
// Function Keys
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() {
// Up
putCode(TerminalKeyEvent(keyCode = 0x26), encode = "${ControlCharacters.ESC}OA")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_UP), encode = "${ControlCharacters.ESC}OA")
// Down
putCode(TerminalKeyEvent(keyCode = 0x28), encode = "${ControlCharacters.ESC}OB")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_DOWN), encode = "${ControlCharacters.ESC}OB")
// Left
putCode(TerminalKeyEvent(keyCode = 0x25), encode = "${ControlCharacters.ESC}OD")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_LEFT), encode = "${ControlCharacters.ESC}OD")
// Right
putCode(TerminalKeyEvent(keyCode = 0x27), encode = "${ControlCharacters.ESC}OC")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_RIGHT), encode = "${ControlCharacters.ESC}OC")
}
fun arrowKeysAnsiCursorSequences() {
// Up
putCode(TerminalKeyEvent(keyCode = 0x26), encode = "${ControlCharacters.ESC}[A")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_UP), encode = "${ControlCharacters.ESC}[A")
// Down
putCode(TerminalKeyEvent(keyCode = 0x28), encode = "${ControlCharacters.ESC}[B")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_DOWN), encode = "${ControlCharacters.ESC}[B")
// Left
putCode(TerminalKeyEvent(keyCode = 0x25), encode = "${ControlCharacters.ESC}[D")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_LEFT), encode = "${ControlCharacters.ESC}[D")
// Right
putCode(TerminalKeyEvent(keyCode = 0x27), encode = "${ControlCharacters.ESC}[C")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_RIGHT), encode = "${ControlCharacters.ESC}[C")
}
/**
* Alt + Left/Right
*/
fun configureLeftRight() {
if (SystemInfo.isMacOS) {
putCode(
@@ -141,32 +146,32 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
fun keypadApplicationSequences() {
// Up
putCode(TerminalKeyEvent(keyCode = 0xE0), encode = "${ControlCharacters.ESC}OA")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_UP), encode = "${ControlCharacters.ESC}OA")
// Down
putCode(TerminalKeyEvent(keyCode = 0xE1), encode = "${ControlCharacters.ESC}OB")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_DOWN), encode = "${ControlCharacters.ESC}OB")
// Left
putCode(TerminalKeyEvent(keyCode = 0xE2), encode = "${ControlCharacters.ESC}OD")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_LEFT), encode = "${ControlCharacters.ESC}OD")
// Right
putCode(TerminalKeyEvent(keyCode = 0xE3), encode = "${ControlCharacters.ESC}OC")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_RIGHT), encode = "${ControlCharacters.ESC}OC")
// Home
putCode(TerminalKeyEvent(keyCode = 0x24), encode = "${ControlCharacters.ESC}OH")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_HOME), encode = "${ControlCharacters.ESC}OH")
// End
putCode(TerminalKeyEvent(keyCode = 0x23), encode = "${ControlCharacters.ESC}OF")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_END), encode = "${ControlCharacters.ESC}OF")
}
fun keypadAnsiSequences() {
// Up
putCode(TerminalKeyEvent(keyCode = 0xE0), encode = "${ControlCharacters.ESC}[A")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_UP), encode = "${ControlCharacters.ESC}[A")
// Down
putCode(TerminalKeyEvent(keyCode = 0xE1), encode = "${ControlCharacters.ESC}[B")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_DOWN), encode = "${ControlCharacters.ESC}[B")
// Left
putCode(TerminalKeyEvent(keyCode = 0xE2), encode = "${ControlCharacters.ESC}[D")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_LEFT), encode = "${ControlCharacters.ESC}[D")
// Right
putCode(TerminalKeyEvent(keyCode = 0xE3), encode = "${ControlCharacters.ESC}[C")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_KP_RIGHT), encode = "${ControlCharacters.ESC}[C")
// Home
putCode(TerminalKeyEvent(keyCode = 0x24), encode = "${ControlCharacters.ESC}[H")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_HOME), encode = "${ControlCharacters.ESC}[H")
// 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) {

View File

@@ -7,7 +7,7 @@ import java.awt.datatransfer.StringSelection
class OperatingSystemCommandProcessor(terminal: Terminal, reader: TerminalReader) :
AbstractProcessor(terminal, reader) {
private val args = StringBuilder()
private val systemCommandSequence = SystemCommandSequence()
private val colorPalette get() = terminal.getTerminalModel().getColorPalette()
companion object {
@@ -20,14 +20,7 @@ class OperatingSystemCommandProcessor(terminal: Terminal, reader: TerminalReader
do {
val c = 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)
if (systemCommandSequence.process(reader.read())) {
break
}
@@ -42,7 +35,7 @@ class OperatingSystemCommandProcessor(terminal: Terminal, reader: TerminalReader
// process osc
processOperatingSystemCommandProcessor()
args.clear()
systemCommandSequence.reset()
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
*/
private fun processOperatingSystemCommandProcessor() {
val args = systemCommandSequence.getCommand()
val idx = args.indexOfFirst { it == ';' }
if (idx == -1) {
return

View File

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

View File

@@ -1,5 +1,6 @@
package app.termora.terminal
import org.apache.commons.lang3.ArrayUtils
import org.slf4j.LoggerFactory
import java.awt.Toolkit
import kotlin.reflect.cast
@@ -8,7 +9,7 @@ open class TerminalModelImpl(private val terminal: Terminal) : TerminalModel {
private var rows: Int = 27
private var cols: Int = 80
private val data = mutableMapOf<DataKey<*>, Any>()
private val listeners = mutableListOf<DataListener>()
private var listeners = emptyArray<DataListener>()
private val colorPalette = ColorPaletteImpl(terminal)
companion object {
@@ -92,11 +93,11 @@ open class TerminalModelImpl(private val terminal: Terminal) : TerminalModel {
}
override fun addDataListener(listener: DataListener) {
listeners.add(listener)
listeners = ArrayUtils.add(listeners, listener)
}
override fun removeDataListener(listener: DataListener) {
listeners.remove(listener)
listeners = ArrayUtils.removeElement(listeners, listener)
}
override fun bell() {
@@ -129,9 +130,8 @@ open class TerminalModelImpl(private val terminal: Terminal) : TerminalModel {
@Suppress("MemberVisibilityCanBePrivate")
protected fun <T : Any> fireDataChanged(key: DataKey<T>, data: T) {
val size = listeners.size
for (i in 0 until size) {
listeners.getOrNull(i)?.onChanged(key, data)
for (listener in listeners) {
listener.onChanged(key, data)
}
}

View File

@@ -129,7 +129,7 @@ private class MyProcessor(private val terminal: Terminal, reader: TerminalReader
TerminalState.CSI to ControlSequenceIntroducerProcessor(terminal, reader),
TerminalState.OSC to OperatingSystemCommandProcessor(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),
)

View File

@@ -1,5 +1,7 @@
package app.termora.terminal.panel
import app.termora.keymap.KeyShortcut
import app.termora.keymap.KeymapManager
import app.termora.terminal.PtyConnector
import app.termora.terminal.Terminal
import com.formdev.flatlaf.util.SystemInfo
@@ -12,8 +14,9 @@ class TerminalPanelKeyAdapter(
private val terminalPanel: TerminalPanel,
private val terminal: Terminal,
private val ptyConnector: PtyConnector
) :
KeyAdapter() {
) : KeyAdapter() {
private val activeKeymap get() = KeymapManager.getInstance().getActiveKeymap()
override fun keyTyped(e: KeyEvent) {
if (Character.isISOControl(e.keyChar)) {
@@ -52,6 +55,11 @@ class TerminalPanelKeyAdapter(
return
}
// 如果命中了全局快捷键,那么不处理
if (keyStroke.modifiers != 0 && activeKeymap.getActionIds(KeyShortcut(keyStroke)).isNotEmpty()) {
return
}
if (Character.isISOControl(e.keyChar)) {
terminal.getSelectionModel().clearSelection()
// 如果不为空表示已经发送过了,所以这里为空的时候再发送

View File

@@ -66,7 +66,10 @@ class FileSystemTableModel(private val fileSystem: FileSystem) : DefaultTableMod
when (column) {
COLUMN_NAME -> path
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")
// 如果是本地的并且还是Windows系统
@@ -173,6 +176,7 @@ class FileSystemTableModel(private val fileSystem: FileSystem) : DefaultTableMod
val extension by lazy { path.extension }
open val isDirectory by lazy { path.isDirectory() }
open val isSymbolicLink by lazy { path.isSymbolicLink() }
open val isHidden by lazy { fileName != ".." && path.isHidden() }
open val fileSize by lazy { path.fileSize() }
open val lastModifiedTime by lazy { Files.getLastModifiedTime(path).toMillis() }
@@ -227,8 +231,10 @@ class FileSystemTableModel(private val fileSystem: FileSystem) : DefaultTableMod
}
}
override val isDirectory: Boolean
get() = attributes.isDirectory
override val isDirectory by lazy { attributes.isDirectory || (isSymbolicLink && Files.isDirectory(path)) }
override val isSymbolicLink: Boolean
get() = attributes.isSymbolicLink
override val isHidden: Boolean
get() = fileName != ".." && fileName.startsWith(".")

View File

@@ -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.pull=Pull
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-open-folder=The export was successful. Do you want to open the folder?
termora.settings.sync.range=Range
@@ -110,6 +113,7 @@ termora.find-everywhere.quick-command.local-terminal=Local Terminal
# Welcome
termora.welcome.my-hosts=My hosts
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.remove=${termora.remove}
termora.welcome.contextmenu.rename=Rename
@@ -221,6 +225,7 @@ termora.transport.bookmarks.down=Down
termora.transport.table.filename=Filename
termora.transport.table.type=Type
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.modified-time=Modified
termora.transport.table.permissions=Permissions

View File

@@ -74,13 +74,14 @@ termora.settings.sync=同步
termora.settings.sync.push=推送
termora.settings.sync.push-warning=推送将覆盖已有配置,建议先拉取再推送
termora.settings.sync.pull=拉取
termora.settings.sync.export=导出
termora.settings.sync.export-done=导出成功
termora.settings.sync.export-done-open-folder=导出成功,是否需要打开所在文件夹?
termora.settings.sync.range=范围
termora.settings.sync.range.keys=我的密钥
termora.settings.sync.last-sync-time=最后同步时间
termora.settings.sync.done=同步数据成功
termora.settings.sync.import.file-too-large=文件太大
termora.settings.sync.import.successful=导入数据成功
termora.settings.sync.gist=片段
termora.settings.sync.token=令牌
termora.settings.sync.type=类型
@@ -217,6 +218,7 @@ termora.transport.bookmarks.down=下移
termora.transport.table.filename=文件名
termora.transport.table.type=类型
termora.transport.table.size=大小
termora.transport.table.type.symbolic-link=软链接
termora.transport.table.modified-time=修改时间
termora.transport.table.permissions=权限
termora.transport.table.owner=所有者

View File

@@ -78,13 +78,14 @@ termora.settings.sync=同步
termora.settings.sync.push=推送
termora.settings.sync.push-warning=推送將覆蓋先前的配置,建議先拉取再推送
termora.settings.sync.pull=拉取
termora.settings.sync.export=匯出
termora.settings.sync.export-done=匯出成功
termora.settings.sync.export-done-open-folder=匯出成功,是否需要打開所在資料夾?
termora.settings.sync.range=範圍
termora.settings.sync.range.keys=我的密鑰
termora.settings.sync.last-sync-time=最後同步時間
termora.settings.sync.done=同步資料成功
termora.settings.sync.import.file-too-large=檔案太大
termora.settings.sync.import.successful=導入資料成功
termora.settings.sync.gist=片段
termora.settings.sync.token=令牌
termora.settings.sync.type=類型
@@ -211,6 +212,7 @@ termora.transport.bookmarks.down=下移
termora.transport.table.filename=檔名
termora.transport.table.type=類型
termora.transport.table.size=大小
termora.transport.table.type.symbolic-link=軟連結
termora.transport.table.modified-time=修改時間
termora.transport.table.permissions=權限
termora.transport.table.owner=所有者