Compare commits

...

28 Commits
1.0.15 ... main

Author SHA1 Message Date
Srar
b499667cbb fix: copy hotkey conflicts with ctrlc 2025-12-12 09:28:29 +08:00
hstyi
1d596e18df chore: disable opengl 2025-07-03 08:48:48 +08:00
hstyi
6f95033009 release: 1.0.17 2025-06-17 09:24:56 +08:00
hstyi
1f08af6575 fix: mixpanel endpoint 2025-06-16 10:16:49 +08:00
hstyi
071a091347 fix: title not showing on Linux 2025-06-16 09:23:10 +08:00
hstyi
ca484618c7 chore: upgrade jdk 21.0.7b1034.51 2025-06-12 17:38:48 +08:00
hstyi
1f68f8a112 fix: text cursor not working (#637) 2025-06-11 10:52:43 +08:00
hstyi
0cd5670bd3 chore: winget.yml 2025-06-11 08:47:47 +08:00
hstyi
8e9c6bcb68 fix: macOS background running (#633) 2025-06-10 17:19:28 +08:00
hstyi
6c1fa0fc53 fix: custom toolbar action missing (#630) 2025-06-10 11:34:31 +08:00
hstyi
5145cfa8a5 release: 1.0.16 2025-06-10 08:34:03 +08:00
hstyi
87b1a5e315 fix: snippet \ character escape (#625) 2025-06-09 14:17:37 +08:00
hstyi
fa59869f2c fix: authentication username not being saved (#622) 2025-06-09 09:47:00 +08:00
kanoshiou
1ae64fe0db perf: lazy loading OptionsPane and Fonts (#619) 2025-06-07 12:07:55 +08:00
hstyi
f8d363836e chore: improve the host text field (#617) 2025-06-05 23:40:20 +08:00
dependabot[bot]
38dccb1d22 chore(deps): bump org.jetbrains.pty4j:pty4j from 0.13.5 to 0.13.6 (#613)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-04 11:11:50 +08:00
hstyi
3e31a89b92 chore: SFTP edit command supports manual file selection (#612) 2025-06-03 16:55:39 +08:00
kanoshiou
d8f892cc02 fix: missing remark when importing keys (#611) 2025-06-03 13:42:09 +08:00
hstyi
873deb55aa fix: SSH authentication causing IP and port changes (#610) 2025-06-03 12:55:41 +08:00
hstyi
c08712d79b fix: Xterm Send Device Attributes (Primary DA) (#607) 2025-05-30 10:44:53 +08:00
dependabot[bot]
61bc905727 chore(deps): bump org.testcontainers:testcontainers-bom from 1.21.0 to 1.21.1 (#606)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-30 10:28:44 +08:00
hstyi
17859be3c5 feat: confirm tab close (#605) 2025-05-30 09:48:48 +08:00
hstyi
7a24e34695 fix: delete leftover files before installing Windows (#604) 2025-05-30 09:11:57 +08:00
dependabot[bot]
58638eaad8 chore(deps): bump org.jetbrains.pty4j:pty4j from 0.13.4 to 0.13.5 (#603)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-29 18:26:50 +08:00
hstyi
09d2f2d193 chore: dialog location (#602) 2025-05-28 13:16:42 +08:00
hstyi
9121eff8d8 feat: support importing RDP protocol from CSV (#600) 2025-05-27 09:57:12 +08:00
dependabot[bot]
8b090b0526 chore(deps): bump org.gradle.toolchains.foojay-resolver-convention from 0.10.0 to 1.0.0 (#595) 2025-05-21 12:45:45 +08:00
hstyi
15a0d642ff feat: support block selection (#594) 2025-05-19 18:31:51 +08:00
40 changed files with 528 additions and 203 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -648,6 +648,11 @@ class Database private constructor(private val env: Environment) : Disposable {
*/
var backgroundRunning by BooleanPropertyDelegate(false)
/**
* 标签关闭前确认
*/
var confirmTabClose by BooleanPropertyDelegate(false)
/**
* 背景图片的地址
*/

View File

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

View File

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

View File

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

View File

@@ -40,7 +40,7 @@ class NewHostTreeDialog(
init()
setLocationRelativeTo(null)
setLocationRelativeTo(owner)
}

View File

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

View File

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

View File

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

View File

@@ -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
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
}
}
for (font in fonts) {
fontComboBox.addItem(font)
}
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

View File

@@ -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,21 +419,33 @@ 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()) {
// 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(
host.copy(
h.copy(
authentication = authentication,
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
)
)
}
}
}
return ref.get()
}

View File

@@ -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() {}
/**
* 是否可以克隆
*/

View File

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

View File

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

View File

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

View File

@@ -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,11 +97,45 @@ class TermoraToolBar(
return storageActions
}
fun rebuild() {
rebuild(this.toolbar)
private inner class MyToolBar : JToolBar() {
init {
// 监听窗口大小变动,然后修改边距避开控制按钮
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
adjust()
}
})
}
private fun rebuild(toolbar: JToolBar) {
fun adjust() {
if (SystemInfo.isWindows || SystemInfo.isLinux) {
val rectangle =
frame.rootPane.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
as? Rectangle ?: return
val right = rectangle.width
val toolbar = this@MyToolBar
for (i in 0 until toolbar.componentCount) {
val c = toolbar.getComponent(i)
if (c.name == "spacing") {
if (c.width == right) {
return
}
toolbar.remove(i)
break
}
}
if (right > 0) {
val spacing = Box.createHorizontalStrut(right)
spacing.name = "spacing"
toolbar.add(spacing)
}
}
}
fun rebuild() {
val toolbar: JToolBar = this
val actionManager = ActionManager.getInstance()
val actionContainerFactory = ActionContainerFactory(actionManager)
@@ -143,41 +187,5 @@ class TermoraToolBar(
toolbar.revalidate()
toolbar.repaint()
}
private inner class MyToolBar : JToolBar() {
init {
// 监听窗口大小变动,然后修改边距避开控制按钮
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
adjust()
}
})
}
fun adjust() {
if (SystemInfo.isWindows || SystemInfo.isLinux) {
val rectangle =
frame.rootPane.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
as? Rectangle ?: return
val right = rectangle.width
val toolbar = this@MyToolBar
for (i in 0 until toolbar.componentCount) {
val c = toolbar.getComponent(i)
if (c.name == "spacing") {
if (c.width == right) {
return
}
toolbar.remove(i)
break
}
}
if (right > 0) {
val spacing = Box.createHorizontalStrut(right)
spacing.name = "spacing"
toolbar.add(spacing)
}
}
}
}
}

View File

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

View File

@@ -645,9 +645,13 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
return
}
if (ohKeyPair.remark.isEmpty()) {
ohKeyPair = ohKeyPair.copy(
name = nameTextField.text,
remark = remarkTextField.text,
)
if (ohKeyPair.remark.isEmpty()) {
ohKeyPair = ohKeyPair.copy(
remark = "Import on " + DateFormatUtils.format(Date(), I18n.getString("termora.date-format"))
)
}

View File

@@ -39,7 +39,7 @@ class MacroDialog(owner: Window) : DialogWrapper(owner) {
initEvents()
init()
setLocationRelativeTo(null)
setLocationRelativeTo(owner)
}
private fun initView() {

View File

@@ -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
}
var text = chars.joinToString(StringUtils.EMPTY)
text = StringEscapeUtils.unescapeJava(text)
for (e in map.entries) {
text = text.replace(e.key, e.value.toString())
// 特殊字符前面不是 `\` 不处理
if (chars.getOrNull(i - 1) != '\\') {
sb.append(c)
continue
}
text = text.replace(Char.Null, '\\')
writer.write(TerminalWriter.WriteRequest.fromBytes(text.toByteArray(writer.getCharset())))
// 如果构成的字符串是:\\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))
}
return sb.toString()
}
}

View File

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

View File

@@ -21,6 +21,16 @@ interface SelectionModel {
*/
fun setSelection(startPosition: Position, endPosition: Position)
/**
* 设置块选中模式
*/
fun setBlockSelection(block: Boolean)
/**
* 是否是块选中模式
*/
fun isBlockSelection(): Boolean
/**
* 获取开始选中的位置
*/

View File

@@ -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,7 +68,37 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
return sb.toString()
}
val iterator = getChars(getSelectionStartPosition(), getSelectionEndPosition())
val start = getSelectionStartPosition()
val end = getSelectionEndPosition()
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()
}
}
} else {
val iterator = getChars(start, end)
while (iterator.hasNext()) {
val line = iterator.next()
val chars = line.chars()
@@ -92,6 +123,7 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
sb.appendLine()
}
}
}
if (sb.isNotEmpty() && sb.last() == ControlCharacters.LF) {
sb.deleteCharAt(sb.length - 1)
@@ -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())
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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"]