mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b499667cbb | ||
|
|
1d596e18df | ||
|
|
6f95033009 | ||
|
|
1f08af6575 | ||
|
|
071a091347 | ||
|
|
ca484618c7 | ||
|
|
1f68f8a112 | ||
|
|
0cd5670bd3 | ||
|
|
8e9c6bcb68 | ||
|
|
6c1fa0fc53 | ||
|
|
5145cfa8a5 | ||
|
|
87b1a5e315 | ||
|
|
fa59869f2c | ||
|
|
1ae64fe0db | ||
|
|
f8d363836e | ||
|
|
38dccb1d22 | ||
|
|
3e31a89b92 | ||
|
|
d8f892cc02 | ||
|
|
873deb55aa | ||
|
|
c08712d79b | ||
|
|
61bc905727 | ||
|
|
17859be3c5 | ||
|
|
7a24e34695 | ||
|
|
58638eaad8 | ||
|
|
09d2f2d193 | ||
|
|
9121eff8d8 | ||
|
|
8b090b0526 | ||
|
|
15a0d642ff |
4
.github/workflows/linux-aarch64.yml
vendored
4
.github/workflows/linux-aarch64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-aarch64-b895.91.tar.gz
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-aarch64-b1034.51.tar.gz
|
||||
|
||||
# appimagetool
|
||||
- run: sudo apt install libfuse2
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
with:
|
||||
distribution: 'jdkfile'
|
||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||
java-version: '21.0.6'
|
||||
java-version: '21.0.7'
|
||||
architecture: aarch64
|
||||
|
||||
- uses: actions/cache@v4
|
||||
|
||||
4
.github/workflows/linux-x86-64.yml
vendored
4
.github/workflows/linux-x86-64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-x64-b895.91.tar.gz
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-x64-b1034.51.tar.gz
|
||||
|
||||
# appimagetool
|
||||
- run: sudo apt install libfuse2
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
with:
|
||||
distribution: 'jdkfile'
|
||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||
java-version: '21.0.6'
|
||||
java-version: '21.0.7'
|
||||
architecture: x64
|
||||
|
||||
- uses: actions/cache@v4
|
||||
|
||||
4
.github/workflows/osx-aarch64.yml
vendored
4
.github/workflows/osx-aarch64.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b895.91.tar.gz
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-osx-aarch64-b1034.51.tar.gz
|
||||
|
||||
# install jdk
|
||||
- name: Installing Java
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
with:
|
||||
distribution: 'jdkfile'
|
||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||
java-version: '21.0.6'
|
||||
java-version: '21.0.7'
|
||||
architecture: aarch64
|
||||
|
||||
- uses: actions/cache@v4
|
||||
|
||||
4
.github/workflows/osx-x86-64.yml
vendored
4
.github/workflows/osx-x86-64.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b895.91.tar.gz
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-osx-x64-b1034.51.tar.gz
|
||||
|
||||
# install jdk
|
||||
- name: Installing Java
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
with:
|
||||
distribution: 'jdkfile'
|
||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||
java-version: '21.0.6'
|
||||
java-version: '21.0.7'
|
||||
architecture: x64
|
||||
|
||||
|
||||
|
||||
4
.github/workflows/windows-x86-64.yml
vendored
4
.github/workflows/windows-x86-64.yml
vendored
@@ -21,9 +21,9 @@ jobs:
|
||||
|
||||
- name: Installing Java
|
||||
run: |
|
||||
curl -s --output ${{ runner.temp }}\java_package.zip -L https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-windows-x64-b895.91.zip
|
||||
curl -s --output ${{ runner.temp }}\java_package.zip -L https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-windows-x64-b1034.51.zip
|
||||
unzip -q ${{ runner.temp }}\java_package.zip -d ${{ runner.temp }}\jbr
|
||||
echo "JAVA_HOME=${{ runner.temp }}\jbr\jbrsdk-21.0.6-windows-x64-b895.91" >> $env:GITHUB_ENV
|
||||
echo "JAVA_HOME=${{ runner.temp }}\jbr\jbrsdk-21.0.7-windows-x64-b1034.51" >> $env:GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
|
||||
2
.github/workflows/winget.yml
vendored
2
.github/workflows/winget.yml
vendored
@@ -10,5 +10,5 @@ jobs:
|
||||
if: github.repository == 'TermoraDev/termora'
|
||||
with:
|
||||
identifier: TermoraDev.Termora
|
||||
installers-regex: 'x86-64\.exe$' # Only x86-64.exe files
|
||||
installers-regex: '\.exe$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
@@ -21,7 +21,7 @@ plugins {
|
||||
|
||||
|
||||
group = "app.termora"
|
||||
version = "1.0.15"
|
||||
version = "1.0.17"
|
||||
|
||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||
@@ -134,16 +134,13 @@ application {
|
||||
args.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||
args.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||
args.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||
args.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
|
||||
args.add("-Dsun.java2d.metal=true")
|
||||
args.add("-Dapple.awt.application.appearance=system")
|
||||
}
|
||||
|
||||
args.add("-Dapp-version=${project.version}")
|
||||
|
||||
if (os.isLinux) {
|
||||
args.add("-Dsun.java2d.opengl=true")
|
||||
}
|
||||
|
||||
applicationDefaultJvmArgs = args
|
||||
mainClass = "app.termora.MainKt"
|
||||
}
|
||||
@@ -388,10 +385,7 @@ tasks.register<Exec>("jpackage") {
|
||||
options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||
options.add("-Dapple.awt.application.appearance=system")
|
||||
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
if (os.isLinux) {
|
||||
options.add("-Dsun.java2d.opengl=true")
|
||||
options.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[versions]
|
||||
kotlin = "2.1.21"
|
||||
slf4j = "2.0.17"
|
||||
pty4j = "0.13.4"
|
||||
pty4j = "0.13.6"
|
||||
tinylog = "2.7.0"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
flatlaf = "3.6"
|
||||
@@ -35,7 +35,7 @@ bip39 = "1.0.9"
|
||||
colorpicker = "2.0.1"
|
||||
rhino = "1.8.0"
|
||||
delight-rhino-sandbox = "0.0.17"
|
||||
testcontainers = "1.21.0"
|
||||
testcontainers = "1.21.1"
|
||||
mixpanel = "1.5.3"
|
||||
jSerialComm = "2.11.0"
|
||||
ini4j = "0.5.5-2"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0"
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||
}
|
||||
rootProject.name = "termora"
|
||||
|
||||
|
||||
@@ -355,7 +355,27 @@ class ApplicationRunner {
|
||||
.event(getAnalyticsUserID(), "launch", properties)
|
||||
val delivery = ClientDelivery()
|
||||
delivery.addMessage(message)
|
||||
MixpanelAPI().deliver(delivery, true)
|
||||
val endpoints = listOf(
|
||||
"https://api-eu.mixpanel.com",
|
||||
"https://api-in.mixpanel.com",
|
||||
"https://api.mixpanel.com",
|
||||
"http://api.mixpanel.com",
|
||||
)
|
||||
for (endpoint in endpoints) {
|
||||
try {
|
||||
MixpanelAPI(
|
||||
"$endpoint/track",
|
||||
"$endpoint/engage",
|
||||
"$endpoint/groups"
|
||||
).deliver(delivery, true)
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
|
||||
import app.termora.actions.MultipleAction
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import kotlinx.serialization.encodeToString
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import java.awt.Component
|
||||
@@ -20,6 +19,7 @@ import kotlin.math.min
|
||||
|
||||
class CustomizeToolBarDialog(
|
||||
owner: Window,
|
||||
private val windowScope: WindowScope,
|
||||
private val toolbar: TermoraToolBar
|
||||
) : DialogWrapper(owner) {
|
||||
|
||||
@@ -147,9 +147,7 @@ class CustomizeToolBarDialog(
|
||||
leftList.model.removeAllElements()
|
||||
rightList.model.removeAllElements()
|
||||
for (action in toolbar.getAllActions()) {
|
||||
actionManager.getAction(action.id)?.let {
|
||||
rightList.model.addElement(ActionHolder(action.id, it))
|
||||
}
|
||||
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,14 +257,11 @@ class CustomizeToolBarDialog(
|
||||
override fun windowOpened(e: WindowEvent) {
|
||||
removeWindowListener(this)
|
||||
|
||||
|
||||
for (action in toolbar.getActions()) {
|
||||
if (action.visible) {
|
||||
actionManager.getAction(action.id)
|
||||
?.let { rightList.model.addElement(ActionHolder(action.id, it)) }
|
||||
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
|
||||
} else {
|
||||
actionManager.getAction(action.id)
|
||||
?.let { leftList.model.addElement(ActionHolder(action.id, it)) }
|
||||
getActionHolder(action.id)?.let { leftList.model.addElement(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,6 +269,17 @@ class CustomizeToolBarDialog(
|
||||
})
|
||||
}
|
||||
|
||||
private fun getActionHolder(actionId: String): ActionHolder? {
|
||||
var action = actionManager.getAction(actionId)
|
||||
if (action == null) {
|
||||
if (actionId == MultipleAction.MULTIPLE) {
|
||||
action = MultipleAction.getInstance(windowScope)
|
||||
}
|
||||
}
|
||||
if (action == null) return null
|
||||
return ActionHolder(actionId, action)
|
||||
}
|
||||
|
||||
private fun resetMoveButtons() {
|
||||
val indices = rightList.selectedIndices
|
||||
if (indices.isEmpty()) {
|
||||
|
||||
@@ -648,6 +648,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
*/
|
||||
var backgroundRunning by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 标签关闭前确认
|
||||
*/
|
||||
var confirmTabClose by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 背景图片的地址
|
||||
*/
|
||||
|
||||
@@ -25,6 +25,7 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
||||
isModal = true
|
||||
title = I18n.getString("termora.new-host.title")
|
||||
setLocationRelativeTo(null)
|
||||
pane.setSelectedIndex(0)
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
@@ -13,11 +13,13 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.apache.commons.lang3.RegExUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.PageantConnector
|
||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocketConnector
|
||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.WinPipeConnector
|
||||
import java.awt.*
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.event.*
|
||||
import java.nio.charset.Charset
|
||||
import javax.swing.*
|
||||
@@ -221,7 +223,24 @@ open class HostOptionsPane : OptionsPane() {
|
||||
val nameTextField = OutlineTextField(128)
|
||||
val protocolTypeComboBox = FlatComboBox<Protocol>()
|
||||
val usernameTextField = OutlineTextField(128)
|
||||
val hostTextField = OutlineTextField(255)
|
||||
val hostTextField = object : OutlineTextField(255) {
|
||||
override fun paste() {
|
||||
if (!toolkit.systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||
return
|
||||
}
|
||||
|
||||
var text = toolkit.systemClipboard.getData(DataFlavor.stringFlavor)?.toString() ?: return
|
||||
if (text.isBlank()) {
|
||||
return
|
||||
}
|
||||
|
||||
// 移除所有不可见字符
|
||||
text = RegExUtils.replaceAll(text, "[\\p{C}\\s]", StringUtils.EMPTY)
|
||||
|
||||
// text
|
||||
replaceSelection(text)
|
||||
}
|
||||
}
|
||||
private val passwordPanel = JPanel(BorderLayout())
|
||||
private val chooseKeyBtn = JButton(Icons.greyKey)
|
||||
val passwordTextField = OutlinePasswordField(255)
|
||||
|
||||
@@ -856,7 +856,8 @@ class NewHostTree : SimpleTree() {
|
||||
val port = map["Port"]?.toIntOrNull() ?: 22
|
||||
val username = map["Username"] ?: StringUtils.EMPTY
|
||||
val protocol = map["Protocol"] ?: "SSH"
|
||||
if (!StringUtils.equalsIgnoreCase(protocol, "SSH")) continue
|
||||
// 仅支持 SSH、RDP 协议
|
||||
if (StringUtils.equalsAnyIgnoreCase(protocol, "SSH", "RDP").not()) continue
|
||||
if (StringUtils.isAllBlank(hostname, label)) continue
|
||||
|
||||
var p: HostTreeNode? = null
|
||||
@@ -891,7 +892,7 @@ class NewHostTree : SimpleTree() {
|
||||
host = hostname,
|
||||
port = port,
|
||||
username = username,
|
||||
protocol = Protocol.SSH,
|
||||
protocol = runCatching { Protocol.valueOf(protocol) }.getOrNull() ?: Protocol.SSH,
|
||||
parentId = p?.host?.id ?: StringUtils.EMPTY,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@ class NewHostTreeDialog(
|
||||
|
||||
|
||||
init()
|
||||
setLocationRelativeTo(null)
|
||||
setLocationRelativeTo(owner)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ open class OptionsPane : JPanel(BorderLayout()) {
|
||||
}
|
||||
private val cardLayout = CardLayout()
|
||||
private val contentPanel = JPanel(cardLayout)
|
||||
private val loadedComponents = mutableMapOf<String, JComponent>()
|
||||
|
||||
init {
|
||||
initView()
|
||||
@@ -103,16 +104,15 @@ open class OptionsPane : JPanel(BorderLayout()) {
|
||||
throw UnsupportedOperationException("Title already exists")
|
||||
}
|
||||
}
|
||||
contentPanel.add(option.getJComponent(), option.getTitle())
|
||||
tabListModel.addElement(option)
|
||||
|
||||
if (tabList.selectedIndex < 0) {
|
||||
tabList.selectedIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
fun removeOption(option: Option) {
|
||||
contentPanel.remove(option.getJComponent())
|
||||
val title = option.getTitle()
|
||||
loadedComponents[title]?.let {
|
||||
contentPanel.remove(it)
|
||||
loadedComponents.remove(title)
|
||||
}
|
||||
tabListModel.removeElement(option)
|
||||
}
|
||||
|
||||
@@ -123,7 +123,17 @@ open class OptionsPane : JPanel(BorderLayout()) {
|
||||
private fun initEvents() {
|
||||
tabList.addListSelectionListener {
|
||||
if (tabList.selectedIndex >= 0) {
|
||||
cardLayout.show(contentPanel, tabListModel.get(tabList.selectedIndex).getTitle())
|
||||
val option = tabListModel.get(tabList.selectedIndex)
|
||||
val title = option.getTitle()
|
||||
|
||||
if (!loadedComponents.containsKey(title)) {
|
||||
val component = option.getJComponent()
|
||||
loadedComponents[title] = component
|
||||
contentPanel.add(component, title)
|
||||
SwingUtilities.updateComponentTreeUI(component)
|
||||
}
|
||||
|
||||
cardLayout.show(contentPanel, title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,10 +223,9 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
}
|
||||
}
|
||||
|
||||
override fun willBeClose(): Boolean {
|
||||
override fun beforeClose() {
|
||||
// 保存窗口状态
|
||||
terminalPanel.storeVisualWindows(host.id)
|
||||
return super.willBeClose()
|
||||
}
|
||||
|
||||
private inner class MySessionListener : SessionListener, Disposable {
|
||||
|
||||
@@ -3,8 +3,6 @@ package app.termora
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
@@ -20,8 +18,10 @@ class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
||||
title = I18n.getString("termora.setting")
|
||||
setLocationRelativeTo(null)
|
||||
|
||||
init()
|
||||
val index = properties.getString("Settings-SelectedOption")?.toIntOrNull() ?: 0
|
||||
optionsPane.setSelectedIndex(index)
|
||||
|
||||
init()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
@@ -31,14 +31,6 @@ class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
||||
properties.putString("Settings-SelectedOption", optionsPane.getSelectedIndex().toString())
|
||||
}
|
||||
})
|
||||
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowActivated(e: WindowEvent) {
|
||||
removeWindowListener(this)
|
||||
val index = properties.getString("Settings-SelectedOption")?.toIntOrNull() ?: return
|
||||
optionsPane.setSelectedIndex(index)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
|
||||
@@ -33,6 +33,10 @@ import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import com.jthemedetecor.OsThemeDetector
|
||||
import com.sun.jna.LastErrorException
|
||||
import com.sun.jna.Native
|
||||
import com.sun.jna.platform.win32.Shell32
|
||||
import com.sun.jna.platform.win32.ShlObj
|
||||
import com.sun.jna.platform.win32.WinDef
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
@@ -132,6 +136,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
val themeComboBox = FlatComboBox<String>()
|
||||
val languageComboBox = FlatComboBox<String>()
|
||||
val backgroundComBoBox = YesOrNoComboBox()
|
||||
val confirmTabCloseComBoBox = YesOrNoComboBox()
|
||||
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
|
||||
val preferredThemeBtn = JButton(Icons.settings)
|
||||
val opacitySpinner = NumberSpinner(100, 0, 100)
|
||||
@@ -180,6 +185,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
followSystemCheckBox.isSelected = appearance.followSystem
|
||||
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
|
||||
backgroundComBoBox.selectedItem = appearance.backgroundRunning
|
||||
confirmTabCloseComBoBox.selectedItem = appearance.confirmTabClose
|
||||
|
||||
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
||||
themeManager.themes.keys.forEach { themeComboBox.addItem(it) }
|
||||
@@ -230,6 +236,13 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
confirmTabCloseComBoBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
appearance.confirmTabClose = confirmTabCloseComBoBox.selectedItem as Boolean
|
||||
}
|
||||
}
|
||||
|
||||
followSystemCheckBox.addActionListener {
|
||||
appearance.followSystem = followSystemCheckBox.isSelected
|
||||
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
||||
@@ -368,7 +381,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private fun getFormPanel(): JPanel {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow, $formMargin, default, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
val box = FlatToolBar()
|
||||
box.add(followSystemCheckBox)
|
||||
@@ -401,7 +414,13 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
.add(opacitySpinner).xy(3, rows).apply { rows += step }
|
||||
|
||||
builder.add("${I18n.getString("termora.settings.appearance.background-running")}:").xy(1, rows)
|
||||
.add(backgroundComBoBox).xy(3, rows)
|
||||
.add(backgroundComBoBox).xy(3, rows).apply { rows += step }
|
||||
|
||||
val confirmTabCloseBox = Box.createHorizontalBox()
|
||||
confirmTabCloseBox.add(JLabel("${I18n.getString("termora.settings.appearance.confirm-tab-close")}:"))
|
||||
confirmTabCloseBox.add(Box.createHorizontalStrut(8))
|
||||
confirmTabCloseBox.add(confirmTabCloseComBoBox)
|
||||
builder.add(confirmTabCloseBox).xyw(1, rows, 3).apply { rows += step }
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
@@ -585,16 +604,28 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
shellComboBox.selectedItem = terminalSetting.localShell
|
||||
|
||||
val fonts = linkedSetOf("JetBrains Mono", "Source Code Pro", "Monospaced")
|
||||
FontUtils.getAllFonts().forEach {
|
||||
if (!fonts.contains(it.family)) {
|
||||
fonts.addLast(it.family)
|
||||
}
|
||||
}
|
||||
fontComboBox.addItem(terminalSetting.font)
|
||||
var fontsLoaded = false
|
||||
|
||||
for (font in fonts) {
|
||||
fontComboBox.addItem(font)
|
||||
}
|
||||
fontComboBox.addPopupMenuListener(object : PopupMenuListener {
|
||||
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
||||
if (!fontsLoaded) {
|
||||
val selectedItem = fontComboBox.selectedItem
|
||||
fontComboBox.removeAllItems();
|
||||
fontComboBox.addItem("JetBrains Mono")
|
||||
fontComboBox.addItem("Source Code Pro")
|
||||
fontComboBox.addItem("Monospaced")
|
||||
FontUtils.getAvailableFontFamilyNames().forEach {
|
||||
fontComboBox.addItem(it)
|
||||
}
|
||||
fontComboBox.selectedItem = selectedItem
|
||||
fontsLoaded = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {}
|
||||
override fun popupMenuCanceled(e: PopupMenuEvent) {}
|
||||
})
|
||||
|
||||
fontComboBox.selectedItem = terminalSetting.font
|
||||
debugComboBox.selectedItem = terminalSetting.debug
|
||||
@@ -1487,6 +1518,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private val sftpCommandField = OutlineTextField(255)
|
||||
private val defaultDirectoryField = OutlineTextField(255)
|
||||
private val browseDirectoryBtn = JButton(Icons.folder)
|
||||
private val browseEditCommandBtn = JButton(Icons.folder)
|
||||
private val pinTabComboBox = YesOrNoComboBox()
|
||||
private val preserveModificationTimeComboBox = YesOrNoComboBox()
|
||||
private val sftp get() = database.sftp
|
||||
@@ -1558,6 +1590,41 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
browseEditCommandBtn.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val chooser = FileChooser()
|
||||
chooser.allowsMultiSelection = false
|
||||
chooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
||||
|
||||
if (SystemInfo.isMacOS) {
|
||||
chooser.defaultDirectory = "/Applications"
|
||||
} else {
|
||||
if (SystemInfo.isWindows) {
|
||||
val pszPath = CharArray(WinDef.MAX_PATH)
|
||||
Shell32.INSTANCE.SHGetFolderPath(
|
||||
null,
|
||||
ShlObj.CSIDL_DESKTOPDIRECTORY, null, ShlObj.SHGFP_TYPE_CURRENT,
|
||||
pszPath
|
||||
)
|
||||
chooser.defaultDirectory = Native.toString(pszPath)
|
||||
} else {
|
||||
chooser.defaultDirectory = SystemUtils.USER_HOME
|
||||
}
|
||||
}
|
||||
|
||||
chooser.showOpenDialog(owner).thenAccept { files ->
|
||||
if (files.isNotEmpty()) {
|
||||
val file = files.first()
|
||||
if (SystemInfo.isMacOS) {
|
||||
editCommandField.text = "open -a ${file.absolutePath} {0}"
|
||||
} else {
|
||||
editCommandField.text = "${file.absolutePath} {0}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1574,6 +1641,8 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
sftpCommandField.placeholderText = "sftp"
|
||||
}
|
||||
|
||||
editCommandField.trailingComponent = browseEditCommandBtn
|
||||
|
||||
defaultDirectoryField.placeholderText = SystemUtils.USER_HOME
|
||||
defaultDirectoryField.trailingComponent = browseDirectoryBtn
|
||||
|
||||
|
||||
@@ -265,9 +265,14 @@ object SshClients {
|
||||
} catch (e: Exception) {
|
||||
if (e !is SshException || e.disconnectCode != SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) throw e
|
||||
val owner = client.properties["owner"] as Window? ?: throw e
|
||||
val authentication = ask(host, owner) ?: throw e
|
||||
if (authentication.type == AuthenticationType.No) throw e
|
||||
return doOpenSession(host.copy(authentication = authentication), client)
|
||||
val askUserInfo = ask(host, entry, owner) ?: throw e
|
||||
if (askUserInfo.authentication.type == AuthenticationType.No) throw e
|
||||
return doOpenSession(
|
||||
host.copy(
|
||||
authentication = askUserInfo.authentication,
|
||||
username = askUserInfo.username
|
||||
), client
|
||||
)
|
||||
}
|
||||
|
||||
session.setAttribute(HOST_KEY, host)
|
||||
@@ -414,20 +419,32 @@ object SshClients {
|
||||
return sshClient
|
||||
}
|
||||
|
||||
private fun ask(host: Host, owner: Window): Authentication? {
|
||||
val ref = AtomicReference<Authentication>(null)
|
||||
|
||||
private data class AskUserInfo(val username: String, val authentication: Authentication)
|
||||
|
||||
private fun ask(host: Host, entry: HostConfigEntry, owner: Window): AskUserInfo? {
|
||||
val ref = AtomicReference<AskUserInfo>(null)
|
||||
|
||||
SwingUtilities.invokeAndWait {
|
||||
val dialog = RequestAuthenticationDialog(owner, host)
|
||||
dialog.setLocationRelativeTo(owner)
|
||||
val authentication = dialog.getAuthentication().apply { ref.set(this) }
|
||||
val authentication = dialog.getAuthentication()
|
||||
ref.set(AskUserInfo(dialog.getUsername(), authentication))
|
||||
|
||||
// save
|
||||
if (dialog.isRemembered()) {
|
||||
hostManager.addHost(
|
||||
host.copy(
|
||||
authentication = authentication,
|
||||
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
||||
// fix https://github.com/TermoraDev/termora/issues/609
|
||||
val hostId = entry.getProperty("Host", host.id)
|
||||
val h = hostManager.getHost(hostId)
|
||||
if (h != null) {
|
||||
hostManager.addHost(
|
||||
h.copy(
|
||||
authentication = authentication,
|
||||
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return ref.get()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.Database.Appearance
|
||||
import app.termora.actions.DataProvider
|
||||
import java.beans.PropertyChangeListener
|
||||
import javax.swing.Icon
|
||||
@@ -44,10 +45,15 @@ interface TerminalTab : Disposable, DataProvider {
|
||||
fun canClose(): Boolean = true
|
||||
|
||||
/**
|
||||
* 返回 true 表示可以关闭
|
||||
* 返回 true 表示可以关闭,只有当 [Appearance.confirmTabClose] 为 false 时才会调用
|
||||
*/
|
||||
fun willBeClose(): Boolean = true
|
||||
|
||||
/**
|
||||
* 即将关闭,已经无法挽回
|
||||
*/
|
||||
fun beforeClose() {}
|
||||
|
||||
/**
|
||||
* 是否可以克隆
|
||||
*/
|
||||
|
||||
@@ -32,6 +32,7 @@ class TerminalTabbed(
|
||||
private val actionManager = ActionManager.getInstance()
|
||||
private val dataProviderSupport = DataProviderSupport()
|
||||
private val titleProperty = UUID.randomUUID().toSimpleString()
|
||||
private val appearance get() = Database.getDatabase().appearance
|
||||
private val iconListener = PropertyChangeListener { e ->
|
||||
val source = e.source
|
||||
if (e.propertyName == "icon" && source is TerminalTab) {
|
||||
@@ -153,8 +154,29 @@ class TerminalTabbed(
|
||||
if (tabbedPane.isTabClosable(index)) {
|
||||
val tab = tabs[index]
|
||||
|
||||
// 询问是否可以关闭
|
||||
if (disposable) {
|
||||
if (!tab.willBeClose()) {
|
||||
// 如果开启了关闭确认,那么直接询问用户
|
||||
if (appearance.confirmTabClose) {
|
||||
if (OptionPane.showConfirmDialog(
|
||||
windowScope.window,
|
||||
I18n.getString("termora.tabbed.tab.close-prompt"),
|
||||
messageType = JOptionPane.QUESTION_MESSAGE,
|
||||
optionType = JOptionPane.OK_CANCEL_OPTION
|
||||
) != JOptionPane.OK_OPTION
|
||||
) {
|
||||
return
|
||||
}
|
||||
} else if (!tab.willBeClose()) { // 如果没有开启则询问用户
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 通知即将关闭
|
||||
if (disposable) {
|
||||
try {
|
||||
tab.beforeClose()
|
||||
} catch (_: Exception) {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -400,12 +422,11 @@ class TerminalTabbed(
|
||||
private fun showContextMenu(event: MouseEvent) {
|
||||
val popupMenu = FlatPopupMenu()
|
||||
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
|
||||
val dialog = CustomizeToolBarDialog(
|
||||
SwingUtilities.getWindowAncestor(this@TerminalTabbed),
|
||||
termoraToolBar
|
||||
)
|
||||
val owner = SwingUtilities.getWindowAncestor(this@TerminalTabbed)
|
||||
val dialog = CustomizeToolBarDialog(owner, windowScope, termoraToolBar)
|
||||
dialog.setLocationRelativeTo(owner)
|
||||
if (dialog.open()) {
|
||||
termoraToolBar.rebuild()
|
||||
TermoraToolBar.rebuild()
|
||||
}
|
||||
}
|
||||
popupMenu.show(event.component, event.x, event.y)
|
||||
|
||||
@@ -38,7 +38,7 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
private val id = UUID.randomUUID().toString()
|
||||
private val windowScope = ApplicationScope.forWindowScope(this)
|
||||
private val tabbedPane = MyTabbedPane()
|
||||
private val toolbar = TermoraToolBar(windowScope, this, tabbedPane)
|
||||
private val toolbar = TermoraToolBar(windowScope, this)
|
||||
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane)
|
||||
private val dataProviderSupport = DataProviderSupport()
|
||||
private val welcomePanel = WelcomePanel(windowScope)
|
||||
@@ -260,6 +260,10 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
|
||||
|
||||
private class GlassPane : JComponent() {
|
||||
init {
|
||||
isFocusable = false
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
|
||||
val g2d = g as Graphics2D
|
||||
@@ -271,5 +275,9 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
g2d.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER)
|
||||
}
|
||||
|
||||
override fun contains(x: Int, y: Int): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ class TermoraFrameManager : Disposable {
|
||||
|
||||
fun createWindow(): TermoraFrame {
|
||||
val frame = TermoraFrame().apply { registerCloseCallback(this) }
|
||||
frame.title = if (SystemInfo.isLinux) null else Application.getName()
|
||||
frame.title = Application.getName()
|
||||
frame.defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
||||
|
||||
val rectangle = getFrameRectangle() ?: FrameRectangle(-1, -1, 1280, 800, 0)
|
||||
|
||||
@@ -5,7 +5,6 @@ import app.termora.actions.*
|
||||
import app.termora.findeverywhere.FindEverywhereAction
|
||||
import app.termora.snippet.SnippetAction
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
@@ -26,10 +25,21 @@ data class ToolBarAction(
|
||||
class TermoraToolBar(
|
||||
private val windowScope: WindowScope,
|
||||
private val frame: TermoraFrame,
|
||||
private val tabbedPane: FlatTabbedPane
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun rebuild() {
|
||||
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
||||
val toolbars = SwingUtils.getDescendantsOfClass(MyToolBar::class.java, frame)
|
||||
for (toolbar in toolbars) {
|
||||
toolbar.rebuild()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val properties by lazy { Database.getDatabase().properties }
|
||||
private val toolbar by lazy { MyToolBar().apply { rebuild(this) } }
|
||||
private val toolbar by lazy { MyToolBar().apply { rebuild() } }
|
||||
|
||||
|
||||
fun getJToolBar(): JToolBar {
|
||||
@@ -87,63 +97,6 @@ class TermoraToolBar(
|
||||
return storageActions
|
||||
}
|
||||
|
||||
fun rebuild() {
|
||||
rebuild(this.toolbar)
|
||||
}
|
||||
|
||||
private fun rebuild(toolbar: JToolBar) {
|
||||
val actionManager = ActionManager.getInstance()
|
||||
val actionContainerFactory = ActionContainerFactory(actionManager)
|
||||
|
||||
toolbar.removeAll()
|
||||
|
||||
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
|
||||
}
|
||||
|
||||
override fun isEnabled(): Boolean {
|
||||
return actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.isEnabled ?: false
|
||||
}
|
||||
}))
|
||||
|
||||
toolbar.add(Box.createHorizontalGlue())
|
||||
|
||||
if (SystemInfo.isLinux || SystemInfo.isWindows) {
|
||||
toolbar.add(Box.createHorizontalStrut(16))
|
||||
}
|
||||
|
||||
|
||||
// update btn
|
||||
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
|
||||
updateBtn.isVisible = updateBtn.isEnabled
|
||||
updateBtn.addChangeListener { updateBtn.isVisible = updateBtn.isEnabled }
|
||||
toolbar.add(updateBtn)
|
||||
|
||||
|
||||
// 获取显示的Action,如果不是 false 那么就是显示出来
|
||||
for (action in getActions()) {
|
||||
if (action.visible) {
|
||||
val ac = actionManager.getAction(action.id)
|
||||
if (ac == null) {
|
||||
if (action.id == MultipleAction.MULTIPLE) {
|
||||
toolbar.add(actionContainerFactory.createButton(MultipleAction.getInstance(windowScope)))
|
||||
}
|
||||
} else {
|
||||
toolbar.add(actionContainerFactory.createButton(ac))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (toolbar is MyToolBar) {
|
||||
toolbar.adjust()
|
||||
}
|
||||
|
||||
toolbar.revalidate()
|
||||
toolbar.repaint()
|
||||
}
|
||||
|
||||
private inner class MyToolBar : JToolBar() {
|
||||
init {
|
||||
// 监听窗口大小变动,然后修改边距避开控制按钮
|
||||
@@ -179,5 +132,60 @@ class TermoraToolBar(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun rebuild() {
|
||||
val toolbar: JToolBar = this
|
||||
val actionManager = ActionManager.getInstance()
|
||||
val actionContainerFactory = ActionContainerFactory(actionManager)
|
||||
|
||||
toolbar.removeAll()
|
||||
|
||||
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
|
||||
}
|
||||
|
||||
override fun isEnabled(): Boolean {
|
||||
return actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.isEnabled ?: false
|
||||
}
|
||||
}))
|
||||
|
||||
toolbar.add(Box.createHorizontalGlue())
|
||||
|
||||
if (SystemInfo.isLinux || SystemInfo.isWindows) {
|
||||
toolbar.add(Box.createHorizontalStrut(16))
|
||||
}
|
||||
|
||||
|
||||
// update btn
|
||||
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
|
||||
updateBtn.isVisible = updateBtn.isEnabled
|
||||
updateBtn.addChangeListener { updateBtn.isVisible = updateBtn.isEnabled }
|
||||
toolbar.add(updateBtn)
|
||||
|
||||
|
||||
// 获取显示的Action,如果不是 false 那么就是显示出来
|
||||
for (action in getActions()) {
|
||||
if (action.visible) {
|
||||
val ac = actionManager.getAction(action.id)
|
||||
if (ac == null) {
|
||||
if (action.id == MultipleAction.MULTIPLE) {
|
||||
toolbar.add(actionContainerFactory.createButton(MultipleAction.getInstance(windowScope)))
|
||||
}
|
||||
} else {
|
||||
toolbar.add(actionContainerFactory.createButton(ac))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (toolbar is MyToolBar) {
|
||||
toolbar.adjust()
|
||||
}
|
||||
|
||||
toolbar.revalidate()
|
||||
toolbar.repaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,10 @@ class TerminalCopyAction : AnAction() {
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val terminalPanel = evt.getData(DataProviders.TerminalPanel) ?: return
|
||||
val selectionModel = terminalPanel.terminal.getSelectionModel()
|
||||
if (!selectionModel.hasSelection()) {
|
||||
return
|
||||
}
|
||||
val text = terminalPanel.copy()
|
||||
val systemClipboard = terminalPanel.toolkit.systemClipboard
|
||||
|
||||
@@ -53,4 +57,4 @@ class TerminalCopyAction : AnAction() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,9 +645,13 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
||||
return
|
||||
}
|
||||
|
||||
ohKeyPair = ohKeyPair.copy(
|
||||
name = nameTextField.text,
|
||||
remark = remarkTextField.text,
|
||||
)
|
||||
|
||||
if (ohKeyPair.remark.isEmpty()) {
|
||||
ohKeyPair = ohKeyPair.copy(
|
||||
name = nameTextField.text,
|
||||
remark = "Import on " + DateFormatUtils.format(Date(), I18n.getString("termora.date-format"))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class MacroDialog(owner: Window) : DialogWrapper(owner) {
|
||||
initEvents()
|
||||
|
||||
init()
|
||||
setLocationRelativeTo(null)
|
||||
setLocationRelativeTo(owner)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
|
||||
@@ -18,6 +18,16 @@ class SnippetAction private constructor() : AnAction(I18n.getString("termora.sni
|
||||
}
|
||||
|
||||
const val SNIPPET = "SnippetAction"
|
||||
|
||||
// \r \n \t \a \e \b
|
||||
private val SpecialChars = mutableMapOf(
|
||||
'r' to '\r',
|
||||
'n' to '\n',
|
||||
't' to '\t',
|
||||
'a' to ControlCharacters.BEL,
|
||||
'e' to ControlCharacters.ESC,
|
||||
'b' to ControlCharacters.BS
|
||||
)
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
@@ -27,31 +37,39 @@ class SnippetAction private constructor() : AnAction(I18n.getString("termora.sni
|
||||
|
||||
fun runSnippet(snippet: Snippet, writer: TerminalWriter) {
|
||||
if (snippet.type != SnippetType.Snippet) return
|
||||
val map = mapOf(
|
||||
"\n" to ControlCharacters.LF,
|
||||
"\r" to ControlCharacters.CR,
|
||||
"\t" to ControlCharacters.TAB,
|
||||
"\b" to ControlCharacters.BS,
|
||||
"\\a" to ControlCharacters.BEL,
|
||||
"\\e" to ControlCharacters.ESC,
|
||||
)
|
||||
val chars = snippet.snippet.toCharArray()
|
||||
writer.write(TerminalWriter.WriteRequest.fromBytes(unescape(snippet.snippet).toByteArray(writer.getCharset())))
|
||||
}
|
||||
|
||||
private fun unescape(text: String): String {
|
||||
val chars = text.toCharArray()
|
||||
val sb = StringBuilder()
|
||||
for (i in chars.indices) {
|
||||
val c = chars[i]
|
||||
if (i == 0) continue
|
||||
if (c != '\n') continue
|
||||
if (chars[i - 1] != '\\') continue
|
||||
// 每一行的最后一个 \ 比较特殊,先转成 null 然后再去 unescapeJava
|
||||
chars[i - 1] = Char.Null
|
||||
|
||||
// 不是特殊字符不处理
|
||||
if (SpecialChars.containsKey(c).not()) {
|
||||
sb.append(c)
|
||||
continue
|
||||
}
|
||||
|
||||
// 特殊字符前面不是 `\` 不处理
|
||||
if (chars.getOrNull(i - 1) != '\\') {
|
||||
sb.append(c)
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果构成的字符串是:\\r 就会生成 \r 字符串,并非转译成:CR
|
||||
if (chars.getOrNull(i - 2) == '\\') {
|
||||
sb.deleteCharAt(sb.length - 1)
|
||||
sb.append(c)
|
||||
continue
|
||||
}
|
||||
|
||||
// 命中条件之后,那么 sb 最后一个字符肯定是 \
|
||||
sb.deleteCharAt(sb.length - 1)
|
||||
sb.append(SpecialChars.getValue(c))
|
||||
}
|
||||
|
||||
var text = chars.joinToString(StringUtils.EMPTY)
|
||||
text = StringEscapeUtils.unescapeJava(text)
|
||||
for (e in map.entries) {
|
||||
text = text.replace(e.key, e.value.toString())
|
||||
}
|
||||
text = text.replace(Char.Null, '\\')
|
||||
|
||||
writer.write(TerminalWriter.WriteRequest.fromBytes(text.toByteArray(writer.getCharset())))
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
@@ -360,8 +360,9 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Send Device Attributes (Primary DA).
|
||||
// Send Device Attributes (Primary DA).
|
||||
'c' -> {
|
||||
sendDeviceAttributes()
|
||||
}
|
||||
|
||||
// CSI Ps M Delete Ps Line(s) (default = 1) (DL).
|
||||
@@ -505,6 +506,22 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
||||
|
||||
}
|
||||
|
||||
private fun sendDeviceAttributes() {
|
||||
|
||||
assertEventDispatchThread()
|
||||
|
||||
if (!terminalModel.hasData(DataKey.TerminalWriter)) {
|
||||
return
|
||||
}
|
||||
|
||||
val writer = terminalModel.getData(DataKey.TerminalWriter)
|
||||
|
||||
// VT102_RESPONSE
|
||||
val bytes = "${ControlCharacters.ESC}[?6c".toByteArray(writer.getCharset())
|
||||
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-?-Pm-h.1D0E
|
||||
*/
|
||||
|
||||
@@ -21,6 +21,16 @@ interface SelectionModel {
|
||||
*/
|
||||
fun setSelection(startPosition: Position, endPosition: Position)
|
||||
|
||||
/**
|
||||
* 设置块选中模式
|
||||
*/
|
||||
fun setBlockSelection(block: Boolean)
|
||||
|
||||
/**
|
||||
* 是否是块选中模式
|
||||
*/
|
||||
fun isBlockSelection(): Boolean
|
||||
|
||||
/**
|
||||
* 获取开始选中的位置
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,7 @@ import kotlin.math.min
|
||||
open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
||||
private var startPosition = Position.unknown
|
||||
private var endPosition = Position.unknown
|
||||
private var block = false
|
||||
private val document = terminal.getDocument()
|
||||
|
||||
internal companion object {
|
||||
@@ -67,29 +68,60 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
val iterator = getChars(getSelectionStartPosition(), getSelectionEndPosition())
|
||||
while (iterator.hasNext()) {
|
||||
val line = iterator.next()
|
||||
val chars = line.chars()
|
||||
if (chars.isEmpty() || chars.first().first.isNull) {
|
||||
continue
|
||||
}
|
||||
val start = getSelectionStartPosition()
|
||||
val end = getSelectionEndPosition()
|
||||
|
||||
for (e in chars) {
|
||||
if (e.first.isSoftHyphen) {
|
||||
continue
|
||||
} else if (e.first.isNull) {
|
||||
break
|
||||
if (isBlockSelection()) {
|
||||
val left = min(start.x, end.x)
|
||||
val right = max(start.x, end.x)
|
||||
val top = min(start.y, end.y)
|
||||
val bottom = max(start.y, end.y)
|
||||
|
||||
for (lineNum in top..bottom) {
|
||||
val line = document.getLine(lineNum)
|
||||
val chars = line.chars()
|
||||
|
||||
// 块选中要处理超出边界
|
||||
val from = (left - 1).coerceAtLeast(0)
|
||||
val to = right.coerceAtMost(chars.size)
|
||||
|
||||
if (from < to) {
|
||||
val selected = chars.subList(from, to)
|
||||
.filter { !it.first.isNull && !it.first.isSoftHyphen }
|
||||
.joinToString("") { it.first.toString() }
|
||||
sb.append(selected)
|
||||
}
|
||||
|
||||
if (lineNum != bottom) {
|
||||
sb.appendLine()
|
||||
}
|
||||
sb.append(e.first)
|
||||
}
|
||||
|
||||
if (line.wrapped) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
val iterator = getChars(start, end)
|
||||
while (iterator.hasNext()) {
|
||||
val line = iterator.next()
|
||||
val chars = line.chars()
|
||||
if (chars.isEmpty() || chars.first().first.isNull) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (iterator.hasNext()) {
|
||||
sb.appendLine()
|
||||
for (e in chars) {
|
||||
if (e.first.isSoftHyphen) {
|
||||
continue
|
||||
} else if (e.first.isNull) {
|
||||
break
|
||||
}
|
||||
sb.append(e.first)
|
||||
}
|
||||
|
||||
if (line.wrapped) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (iterator.hasNext()) {
|
||||
sb.appendLine()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +203,12 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
||||
fireSelectionChanged()
|
||||
}
|
||||
|
||||
override fun setBlockSelection(block: Boolean) {
|
||||
this.block = block
|
||||
}
|
||||
|
||||
override fun isBlockSelection() = block
|
||||
|
||||
override fun getSelectionStartPosition(): Position {
|
||||
return startPosition
|
||||
}
|
||||
@@ -202,13 +240,20 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
||||
}
|
||||
|
||||
override fun hasSelection(x: Int, y: Int): Boolean {
|
||||
return hasSelection() && isPointInsideArea(
|
||||
startPosition,
|
||||
endPosition,
|
||||
x,
|
||||
y,
|
||||
terminal.getTerminalModel().getCols()
|
||||
)
|
||||
|
||||
if (hasSelection().not()) return false
|
||||
|
||||
// 如果是块选中
|
||||
if (isBlockSelection()) {
|
||||
val left = min(startPosition.x, endPosition.x)
|
||||
val right = max(startPosition.x, endPosition.x)
|
||||
val top = min(startPosition.y, endPosition.y)
|
||||
val bottom = max(startPosition.y, endPosition.y)
|
||||
|
||||
return x in left..right && y in top..bottom
|
||||
}
|
||||
|
||||
return isPointInsideArea(startPosition, endPosition, x, y, terminal.getTerminalModel().getCols())
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora.terminal.panel
|
||||
|
||||
import app.termora.actions.TerminalCopyAction
|
||||
import app.termora.keymap.KeyShortcut
|
||||
import app.termora.keymap.KeymapManager
|
||||
import app.termora.terminal.ControlCharacters
|
||||
@@ -69,6 +70,7 @@ class TerminalPanelKeyAdapter(
|
||||
}
|
||||
|
||||
val keyStroke = KeyStroke.getKeyStrokeForEvent(e)
|
||||
val keymapActions = activeKeymap.getActionIds(KeyShortcut(keyStroke))
|
||||
for (action in terminalPanel.getTerminalActions()) {
|
||||
if (action.test(keyStroke, e)) {
|
||||
action.actionPerformed(e)
|
||||
@@ -100,7 +102,9 @@ class TerminalPanelKeyAdapter(
|
||||
}
|
||||
|
||||
// 如果命中了全局快捷键,那么不处理
|
||||
if (keyStroke.modifiers != 0 && activeKeymap.getActionIds(KeyShortcut(keyStroke)).isNotEmpty()) {
|
||||
val copyShortcutWithoutSelection =
|
||||
keymapActions.contains(TerminalCopyAction.COPY) && terminal.getSelectionModel().hasSelection().not()
|
||||
if (keyStroke.modifiers != 0 && keymapActions.isNotEmpty() && !copyShortcutWithoutSelection) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -155,4 +159,4 @@ class TerminalPanelKeyAdapter(
|
||||
return Character.toLowerCase(e.keyCode.toChar())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +134,8 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
|
||||
// 如果不判断的话可能会导致移动了一点点就就进入选择状态了
|
||||
val diff = terminalPanel.getAverageCharWidth() / 5.0
|
||||
if (abs(mousePressedPoint.y - e.y) >= diff || abs(mousePressedPoint.x - e.x) >= diff) {
|
||||
// 设置选中模式
|
||||
terminal.getSelectionModel().setBlockSelection(isOnlyAltDown(e))
|
||||
beginSelect(
|
||||
Position(x = mousePressedPoint.x, y = mousePressedPoint.y),
|
||||
)
|
||||
@@ -141,6 +143,13 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
|
||||
}
|
||||
}
|
||||
|
||||
private fun isOnlyAltDown(e: MouseEvent): Boolean {
|
||||
return e.isAltDown &&
|
||||
e.isMetaDown.not() &&
|
||||
e.isControlDown.not() &&
|
||||
e.isShiftDown.not() &&
|
||||
e.isAltGraphDown.not()
|
||||
}
|
||||
|
||||
private fun beginSelect(position: Position) {
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ termora.settings.appearance.follow-system=Sync with OS
|
||||
termora.settings.appearance.opacity=Opacity
|
||||
termora.settings.appearance.background-image=BG Image
|
||||
termora.settings.appearance.background-running=Backgrounding
|
||||
termora.settings.appearance.confirm-tab-close=Confirm tab close
|
||||
|
||||
termora.setting.security=Security
|
||||
termora.setting.security.enter-password=Enter password
|
||||
@@ -233,6 +234,7 @@ termora.tabbed.contextmenu.close-other-tabs=Close Other Tabs
|
||||
termora.tabbed.contextmenu.close-all-tabs=Close All Tabs
|
||||
termora.tabbed.contextmenu.reconnect=Reconnect
|
||||
termora.tabbed.local-tab.close-prompt=Do you want to terminal a running process in this terminal?
|
||||
termora.tabbed.tab.close-prompt=Are you sure you want to close this tab?
|
||||
|
||||
# Terminal logger
|
||||
termora.terminal-logger=Terminal Logger
|
||||
|
||||
@@ -54,6 +54,7 @@ termora.settings.appearance.follow-system=跟随系统
|
||||
termora.settings.appearance.opacity=透明度
|
||||
termora.settings.appearance.background-image=背景图
|
||||
termora.settings.appearance.background-running=后台运行
|
||||
termora.settings.appearance.confirm-tab-close=标签关闭前确认
|
||||
|
||||
termora.setting.security=安全
|
||||
termora.setting.security.enter-password=请输入密码
|
||||
@@ -222,6 +223,7 @@ termora.tabbed.contextmenu.close-other-tabs=关闭其他标签页
|
||||
termora.tabbed.contextmenu.close-all-tabs=关闭所有标签页
|
||||
termora.tabbed.contextmenu.reconnect=重新连接
|
||||
termora.tabbed.local-tab.close-prompt=你想要终止这个终端中正在运行的进程吗?
|
||||
termora.tabbed.tab.close-prompt=你确定要关闭这个标签页吗?
|
||||
|
||||
|
||||
# Terminal logger
|
||||
|
||||
@@ -55,6 +55,7 @@ termora.settings.appearance.follow-system=跟隨系統
|
||||
termora.settings.appearance.opacity=透明度
|
||||
termora.settings.appearance.background-image=背景圖
|
||||
termora.settings.appearance.background-running=後台運行
|
||||
termora.settings.appearance.confirm-tab-close=關閉分頁確認
|
||||
|
||||
termora.setting.security=安全
|
||||
termora.setting.security.enter-password=請輸入密碼
|
||||
@@ -218,7 +219,7 @@ termora.tabbed.contextmenu.close-other-tabs=關閉其他標籤頁
|
||||
termora.tabbed.contextmenu.close-all-tabs=關閉所有標籤
|
||||
termora.tabbed.contextmenu.reconnect=重新連接
|
||||
termora.tabbed.local-tab.close-prompt=你想要終止這個終端機中正在運作的進程嗎?
|
||||
|
||||
termora.tabbed.tab.close-prompt=你確定要關閉這個分頁嗎?
|
||||
|
||||
|
||||
# Terminal logger
|
||||
|
||||
@@ -55,6 +55,10 @@ Source: "{#MySourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdir
|
||||
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
|
||||
[InstallDelete]
|
||||
Type: files; Name: "{app}\app\*.jar"
|
||||
Type: filesandordirs; Name: "{app}\runtime\*"
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall; Check: ShouldPromptStart
|
||||
Filename: "{app}\{#MyAppExeName}"; Flags: nowait runhidden; Check: ShouldAutoStart
|
||||
|
||||
29
src/test/resources/issue-564/Dockerfile
Normal file
29
src/test/resources/issue-564/Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
FROM debian:bookworm-slim
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV TZ=Asia/Shanghai
|
||||
# 安装基础包 + sshd + nvim 依赖
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
openssh-server curl ca-certificates tzdata git unzip \
|
||||
libfuse2 locales && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
# 安装 nvim 最新版(AppImage 提取)
|
||||
RUN curl -LO https://github.com/neovim/neovim/releases/download/v0.11.1/nvim-linux-arm64.appimage && \
|
||||
mv nvim-linux-arm64.appimage nvim.appimage && chmod u+x nvim.appimage && ./nvim.appimage --appimage-extract && \
|
||||
mv squashfs-root/usr/bin/nvim /usr/local/bin/nvim && \
|
||||
rm -rf squashfs-root nvim.appimage
|
||||
# 配置 SSH
|
||||
RUN mkdir /var/run/sshd && \
|
||||
echo 'root:root' | chpasswd && \
|
||||
echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config && \
|
||||
echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config
|
||||
# 设置语言环境(可选)
|
||||
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \
|
||||
apt-get update && apt-get install -y locales && \
|
||||
locale-gen en_US.UTF-8 && \
|
||||
update-locale LANG=en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
LANGUAGE=en_US:en \
|
||||
LC_ALL=en_US.UTF-8
|
||||
# 启动 SSHD
|
||||
EXPOSE 22
|
||||
CMD ["/usr/sbin/sshd", "-D"]
|
||||
Reference in New Issue
Block a user