Compare commits

...

20 Commits

Author SHA1 Message Date
hstyi
f92e43ee41 release: 2.0.0-beta.10 2025-07-31 10:33:37 +08:00
dependabot[bot]
f6243e33da chore(deps): bump com.github.hstyi:geolite2
Bumps com.github.hstyi:geolite2 from v1.0-202507070058 to v1.0-202507280101.

---
updated-dependencies:
- dependency-name: com.github.hstyi:geolite2
  dependency-version: v1.0-202507280101
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-31 10:28:32 +08:00
hstyi
72f334d572 chore: import CSV with password support 2025-07-31 10:28:17 +08:00
dependabot[bot]
68cbb10dec chore(deps): bump org.apache.commons:commons-csv from 1.14.0 to 1.14.1
Bumps [org.apache.commons:commons-csv](https://github.com/apache/commons-csv) from 1.14.0 to 1.14.1.
- [Changelog](https://github.com/apache/commons-csv/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-csv/compare/rel/commons-csv-1.14.0...rel/commons-csv-1.14.1)

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-csv
  dependency-version: 1.14.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-31 10:26:16 +08:00
dependabot[bot]
45f5c4ee91 chore(deps): bump com.qcloud:cos_api from 5.6.247 to 5.6.249
Bumps [com.qcloud:cos_api](https://github.com/tencentyun/cos-java-sdk-v5) from 5.6.247 to 5.6.249.
- [Release notes](https://github.com/tencentyun/cos-java-sdk-v5/releases)
- [Changelog](https://github.com/tencentyun/cos-java-sdk-v5/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tencentyun/cos-java-sdk-v5/commits)

---
updated-dependencies:
- dependency-name: com.qcloud:cos_api
  dependency-version: 5.6.249
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-30 12:29:12 +08:00
dependabot[bot]
48c511613e chore(deps): bump org.apache.commons:commons-compress
Bumps [org.apache.commons:commons-compress](https://github.com/apache/commons-compress) from 1.27.1 to 1.28.0.
- [Changelog](https://github.com/apache/commons-compress/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-compress/compare/rel/commons-compress-1.27.1...rel/commons-compress-1.28.0)

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-compress
  dependency-version: 1.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-30 12:29:01 +08:00
hstyi
c94063d459 fix: SSH timeout is always 30 seconds 2025-07-29 17:57:14 +08:00
hstyi
c26aafd831 chore: heartbeat interval 60 seconds 2025-07-29 17:31:00 +08:00
hstyi
a5638329e7 feat: support transfer double-click behavior 2025-07-29 09:23:31 +08:00
hstyi
8323f8eb5d fix: missing placeholder 2025-07-28 19:46:26 +08:00
hstyi
35199ed094 chore: RDP encrypt password on Windows 2025-07-28 10:02:02 +08:00
dependabot[bot]
b5d53cf416 chore(deps): bump org.xerial:sqlite-jdbc from 3.50.2.0 to 3.50.3.0
Bumps [org.xerial:sqlite-jdbc](https://github.com/xerial/sqlite-jdbc) from 3.50.2.0 to 3.50.3.0.
- [Release notes](https://github.com/xerial/sqlite-jdbc/releases)
- [Changelog](https://github.com/xerial/sqlite-jdbc/blob/master/CHANGELOG)
- [Commits](https://github.com/xerial/sqlite-jdbc/compare/3.50.2.0...3.50.3.0)

---
updated-dependencies:
- dependency-name: org.xerial:sqlite-jdbc
  dependency-version: 3.50.3.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-27 07:10:19 +08:00
hstyi
39e26a6e3d fix: keyword background color 2025-07-26 07:38:18 +08:00
dependabot[bot]
15cb06af0f chore(deps): bump commons-codec:commons-codec from 1.18.0 to 1.19.0
Bumps [commons-codec:commons-codec](https://github.com/apache/commons-codec) from 1.18.0 to 1.19.0.
- [Changelog](https://github.com/apache/commons-codec/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-codec/compare/rel/commons-codec-1.18.0...rel/commons-codec-1.19.0)

---
updated-dependencies:
- dependency-name: commons-codec:commons-codec
  dependency-version: 1.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-25 20:06:59 +08:00
dependabot[bot]
1e0bbb5a00 chore(deps): bump org.apache.commons:commons-text from 1.13.1 to 1.14.0
Bumps [org.apache.commons:commons-text](https://github.com/apache/commons-text) from 1.13.1 to 1.14.0.
- [Changelog](https://github.com/apache/commons-text/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-text/compare/rel/commons-text-1.13.1...rel/commons-text-1.14.0)

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-text
  dependency-version: 1.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-25 20:06:48 +08:00
hstyi
fb6fdbc14c chore: sync plugin 0.0.4 2025-07-23 09:45:30 +08:00
dependabot[bot]
96df53ce40 chore(deps): bump commons-io:commons-io from 2.19.0 to 2.20.0
Bumps [commons-io:commons-io](https://github.com/apache/commons-io) from 2.19.0 to 2.20.0.
- [Changelog](https://github.com/apache/commons-io/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-io/compare/rel/commons-io-2.19.0...rel/commons-io-2.20.0)

---
updated-dependencies:
- dependency-name: commons-io:commons-io
  dependency-version: 2.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 10:47:18 +08:00
hstyi
42f86dc3a3 chore: linux appimagetool 2025-07-21 10:47:07 +08:00
hstyi
32b11c6063 fix: macOS not opening local terminal 2025-07-21 10:47:07 +08:00
hstyi
b2f43ba439 chore: improve upgrade 2025-07-21 09:58:37 +08:00
31 changed files with 647 additions and 427 deletions

View File

@@ -1 +1 @@
2.0.0-beta.9
2.0.0-beta.10

View File

@@ -229,7 +229,8 @@ tasks.register<Copy>("copy-dependencies") {
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/dragonflybsd-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
val targetDir = FileUtils.getFile(dylib, pty4j.name, if (os.isWindows) "win32" else "linux")
val osName = if (os.isWindows) "win32" else if (os.isMacOsX) "darwin" else "linux"
val targetDir = FileUtils.getFile(dylib, pty4j.name, osName)
FileUtils.forceMkdir(targetDir)
val myArchName = if (arch.isArm) "aarch64" else "x86-64"
if (os.isWindows) {
@@ -548,7 +549,16 @@ fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: Str
"/DMyAppVersion=${appVersion}",
"/DMyOutputDir=${distributionDir.asFile.absolutePath}",
"/DMySetupIconFile=${FileUtils.getFile(projectDir, "src", "main", "resources", "icons", "termora.ico")}",
"/DMyWizardSmallImageFile=${FileUtils.getFile(projectDir, "src", "main", "resources", "icons", "termora_128x128.bmp")}",
"/DMyWizardSmallImageFile=${
FileUtils.getFile(
projectDir,
"src",
"main",
"resources",
"icons",
"termora_128x128.bmp"
)
}",
"/DMySourceDir=${FileUtils.getFile(dir, projectName).absolutePath}",
"/F${finalFilenameWithoutExtension}",
FileUtils.getFile(projectDir, "src", "main", "resources", "termora.iss")
@@ -658,7 +668,7 @@ fun packOnLinux(distributionDir: Directory, finalFilenameWithoutExtension: Strin
commandLine(
"wget",
"-O", appimagetool.absolutePath,
"https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-${if (arch.isArm) "aarch64" else "x86_64"}.AppImage"
"https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${if (arch.isArm) "aarch64" else "x86_64"}.AppImage"
)
workingDir = distributionDir.asFile
}

View File

@@ -6,12 +6,12 @@ tinylog = "2.7.0"
kotlinx-coroutines = "1.10.2"
flatlaf = "3.6.1"
kotlinx-serialization-json = "1.9.0"
commons-codec = "1.18.0"
commons-codec = "1.19.0"
commons-lang3 = "3.18.0"
commons-csv = "1.14.0"
commons-csv = "1.14.1"
commons-net = "3.11.1"
commons-text = "1.13.1"
commons-compress = "1.27.1"
commons-text = "1.14.0"
commons-compress = "1.28.0"
commons-vfs2 = "2.10.0"
swingx = "1.6.5-1"
jgoodies-forms = "1.9.0"
@@ -20,7 +20,7 @@ oshi = "6.8.1"
versioncompare = "1.4.1"
jna = "5.17.0"
jSystemThemeDetector = "3.9.1"
commons-io = "2.19.0"
commons-io = "2.20.0"
jbr-api = "17.1.10.1"
hutool = "5.8.39"
jsch = "2.27.2"
@@ -43,7 +43,7 @@ restart4j = "0.0.1"
eddsa = "0.3.0"
exposed = "1.0.0-beta-4"
h2 = "2.3.232"
sqlite = "3.50.2.0"
sqlite = "3.50.3.0"
jug = "5.1.0"
semver4j = "6.0.0"
jsvg = "2.0.0"

View File

@@ -2,13 +2,13 @@ plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.3"
project.version = "0.0.4"
dependencies {
testImplementation(kotlin("test"))
implementation("com.qcloud:cos_api:5.6.247")
implementation("com.qcloud:cos_api:5.6.249")
compileOnly(project(":"))
}

View File

@@ -2,14 +2,14 @@ plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.7"
project.version = "0.0.8"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
implementation("com.maxmind.geoip2:geoip2:4.3.1")
// https://github.com/hstyi/geolite2
implementation("com.github.hstyi:geolite2:v1.0-202507070058")
implementation("com.github.hstyi:geolite2:v1.0-202507280101")
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -3,7 +3,7 @@ plugins {
}
project.version = "0.0.3"
project.version = "0.0.4"
dependencies {

View File

@@ -127,12 +127,19 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
}
protected open fun createSouthPanel(): JComponent? {
val westSourcePanel = createWestSourcePanel()
val box = Box.createHorizontalBox()
if (westSourcePanel != null) {
box.add(westSourcePanel)
} else {
box.add(Box.createHorizontalGlue())
}
box.border = BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor),
BorderFactory.createEmptyBorder(8, 12, 8, 12)
)
box.add(Box.createHorizontalGlue())
val actions = createActions()
for (i in actions.size - 1 downTo 0) {
@@ -145,6 +152,10 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
return box
}
protected open fun createWestSourcePanel(): JComponent? {
return null
}
protected open fun createActions(): List<AbstractAction> {
return listOf(createOkAction(), createCancelAction())
}

View File

@@ -2,7 +2,6 @@ package app.termora
import app.termora.actions.StateAction
import app.termora.findeverywhere.FindEverywhereAction
import app.termora.plugin.internal.update.AppUpdateAction
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.extras.components.FlatToolBar
@@ -82,10 +81,11 @@ internal class MyTermoraToolbar(private val windowScope: WindowScope, private va
}
}))
add(Box.createHorizontalGlue())
if (SystemInfo.isLinux || SystemInfo.isWindows) {
add(Box.createHorizontalStrut(24))
}
// update
add(redirectUpdateAction(disposable))
add(Box.createHorizontalGlue())
for (action in model.getActions()) {
if (action.visible.not()) continue
@@ -122,34 +122,6 @@ internal class MyTermoraToolbar(private val windowScope: WindowScope, private va
toolbar.add(spacing)
}
private fun redirectUpdateAction(disposable: Disposable): AbstractButton {
val action = AppUpdateAction.getInstance()
val button = JButton(action.smallIcon)
button.toolTipText = (action.getValue(Action.SHORT_DESCRIPTION) as? String)
?: action.getValue(Action.NAME) as? String
button.isVisible = action.isEnabled
button.addActionListener(object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
action.actionPerformed(e)
}
})
val listener = object : PropertyChangeListener, Disposable {
override fun propertyChange(evt: PropertyChangeEvent) {
button.isVisible = action.isEnabled
}
override fun dispose() {
action.removePropertyChangeListener(this)
}
}
action.addPropertyChangeListener(listener)
Disposer.register(disposable, listener)
return button
}
private fun redirectAction(action: Action, disposable: Disposable): AbstractButton {
val button = if (action is StateAction) JToggleButton() else JButton()
button.toolTipText = (action.getValue(Action.SHORT_DESCRIPTION) as? String)

View File

@@ -650,6 +650,7 @@ class SettingsOptionsPane : OptionsPane() {
private val browseEditCommandBtn = JButton(Icons.folder)
private val pinTabComboBox = YesOrNoComboBox()
private val preserveModificationTimeComboBox = YesOrNoComboBox()
private val doubleClickComboBox = OutlineComboBox<String>()
private val sftp get() = database.sftp
init {
@@ -699,6 +700,13 @@ class SettingsOptionsPane : OptionsPane() {
})
doubleClickComboBox.addItemListener(object : ItemListener {
override fun itemStateChanged(e: ItemEvent) {
if (e.stateChange != ItemEvent.SELECTED) return
sftp.dbClickBehavior = doubleClickComboBox.selectedItem as String
}
})
preserveModificationTimeComboBox.addItemListener {
if (it.stateChange == ItemEvent.SELECTED) {
sftp.preserveModificationTime = preserveModificationTimeComboBox.selectedItem as Boolean
@@ -780,6 +788,26 @@ class SettingsOptionsPane : OptionsPane() {
sftpCommandField.text = sftp.sftpCommand
pinTabComboBox.selectedItem = sftp.pinTab
preserveModificationTimeComboBox.selectedItem = sftp.preserveModificationTime
doubleClickComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component? {
var text = value?.toString()
if (value == "Edit") text = I18n.getString("termora.keymgr.edit")
if (value == "Transfer") text = getTitle()
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
}
}
doubleClickComboBox.addItem("Transfer")
doubleClickComboBox.addItem("Edit")
doubleClickComboBox.selectedItem = sftp.dbClickBehavior
}
override fun getIcon(isSelected: Boolean): Icon {
@@ -813,6 +841,8 @@ class SettingsOptionsPane : OptionsPane() {
builder.add(editCommandField).xy(3, rows).apply { rows += 2 }
builder.add("${I18n.getString("termora.tabbed.contextmenu.sftp-command")}:").xy(1, rows)
builder.add(sftpCommandField).xy(3, rows).apply { rows += 2 }
builder.add("${I18n.getString("termora.settings.sftp.db-click-behavior")}:").xy(1, rows)
builder.add(doubleClickComboBox).xy(3, rows).apply { rows += 2 }
builder.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
builder.add(defaultDirectoryField).xy(3, rows).apply { rows += 2 }
builder.add(box).xyw(1, rows, 3).apply { rows += 2 }

View File

@@ -763,6 +763,12 @@ class DatabaseManager private constructor() : Disposable {
*/
var editCommand by StringPropertyDelegate(StringUtils.EMPTY)
/**
* 双击行为
*
* Transfer、Edit
*/
var dbClickBehavior by StringPropertyDelegate("Transfer")
/**
* sftp command

View File

@@ -1,6 +1,8 @@
package app.termora.highlight
import app.termora.DialogWrapper
import app.termora.Disposable
import app.termora.Disposer
import app.termora.TerminalFactory
import com.formdev.flatlaf.util.SystemInfo
import java.awt.*
@@ -15,8 +17,9 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
var colorIndex = -1
var defaultColor: Color = Color.white
var ok = false
init {
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
isModal = true
super.setTitle(title)
controlsVisible = false
@@ -30,11 +33,12 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
override fun createCenterPanel(): JComponent {
val panel = JPanel(GridLayout(2, 8, 4, 4))
val colorPalette = TerminalFactory.getInstance()
.createTerminal().getTerminalModel().getColorPalette()
val terminal = TerminalFactory.getInstance().createTerminal()
val colorPalette = terminal.getTerminalModel().getColorPalette()
for (i in 1..16) {
val c = JPanel()
c.preferredSize = Dimension(24, 24)
c.minimumSize = c.preferredSize
c.background = Color(colorPalette.getXTerm256Color(i))
c.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
@@ -67,6 +71,12 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
cPanel.add(panel, BorderLayout.CENTER)
cPanel.add(customBtn, BorderLayout.SOUTH)
cPanel.border = BorderFactory.createEmptyBorder(if (SystemInfo.isLinux) 6 else 0, 12, 12, 12)
Disposer.register(disposable, object : Disposable {
override fun dispose() {
terminal.close()
}
})
return cPanel
}
@@ -74,4 +84,9 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
override fun createSouthPanel(): JComponent? {
return null
}
override fun doOKAction() {
ok = true
super.doOKAction()
}
}

View File

@@ -3,8 +3,8 @@ package app.termora.highlight
import java.awt.Color
import javax.swing.JPanel
class ColorPanel : JPanel {
var color: Color = Color.WHITE
class ColorPanel : JPanel() {
var color: Color? = null
set(value) {
background = value
val old = field
@@ -13,7 +13,4 @@ class ColorPanel : JPanel {
}
var colorIndex = -1
constructor(color: Color) : super() {
this.color = color
}
}

View File

@@ -285,29 +285,27 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
dialog.keywordTextField.text = keywordHighlight.keyword
dialog.descriptionTextField.text = keywordHighlight.description
if (keywordHighlight.textColor <= 16) {
if (keywordHighlight.textColor in 0..16) {
if (keywordHighlight.textColor == 0) {
dialog.textColor.color = Color(colorPalette.getColor(TerminalColor.Basic.FOREGROUND))
dialog.textColor.background = Color(colorPalette.getColor(TerminalColor.Basic.FOREGROUND))
dialog.textColor.colorIndex = -1
} else {
dialog.textColor.color = Color(colorPalette.getXTerm256Color(keywordHighlight.textColor))
dialog.textColor.colorIndex = keywordHighlight.textColor
}
dialog.textColor.colorIndex = keywordHighlight.textColor
} else {
dialog.textColor.color = Color(keywordHighlight.textColor)
dialog.textColor.colorIndex = -1
}
if (keywordHighlight.backgroundColor <= 16) {
if (keywordHighlight.backgroundColor in 0..16) {
if (keywordHighlight.backgroundColor == 0) {
dialog.backgroundColor.color = Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND))
dialog.backgroundColor.background = Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND))
dialog.backgroundColor.colorIndex = -1
} else {
dialog.backgroundColor.color =
Color(colorPalette.getXTerm256Color(keywordHighlight.backgroundColor))
dialog.backgroundColor.colorIndex = keywordHighlight.backgroundColor
}
dialog.backgroundColor.colorIndex = keywordHighlight.backgroundColor
} else {
dialog.backgroundColor.color = Color(keywordHighlight.backgroundColor)
dialog.backgroundColor.colorIndex = -1
}
dialog.boldCheckBox.isSelected = keywordHighlight.bold

View File

@@ -23,6 +23,7 @@ import java.awt.event.MouseEvent
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.swing.*
import kotlin.math.max
class NewKeywordHighlightDialog(
owner: Window,
@@ -95,7 +96,7 @@ class NewKeywordHighlightDialog(
init()
pack()
size = Dimension(UIManager.getInt("Dialog.width") - 200, height)
size = Dimension(UIManager.getInt("Dialog.width") - 200, max(height, preferredSize.height))
setLocationRelativeTo(null)
}
@@ -121,13 +122,15 @@ class NewKeywordHighlightDialog(
lineThroughCheckBox.addActionListener { repaintKeywordHighlightView() }
textColorRevert.addActionListener {
textColor.color = Color(colorPalette.getColor(TerminalColor.Basic.FOREGROUND))
textColor.colorIndex = 0
textColor.color = null
textColor.background = Color(colorPalette.getColor(TerminalColor.Basic.FOREGROUND))
textColor.colorIndex = -1
repaintKeywordHighlightView()
}
backgroundColorRevert.addActionListener {
backgroundColor.color = Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND))
backgroundColor.colorIndex = 0
backgroundColor.color = null
backgroundColor.background = Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND))
backgroundColor.colorIndex = -1
repaintKeywordHighlightView()
}
@@ -145,8 +148,22 @@ class NewKeywordHighlightDialog(
keywordHighlightView.italic = italicCheckBox.isSelected
keywordHighlightView.underline = underlineCheckBox.isSelected
keywordHighlightView.lineThrough = lineThroughCheckBox.isSelected
keywordHighlightView.textColor = textColor.color
keywordHighlightView.backgroundColor = backgroundColor.color
if (textColor.color == null && textColor.colorIndex == -1) {
keywordHighlightView.textColor = Color(colorPalette.getColor(TerminalColor.Basic.FOREGROUND))
} else if (textColor.color != null) {
keywordHighlightView.textColor = textColor.color
} else {
keywordHighlightView.textColor = Color(colorPalette.getXTerm256Color(textColor.colorIndex))
}
if (backgroundColor.color == null && backgroundColor.colorIndex == -1) {
keywordHighlightView.backgroundColor = Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND))
} else if (backgroundColor.color != null) {
keywordHighlightView.backgroundColor = backgroundColor.color
} else {
keywordHighlightView.backgroundColor = Color(colorPalette.getXTerm256Color(backgroundColor.colorIndex))
}
keywordHighlightView.repaint()
}
@@ -192,7 +209,8 @@ class NewKeywordHighlightDialog(
val owner = this
val arc = UIManager.getInt("Component.arc")
val lineBorder = FlatLineBorder(Insets(1, 1, 1, 1), DynamicColor.BorderColor, 1f, arc)
val colorPanel = ColorPanel(color)
val colorPanel = ColorPanel()
colorPanel.background = color
colorPanel.preferredSize = keywordTextField.preferredSize
colorPanel.border = lineBorder
colorPanel.addMouseListener(object : MouseAdapter() {
@@ -200,10 +218,19 @@ class NewKeywordHighlightDialog(
if (SwingUtilities.isLeftMouseButton(e)) {
val dialog = ChooseColorTemplateDialog(owner, title)
dialog.setLocationRelativeTo(owner)
dialog.defaultColor = colorPanel.color
dialog.defaultColor = colorPanel.color ?: Color.orange
dialog.isVisible = true
colorPanel.color = dialog.color ?: return
colorPanel.colorIndex = dialog.colorIndex
if (dialog.ok.not()) return
colorPanel.colorIndex = -1
colorPanel.color = null
if (dialog.colorIndex in 1..16) {
colorPanel.colorIndex = dialog.colorIndex
colorPanel.background = Color(colorPalette.getXTerm256Color(dialog.colorIndex))
} else {
colorPanel.color = dialog.color
}
repaintKeywordHighlightView()
}
}
})
@@ -218,13 +245,22 @@ class NewKeywordHighlightDialog(
}
val newTextColor = if (textColor.color != null) textColor.color?.toRGB() ?: 0
else if (textColor.colorIndex == -1) 0
else textColor.colorIndex
val newBackgroundColor = if (backgroundColor.color != null) backgroundColor.color?.toRGB() ?: 0
else if (backgroundColor.colorIndex == -1) 0
else backgroundColor.colorIndex
keywordHighlight = KeywordHighlight(
keyword = keywordTextField.text,
description = descriptionTextField.text,
matchCase = matchCaseBtn.isSelected,
regex = regexBtn.isSelected,
textColor = if (textColor.colorIndex != -1) textColor.colorIndex else textColor.color.toRGB(),
backgroundColor = if (backgroundColor.colorIndex != -1) backgroundColor.colorIndex else backgroundColor.color.toRGB(),
textColor = newTextColor,
backgroundColor = newBackgroundColor,
bold = boldCheckBox.isSelected,
italic = italicCheckBox.isSelected,
lineThrough = lineThroughCheckBox.isSelected,

View File

@@ -12,7 +12,7 @@ import app.termora.plugin.internal.rdp.RDPInternalPlugin
import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin
import app.termora.plugin.internal.ssh.SSHInternalPlugin
import app.termora.plugin.internal.telnet.TelnetInternalPlugin
import app.termora.plugin.internal.update.UpdatePlugin
import app.termora.plugin.internal.updater.UpdaterPlugin
import app.termora.plugin.internal.wsl.WSLInternalPlugin
import app.termora.swingCoroutineScope
import app.termora.terminal.panel.vw.FloatingToolbarPlugin
@@ -111,7 +111,7 @@ internal class PluginManager private constructor() {
// badge plugin
plugins.add(PluginDescriptor(BadgePlugin(), origin = PluginOrigin.Internal, version = version))
// update plugin
plugins.add(PluginDescriptor(UpdatePlugin(), origin = PluginOrigin.Internal, version = version))
plugins.add(PluginDescriptor(UpdaterPlugin(), origin = PluginOrigin.Internal, version = version))
// frame plugin
plugins.add(PluginDescriptor(FramePlugin(), origin = PluginOrigin.Internal, version = version))

View File

@@ -23,6 +23,7 @@ open class BasicTerminalOption() : JPanel(BorderLayout()), Option {
var showCharsetComboBox: Boolean = false
var showStartupCommandTextField: Boolean = false
var showHeartbeatIntervalTextField: Boolean = false
var showTimeoutTextField: Boolean = false
var showEnvironmentTextArea: Boolean = false
var showLoginScripts: Boolean = false
var showBackspaceComboBox: Boolean = false
@@ -33,7 +34,8 @@ open class BasicTerminalOption() : JPanel(BorderLayout()), Option {
val charsetComboBox = JComboBox<String>()
val startupCommandTextField = OutlineTextField()
val heartbeatIntervalTextField = IntSpinner(30, minimum = 3, maximum = Int.MAX_VALUE)
val heartbeatIntervalTextField = IntSpinner(60, minimum = 3, maximum = Int.MAX_VALUE)
val timeoutTextField = IntSpinner(60, minimum = 10, maximum = Int.MAX_VALUE)
val environmentTextArea = FixedLengthTextArea(2048)
val loginScripts = mutableListOf<LoginScript>()
val backspaceComboBox = JComboBox<Backspace>()
@@ -173,7 +175,7 @@ open class BasicTerminalOption() : JPanel(BorderLayout()), Option {
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
val accountOwner = this.accountOwner
@@ -210,6 +212,11 @@ open class BasicTerminalOption() : JPanel(BorderLayout()), Option {
.add(characterAtATimeTextField).xy(3, rows).apply { rows += step }
}
if (showTimeoutTextField) {
builder.add("${I18n.getString("termora.new-host.terminal.timeout")}:").xy(1, rows)
.add(timeoutTextField).xy(3, rows).apply { rows += step }
}
if (showHeartbeatIntervalTextField) {
builder.add("${I18n.getString("termora.new-host.terminal.heartbeat-interval")}:").xy(1, rows)
.add(heartbeatIntervalTextField).xy(3, rows).apply { rows += step }
@@ -220,14 +227,12 @@ open class BasicTerminalOption() : JPanel(BorderLayout()), Option {
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
}
if (showEnvironmentTextArea) {
builder.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
.apply { rows += step }
}
return builder.build()
}

View File

@@ -82,24 +82,42 @@ internal class RDPProtocolProvider private constructor() : GenericProtocolProvid
}
}
val file = FileUtils.getFile(Application.getTemporaryDir(), randomUUID() + ".rdp")
file.outputStream().use { IOUtils.write(sb.toString(), it, Charsets.UTF_8) }
if (host.authentication.type == AuthenticationType.Password) {
val systemClipboard = windowScope.window.toolkit.systemClipboard
val password = host.authentication.password
systemClipboard.setContents(StringSelection(password), null)
// clear password
swingCoroutineScope.launch(Dispatchers.IO) {
delay(30.seconds)
if (systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
if (systemClipboard.getData(DataFlavor.stringFlavor) == password) {
systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
var ep = StringUtils.EMPTY
if (SystemInfo.isWindows) {
val cmd = "ConvertTo-SecureString '${password}' -AsPlainText -Force | ConvertFrom-SecureString"
val process = ProcessBuilder("powershell.exe", "-Command", cmd).start()
if (process.waitFor() == 0) {
ep = String(process.inputStream.readAllBytes())
}
}
if (ep.isNotBlank()) {
sb.append("password 51:b:").append(ep).appendLine()
}
// 如果获取加密密码失败,那么依然要走剪切板
if (ep.isBlank() || SystemInfo.isMacOS) {
val systemClipboard = windowScope.window.toolkit.systemClipboard
systemClipboard.setContents(StringSelection(password), null)
// clear password
swingCoroutineScope.launch(Dispatchers.IO) {
delay(30.seconds)
if (systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
if (systemClipboard.getData(DataFlavor.stringFlavor) == password) {
systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
}
}
}
}
}
val file = FileUtils.getFile(Application.getTemporaryDir(), randomUUID() + ".rdp")
file.outputStream().use { IOUtils.write(sb.toString(), it, Charsets.UTF_8) }
if (SystemInfo.isMacOS) {
ProcessBuilder("open", file.absolutePath).start()
} else if (SystemInfo.isWindows) {

View File

@@ -38,6 +38,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
showEnvironmentTextArea = true
showStartupCommandTextField = true
showHeartbeatIntervalTextField = true
showTimeoutTextField = true
showHighlightSet = true
accountOwner = this@SSHHostOptionsPane.accountOwner
init()
@@ -113,6 +114,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
?: AltKeyModifier.EightBit.name),
"keywordHighlightSetId" to ((terminalOption.highlightSetComboBox.selectedItem as? KeywordHighlight)?.id
?: "-1"),
"timeout" to (terminalOption.timeoutTextField.value ?: 60).toString()
)
)
@@ -163,6 +165,10 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
.getOrNull() ?: AltKeyModifier.EightBit
val timeout = host.options.extras["timeout"] ?: "60"
terminalOption.timeoutTextField.value = timeout.toIntOrNull() ?: 60
val keywordHighlightSetId = host.options.extras["keywordHighlightSetId"]
for (i in 0 until terminalOption.highlightSetComboBox.itemCount) {
val item = terminalOption.highlightSetComboBox.getItemAt(i)

View File

@@ -88,7 +88,6 @@ object SshClients {
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
private val timeout = Duration.ofSeconds(30)
private val hostManager get() = HostManager.Companion.getInstance()
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
@@ -101,6 +100,7 @@ object SshClients {
session: ClientSession,
): ChannelShell {
val timeout = Duration.ofSeconds(host.options.extras["timeout"]?.toLongOrNull() ?: 60)
val configuration = PtyChannelConfiguration()
configuration.ptyColumns = size.cols
@@ -136,6 +136,7 @@ object SshClients {
command: String
): Pair<Int, String> {
val timeout = Duration.ofSeconds(60)
val baos = ByteArrayOutputStream()
val channel = session.createExecChannel(command)
channel.out = baos
@@ -248,6 +249,7 @@ object SshClients {
}
}
val timeout = Duration.ofSeconds(host.options.extras["timeout"]?.toLongOrNull() ?: 60)
val session = client.connect(entry).verify(timeout).session
if (host.authentication.type == AuthenticationType.Password) {
if (StringUtils.isNotBlank(host.authentication.password))

View File

@@ -1,134 +0,0 @@
package app.termora.plugin.internal.update
import app.termora.*
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import com.formdev.flatlaf.util.SystemInfo
import com.sun.jna.platform.win32.Advapi32
import com.sun.jna.platform.win32.WinError
import com.sun.jna.platform.win32.WinNT
import com.sun.jna.platform.win32.WinReg
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.JXEditorPane
import org.slf4j.LoggerFactory
import java.awt.Dimension
import java.awt.KeyboardFocusManager
import java.net.URI
import javax.swing.BorderFactory
import javax.swing.JOptionPane
import javax.swing.JScrollPane
import javax.swing.UIManager
import javax.swing.event.HyperlinkEvent
internal class AppUpdateAction private constructor() : AnAction(StringUtils.EMPTY, Icons.ideUpdate) {
companion object {
private val log = LoggerFactory.getLogger(AppUpdateAction::class.java)
fun getInstance(): AppUpdateAction {
return ApplicationScope.forApplicationScope().getOrCreate(AppUpdateAction::class) { AppUpdateAction() }
}
}
private val updaterManager get() = UpdaterManager.getInstance()
init {
isEnabled = false
}
override fun actionPerformed(evt: AnActionEvent) {
showUpdateDialog()
}
private fun showUpdateDialog() {
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
val lastVersion = updaterManager.lastVersion
val editorPane = JXEditorPane()
editorPane.contentType = "text/html"
editorPane.text = lastVersion.htmlBody
editorPane.isEditable = false
editorPane.addHyperlinkListener {
if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) {
Application.browse(it.url.toURI())
}
}
editorPane.background = DynamicColor("window")
val scrollPane = JScrollPane(editorPane)
scrollPane.border = BorderFactory.createEmptyBorder()
scrollPane.preferredSize = Dimension(
UIManager.getInt("Dialog.width") - 100,
UIManager.getInt("Dialog.height") - 100
)
val option = OptionPane.showConfirmDialog(
owner,
scrollPane,
title = I18n.getString("termora.update.title"),
messageType = JOptionPane.PLAIN_MESSAGE,
optionType = JOptionPane.OK_CANCEL_OPTION,
options = arrayOf(
I18n.getString("termora.update.update"),
I18n.getString("termora.cancel")
),
initialValue = I18n.getString("termora.update.update")
)
if (option == JOptionPane.CANCEL_OPTION) {
return
} else if (option == JOptionPane.OK_OPTION) {
updateSelf(lastVersion)
}
}
private fun updateSelf(latestVersion: UpdaterManager.LatestVersion) {
val pkg = Updater.getInstance().getLatestPkg()
if (SystemInfo.isLinux || pkg == null) {
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${latestVersion.version}"))
return
}
val file = pkg.file
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
val commands = if (SystemInfo.isMacOS) listOf("open", "-n", file.absolutePath)
// 如果安装过,那么直接静默安装和自动启动
else if (isAppInstalled()) listOf(
file.absolutePath,
"/SILENT",
"/AUTOSTART",
"/NORESTART",
"/FORCECLOSEAPPLICATIONS"
)
// 没有安装过 则打开安装向导
else listOf(file.absolutePath)
if (log.isInfoEnabled) {
log.info("restart {}", commands.joinToString(StringUtils.SPACE))
}
TermoraRestarter.getInstance().scheduleRestart(owner, true, commands)
}
private fun isAppInstalled(): Boolean {
val keyPath = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${Application.getName()}_is1"
val phkKey = WinReg.HKEYByReference()
// 尝试打开注册表键
val result = Advapi32.INSTANCE.RegOpenKeyEx(
WinReg.HKEY_LOCAL_MACHINE,
keyPath,
0,
WinNT.KEY_READ,
phkKey
)
if (result == WinError.ERROR_SUCCESS) {
// 键存在,关闭句柄
Advapi32.INSTANCE.RegCloseKey(phkKey.getValue())
return true
} else {
// 键不存在或无权限
return false
}
}
}

View File

@@ -1,13 +0,0 @@
package app.termora.plugin.internal.update
import app.termora.ApplicationRunnerExtension
internal class MyApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
companion object {
val instance = MyApplicationRunnerExtension()
}
override fun ready() {
Updater.getInstance().scheduleUpdate()
}
}

View File

@@ -1,166 +0,0 @@
package app.termora.plugin.internal.update
import app.termora.*
import app.termora.Application.httpClient
import com.formdev.flatlaf.util.SystemInfo
import kotlinx.coroutines.*
import okhttp3.Request
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.semver4j.Semver
import org.slf4j.LoggerFactory
import java.io.File
import java.net.ProxySelector
import java.util.*
import java.util.concurrent.TimeUnit
import javax.swing.SwingUtilities
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
internal class Updater private constructor() : Disposable {
companion object {
private val log = LoggerFactory.getLogger(Updater::class.java)
fun getInstance(): Updater {
return ApplicationScope.forApplicationScope().getOrCreate(Updater::class) { Updater() }
}
}
private val updaterManager get() = UpdaterManager.getInstance()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var isRemindMeNextTime = false
private val disabledUpdater get() = Application.getLayout() == AppLayout.Appx
/**
* 安装包位置
*/
private var pkg: LatestPkg? = null
fun scheduleUpdate() {
if (disabledUpdater) {
if (coroutineScope.isActive) {
coroutineScope.cancel()
}
return
}
coroutineScope.launch(Dispatchers.IO) {
// 启动 3 分钟后才是检查
if (Application.isUnknownVersion().not()) {
delay(3.seconds)
}
while (coroutineScope.isActive) {
// 下次提醒我
if (isRemindMeNextTime) break
try {
checkUpdate()
} catch (e: Exception) {
if (log.isWarnEnabled) {
log.warn(e.message, e)
}
}
// 之后每 3 小时检查一次
delay(3.hours.inWholeMilliseconds)
}
}
}
private fun checkUpdate() {
// Windows 应用商店
if (disabledUpdater) return
val latestVersion = updaterManager.fetchLatestVersion()
if (latestVersion.isSelf) {
return
}
// 之所以放到后面检查是不是开发版本,是需要发起一次检测请求,以方便调试
if (Application.isUnknownVersion()) {
return
}
val newVersion = Semver.parse(latestVersion.version) ?: return
val version = Semver.parse(Application.getVersion()) ?: return
if (newVersion <= version) {
return
}
try {
downloadLatestPkg(latestVersion)
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
}
private fun downloadLatestPkg(latestVersion: UpdaterManager.LatestVersion) {
if (SystemInfo.isLinux) return
setLatestPkg(null)
val arch = if (SystemInfo.isAARCH64) "aarch64" else "x86-64"
val osName = if (SystemInfo.isWindows) "windows" else "osx"
val suffix = if (SystemInfo.isWindows) "exe" else "dmg"
val filename = "termora-${latestVersion.version}-${osName}-${arch}.${suffix}"
val asset = latestVersion.assets.find { it.name == filename } ?: return
val response = httpClient
.newBuilder()
.callTimeout(15, TimeUnit.MINUTES)
.readTimeout(15, TimeUnit.MINUTES)
.proxySelector(ProxySelector.getDefault())
.build()
.newCall(Request.Builder().url(asset.downloadUrl).build())
.execute()
if (response.isSuccessful.not()) {
if (log.isErrorEnabled) {
log.warn("Failed to download latest version ${latestVersion.version}, response code ${response.code}")
}
IOUtils.closeQuietly(response)
return
}
val body = response.body
val input = body.byteStream()
val file = FileUtils.getFile(Application.getTemporaryDir(), "${UUID.randomUUID()}-${filename}")
val output = file.outputStream()
val downloaded = runCatching { IOUtils.copy(input, output) }.isSuccess
IOUtils.closeQuietly(input, output, body, response)
if (!downloaded) {
if (log.isErrorEnabled) {
log.error("Failed to download latest version to $filename")
}
return
}
if (log.isInfoEnabled) {
log.info("Successfully downloaded latest version to $file")
}
setLatestPkg(LatestPkg(latestVersion.version, file))
}
private fun setLatestPkg(pkg: LatestPkg?) {
this.pkg = pkg
SwingUtilities.invokeLater { AppUpdateAction.getInstance().isEnabled = pkg != null }
}
fun getLatestPkg(): LatestPkg? {
return pkg
}
data class LatestPkg(val version: String, val file: File)
}

View File

@@ -0,0 +1,57 @@
package app.termora.plugin.internal.updater
import app.termora.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.semver4j.Semver
import org.slf4j.LoggerFactory
import java.awt.KeyboardFocusManager
import kotlin.time.Duration.Companion.seconds
internal class MyApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
companion object {
val instance = MyApplicationRunnerExtension()
private val log = LoggerFactory.getLogger(MyApplicationRunnerExtension::class.java)
}
private val disabledUpdater get() = Application.getLayout() == AppLayout.Appx
private val updaterManager get() = UpdaterManager.getInstance()
override fun ready() {
swingCoroutineScope.launch {
try {
delay(3.seconds)
scheduleUpdate()
} catch (e: Exception) {
log.error(e.message, e)
}
}
}
private fun scheduleUpdate() {
if (disabledUpdater) return
val latestVersion = updaterManager.fetchLatestVersion()
if (latestVersion.isSelf) {
return
}
val newVersion = Semver.parse(latestVersion.version) ?: return
val version = Semver.parse(Application.getVersion()) ?: return
if (newVersion <= version) {
return
}
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
?: TermoraFrameManager.getInstance().getWindows().firstOrNull()
if (owner == null) return
val dialog = UpdaterDialog(owner, latestVersion)
dialog.isModal = true
dialog.isVisible = true
}
}

View File

@@ -0,0 +1,317 @@
package app.termora.plugin.internal.updater
import app.termora.*
import app.termora.Application.httpClient
import com.formdev.flatlaf.util.SystemInfo
import com.sun.jna.platform.win32.Advapi32
import com.sun.jna.platform.win32.WinError
import com.sun.jna.platform.win32.WinNT
import com.sun.jna.platform.win32.WinReg
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import okhttp3.Request
import okhttp3.internal.closeQuietly
import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.Strings
import org.apache.commons.lang3.exception.ExceptionUtils
import org.apache.commons.net.io.CopyStreamEvent
import org.apache.commons.net.io.CopyStreamListener
import org.apache.commons.net.io.Util
import org.jdesktop.swingx.JXEditorPane
import org.slf4j.LoggerFactory
import java.awt.Dimension
import java.awt.Window
import java.net.URI
import java.util.*
import java.util.concurrent.Executors
import javax.swing.*
import javax.swing.event.HyperlinkEvent
import kotlin.math.floor
internal class UpdaterDialog(owner: Window, private val latestVersion: UpdaterManager.LatestVersion) :
DialogWrapper(owner) {
companion object {
private val log = LoggerFactory.getLogger(UpdaterDialog::class.java)
}
private enum class State {
Ready,
Downloading,
Downloaded,
}
private val progressBar = JProgressBar()
private val okAction = OkAction(I18n.getString("termora.update.update"))
private val okButton = JButton(okAction)
private var state = State.Ready
private val westSourcePanel = Box.createHorizontalBox()
private val glue = Box.createHorizontalGlue()
private val layout = Application.getLayout()
private val dialog get() = this
private val executorService = Executors.newVirtualThreadPerTaskExecutor()
private val coroutineDispatcher = executorService.asCoroutineDispatcher()
private val coroutineScope = CoroutineScope(coroutineDispatcher)
init {
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
isModal = true
controlsVisible = false
isResizable = false
escapeDispose = false
title = I18n.getString("termora.update.title")
setLocationRelativeTo(owner)
init()
initView()
initEvents()
}
private fun initView() {
progressBar.isIndeterminate = true
progressBar.isStringPainted = true
westSourcePanel.add(glue)
westSourcePanel.add(progressBar)
westSourcePanel.add(Box.createHorizontalStrut(20))
progressBar.isVisible = false
}
private fun initEvents() {
Disposer.register(disposable, object : Disposable {
override fun dispose() {
coroutineScope.cancel()
coroutineDispatcher.close()
executorService.shutdownNow()
}
})
}
override fun createCenterPanel(): JComponent {
val editorPane = JXEditorPane()
editorPane.contentType = "text/html"
editorPane.text = latestVersion.htmlBody
editorPane.isEditable = false
editorPane.addHyperlinkListener {
if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) {
Application.browse(it.url.toURI())
}
}
editorPane.background = DynamicColor("window")
val scrollPane = JScrollPane(editorPane)
scrollPane.border = BorderFactory.createEmptyBorder()
return scrollPane
}
override fun createWestSourcePanel(): JComponent {
return westSourcePanel
}
override fun createActions(): List<AbstractAction> {
return listOf(okAction, createCancelAction())
}
override fun createJButtonForAction(action: Action): JButton {
if (action == okAction) {
rootPane.defaultButton = okButton
return okButton
}
return super.createJButtonForAction(action)
}
@Suppress("CascadeIf")
override fun doOKAction() {
if (state == State.Ready) {
okButton.text = "${okButton.text}..."
okButton.isEnabled = false
progressBar.isVisible = true
glue.isVisible = false
state = State.Downloading
coroutineScope.launch {
try {
downloadPkg()
} catch (_: LatestReleaseException) {
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/latest"))
SwingUtilities.invokeLater { doCancelAction() }
} catch (e: Exception) {
if (log.isErrorEnabled) log.error(e.message, e)
withContext(Dispatchers.Swing) {
state = State.Ready
okButton.isEnabled = true
okButton.text = okAction.name
progressBar.isVisible = false
glue.isVisible = true
OptionPane.showMessageDialog(
dialog,
StringUtils.defaultIfBlank(e.message, ExceptionUtils.getRootCauseMessage(e)).toString(),
messageType = JOptionPane.ERROR_MESSAGE
)
}
} finally {
withContext(Dispatchers.Swing) {
progressBar.isVisible = false
glue.isVisible = true
okButton.isEnabled = true
}
}
}
return
} else if (state == State.Downloading) {
return
} else if (state == State.Downloaded) {
super.doOKAction()
}
}
private suspend fun downloadPkg() {
val arch = if (SystemInfo.isAARCH64) "aarch64" else "x86-64"
val osName = if (SystemInfo.isWindows) "windows" else if (SystemInfo.isLinux) "linux" else "osx"
val suffix = when (layout) {
AppLayout.Zip -> "zip"
AppLayout.Exe -> "exe"
AppLayout.App -> "dmg"
AppLayout.TarGz -> "tar.gz"
AppLayout.AppImage -> "AppImage"
AppLayout.Deb -> "deb"
else -> throw LatestReleaseException()
}
val filename = "termora-${latestVersion.version}-${osName}-${arch}.${suffix}"
val asset = latestVersion.assets.find { it.name == filename } ?: throw LatestReleaseException()
var url = asset.downloadUrl
if (I18n.isChinaMainland()) {
url = Strings.CI.replace(
url,
"https://github.com/TermoraDev/termora/releases/download/",
"https://dl.termora.cn/termora/"
)
}
val response = httpClient.newCall(Request.Builder().url(url).get().build())
.execute()
if (response.isSuccessful.not()) {
response.closeQuietly()
throw IllegalStateException("Failed to download asset $filename")
}
withContext(Dispatchers.Swing) {
progressBar.isIndeterminate = false
}
val listener = object : CopyStreamListener {
override fun bytesTransferred(event: CopyStreamEvent?) {
TODO("Not yet implemented")
}
override fun bytesTransferred(
totalBytesTransferred: Long,
bytesTransferred: Int,
streamSize: Long
) {
SwingUtilities.invokeLater {
val progress = 1.0 * totalBytesTransferred / asset.size * 100
progressBar.value = floor(progress).toInt()
progressBar.string = formatBytes(totalBytesTransferred) + " / " + formatBytes(asset.size)
}
}
}
val file = FileUtils.getFile(Application.getTemporaryDir(), "${UUID.randomUUID()}-${filename}")
response.use {
response.body.byteStream().use { input ->
file.outputStream().use { output ->
Util.copyStream(
input, output, Util.DEFAULT_COPY_BUFFER_SIZE,
asset.size, listener
)
}
}
}
withContext(Dispatchers.Swing) {
state = State.Downloaded
}
val commands = mutableListOf<String>()
if (SystemInfo.isMacOS) {
commands.addAll(listOf("open", "-n", file.absolutePath))
} else if (layout == AppLayout.Zip) {
commands.addAll(listOf("explorer", "/select," + file.absolutePath))
} else if (layout == AppLayout.Exe) {
// 如果安装过,那么直接静默安装和自动启动
if (isAppInstalled()) {
commands.addAll(
listOf(
file.absolutePath,
"/SILENT",
"/AUTOSTART",
"/NORESTART",
"/FORCECLOSEAPPLICATIONS"
)
)
} else {
commands.addAll(listOf(file.absolutePath))
}
} else if (SystemInfo.isLinux) {
commands.addAll(listOf("xdg-open", file.parentFile.absolutePath))
}
if (log.isInfoEnabled) {
log.info("commands: {}", commands.joinToString(StringUtils.SPACE))
}
SwingUtilities.invokeLater {
super.doOKAction()
TermoraRestarter.getInstance().scheduleRestart(owner, true, commands)
}
}
private class LatestReleaseException() : RuntimeException()
private fun isAppInstalled(): Boolean {
try {
val keyPath = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${Application.getName()}_is1"
val phkKey = WinReg.HKEYByReference()
// 尝试打开注册表键
val result = Advapi32.INSTANCE.RegOpenKeyEx(
WinReg.HKEY_LOCAL_MACHINE,
keyPath,
0,
WinNT.KEY_READ,
phkKey
)
if (result == WinError.ERROR_SUCCESS) {
// 键存在,关闭句柄
Advapi32.INSTANCE.RegCloseKey(phkKey.getValue())
return true
} else {
// 键不存在或无权限
return false
}
} catch (_: Exception) {
return false
}
}
override fun addNotify() {
super.addNotify()
}
}

View File

@@ -1,21 +1,22 @@
package app.termora.plugin.internal.update
package app.termora.plugin.internal.updater
import app.termora.ApplicationRunnerExtension
import app.termora.plugin.Extension
import app.termora.plugin.InternalPlugin
internal class UpdatePlugin : InternalPlugin() {
internal class UpdaterPlugin : InternalPlugin() {
init {
support.addExtension(ApplicationRunnerExtension::class.java) { MyApplicationRunnerExtension.instance }
}
override fun getName(): String {
return "Update"
return "Updater"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -430,6 +430,7 @@ internal class TransportPanel(
})
table.addMouseListener(object : MouseAdapter() {
private val sftp get() = DatabaseManager.getInstance().sftp
override fun mouseClicked(e: MouseEvent) {
if (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
var row = table.selectedRow
@@ -438,10 +439,18 @@ internal class TransportPanel(
val attributes = model.getAttributes(row)
if (attributes.isDirectory) {
enterSelectionFolder()
} else if (sftp.dbClickBehavior == "Edit") {
val path = model.getPath(row)
val target = Application.createSubTemporaryDir().resolve(path.name)
val transferId = internalTransferManager.addHighTransfer(path, target)
editTransferListener.addListenTransfer(transferId)
} else {
val paths = listOf(model.getPath(row) to attributes)
if (loader.isOpened() && internalTransferManager.canTransfer(paths.map { it.first })) {
internalTransferManager.addTransfer(paths, InternalTransferManager.TransferMode.Transfer)
internalTransferManager.addTransfer(
paths,
InternalTransferManager.TransferMode.Transfer
)
}
}
} else if (SwingUtilities.isRightMouseButton(e)) {

View File

@@ -52,7 +52,7 @@ class NewHostTree : SimpleTree(), Disposable {
companion object {
private val log = LoggerFactory.getLogger(NewHostTree::class.java)
private val CSV_HEADERS = arrayOf("Folders", "Label", "Hostname", "Port", "Username", "Protocol")
private val CSV_HEADERS = arrayOf("Folders", "Label", "Protocol", "Hostname", "Port", "Username", "Password")
init {
// 基本信息
@@ -577,26 +577,29 @@ class NewHostTree : SimpleTree(), Disposable {
printer.printRecord(
"Projects/Dev",
"Web Server",
SSHProtocolProvider.PROTOCOL,
"192.168.1.1",
"22",
"root",
SSHProtocolProvider.PROTOCOL
StringUtils.EMPTY,
)
printer.printRecord(
"Projects/Prod",
"Web Server",
SSHProtocolProvider.PROTOCOL,
"serverhost.com",
"2222",
"root",
SSHProtocolProvider.PROTOCOL
StringUtils.EMPTY,
)
printer.printRecord(
StringUtils.EMPTY,
"Web Server",
SSHProtocolProvider.PROTOCOL,
"serverhost.com",
"2222",
"user",
SSHProtocolProvider.PROTOCOL
StringUtils.EMPTY,
)
}
OptionPane.openFileInFolder(
@@ -683,10 +686,11 @@ class NewHostTree : SimpleTree(), Disposable {
printer.printRecord(
groups.joinToString("/"),
label,
SSHProtocolProvider.PROTOCOL,
target,
port,
StringUtils.EMPTY,
SSHProtocolProvider.PROTOCOL
StringUtils.EMPTY,
)
}
}
@@ -703,10 +707,11 @@ class NewHostTree : SimpleTree(), Disposable {
printer.printRecord(
StringUtils.EMPTY,
StringUtils.defaultString(entry.host),
SSHProtocolProvider.PROTOCOL,
StringUtils.defaultString(entry.hostName),
if (entry.port == 0) 22 else entry.port,
StringUtils.defaultString(entry.username),
SSHProtocolProvider.PROTOCOL
StringUtils.EMPTY,
)
}
}
@@ -746,10 +751,11 @@ class NewHostTree : SimpleTree(), Disposable {
printer.printRecord(
folders.joinToString("/"),
label,
SSHProtocolProvider.PROTOCOL,
hostname,
port.toString(),
username,
SSHProtocolProvider.PROTOCOL
StringUtils.EMPTY,
)
}
}
@@ -776,10 +782,11 @@ class NewHostTree : SimpleTree(), Disposable {
printer.printRecord(
StringUtils.EMPTY,
label,
SSHProtocolProvider.PROTOCOL,
hostname,
port.toString(),
username,
SSHProtocolProvider.PROTOCOL
StringUtils.EMPTY,
)
}
}
@@ -819,7 +826,15 @@ class NewHostTree : SimpleTree(), Disposable {
if (segments.first() != "#109#0") continue
val hostname = segments.getOrNull(1) ?: StringUtils.EMPTY
val port = segments.getOrNull(2) ?: 22
printer.printRecord(folders, key, hostname, port, StringUtils.EMPTY, SSHProtocolProvider.PROTOCOL)
printer.printRecord(
folders,
key,
SSHProtocolProvider.PROTOCOL,
hostname,
port,
StringUtils.EMPTY,
StringUtils.EMPTY
)
}
}
}
@@ -848,7 +863,15 @@ class NewHostTree : SimpleTree(), Disposable {
val label = file.nameWithoutExtension
val port = ini.get("CONNECTION", "Port")?.toIntOrNull() ?: 22
val username = ini.get("CONNECTION:AUTHENTICATION", "UserName") ?: StringUtils.EMPTY
printer.printRecord(folders, label, hostname, port, username, SSHProtocolProvider.PROTOCOL)
printer.printRecord(
folders,
label,
SSHProtocolProvider.PROTOCOL,
hostname,
port,
username,
StringUtils.EMPTY
)
}
}
@@ -885,10 +908,11 @@ class NewHostTree : SimpleTree(), Disposable {
printer.printRecord(
folders,
StringUtils.defaultIfBlank(label, host),
SSHProtocolProvider.PROTOCOL,
host,
port,
username,
SSHProtocolProvider.PROTOCOL
StringUtils.EMPTY,
)
} catch (e: Exception) {
if (log.isErrorEnabled) {
@@ -940,10 +964,11 @@ class NewHostTree : SimpleTree(), Disposable {
printer.printRecord(
folderNames.joinToString("/"),
StringUtils.defaultIfBlank(title, hostname),
SSHProtocolProvider.PROTOCOL,
hostname,
port,
username,
SSHProtocolProvider.PROTOCOL
StringUtils.EMPTY,
)
}
@@ -975,6 +1000,7 @@ class NewHostTree : SimpleTree(), Disposable {
val hostname = map["Hostname"] ?: StringUtils.EMPTY
val port = map["Port"]?.toIntOrNull() ?: 22
val username = map["Username"] ?: StringUtils.EMPTY
val password = map["Password"] ?: StringUtils.EMPTY
val protocol = map["Protocol"] ?: "SSH"
// 仅支持 SSH、RDP 协议
if (StringUtils.equalsAnyIgnoreCase(protocol, "SSH", "RDP").not()) continue
@@ -1017,6 +1043,15 @@ class NewHostTree : SimpleTree(), Disposable {
)
)
if (password.isNotBlank()) {
n.host = n.host.copy(
authentication = Authentication.No.copy(
type = AuthenticationType.Password,
password = password,
)
)
}
if (p == null) {
nodes.add(n)
} else {

View File

@@ -115,6 +115,7 @@ termora.settings.keymap.already-exists=The shortcut [{0}] is already in use by [
termora.settings.sftp.edit-command=Edit Command
termora.settings.sftp.db-click-behavior=Double-click
termora.settings.sftp.fixed-tab=Fixed tab
termora.settings.sftp.default-directory=Default Directory
termora.settings.sftp.preserve-time=Preserve original file modification time
@@ -182,6 +183,7 @@ termora.new-host.terminal.encoding=Encoding
termora.new-host.terminal.backspace=Backspace
termora.new-host.terminal.character-mode=Character-at-a-time
termora.new-host.terminal.heartbeat-interval=Heartbeat Interval
termora.new-host.terminal.timeout=Timeout
termora.new-host.terminal.startup-commands=Startup Command
termora.new-host.terminal.alt-modifier=Alt modifier
termora.new-host.terminal.alt-modifier.eight-bit=8-bit characters

View File

@@ -101,6 +101,7 @@ termora.settings.keymap.already-exists=Комбинация [{0}] уже исп
termora.settings.sftp.edit-command=Редактировать команду
termora.settings.sftp.db-click-behavior=двойной щелчок
termora.settings.sftp.fixed-tab=Отображать вкладку
termora.settings.sftp.default-directory=Директория по умолчанию
termora.settings.sftp.preserve-time=Сохранять исходное время модификации файла
@@ -162,6 +163,7 @@ termora.new-host.proxy=Прокси
termora.new-host.terminal=${termora.settings.terminal}
termora.new-host.terminal.encoding=Кодировка
termora.new-host.terminal.heartbeat-interval=Интервал опроса
termora.new-host.terminal.timeout=Тайм-аут
termora.new-host.terminal.startup-commands=Команда запуска
termora.new-host.terminal.env=переменные

View File

@@ -127,6 +127,7 @@ termora.settings.keymap.already-exists=快捷键 [{0}] 已经被 [{1}] 占用
termora.settings.sftp.edit-command=编辑命令
termora.settings.sftp.db-click-behavior=双击行为
termora.settings.sftp.fixed-tab=固定标签
termora.settings.sftp.default-directory=默认目录
termora.settings.sftp.preserve-time=保留原始文件修改时间
@@ -175,6 +176,7 @@ termora.new-host.terminal.encoding=编码
termora.new-host.terminal.backspace=退格键
termora.new-host.terminal.character-mode=单字符模式
termora.new-host.terminal.heartbeat-interval=心跳间隔
termora.new-host.terminal.timeout=超时时间
termora.new-host.terminal.startup-commands=启动命令
termora.new-host.terminal.alt-modifier=Alt 键修饰
termora.new-host.terminal.alt-modifier.eight-bit=8 位字符

View File

@@ -56,6 +56,7 @@ termora.settings.keymap.action=操作
termora.settings.keymap.already-exists=快捷鍵 [{0}] 已經被 [{1}] 占用
termora.settings.sftp.edit-command=編輯命令
termora.settings.sftp.db-click-behavior=按兩下行為
termora.settings.sftp.fixed-tab=固定標籤
termora.settings.sftp.default-directory=預設目錄
termora.settings.sftp.preserve-time=保留原始文件修改時間
@@ -178,6 +179,7 @@ termora.new-host.terminal.alt-modifier=Alt 鍵修飾
termora.new-host.terminal.alt-modifier.eight-bit=8 位元字符
termora.new-host.terminal.alt-modifier.by-esc=ESC 鍵作為前綴
termora.new-host.terminal.heartbeat-interval=心跳間隔
termora.new-host.terminal.timeout=超時時間
termora.new-host.terminal.env=環境
termora.new-host.terminal.login-scripts=登入腳本
termora.new-host.terminal.expect=預期