mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
78 Commits
2.0.0-beta
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8a165d715 | ||
|
|
ca757f975a | ||
|
|
79c304ae3d | ||
|
|
1848c869e7 | ||
|
|
029e570551 | ||
|
|
905c570e4c | ||
|
|
a3069229b8 | ||
|
|
1e930d61c9 | ||
|
|
0015c3a7fb | ||
|
|
4bfb87e5c7 | ||
|
|
4fbb626c42 | ||
|
|
35b175d944 | ||
|
|
5939297550 | ||
|
|
e6e5867742 | ||
|
|
bd9b73ad6a | ||
|
|
dbea769994 | ||
|
|
9cd83c4025 | ||
|
|
d4cc080e7b | ||
|
|
a324bc3d96 | ||
|
|
36929e9ea3 | ||
|
|
dd73b933d9 | ||
|
|
117a9ea692 | ||
|
|
2f932de295 | ||
|
|
679b24a74d | ||
|
|
c6b33ea828 | ||
|
|
a4ea8f2491 | ||
|
|
1c2315b5e9 | ||
|
|
d48e412580 | ||
|
|
3b3fb41384 | ||
|
|
190ac697fb | ||
|
|
8cdbf24cdc | ||
|
|
6e182b6813 | ||
|
|
3fa4064655 | ||
|
|
a77a03d8b3 | ||
|
|
5f8b9d36e2 | ||
|
|
1ed5e164de | ||
|
|
c67d5b0276 | ||
|
|
9646a98f6d | ||
|
|
aee34415a7 | ||
|
|
e4e70cc72c | ||
|
|
49779fe8f2 | ||
|
|
969ddc3662 | ||
|
|
de9b418c75 | ||
|
|
f8588745cd | ||
|
|
7c0cbab187 | ||
|
|
176fa64de0 | ||
|
|
495ab69195 | ||
|
|
93c28242fb | ||
|
|
57662f717b | ||
|
|
3669bd1f88 | ||
|
|
00e695b7d5 | ||
|
|
02c92e6019 | ||
|
|
8ba74f0846 | ||
|
|
79ed6d3858 | ||
|
|
8a66606275 | ||
|
|
3ebdf73fbf | ||
|
|
d249e5da5a | ||
|
|
7243e933e6 | ||
|
|
f92e43ee41 | ||
|
|
f6243e33da | ||
|
|
72f334d572 | ||
|
|
68cbb10dec | ||
|
|
45f5c4ee91 | ||
|
|
48c511613e | ||
|
|
c94063d459 | ||
|
|
c26aafd831 | ||
|
|
a5638329e7 | ||
|
|
8323f8eb5d | ||
|
|
35199ed094 | ||
|
|
b5d53cf416 | ||
|
|
39e26a6e3d | ||
|
|
15cb06af0f | ||
|
|
1e0bbb5a00 | ||
|
|
fb6fdbc14c | ||
|
|
96df53ce40 | ||
|
|
42f86dc3a3 | ||
|
|
32b11c6063 | ||
|
|
b2f43ba439 |
11
.github/workflows/osx.yml
vendored
11
.github/workflows/osx.yml
vendored
@@ -81,6 +81,10 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||
|
||||
- name: Install create-dmg
|
||||
shell: bash
|
||||
run: brew install create-dmg
|
||||
|
||||
- name: Compile
|
||||
shell: bash
|
||||
run: ./gradlew :check-license && ./gradlew classes -x test
|
||||
@@ -93,13 +97,6 @@ jobs:
|
||||
shell: bash
|
||||
run: ./gradlew :jpackage && ./gradlew :dist
|
||||
|
||||
- name: Upload zip artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: termora-osx-zip-${{ runner.arch }}
|
||||
path: |
|
||||
build/distributions/*.zip
|
||||
|
||||
- name: Upload dmg artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -82,6 +82,7 @@ Termora is developed using [**Kotlin/JVM**](https://kotlinlang.org/) and partial
|
||||
- 🧾 [Latest Release](https://github.com/TermoraDev/termora/releases/latest)
|
||||
- 🍺 **Homebrew**: `brew install --cask termora`
|
||||
- 🔨 **WinGet**: `winget install termora`
|
||||
- <img src="https://apps.microsoft.com/assets/icons/logo-16x16.png" alt="microsoft logo"/> <b>Microsoft Store</b>: <a href="https://apps.microsoft.com/store/detail/9NRZBHG43SB9?cid=DevShareMCLPCS">Visit Termora in the Microsoft Store</a>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ Termora 使用 [**Kotlin/JVM**](https://kotlinlang.org/) 开发,支持(正
|
||||
- 🧾 [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||
- 🍺 **Homebrew**:`brew install --cask termora`
|
||||
- 🪟 **WinGet**:`winget install termora`
|
||||
- <img src="https://apps.microsoft.com/assets/icons/logo-16x16.png" alt="microsoft logo"/> <b>Microsoft Store</b>: <a href="https://apps.microsoft.com/store/detail/9NRZBHG43SB9?cid=DevShareMCLPCS">Termora</a>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ val appVersion = project.version.toString().split("-")[0]
|
||||
val makeAppx = if (os.isWindows) StringUtils.defaultString(System.getenv("MAKEAPPX_PATH")) else StringUtils.EMPTY
|
||||
val isDeb = os.isLinux && System.getenv("TERMORA_TYPE") == "deb"
|
||||
val isAppx = os.isWindows && makeAppx.isNotBlank() && System.getenv("TERMORA_TYPE") == "appx"
|
||||
val isBeta = project.version.toString().contains("beta", ignoreCase = true)
|
||||
|
||||
// macOS 签名信息
|
||||
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
||||
@@ -173,10 +174,12 @@ publishing {
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
val betaVersion = project.version.toString().substringAfterLast('.')
|
||||
filesMatching("**/AppxManifest.xml") {
|
||||
filter<ReplaceTokens>(
|
||||
"tokens" to mapOf(
|
||||
"version" to appVersion,
|
||||
"betaVersion" to if (isBeta) betaVersion else "0",
|
||||
"architecture" to if (arch.isArm64) "arm64" else "x64",
|
||||
"projectDir" to project.projectDir.absolutePath,
|
||||
)
|
||||
@@ -229,9 +232,11 @@ 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")
|
||||
FileUtils.forceMkdir(targetDir)
|
||||
val osName = if (os.isWindows) "win32" else if (os.isMacOsX) "darwin" else "linux"
|
||||
val myArchName = if (arch.isArm) "aarch64" else "x86-64"
|
||||
val targetDir = if (os.isMacOsX) FileUtils.getFile(dylib, pty4j.name, osName)
|
||||
else FileUtils.getFile(dylib, pty4j.name, osName, myArchName)
|
||||
FileUtils.forceMkdir(targetDir)
|
||||
if (os.isWindows) {
|
||||
// @formatter:off
|
||||
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/*win/${myArchName}/*", "-d", targetDir.absolutePath) }
|
||||
@@ -379,6 +384,7 @@ tasks.register<Exec>("jpackage") {
|
||||
}
|
||||
|
||||
if (os.isLinux) {
|
||||
options.add("--add-opens=java.desktop/sun.awt.X11=ALL-UNNAMED")
|
||||
if (isDeb) {
|
||||
options.add("-Djpackage.app-layout=deb")
|
||||
}
|
||||
@@ -398,18 +404,6 @@ tasks.register<Exec>("jpackage") {
|
||||
arguments.addAll(listOf("--copyright", "TermoraDev"))
|
||||
arguments.addAll(listOf("--app-content", "$buildDir/plugins"))
|
||||
|
||||
if (os.isWindows) {
|
||||
arguments.addAll(
|
||||
listOf(
|
||||
"--description",
|
||||
"${project.name.uppercaseFirstChar()}: A terminal emulator and SSH client"
|
||||
)
|
||||
)
|
||||
} else {
|
||||
arguments.addAll(listOf("--description", "A terminal emulator and SSH client."))
|
||||
}
|
||||
|
||||
|
||||
if (os.isMacOsX) {
|
||||
arguments.addAll(listOf("--mac-package-name", project.name.uppercaseFirstChar()))
|
||||
arguments.addAll(listOf("--mac-app-category", "developer-tools"))
|
||||
@@ -441,7 +435,7 @@ tasks.register<Exec>("jpackage") {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
if (os.isMacOsX && macOSSign) {
|
||||
if (macOSSign) {
|
||||
arguments.add("--mac-sign")
|
||||
arguments.add("--mac-signing-key-user-name")
|
||||
arguments.add(macOSSignUsername)
|
||||
@@ -548,7 +542,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 +661,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
|
||||
}
|
||||
@@ -667,17 +670,24 @@ fun packOnLinux(distributionDir: Directory, finalFilenameWithoutExtension: Strin
|
||||
exec { commandLine("chmod", "+x", appimagetool.absolutePath) }
|
||||
}
|
||||
|
||||
|
||||
// Desktop file
|
||||
val termoraName = project.name.uppercaseFirstChar()
|
||||
|
||||
// copy icon
|
||||
FileUtils.copyFile(
|
||||
File("${projectDir.absolutePath}/src/main/resources/icons/termora_256x256.png"),
|
||||
distributionDir.file(termoraName + File.separator + termoraName + ".png").asFile
|
||||
)
|
||||
|
||||
val desktopFile = distributionDir.file(termoraName + File.separator + termoraName + ".desktop").asFile
|
||||
desktopFile.writeText(
|
||||
"""[Desktop Entry]
|
||||
Type=Application
|
||||
Name=${termoraName}
|
||||
Comment=Terminal emulator and SSH client
|
||||
Icon=/lib/${termoraName}
|
||||
Icon=${termoraName}
|
||||
Categories=Development;
|
||||
StartupWMClass=${termoraName}
|
||||
Terminal=false
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
[versions]
|
||||
kotlin = "2.2.0"
|
||||
kotlin = "2.2.10"
|
||||
slf4j = "2.0.17"
|
||||
pty4j = "0.13.10"
|
||||
pty4j = "0.13.11"
|
||||
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-net = "3.11.1"
|
||||
commons-text = "1.13.1"
|
||||
commons-compress = "1.27.1"
|
||||
commons-csv = "1.14.1"
|
||||
commons-net = "3.12.0"
|
||||
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,15 +20,15 @@ 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"
|
||||
hutool = "5.8.40"
|
||||
jsch = "2.27.3"
|
||||
okhttp = "5.1.0"
|
||||
sshj = "0.39.0"
|
||||
sshd-core = "2.15.0"
|
||||
jgit = "7.2.0.202503040940-r"
|
||||
commonmark = "0.25.0"
|
||||
commonmark = "0.25.1"
|
||||
jnafilechooser = "1.1.2"
|
||||
xodus = "2.0.1"
|
||||
bip39 = "1.0.9"
|
||||
@@ -41,9 +41,9 @@ jSerialComm = "2.11.2"
|
||||
ini4j = "0.5.5-2"
|
||||
restart4j = "0.0.1"
|
||||
eddsa = "0.3.0"
|
||||
exposed = "1.0.0-beta-4"
|
||||
exposed = "1.0.0-beta-5"
|
||||
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"
|
||||
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.5"
|
||||
project.version = "0.0.6"
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,4 +18,8 @@ object Appearance {
|
||||
set(value) {
|
||||
enableManager.setFlag("Plugins.bg.interval", value)
|
||||
}
|
||||
|
||||
var fillMode: String
|
||||
get() = enableManager.getFlag("Plugins.bg.fillMode", FillMode.STRETCH.name)
|
||||
set(value) = enableManager.setFlag("Plugins.bg.fillMode", value)
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package app.termora.plugins.bg
|
||||
|
||||
import app.termora.GlassPaneExtension
|
||||
import app.termora.WindowScope
|
||||
import app.termora.restore
|
||||
import app.termora.save
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import java.awt.AlphaComposite
|
||||
import java.awt.Graphics2D
|
||||
@@ -12,15 +14,52 @@ class BGGlassPaneExtension private constructor() : GlassPaneExtension {
|
||||
val instance = BGGlassPaneExtension()
|
||||
}
|
||||
|
||||
|
||||
override fun paint(scope: WindowScope, c: JComponent, g2d: Graphics2D) {
|
||||
|
||||
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
|
||||
g2d.save()
|
||||
g2d.composite = AlphaComposite.getInstance(
|
||||
AlphaComposite.SRC_OVER,
|
||||
if (FlatLaf.isLafDark()) 0.2f else 0.1f
|
||||
)
|
||||
g2d.drawImage(img, 0, 0, c.width, c.height, null)
|
||||
g2d.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER)
|
||||
|
||||
when (Appearance.fillMode) {
|
||||
FillMode.STRETCH.name -> {
|
||||
g2d.drawImage(img, 0, 0, c.width, c.height, null)
|
||||
}
|
||||
|
||||
FillMode.CENTER.name -> {
|
||||
val x = (c.width - img.width) / 2
|
||||
val y = (c.height - img.height) / 2
|
||||
g2d.drawImage(img, x, y, null)
|
||||
}
|
||||
|
||||
FillMode.TILE.name -> {
|
||||
val iw = img.width
|
||||
val ih = img.height
|
||||
var y = 0
|
||||
while (y < c.height) {
|
||||
var x = 0
|
||||
while (x < c.width) {
|
||||
g2d.drawImage(img, x, y, null)
|
||||
x += iw
|
||||
}
|
||||
y += ih
|
||||
}
|
||||
}
|
||||
|
||||
FillMode.FIT.name -> {
|
||||
val scale = maxOf(c.width.toDouble() / img.width, c.height.toDouble() / img.height)
|
||||
val newW = (img.width * scale).toInt()
|
||||
val newH = (img.height * scale).toInt()
|
||||
val x = (c.width - newW) / 2
|
||||
val y = (c.height - newH) / 2
|
||||
g2d.drawImage(img, x, y, newW, newH, null)
|
||||
}
|
||||
}
|
||||
|
||||
g2d.restore()
|
||||
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.event.ItemEvent
|
||||
import java.io.File
|
||||
import java.nio.file.StandardCopyOption
|
||||
import javax.swing.*
|
||||
@@ -23,6 +25,7 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||
|
||||
val backgroundImageTextField = OutlineTextField()
|
||||
val fillModeComboBox = OutlineComboBox<FillMode>()
|
||||
val intervalSpinner = NumberSpinner(360, minimum = 30, maximum = 86400)
|
||||
|
||||
private val backgroundButton = JButton(Icons.folder)
|
||||
@@ -36,6 +39,38 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
||||
|
||||
private fun initView() {
|
||||
|
||||
fillModeComboBox.addItem(FillMode.STRETCH)
|
||||
fillModeComboBox.addItem(FillMode.FIT)
|
||||
fillModeComboBox.addItem(FillMode.CENTER)
|
||||
fillModeComboBox.addItem(FillMode.TILE)
|
||||
|
||||
fillModeComboBox.selectedItem = runCatching { FillMode.valueOf(Appearance.fillMode) }
|
||||
.getOrNull() ?: FillMode.STRETCH
|
||||
|
||||
fillModeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component? {
|
||||
var text = value?.toString()
|
||||
|
||||
if (value == FillMode.STRETCH) {
|
||||
text = BGI18n.getString("termora.plugins.bg.fill-mode.stretch")
|
||||
} else if (value == FillMode.FIT) {
|
||||
text = BGI18n.getString("termora.plugins.bg.fill-mode.fit")
|
||||
} else if (value == FillMode.CENTER) {
|
||||
text = BGI18n.getString("termora.plugins.bg.fill-mode.center")
|
||||
} else if (value == FillMode.TILE) {
|
||||
text = BGI18n.getString("termora.plugins.bg.fill-mode.tile")
|
||||
}
|
||||
|
||||
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
||||
}
|
||||
}
|
||||
|
||||
backgroundImageTextField.isEditable = false
|
||||
backgroundImageTextField.trailingComponent = backgroundButton
|
||||
backgroundImageTextField.text = Appearance.backgroundImage
|
||||
@@ -80,6 +115,15 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
||||
Appearance.interval = value
|
||||
}
|
||||
}
|
||||
|
||||
fillModeComboBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
Appearance.fillMode = fillModeComboBox.selectedItem?.toString() ?: FillMode.STRETCH.name
|
||||
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
||||
SwingUtilities.invokeLater { SwingUtilities.updateComponentTreeUI(frame) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSelectedBackgroundImage(file: File) {
|
||||
@@ -124,7 +168,7 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
||||
private fun getFormPanel(): JPanel {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default",
|
||||
"pref, $FORM_MARGIN, pref"
|
||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
|
||||
var rows = 1
|
||||
@@ -138,6 +182,10 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
||||
.add(bgClearBox).xy(5, rows)
|
||||
.apply { rows += step }
|
||||
|
||||
builder.add("${BGI18n.getString("termora.plugins.bg.fill-mode")}:").xy(1, rows)
|
||||
.add(fillModeComboBox).xy(3, rows)
|
||||
.apply { rows += step }
|
||||
|
||||
builder.add("${BGI18n.getString("termora.plugins.bg.interval")}:").xy(1, rows)
|
||||
.add(intervalSpinner).xy(3, rows)
|
||||
.apply { rows += step }
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package app.termora.plugins.bg
|
||||
|
||||
enum class FillMode {
|
||||
STRETCH, // 拉伸
|
||||
FIT, // 等比例铺满
|
||||
CENTER, // 居中
|
||||
TILE, // 平铺
|
||||
}
|
||||
@@ -1,2 +1,7 @@
|
||||
termora.plugins.bg.interval=Interval
|
||||
termora.plugins.bg.fill-mode=Fill Mode
|
||||
termora.plugins.bg.fill-mode.stretch=Stretch
|
||||
termora.plugins.bg.fill-mode.fit=Fit
|
||||
termora.plugins.bg.fill-mode.center=Center
|
||||
termora.plugins.bg.fill-mode.tile=Tile
|
||||
termora.plugins.bg.background-image=Background Image
|
||||
|
||||
@@ -1,2 +1,8 @@
|
||||
termora.plugins.bg.background-image=背景图
|
||||
termora.plugins.bg.interval=切换间隔
|
||||
|
||||
termora.plugins.bg.fill-mode=填充模式
|
||||
termora.plugins.bg.fill-mode.stretch=拉伸
|
||||
termora.plugins.bg.fill-mode.fit=适合
|
||||
termora.plugins.bg.fill-mode.center=居中
|
||||
termora.plugins.bg.fill-mode.tile=平铺
|
||||
|
||||
@@ -1,2 +1,8 @@
|
||||
termora.plugins.bg.background-image=背景圖
|
||||
termora.plugins.bg.interval=切換間隔
|
||||
|
||||
termora.plugins.bg.fill-mode=填充模式
|
||||
termora.plugins.bg.fill-mode.stretch=拉伸
|
||||
termora.plugins.bg.fill-mode.fit=適合
|
||||
termora.plugins.bg.fill-mode.center=居中
|
||||
termora.plugins.bg.fill-mode.tile=平鋪
|
||||
|
||||
@@ -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.251")
|
||||
compileOnly(project(":"))
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
|
||||
|
||||
|
||||
project.version = "0.0.6"
|
||||
project.version = "0.0.7"
|
||||
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
package app.termora.plugins.editor
|
||||
|
||||
import app.termora.DialogWrapper
|
||||
import app.termora.Disposable
|
||||
import app.termora.Disposer
|
||||
import app.termora.OptionPane
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.UIManager
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.name
|
||||
|
||||
|
||||
class EditorDialog(file: Path, owner: Window, private val myDisposable: Disposable) : DialogWrapper(null) {
|
||||
|
||||
private val filename = file.name
|
||||
private val filepath = File(file.absolutePathString())
|
||||
private val editorPanel = EditorPanel(this, filepath)
|
||||
private val disposed = AtomicBoolean()
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||
isModal = false
|
||||
controlsVisible = true
|
||||
isResizable = true
|
||||
title = filename
|
||||
iconImages = owner.iconImages
|
||||
escapeDispose = false
|
||||
defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
||||
|
||||
initEvents()
|
||||
|
||||
setLocationRelativeTo(owner)
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosing(e: WindowEvent?) {
|
||||
if (disposed.compareAndSet(false, true)) {
|
||||
doCancelAction()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Disposer.register(myDisposable, object : Disposable {
|
||||
override fun dispose() {
|
||||
if (disposed.compareAndSet(false, true)) {
|
||||
doCancelAction()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Disposer.register(disposable, object : Disposable {
|
||||
override fun dispose() {
|
||||
if (disposed.compareAndSet(false, true)) {
|
||||
Disposer.dispose(myDisposable)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun doCancelAction() {
|
||||
if (editorPanel.changes()) {
|
||||
if (OptionPane.showConfirmDialog(
|
||||
this,
|
||||
"文件尚未保存,你确定要退出吗?",
|
||||
optionType = JOptionPane.OK_CANCEL_OPTION,
|
||||
) != JOptionPane.OK_OPTION
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
return editorPanel
|
||||
}
|
||||
|
||||
override fun createSouthPanel(): JComponent? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package app.termora.plugins.editor
|
||||
|
||||
import app.termora.Disposable
|
||||
import app.termora.Disposer
|
||||
import app.termora.EnableManager
|
||||
import app.termora.OptionPane
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.swing.JFrame
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.UIManager
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.name
|
||||
import kotlin.math.max
|
||||
|
||||
class EditorFrame(private val file: Path, private val owner: Window, private val disposable: Disposable) : JFrame() {
|
||||
private val enableManager get() = EnableManager.getInstance()
|
||||
private val disposed = AtomicBoolean()
|
||||
private val filepath = File(file.absolutePathString())
|
||||
private val frame get() = this
|
||||
private val editorPanel = EditorPanel(this, filepath)
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvent()
|
||||
}
|
||||
|
||||
private fun initEvent() {
|
||||
|
||||
Disposer.register(disposable, object : Disposable {
|
||||
override fun dispose() {
|
||||
if (disposed.compareAndSet(false, true)) frame.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
if (disposed.compareAndSet(false, true)) Disposer.dispose(disposable)
|
||||
enableManager.setFlag("Plugins.editor.dialog.width", width)
|
||||
enableManager.setFlag("Plugins.editor.dialog.height", height)
|
||||
enableManager.setFlag("Plugins.editor.dialog.extendedState", extendedState)
|
||||
}
|
||||
|
||||
override fun windowClosing(e: WindowEvent?) {
|
||||
if (editorPanel.changes()) {
|
||||
if (OptionPane.showConfirmDialog(
|
||||
frame,
|
||||
EditorI18n.getString("termora.plugins.editor.not-save"),
|
||||
optionType = JOptionPane.OK_CANCEL_OPTION,
|
||||
) == JOptionPane.OK_OPTION
|
||||
) {
|
||||
frame.dispose()
|
||||
}
|
||||
} else {
|
||||
frame.dispose()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||
val state = enableManager.getFlag("Plugins.editor.dialog.extendedState", 0)
|
||||
|
||||
if ((state and MAXIMIZED_BOTH) == MAXIMIZED_BOTH) {
|
||||
frame.setLocationRelativeTo(null)
|
||||
frame.extendedState = state
|
||||
} else {
|
||||
val mySize = size
|
||||
mySize.width = max(enableManager.getFlag("Plugins.editor.dialog.width", mySize.width), mySize.width)
|
||||
mySize.height = max(enableManager.getFlag("Plugins.editor.dialog.height", mySize.height), mySize.height)
|
||||
size = mySize
|
||||
setLocationRelativeTo(owner)
|
||||
}
|
||||
|
||||
title = file.name
|
||||
iconImages = owner.iconImages
|
||||
defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
||||
|
||||
rootPane.contentPane.layout = BorderLayout()
|
||||
rootPane.contentPane.add(editorPanel, BorderLayout.CENTER)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package app.termora.plugins.editor
|
||||
|
||||
import app.termora.NamedI18n
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
object EditorI18n : NamedI18n("i18n/messages") {
|
||||
private val log = LoggerFactory.getLogger(EditorI18n::class.java)
|
||||
|
||||
override fun getLogger(): Logger {
|
||||
return log
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ import javax.swing.event.DocumentEvent
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class EditorPanel(private val window: JDialog, private val file: File) : JPanel(BorderLayout()) {
|
||||
class EditorPanel(private val window: JFrame, private val file: File) : JPanel(BorderLayout()) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(EditorPanel::class.java)
|
||||
|
||||
@@ -14,7 +14,7 @@ class MyTransportEditFileExtension private constructor() : TransportEditFileExte
|
||||
|
||||
override fun edit(owner: Window, path: Path): Disposable {
|
||||
val disposable = Disposer.newDisposable()
|
||||
SwingUtilities.invokeLater { EditorDialog(path, owner, disposable).isVisible = true }
|
||||
SwingUtilities.invokeLater { EditorFrame(path, owner, disposable).isVisible = true }
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
termora.plugins.editor.not-save=The file has not been saved. Are you sure you want to exit?
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
termora.plugins.editor.not-save=Файл не сохранён. Вы уверены, что хотите выйти?
|
||||
@@ -0,0 +1 @@
|
||||
termora.plugins.editor.not-save=文件尚未保存,你确定要退出吗?
|
||||
@@ -0,0 +1 @@
|
||||
termora.plugins.editor.not-save=檔案尚未儲存,你確定要退出嗎?
|
||||
@@ -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")
|
||||
implementation("com.maxmind.geoip2:geoip2:4.4.0")
|
||||
// https://github.com/hstyi/geolite2
|
||||
implementation("com.github.hstyi:geolite2:v1.0-202507070058")
|
||||
implementation("com.github.hstyi:geolite2:v1.0-202508180058")
|
||||
}
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
|
||||
@@ -8,7 +8,7 @@ project.version = "0.0.2"
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.5")
|
||||
implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.7")
|
||||
compileOnly(project(":"))
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
|
||||
|
||||
|
||||
project.version = "0.0.4"
|
||||
project.version = "0.0.5"
|
||||
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.Host
|
||||
import app.termora.Icons
|
||||
import app.termora.PtyHostTerminalTab
|
||||
import app.termora.WindowScope
|
||||
import app.termora.*
|
||||
import app.termora.terminal.PtyConnector
|
||||
import org.apache.commons.io.Charsets
|
||||
import java.nio.charset.StandardCharsets
|
||||
@@ -11,6 +8,8 @@ import javax.swing.Icon
|
||||
|
||||
class SerialTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
PtyHostTerminalTab(windowScope, host) {
|
||||
|
||||
|
||||
override suspend fun openPtyConnector(): PtyConnector {
|
||||
val serialPort = Serials.openPort(host)
|
||||
return SerialPortPtyConnector(
|
||||
@@ -19,6 +18,10 @@ class SerialTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
)
|
||||
}
|
||||
|
||||
override fun createReconnectTerminalTab(): TerminalTab {
|
||||
return SerialTerminalTab(windowScope, host)
|
||||
}
|
||||
|
||||
override fun getIcon(): Icon {
|
||||
return Icons.plugin
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.3"
|
||||
project.version = "0.0.4"
|
||||
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -2,13 +2,6 @@ termora.plugins.sync.disabled-sync=You are already logged in and cannot use this
|
||||
|
||||
termora.settings.sync=Sync
|
||||
termora.settings.sync.done=Synchronized data successfully
|
||||
termora.settings.sync.export=${termora.keymgr.export}
|
||||
termora.settings.sync.import=${termora.keymgr.import}
|
||||
termora.settings.sync.import.file-too-large=The file is too large
|
||||
termora.settings.sync.import.successful=Import data successfully
|
||||
termora.settings.sync.export-done=The export was successful
|
||||
termora.settings.sync.export-encrypt=Enter password to encrypt file (optional)
|
||||
termora.settings.sync.export-done-open-folder=The export was successful. Do you want to open the folder?
|
||||
termora.settings.sync.range=Range
|
||||
termora.settings.sync.range.keys=My keys
|
||||
termora.settings.sync.range.keyword-highlights=${termora.highlight}
|
||||
|
||||
@@ -2,15 +2,10 @@ termora.plugins.sync.disabled-sync=你已登录,无法使用此功能
|
||||
|
||||
|
||||
termora.settings.sync=同步
|
||||
termora.settings.sync.export-done=导出成功
|
||||
termora.settings.sync.export-encrypt=输入密码加密文件 (可选)
|
||||
termora.settings.sync.export-done-open-folder=导出成功,是否需要打开所在文件夹?
|
||||
termora.settings.sync.range=范围
|
||||
termora.settings.sync.range.keys=我的密钥
|
||||
termora.settings.sync.last-sync-time=最后同步时间
|
||||
termora.settings.sync.done=同步数据成功
|
||||
termora.settings.sync.import.file-too-large=文件太大
|
||||
termora.settings.sync.import.successful=导入数据成功
|
||||
termora.settings.sync.gist=片段
|
||||
termora.settings.sync.token=令牌
|
||||
termora.settings.sync.type=类型
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
termora.plugins.sync.disabled-sync=你已登錄,無法使用此功能
|
||||
|
||||
termora.settings.sync=同步
|
||||
termora.settings.sync.export-done=匯出成功
|
||||
termora.settings.sync.export-encrypt=輸入密碼加密檔案 (可選)
|
||||
termora.settings.sync.export-done-open-folder=匯出成功,是否需要打開所在資料夾?
|
||||
termora.settings.sync.range=範圍
|
||||
termora.settings.sync.range.keys=我的密鑰
|
||||
termora.settings.sync.last-sync-time=最後同步時間
|
||||
|
||||
@@ -12,6 +12,7 @@ enum class AppLayout {
|
||||
* macOS
|
||||
*/
|
||||
App,
|
||||
AppStore,
|
||||
|
||||
/**
|
||||
* Linux
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class ApplePressAndHoldEnabledApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
|
||||
companion object {
|
||||
val instance = ApplePressAndHoldEnabledApplicationRunnerExtension()
|
||||
}
|
||||
|
||||
override fun ready() {
|
||||
if (SystemInfo.isMacOS.not()) return
|
||||
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
Runtime.getRuntime()
|
||||
.exec(arrayOf("defaults", "write", "app.termora", "ApplePressAndHoldEnabled", "-bool", "false"))
|
||||
.waitFor()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import org.apache.commons.lang3.SystemUtils
|
||||
import org.apache.commons.lang3.math.NumberUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.tinylog.configuration.Configuration
|
||||
import java.awt.Toolkit
|
||||
import java.io.File
|
||||
import kotlin.system.exitProcess
|
||||
import kotlin.system.measureTimeMillis
|
||||
@@ -35,10 +36,20 @@ class ApplicationInitializr {
|
||||
// 检查是否单例
|
||||
checkSingleton()
|
||||
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
if (SystemInfo.isMacOS) {
|
||||
System.setProperty("apple.awt.application.name", Application.getName())
|
||||
}
|
||||
|
||||
if (SystemInfo.isLinux) {
|
||||
// https://stackoverflow.com/questions/10593075
|
||||
runCatching {
|
||||
val toolkit = Toolkit.getDefaultToolkit()
|
||||
val awtAppClassNameField = toolkit.javaClass.getDeclaredField("awtAppClassName")
|
||||
awtAppClassNameField.setAccessible(true)
|
||||
awtAppClassNameField.set(toolkit, Application.getName())
|
||||
}
|
||||
}
|
||||
|
||||
// 启动
|
||||
val runtime = measureTimeMillis { ApplicationRunner().run() }
|
||||
val log = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ internal class FramePlugin : InternalPlugin() {
|
||||
init {
|
||||
support.addExtension(DatabasePropertiesChangedExtension::class.java) { KeymapRefresher.getInstance() }
|
||||
support.addExtension(DatabaseChangedExtension::class.java) { KeymapRefresher.getInstance() }
|
||||
support.addExtension(ApplicationRunnerExtension::class.java) { ApplePressAndHoldEnabledApplicationRunnerExtension.instance }
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
|
||||
@@ -3,14 +3,16 @@ package app.termora
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.terminal.*
|
||||
import app.termora.terminal.ControlCharacters
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.DataListener
|
||||
import app.termora.terminal.Terminal
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.beans.PropertyChangeEvent
|
||||
import java.util.*
|
||||
import javax.swing.Icon
|
||||
|
||||
@@ -27,13 +29,8 @@ abstract class HostTerminalTab(
|
||||
protected val terminalTabbedManager
|
||||
get() = AnActionEvent(getJComponent(), StringUtils.EMPTY, EventObject(getJComponent()))
|
||||
.getData(DataProviders.TerminalTabbedManager)
|
||||
protected val coroutineScope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.Swing) }
|
||||
protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Swing)
|
||||
protected val terminalModel get() = terminal.getTerminalModel()
|
||||
protected var unread = false
|
||||
set(value) {
|
||||
field = value
|
||||
firePropertyChange(PropertyChangeEvent(this, "icon", null, null))
|
||||
}
|
||||
|
||||
|
||||
/* visualTerminal */
|
||||
@@ -45,15 +42,6 @@ abstract class HostTerminalTab(
|
||||
terminal.getTerminalModel().setData(Host, host)
|
||||
terminal.getTerminalModel().addDataListener(object : DataListener {
|
||||
override fun onChanged(key: DataKey<*>, data: Any) {
|
||||
if (key == VisualTerminal.Written) {
|
||||
if (hasFocus || unread) {
|
||||
return
|
||||
}
|
||||
// 如果当前选中的不是这个 Tab,那么设置成未读
|
||||
if (terminalTabbedManager?.getSelectedTerminalTab() != this@HostTerminalTab) {
|
||||
unread = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -75,8 +63,6 @@ abstract class HostTerminalTab(
|
||||
|
||||
override fun onGrabFocus() {
|
||||
super.onGrabFocus()
|
||||
if (!unread) return
|
||||
unread = false
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
||||
@@ -533,7 +533,7 @@ class AuraLaf : FlatPropertiesLaf("Aura", Properties().apply {
|
||||
TerminalColor.Bright.WHITE -> 0xffffff
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0xedecee
|
||||
TerminalColor.Cursor.BACKGROUND -> 0xacacac
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
|
||||
@@ -2,19 +2,21 @@ package app.termora
|
||||
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.actions.SwitchTabAction
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.keymap.KeyShortcut
|
||||
import app.termora.keymap.KeymapManager
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import com.formdev.flatlaf.ui.FlatTabbedPaneUI
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.*
|
||||
import java.awt.event.*
|
||||
import java.awt.image.BufferedImage
|
||||
import java.util.*
|
||||
import javax.swing.ImageIcon
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.*
|
||||
import kotlin.math.abs
|
||||
|
||||
class MyTabbedPane : FlatTabbedPane() {
|
||||
internal class MyTabbedPane : FlatTabbedPane(), Disposable {
|
||||
|
||||
private val dragMouseAdaptor = DragMouseAdaptor()
|
||||
private val terminalTabbedManager
|
||||
@@ -23,6 +25,30 @@ class MyTabbedPane : FlatTabbedPane() {
|
||||
private val owner
|
||||
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||
.getData(DataProviders.TermoraFrame) as TermoraFrame
|
||||
private val keymap get() = KeymapManager.getInstance().getActiveKeymap()
|
||||
private val tabOrder get() = DatabaseManager.getInstance().appearance.tabOrder
|
||||
private val isScreen get() = TermoraLayout.Layout == TermoraLayout.Screen
|
||||
private var isSwitchTabMode = false
|
||||
set(value) {
|
||||
if (tabOrder == TabOrder.Always.name) {
|
||||
if (field.not()) {
|
||||
field = true
|
||||
repaint()
|
||||
}
|
||||
return
|
||||
} else if (tabOrder == TabOrder.Hide.name) {
|
||||
if (field) {
|
||||
field = false
|
||||
repaint()
|
||||
}
|
||||
return
|
||||
} else if (tabOrder == TabOrder.AsNeed.name) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
isFocusable = false
|
||||
@@ -38,6 +64,16 @@ class MyTabbedPane : FlatTabbedPane() {
|
||||
private fun initEvents() {
|
||||
addMouseListener(dragMouseAdaptor)
|
||||
addMouseMotionListener(dragMouseAdaptor)
|
||||
|
||||
val awtEventListener = MyAWTEventListener()
|
||||
toolkit.addAWTEventListener(awtEventListener, AWTEvent.KEY_EVENT_MASK or AWTEvent.WINDOW_EVENT_MASK)
|
||||
|
||||
Disposer.register(this, object : Disposable {
|
||||
override fun dispose() {
|
||||
toolkit.removeAWTEventListener(awtEventListener)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
override fun processMouseEvent(e: MouseEvent) {
|
||||
@@ -70,6 +106,32 @@ class MyTabbedPane : FlatTabbedPane() {
|
||||
firePropertyChange("selectedIndex", oldIndex, index)
|
||||
}
|
||||
|
||||
override fun updateUI() {
|
||||
super.updateUI()
|
||||
setUI(MyMyTabbedPaneUI())
|
||||
}
|
||||
|
||||
private inner class MyAWTEventListener : AWTEventListener {
|
||||
override fun eventDispatched(event: AWTEvent) {
|
||||
if (event is KeyEvent) {
|
||||
if (isSwitchTabMode) isSwitchTabMode = false
|
||||
val shortcuts = keymap.getShortcut(SwitchTabAction.SWITCH_TAB)
|
||||
if (shortcuts.isEmpty()) return
|
||||
val shortcut = shortcuts.first() as KeyShortcut
|
||||
val modifiers = KeyStroke.getKeyStroke(event.keyCode, event.modifiersEx).modifiers
|
||||
if (shortcut.keyStroke.modifiers != modifiers) return
|
||||
if (SwingUtilities.getWindowAncestor(event.component) != owner) return
|
||||
if (isSwitchTabMode.not()) isSwitchTabMode = true
|
||||
} else if (event is WindowEvent) {
|
||||
if (event.id == WindowEvent.WINDOW_LOST_FOCUS || event.id == WindowEvent.WINDOW_DEACTIVATED) {
|
||||
if (isSwitchTabMode) isSwitchTabMode = false
|
||||
} else if (event.id == WindowEvent.WINDOW_GAINED_FOCUS || event.id == WindowEvent.WINDOW_ACTIVATED) {
|
||||
// 触发一次刷新
|
||||
isSwitchTabMode = isSwitchTabMode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class DragMouseAdaptor : MouseAdapter(), KeyEventDispatcher {
|
||||
private var mousePressedPoint = Point()
|
||||
@@ -267,5 +329,81 @@ class MyTabbedPane : FlatTabbedPane() {
|
||||
}
|
||||
}
|
||||
|
||||
private inner class MyMyTabbedPaneUI : FlatTabbedPaneUI() {
|
||||
override fun paintIcon(
|
||||
g: Graphics,
|
||||
tabPlacement: Int,
|
||||
tabIndex: Int,
|
||||
icon: Icon,
|
||||
iconRect: Rectangle?,
|
||||
isSelected: Boolean
|
||||
) {
|
||||
super.paintIcon(g, tabPlacement, tabIndex, MyIcon(icon, tabIndex, isSelected), iconRect, isSelected)
|
||||
}
|
||||
|
||||
|
||||
override fun createMoreTabsButton(): JButton {
|
||||
return MyMoreTabsButton()
|
||||
}
|
||||
|
||||
private inner class MyMoreTabsButton : FlatMoreTabsButton() {
|
||||
override fun createTabMenuItem(tabIndex: Int): JMenuItem? {
|
||||
val item = super.createTabMenuItem(tabIndex)
|
||||
if (tabIndex == 0 && isScreen) {
|
||||
item.text = Application.getName()
|
||||
}
|
||||
return item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun getIconAt(index: Int): Icon? {
|
||||
if (isSwitchTabMode) {
|
||||
return MyIcon(super.getIconAt(index), index, selectedIndex == index)
|
||||
}
|
||||
return super.getIconAt(index)
|
||||
}
|
||||
|
||||
private inner class MyIcon(private val icon: Icon, private val tabIndex: Int, private val isSelected: Boolean) :
|
||||
Icon {
|
||||
override fun paintIcon(c: Component, g: Graphics, x: Int, y: Int) {
|
||||
if (isScreen && tabIndex == 0) {
|
||||
icon.paintIcon(c, g, x, y)
|
||||
return
|
||||
}
|
||||
|
||||
if (isSwitchTabMode.not()) {
|
||||
icon.paintIcon(c, g, x, y)
|
||||
return
|
||||
}
|
||||
|
||||
if (g !is Graphics2D) return
|
||||
|
||||
g.save()
|
||||
setupAntialiasing(g)
|
||||
|
||||
val fm = g.getFontMetrics(g.font)
|
||||
val text = "${tabIndex + 1}"
|
||||
val textWidth = fm.stringWidth(text)
|
||||
val textHeight = fm.ascent
|
||||
|
||||
val centerX = x + (icon.iconWidth - textWidth) / 2
|
||||
val centerY = y + (icon.iconHeight + textHeight) / 2 - 1
|
||||
|
||||
g.color = c.getForeground()
|
||||
g.drawString(text, centerX, centerY)
|
||||
|
||||
g.restore()
|
||||
}
|
||||
|
||||
override fun getIconWidth(): Int {
|
||||
return icon.iconWidth
|
||||
}
|
||||
|
||||
override fun getIconHeight(): Int {
|
||||
return icon.iconHeight
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -173,10 +173,21 @@ abstract class PtyHostTerminalTab(
|
||||
}
|
||||
|
||||
override fun reconnect() {
|
||||
stop()
|
||||
start()
|
||||
val manager = terminalTabbedManager ?: return
|
||||
val index = manager.indexOfTerminalTab(this)
|
||||
if (index < 0) return
|
||||
|
||||
val tab = createReconnectTerminalTab()
|
||||
manager.addTerminalTab(index, tab, true)
|
||||
manager.closeTerminalTab(this, true)
|
||||
|
||||
if (tab is HostTerminalTab) {
|
||||
tab.start()
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun createReconnectTerminalTab(): TerminalTab
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return terminalPanel
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.database.DatabaseManager
|
||||
import com.formdev.flatlaf.util.UIScale
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
@@ -14,7 +15,10 @@ internal class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
||||
private val properties get() = DatabaseManager.getInstance().properties
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||
size = Dimension(
|
||||
UIScale.scale(UIManager.getInt("Dialog.width")),
|
||||
UIScale.scale(UIManager.getInt("Dialog.height"))
|
||||
)
|
||||
isModal = true
|
||||
title = I18n.getString("termora.setting")
|
||||
setLocationRelativeTo(null)
|
||||
|
||||
@@ -21,6 +21,7 @@ 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.WindowUtils
|
||||
import com.sun.jna.platform.win32.Shell32
|
||||
import com.sun.jna.platform.win32.ShlObj
|
||||
import com.sun.jna.platform.win32.WinDef
|
||||
@@ -114,6 +115,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
val languageComboBox = FlatComboBox<String>()
|
||||
val backgroundComBoBox = YesOrNoComboBox()
|
||||
val confirmTabCloseComBoBox = YesOrNoComboBox()
|
||||
val tabOrderComboBox = FlatComboBox<TabOrder>()
|
||||
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
|
||||
val preferredThemeBtn = JButton(Icons.settings)
|
||||
val opacitySpinner = NumberSpinner(100, 0, 100)
|
||||
@@ -128,6 +130,12 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
private fun initView() {
|
||||
|
||||
tabOrderComboBox.addItem(TabOrder.Hide)
|
||||
tabOrderComboBox.addItem(TabOrder.AsNeed)
|
||||
tabOrderComboBox.addItem(TabOrder.Always)
|
||||
tabOrderComboBox.selectedItem = runCatching { TabOrder.valueOf(appearance.tabOrder) }
|
||||
.getOrNull() ?: TabOrder.Hide
|
||||
|
||||
layoutComboBox.addItem(TermoraLayout.Screen)
|
||||
layoutComboBox.addItem(TermoraLayout.Fence)
|
||||
layoutComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
@@ -162,7 +170,8 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
backgroundComBoBox.isEnabled = SystemInfo.isWindows || SystemInfo.isMacOS
|
||||
|
||||
opacitySpinner.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows
|
||||
opacitySpinner.isEnabled = (SystemInfo.isMacOS || SystemInfo.isWindows)
|
||||
|| (SystemInfo.isLinux && WindowUtils.isWindowAlphaSupported())
|
||||
opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) {
|
||||
override fun getNextValue(): Any {
|
||||
return super.getNextValue() ?: maximum
|
||||
@@ -226,6 +235,14 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
})
|
||||
|
||||
tabOrderComboBox.addItemListener(object : ItemListener {
|
||||
override fun itemStateChanged(e: ItemEvent) {
|
||||
if (e.stateChange == ItemEvent.SELECTED) {
|
||||
appearance.tabOrder = tabOrderComboBox.selectedItem?.toString() ?: return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
opacitySpinner.addChangeListener {
|
||||
val opacity = opacitySpinner.value
|
||||
if (opacity is Double) {
|
||||
@@ -349,7 +366,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private fun getFormPanel(): JPanel {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default, default:grow",
|
||||
"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"
|
||||
)
|
||||
val box = FlatToolBar()
|
||||
box.add(followSystemCheckBox)
|
||||
@@ -380,6 +397,9 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
builder.add("${I18n.getString("termora.settings.appearance.background-running")}:").xy(1, rows)
|
||||
.add(backgroundComBoBox).xy(3, rows).apply { rows += step }
|
||||
|
||||
builder.add("${I18n.getString("termora.settings.appearance.tab-order")}:").xy(1, rows)
|
||||
.add(tabOrderComboBox).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))
|
||||
@@ -404,6 +424,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private val fontSizeTextField = IntSpinner(0, 9, 99)
|
||||
private val terminalSetting get() = DatabaseManager.getInstance().terminal
|
||||
private val selectCopyComboBox = YesOrNoComboBox()
|
||||
private val rightClickComboBox = OutlineComboBox<String>()
|
||||
private val autoCloseTabComboBox = YesOrNoComboBox()
|
||||
private val floatingToolbarComboBox = YesOrNoComboBox()
|
||||
private val hyperlinkComboBox = YesOrNoComboBox()
|
||||
@@ -417,6 +438,12 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
rightClickComboBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
terminalSetting.rightClick = rightClickComboBox.selectedItem as String
|
||||
}
|
||||
}
|
||||
|
||||
fallbackFontComboBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
terminalSetting.fallbackFont = fallbackFontComboBox.selectedItem as String
|
||||
@@ -518,6 +545,10 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
fontSizeTextField.value = terminalSetting.fontSize
|
||||
maxRowsTextField.value = terminalSetting.maxRows
|
||||
|
||||
rightClickComboBox.addItem("Copy")
|
||||
rightClickComboBox.addItem("CopyAndPaste")
|
||||
|
||||
rightClickComboBox.selectedItem = terminalSetting.rightClick
|
||||
|
||||
cursorStyleComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
@@ -532,6 +563,24 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
rightClickComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = value?.toString()
|
||||
if (value == "Copy") {
|
||||
text = I18n.getString("termora.settings.terminal.right-click.copy")
|
||||
} else if (value == "CopyAndPaste") {
|
||||
text = I18n.getString("termora.settings.terminal.right-click.copy-and-paste")
|
||||
}
|
||||
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
||||
}
|
||||
}
|
||||
|
||||
cursorStyleComboBox.addItem(CursorStyle.Block)
|
||||
cursorStyleComboBox.addItem(CursorStyle.Bar)
|
||||
cursorStyleComboBox.addItem(CursorStyle.Underline)
|
||||
@@ -595,7 +644,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, left:pref, $FORM_MARGIN, pref, 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, $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, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
|
||||
val beepBtn = JButton(Icons.run)
|
||||
@@ -624,6 +673,8 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
.add(hyperlinkComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.select-copy")}:").xy(1, rows)
|
||||
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.right-click")}:").xy(1, rows)
|
||||
.add(rightClickComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
|
||||
.add(cursorStyleComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.cursor-blink")}:").xy(1, rows)
|
||||
@@ -650,6 +701,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 +751,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 +839,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 +892,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 }
|
||||
|
||||
7
src/main/kotlin/app/termora/TabOrder.kt
Normal file
7
src/main/kotlin/app/termora/TabOrder.kt
Normal file
@@ -0,0 +1,7 @@
|
||||
package app.termora
|
||||
|
||||
internal enum class TabOrder {
|
||||
Hide,
|
||||
AsNeed,
|
||||
Always,
|
||||
}
|
||||
@@ -337,13 +337,7 @@ class TerminalTabbed(
|
||||
val c = tab.getJComponent()
|
||||
val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
|
||||
|
||||
tabbedPane.insertTab(
|
||||
title,
|
||||
tab.getIcon(),
|
||||
c,
|
||||
StringUtils.EMPTY,
|
||||
index
|
||||
)
|
||||
tabbedPane.insertTab(title, tab.getIcon(), c, StringUtils.EMPTY, index)
|
||||
|
||||
// 设置标题
|
||||
c.putClientProperty(titleProperty, title)
|
||||
@@ -367,6 +361,10 @@ class TerminalTabbed(
|
||||
}
|
||||
}
|
||||
|
||||
override fun indexOfTerminalTab(tab: TerminalTab):Int {
|
||||
return tabbedPane.indexOfComponent(tab.getJComponent())
|
||||
}
|
||||
|
||||
private inner class SwitchFindEverywhereResult(
|
||||
private val title: String,
|
||||
private val icon: Icon?,
|
||||
|
||||
@@ -8,4 +8,5 @@ interface TerminalTabbedManager {
|
||||
fun setSelectedTerminalTab(tab: TerminalTab)
|
||||
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true)
|
||||
fun refreshTerminalTabs()
|
||||
fun indexOfTerminalTab(tab: TerminalTab): Int
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package app.termora
|
||||
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||
import app.termora.tree.NewHostTree
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
@@ -9,15 +10,14 @@ import com.formdev.flatlaf.util.SystemInfo
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Font
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import java.awt.event.KeyEvent
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.*
|
||||
import javax.swing.*
|
||||
import javax.swing.tree.TreePath
|
||||
import kotlin.math.max
|
||||
|
||||
|
||||
class TermoraFencePanel(
|
||||
private val ws: WindowScope,
|
||||
private val terminalTabbed: TerminalTabbed,
|
||||
private val tabbed: FlatTabbedPane,
|
||||
private val moveMouseAdapter: MouseAdapter,
|
||||
@@ -72,10 +72,12 @@ class TermoraFencePanel(
|
||||
leftTreePanel.addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentHidden(e: ComponentEvent) {
|
||||
toolbar.isVisible = true
|
||||
enableManager.setFlag("Termora.Fence.colspan", true)
|
||||
}
|
||||
|
||||
override fun componentShown(e: ComponentEvent) {
|
||||
toolbar.isVisible = false
|
||||
enableManager.setFlag("Termora.Fence.colspan", false)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -86,6 +88,50 @@ class TermoraFencePanel(
|
||||
toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK
|
||||
), "toggle"
|
||||
)
|
||||
|
||||
splitPane.addPropertyChangeListener("dividerLocation") {
|
||||
if (leftTreePanel.isVisible)
|
||||
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
|
||||
}
|
||||
|
||||
if (enableManager.getFlag("Termora.Fence.colspan", false)) {
|
||||
toggle()
|
||||
}
|
||||
|
||||
|
||||
DynamicExtensionHandler.getInstance()
|
||||
.register(TerminalTabbedContextMenuExtension::class.java, object : TerminalTabbedContextMenuExtension {
|
||||
override fun createJMenuItem(
|
||||
windowScope: WindowScope,
|
||||
tab: TerminalTab
|
||||
): JMenuItem {
|
||||
if (windowScope != ws) throw UnsupportedOperationException()
|
||||
if (tab !is HostTerminalTab) throw UnsupportedOperationException()
|
||||
if (tab.host.isTemporary) throw UnsupportedOperationException()
|
||||
if (tab.host.id == "local") throw UnsupportedOperationException()
|
||||
|
||||
val item = JMenuItem(I18n.getString("termora.tabbed.contextmenu.select-host"))
|
||||
item.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val tree = getHostTree()
|
||||
for (node in tree.simpleTreeModel.root.getAllChildren()) {
|
||||
if (node.id == tab.host.id) {
|
||||
tree.selectionPath = TreePath(tree.simpleTreeModel.getPathToRoot(node))
|
||||
tree.requestFocusInWindow()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
override fun ordered(): Long {
|
||||
return Long.MAX_VALUE
|
||||
}
|
||||
|
||||
}).let { Disposer.register(this, it) }
|
||||
}
|
||||
|
||||
private inner class LeftTreePanel : JPanel(BorderLayout()), Disposable {
|
||||
@@ -144,19 +190,19 @@ class TermoraFencePanel(
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
if (leftTreePanel.isVisible) dividerLocation = splitPane.dividerLocation
|
||||
leftTreePanel.isVisible = leftTreePanel.isVisible.not()
|
||||
if (leftTreePanel.isVisible) splitPane.dividerLocation = dividerLocation
|
||||
toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun dispose() {
|
||||
if (leftTreePanel.isVisible)
|
||||
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
|
||||
private fun toggle() {
|
||||
if (leftTreePanel.isVisible) dividerLocation = splitPane.dividerLocation
|
||||
leftTreePanel.isVisible = leftTreePanel.isVisible.not()
|
||||
if (leftTreePanel.isVisible) splitPane.dividerLocation = dividerLocation
|
||||
mySplitPane.doLayout()
|
||||
}
|
||||
|
||||
|
||||
fun getHostTree(): NewHostTree {
|
||||
return leftTreePanel.hostTree
|
||||
}
|
||||
|
||||
@@ -164,6 +164,8 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
|
||||
}).let { Disposer.register(windowScope, it) }
|
||||
|
||||
Disposer.register(windowScope, tabbedPane)
|
||||
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
@@ -210,7 +212,7 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
}
|
||||
|
||||
if (layout == TermoraLayout.Fence) {
|
||||
val fencePanel = TermoraFencePanel(terminalTabbed, tabbedPane, moveMouseAdapter)
|
||||
val fencePanel = TermoraFencePanel(windowScope, terminalTabbed, tabbedPane, moveMouseAdapter)
|
||||
add(fencePanel, BorderLayout.CENTER)
|
||||
dataProviderSupport.addData(DataProviders.Welcome.HostTree, fencePanel.getHostTree())
|
||||
Disposer.register(windowScope, fencePanel)
|
||||
|
||||
@@ -5,6 +5,7 @@ import app.termora.plugin.ExtensionManager
|
||||
import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.sun.jna.Pointer
|
||||
import com.sun.jna.platform.WindowUtils
|
||||
import com.sun.jna.platform.win32.User32
|
||||
import com.sun.jna.platform.win32.WinDef
|
||||
import com.sun.jna.platform.win32.WinUser.*
|
||||
@@ -206,7 +207,7 @@ class TermoraFrameManager : Disposable {
|
||||
}
|
||||
|
||||
fun setOpacity(opacity: Double) {
|
||||
if (opacity < 0 || opacity > 1 || SystemInfo.isLinux) return
|
||||
if (opacity < 0 || opacity > 1) return
|
||||
for (window in getWindows()) {
|
||||
setOpacity(window, opacity)
|
||||
}
|
||||
@@ -227,6 +228,8 @@ class TermoraFrameManager : Disposable {
|
||||
User32.INSTANCE.SetWindowLong(hwnd, GWL_EXSTYLE, exStyle or WS_EX_LAYERED)
|
||||
}
|
||||
User32.INSTANCE.SetLayeredWindowAttributes(hwnd, 0, alpha, LWA_ALPHA)
|
||||
} else if (SystemInfo.isLinux && WindowUtils.isWindowAlphaSupported()) {
|
||||
WindowUtils.setWindowAlpha(window, opacity.toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -224,7 +224,7 @@ class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProv
|
||||
|
||||
|
||||
override fun getTitle(): String {
|
||||
return I18n.getString("termora.title")
|
||||
return StringUtils.EMPTY
|
||||
}
|
||||
|
||||
override fun getIcon(): Icon {
|
||||
|
||||
@@ -139,6 +139,7 @@ object AccountHttp {
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
if (cidr == "localhost" || cidr == "127.0.0.1") continue
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug(e.message, e)
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
|
||||
while (true) {
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url("${accountManager.getServer()}/v1/data/changes?since=${since}&after=${after}&limit=${limit}")
|
||||
.url("${accountManager.getServer()}/v1/data/changes?since=${nextSince}&after=${after}&limit=${limit}")
|
||||
.build()
|
||||
val text = AccountHttp.execute(request = request)
|
||||
val response = ohMyJson.decodeFromString<DataChangesResponse>(text)
|
||||
|
||||
@@ -8,7 +8,7 @@ object DataProviders {
|
||||
val Terminal = DataKey(app.termora.terminal.Terminal::class)
|
||||
val TerminalWriter get() = DataKey.TerminalWriter
|
||||
|
||||
val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
|
||||
internal val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
|
||||
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
|
||||
val TerminalTab = DataKey(app.termora.TerminalTab::class)
|
||||
val TerminalTabbedManager = DataKey(app.termora.TerminalTabbedManager::class)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.SettingsDialog
|
||||
import app.termora.*
|
||||
import com.formdev.flatlaf.extras.FlatDesktop
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.KeyboardFocusManager
|
||||
@@ -32,13 +29,13 @@ class SettingsAction private constructor() : AnAction(
|
||||
private val action get() = this
|
||||
|
||||
init {
|
||||
FlatDesktop.setPreferencesHandler {
|
||||
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||
// Doorman 的情况下不允许打开
|
||||
if (owner != null && ApplicationScope.windowScopes().isNotEmpty()) {
|
||||
actionPerformed(ActionEvent(owner, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
||||
FlatDesktop.setPreferencesHandler(object : Runnable {
|
||||
override fun run() {
|
||||
val focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow ?: return
|
||||
if (focusedWindow !is TermoraFrame) return
|
||||
actionPerformed(ActionEvent(focusedWindow, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
|
||||
@@ -666,6 +666,11 @@ class DatabaseManager private constructor() : Disposable {
|
||||
*/
|
||||
var selectCopy by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 右键点击:Copy、CopyAndPaste
|
||||
*/
|
||||
var rightClick by StringPropertyDelegate("Copy")
|
||||
|
||||
/**
|
||||
* 光标样式
|
||||
*/
|
||||
@@ -716,6 +721,11 @@ class DatabaseManager private constructor() : Disposable {
|
||||
*/
|
||||
var layout by StringPropertyDelegate(TermoraLayout.Screen.name)
|
||||
|
||||
/**
|
||||
* 标签序号
|
||||
*/
|
||||
var tabOrder by StringPropertyDelegate(TabOrder.Hide.name)
|
||||
|
||||
/**
|
||||
* 跟随系统
|
||||
*/
|
||||
@@ -763,6 +773,12 @@ class DatabaseManager private constructor() : Disposable {
|
||||
*/
|
||||
var editCommand by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
|
||||
/**
|
||||
* 双击行为
|
||||
*
|
||||
* Transfer、Edit
|
||||
*/
|
||||
var dbClickBehavior by StringPropertyDelegate("Transfer")
|
||||
|
||||
/**
|
||||
* sftp command
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,16 @@ internal class KeywordHighlightPaintListener private constructor() : TerminalPai
|
||||
// -1 表示不使用高亮集
|
||||
if (keywordHighlightSetId == "-1") return
|
||||
|
||||
try {
|
||||
doFind(offset, count, terminal, keywordHighlightSetId)
|
||||
} catch (e: Exception) {
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doFind(offset: Int, count: Int, terminal: Terminal, keywordHighlightSetId: String) {
|
||||
for (highlight in keywordHighlights) {
|
||||
if (highlight.enabled.not()) continue
|
||||
if (highlight.type != KeywordHighlightType.Highlight) continue
|
||||
@@ -151,7 +161,6 @@ internal class KeywordHighlightPaintListener private constructor() : TerminalPai
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun after(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -81,7 +82,7 @@ class NewKeywordHighlightDialog(
|
||||
FlatClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON
|
||||
)
|
||||
|
||||
matchCaseBtn.toolTipText = "Match case"
|
||||
matchCaseBtn.toolTipText = I18n.getString("termora.match-case")
|
||||
|
||||
|
||||
val box = FlatToolBar()
|
||||
@@ -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,
|
||||
|
||||
@@ -262,8 +262,8 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
|
||||
|
||||
OptionPane.openFileInFolder(
|
||||
SwingUtilities.getWindowAncestor(this),
|
||||
file, I18n.getString("termora.settings.sync.export-done-open-folder"),
|
||||
I18n.getString("termora.settings.sync.export-done")
|
||||
file, I18n.getString("termora.keymgr.export-done-open-folder"),
|
||||
I18n.getString("termora.keymgr.export-done")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
}
|
||||
|
||||
override fun getIcon(): Icon {
|
||||
return if (unread) Icons.terminalUnread else Icons.terminal
|
||||
return Icons.terminal
|
||||
}
|
||||
|
||||
override fun willBeClose(): Boolean {
|
||||
@@ -62,6 +62,9 @@ class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
) == JOptionPane.OK_OPTION
|
||||
}
|
||||
|
||||
override fun createReconnectTerminalTab(): TerminalTab {
|
||||
return LocalTerminalTab(windowScope, host)
|
||||
}
|
||||
|
||||
private fun getPtyProcessConnector(): PtyProcessConnector? {
|
||||
var p = getPtyConnector() as PtyConnector?
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -103,9 +103,20 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
||||
commands.add("Compression=yes")
|
||||
|
||||
// HostKeyAlgorithms 让 SFTP 命令的顺序和 sshd 的一致 这样可以避免 known_hosts 文件不一致问题
|
||||
val hostKeyAlgorithms = ClientBuilder.setUpDefaultSignatureFactories(true).joinToString(",") { it.name }
|
||||
val hostKeyAlgorithms = ClientBuilder.setUpDefaultSignatureFactories(true).map { it.name }.toMutableList()
|
||||
val localHostKeyAlgorithms = getLocalSSHHostKeyAlgorithms()
|
||||
// 删除本地 ssh 不存在的算法
|
||||
hostKeyAlgorithms.removeIf { localHostKeyAlgorithms.contains(it).not() }
|
||||
|
||||
// 把本地支持的再添加进去
|
||||
for (algorithm in localHostKeyAlgorithms) {
|
||||
if (hostKeyAlgorithms.contains(algorithm).not()) {
|
||||
hostKeyAlgorithms.add(algorithm)
|
||||
}
|
||||
}
|
||||
|
||||
commands.add("-o")
|
||||
commands.add("HostKeyAlgorithms=${hostKeyAlgorithms}")
|
||||
commands.add("HostKeyAlgorithms=${hostKeyAlgorithms.joinToString(",")}")
|
||||
|
||||
// 不使用配置文件
|
||||
commands.add("-F")
|
||||
@@ -143,6 +154,15 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
||||
return ptyConnector
|
||||
}
|
||||
|
||||
private fun getLocalSSHHostKeyAlgorithms(): Set<String> {
|
||||
val pb = ProcessBuilder("ssh", "-Q", "key")
|
||||
val process = pb.start()
|
||||
if (process.waitFor() != 0) {
|
||||
return emptySet()
|
||||
}
|
||||
return String(process.inputStream.readAllBytes()).lines().filter { it.isNotBlank() }.toSet()
|
||||
}
|
||||
|
||||
private fun setAuthentication(commands: MutableList<String>, host: Host) {
|
||||
// 如果通过公钥连接
|
||||
if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||
@@ -185,6 +205,10 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
||||
return Icons.fileFormat
|
||||
}
|
||||
|
||||
override fun createReconnectTerminalTab(): TerminalTab {
|
||||
return SFTPPtyTerminalTab(windowScope, host)
|
||||
}
|
||||
|
||||
override fun sendStartupCommand(ptyConnector: PtyConnector, bytes: ByteArray) {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import app.termora.keymgr.KeyManagerDialog
|
||||
import app.termora.plugin.internal.AltKeyModifier
|
||||
import app.termora.plugin.internal.BasicProxyOption
|
||||
import app.termora.plugin.internal.BasicTerminalOption
|
||||
import app.termora.plugin.internal.telnet.TelnetHostOptionsPane.Backspace
|
||||
import app.termora.tree.Filter
|
||||
import app.termora.tree.HostTreeNode
|
||||
import app.termora.tree.NewHostTreeDialog
|
||||
@@ -24,6 +25,7 @@ import org.eclipse.jgit.internal.transport.sshd.agent.connector.WinPipeConnector
|
||||
import java.awt.*
|
||||
import java.awt.event.*
|
||||
import javax.swing.*
|
||||
import javax.swing.event.DocumentEvent
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
import javax.swing.table.DefaultTableModel
|
||||
|
||||
@@ -35,9 +37,11 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
private val terminalOption = BasicTerminalOption().apply {
|
||||
showCharsetComboBox = true
|
||||
showLoginScripts = true
|
||||
showBackspaceComboBox = true
|
||||
showEnvironmentTextArea = true
|
||||
showStartupCommandTextField = true
|
||||
showHeartbeatIntervalTextField = true
|
||||
showTimeoutTextField = true
|
||||
showHighlightSet = true
|
||||
accountOwner = this@SSHHostOptionsPane.accountOwner
|
||||
init()
|
||||
@@ -45,6 +49,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
private val jumpHostsOption = JumpHostsOption()
|
||||
private val sftpOption = SFTPOption()
|
||||
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||
private var setHostMode = false
|
||||
|
||||
init {
|
||||
addOption(generalOption)
|
||||
@@ -109,10 +114,13 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
x11Forwarding = tunnelingOption.x11ServerTextField.text,
|
||||
loginScripts = terminalOption.loginScripts,
|
||||
extras = mutableMapOf(
|
||||
"backspace" to (terminalOption.backspaceComboBox.selectedItem as Backspace).name,
|
||||
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
|
||||
?: AltKeyModifier.EightBit.name),
|
||||
"keywordHighlightSetId" to ((terminalOption.highlightSetComboBox.selectedItem as? KeywordHighlight)?.id
|
||||
?: "-1"),
|
||||
"timeout" to (terminalOption.timeoutTextField.value ?: 60).toString(),
|
||||
"forwardAgent" to tunnelingOption.forwardAgentCheckBox.isSelected.toString(),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -132,6 +140,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
}
|
||||
|
||||
fun setHost(host: Host) {
|
||||
setHostMode = true
|
||||
generalOption.portTextField.value = host.port
|
||||
generalOption.nameTextField.text = host.name
|
||||
generalOption.usernameTextField.text = host.username
|
||||
@@ -163,6 +172,13 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
.getOrNull() ?: AltKeyModifier.EightBit
|
||||
|
||||
|
||||
terminalOption.backspaceComboBox.selectedItem =
|
||||
Backspace.valueOf(host.options.extras["backspace"] ?: Backspace.Delete.name)
|
||||
|
||||
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)
|
||||
@@ -176,6 +192,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
tunnelingOption.tunnelings.addAll(host.tunnelings)
|
||||
tunnelingOption.x11ForwardingCheckBox.isSelected = host.options.enableX11Forwarding
|
||||
tunnelingOption.x11ServerTextField.text = StringUtils.defaultIfBlank(host.options.x11Forwarding, "localhost:0")
|
||||
tunnelingOption.forwardAgentCheckBox.isSelected = host.options.extras["forwardAgent"]?.toBoolean() ?: false
|
||||
|
||||
if (host.options.jumpHosts.isNotEmpty()) {
|
||||
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
||||
@@ -290,6 +307,8 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
val remarkTextArea = FixedLengthTextArea(512)
|
||||
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||
|
||||
private var hostFocused = false
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
@@ -397,6 +416,26 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
removeComponentListener(this)
|
||||
}
|
||||
})
|
||||
|
||||
hostTextField.addFocusListener(object : FocusAdapter() {
|
||||
override fun focusGained(e: FocusEvent) {
|
||||
hostTextField.removeFocusListener(this)
|
||||
hostFocused = true
|
||||
}
|
||||
})
|
||||
|
||||
nameTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||
override fun changedUpdate(e: DocumentEvent) {
|
||||
if (nameTextField.hasFocus().not()) return
|
||||
|
||||
if (hostFocused || setHostMode) {
|
||||
nameTextField.document.removeDocumentListener(this)
|
||||
return
|
||||
}
|
||||
|
||||
hostTextField.text = nameTextField.text
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun chooseKeyPair() {
|
||||
@@ -564,9 +603,10 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
}
|
||||
}
|
||||
|
||||
protected inner class TunnelingOption : JPanel(BorderLayout()), Option {
|
||||
private inner class TunnelingOption : JPanel(BorderLayout()), Option {
|
||||
val tunnelings = mutableListOf<Tunneling>()
|
||||
val x11ForwardingCheckBox = JCheckBox("X DISPLAY:")
|
||||
val forwardAgentCheckBox = JCheckBox("Enable ForwardAgent")
|
||||
val x11ServerTextField = OutlineTextField(255)
|
||||
|
||||
private val model = object : DefaultTableModel() {
|
||||
@@ -643,6 +683,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
box.add(deleteBtn)
|
||||
|
||||
x11ForwardingCheckBox.isFocusable = false
|
||||
forwardAgentCheckBox.isFocusable = false
|
||||
|
||||
if (x11ServerTextField.text.isBlank()) {
|
||||
x11ServerTextField.text = "localhost:0"
|
||||
@@ -656,6 +697,13 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
x11Forwarding.add(x11ForwardingCheckBox)
|
||||
x11Forwarding.add(x11ServerTextField)
|
||||
|
||||
val forwardAgent = Box.createHorizontalBox()
|
||||
forwardAgent.border = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createTitledBorder("Agent Forwarding"),
|
||||
BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
)
|
||||
forwardAgent.add(forwardAgentCheckBox)
|
||||
|
||||
x11ServerTextField.isEnabled = x11ForwardingCheckBox.isSelected
|
||||
|
||||
val panel = JPanel(BorderLayout())
|
||||
@@ -664,8 +712,13 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
||||
panel.add(box, BorderLayout.SOUTH)
|
||||
panel.border = BorderFactory.createEmptyBorder(0, 0, 8, 0)
|
||||
|
||||
val forwardingBox = Box.createHorizontalBox()
|
||||
forwardingBox.add(x11Forwarding)
|
||||
forwardingBox.add(Box.createHorizontalStrut(4))
|
||||
forwardingBox.add(forwardAgent)
|
||||
|
||||
add(panel, BorderLayout.CENTER)
|
||||
add(x11Forwarding, BorderLayout.SOUTH)
|
||||
add(forwardingBox, BorderLayout.SOUTH)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,8 @@ import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.keymap.KeyShortcut
|
||||
import app.termora.keymap.KeymapManager
|
||||
import app.termora.terminal.ControlCharacters
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.PtyConnector
|
||||
import app.termora.plugin.internal.telnet.TelnetHostOptionsPane
|
||||
import app.termora.terminal.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@@ -20,6 +19,7 @@ import org.apache.sshd.client.session.ClientSession
|
||||
import org.apache.sshd.common.future.CloseFuture
|
||||
import org.apache.sshd.common.future.SshFutureListener
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.event.KeyEvent
|
||||
import java.nio.charset.StandardCharsets
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComponent
|
||||
@@ -54,6 +54,10 @@ class SSHTerminalTab(
|
||||
return mutex.isLocked.not()
|
||||
}
|
||||
|
||||
override fun createReconnectTerminalTab(): TerminalTab {
|
||||
return SSHTerminalTab(windowScope, host)
|
||||
}
|
||||
|
||||
override suspend fun openPtyConnector(): PtyConnector {
|
||||
if (mutex.tryLock()) {
|
||||
try {
|
||||
@@ -79,15 +83,14 @@ class SSHTerminalTab(
|
||||
}
|
||||
|
||||
val loading = coroutineScope.launch(Dispatchers.Swing) {
|
||||
val braille = "⡿⣟⣯⣷⣾⣽⣻⢿".reversed().toCharArray()
|
||||
// val braille = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏".toCharArray()
|
||||
var c = 0
|
||||
while (isActive) {
|
||||
if (++c > 6) c = 1
|
||||
terminal.write("${ControlCharacters.ESC}[1;32m")
|
||||
terminal.write(".".repeat(c))
|
||||
terminal.write(" ".repeat(6 - c))
|
||||
terminal.write("${ControlCharacters.ESC}[0m")
|
||||
delay(350.milliseconds)
|
||||
terminal.write("${ControlCharacters.BS}".repeat(6))
|
||||
if (++c >= braille.size) c = 0
|
||||
terminal.write("${braille[c]}")
|
||||
delay(100.milliseconds)
|
||||
terminal.write("${ControlCharacters.BS}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +110,18 @@ class SSHTerminalTab(
|
||||
// clear screen
|
||||
terminal.clearScreen()
|
||||
// show cursor
|
||||
terminalModel.setData(DataKey.Companion.ShowCursor, true)
|
||||
terminalModel.setData(DataKey.ShowCursor, true)
|
||||
|
||||
val encoder = terminal.getKeyEncoder()
|
||||
if (encoder is KeyEncoderImpl) {
|
||||
val backspace = host.options.extras["backspace"]
|
||||
if (backspace == TelnetHostOptionsPane.Backspace.Backspace.name) {
|
||||
encoder.putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_BACK_SPACE), String(byteArrayOf(0x08)))
|
||||
} else if (backspace == TelnetHostOptionsPane.Backspace.VT220.name) {
|
||||
encoder.putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_BACK_SPACE), "${ControlCharacters.ESC}[3~")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return ptyConnectorFactory.decorate(
|
||||
@@ -211,17 +225,6 @@ class SSHTerminalTab(
|
||||
return super.getData(dataKey)
|
||||
}
|
||||
|
||||
override fun reconnect() {
|
||||
stop()
|
||||
|
||||
// 重新连接时就等于重新打开了一个标签,handler 重置
|
||||
handler.client = null
|
||||
handler.session = null
|
||||
handler.client = null
|
||||
|
||||
start()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
if (mutex.tryLock()) {
|
||||
try {
|
||||
@@ -234,7 +237,7 @@ class SSHTerminalTab(
|
||||
}
|
||||
|
||||
override fun getIcon(): Icon {
|
||||
return if (unread) Icons.terminalUnread else Icons.terminal
|
||||
return Icons.terminal
|
||||
}
|
||||
|
||||
override fun beforeClose() {
|
||||
|
||||
@@ -69,4 +69,7 @@ class SftpCommandTerminalTabbedContextMenuExtension private constructor() : Term
|
||||
openHostAction.actionPerformed(OpenHostActionEvent(evt.source, host, evt))
|
||||
}
|
||||
|
||||
override fun ordered(): Long {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package app.termora.plugin.internal.ssh
|
||||
|
||||
import org.apache.sshd.agent.local.ChannelAgentForwardingFactory
|
||||
import org.apache.sshd.common.FactoryManager
|
||||
import org.apache.sshd.common.channel.ChannelFactory
|
||||
import org.eclipse.jgit.internal.transport.sshd.agent.JGitSshAgentFactory
|
||||
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory
|
||||
import java.io.File
|
||||
|
||||
internal class SshAgentFactory(factory: ConnectorFactory, homeDir: File?) : JGitSshAgentFactory(factory, homeDir) {
|
||||
override fun getChannelForwardingFactories(manager: FactoryManager?): List<ChannelFactory> {
|
||||
return listOf(ChannelAgentForwardingFactory.OPENSSH, ChannelAgentForwardingFactory.IETF)
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,6 @@ import org.apache.sshd.server.forward.AcceptAllForwardingFilter
|
||||
import org.apache.sshd.server.forward.RejectAllForwardingFilter
|
||||
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession
|
||||
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient
|
||||
import org.eclipse.jgit.internal.transport.sshd.agent.JGitSshAgentFactory
|
||||
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.proxy.AbstractClientProxyConnector
|
||||
@@ -88,7 +87,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 +99,7 @@ object SshClients {
|
||||
session: ClientSession,
|
||||
): ChannelShell {
|
||||
|
||||
val timeout = Duration.ofSeconds(host.options.extras["timeout"]?.toLongOrNull() ?: 60)
|
||||
|
||||
val configuration = PtyChannelConfiguration()
|
||||
configuration.ptyColumns = size.cols
|
||||
@@ -112,6 +111,8 @@ object SshClients {
|
||||
env.putAll(host.options.envs())
|
||||
|
||||
val channel = session.createShellChannel(configuration, env)
|
||||
channel.isAgentForwarding = host.options.extras["forwardAgent"]?.toBoolean() == true
|
||||
|
||||
if (host.options.enableX11Forwarding) {
|
||||
if (channel is app.termora.x11.ChannelShell) {
|
||||
channel.xForwarding = true
|
||||
@@ -136,6 +137,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 +250,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))
|
||||
@@ -384,7 +387,7 @@ object SshClients {
|
||||
|
||||
val channelFactories = mutableListOf<ChannelFactory>()
|
||||
channelFactories.addAll(ClientBuilder.DEFAULT_CHANNEL_FACTORIES)
|
||||
channelFactories.add(X11ChannelFactory.Companion.INSTANCE)
|
||||
channelFactories.add(X11ChannelFactory.INSTANCE)
|
||||
builder.channelFactories(channelFactories)
|
||||
|
||||
val sshClient = builder.build() as JGitSshClient
|
||||
@@ -393,12 +396,14 @@ object SshClients {
|
||||
// JGit 会尝试读取本地的私钥或缓存的私钥
|
||||
sshClient.keyIdentityProvider = KeyIdentityProvider { mutableListOf() }
|
||||
|
||||
// https://github.com/TermoraDev/termora/issues/1001
|
||||
if (host.authentication.type == AuthenticationType.SSHAgent || host.options.extras["forwardAgent"]?.toBoolean() == true) {
|
||||
// ssh-agent
|
||||
sshClient.agentFactory = SshAgentFactory(ConnectorFactory.getDefault(), null)
|
||||
}
|
||||
|
||||
// 设置优先级
|
||||
if (host.authentication.type == AuthenticationType.PublicKey || host.authentication.type == AuthenticationType.SSHAgent) {
|
||||
if (host.authentication.type == AuthenticationType.SSHAgent) {
|
||||
// ssh-agent
|
||||
sshClient.agentFactory = JGitSshAgentFactory(ConnectorFactory.getDefault(), null)
|
||||
}
|
||||
CoreModuleProperties.PREFERRED_AUTHS.set(
|
||||
sshClient,
|
||||
listOf(
|
||||
@@ -420,8 +425,11 @@ object SshClients {
|
||||
|
||||
|
||||
val heartbeatInterval = max(host.options.heartbeatInterval, 3)
|
||||
val timeout = Duration.ofSeconds(host.options.extras["timeout"]?.toLongOrNull() ?: 60)
|
||||
|
||||
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
|
||||
CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true)
|
||||
CoreModuleProperties.IO_CONNECT_TIMEOUT.set(sshClient, timeout)
|
||||
|
||||
sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) }
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package app.termora.plugin.internal.telnet
|
||||
|
||||
import app.termora.Host
|
||||
import app.termora.ProxyType
|
||||
import app.termora.PtyHostTerminalTab
|
||||
import app.termora.WindowScope
|
||||
import app.termora.*
|
||||
import app.termora.terminal.ControlCharacters
|
||||
import app.termora.terminal.KeyEncoderImpl
|
||||
import app.termora.terminal.PtyConnector
|
||||
@@ -71,5 +68,9 @@ class TelnetTerminalTab(
|
||||
return ptyConnectorFactory.decorate(TelnetStreamPtyConnector(telnet, telnet.charset, characterMode))
|
||||
}
|
||||
|
||||
override fun createReconnectTerminalTab(): TerminalTab {
|
||||
return TelnetTerminalTab(windowScope, host)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package app.termora.plugin.internal.updater
|
||||
|
||||
import app.termora.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import kotlinx.coroutines.withContext
|
||||
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 || Application.getLayout() == AppLayout.AppStore
|
||||
private val updaterManager get() = UpdaterManager.getInstance()
|
||||
|
||||
|
||||
override fun ready() {
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
delay(3.seconds)
|
||||
scheduleUpdate()
|
||||
} catch (e: Exception) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend 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
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
|
||||
?: TermoraFrameManager.getInstance().getWindows().firstOrNull()
|
||||
if (owner != null) {
|
||||
val dialog = UpdaterDialog(owner, latestVersion)
|
||||
dialog.isModal = true
|
||||
dialog.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
package app.termora.plugin.internal.wsl
|
||||
|
||||
import app.termora.Host
|
||||
import app.termora.PtyConnectorFactory
|
||||
import app.termora.PtyHostTerminalTab
|
||||
import app.termora.WindowScope
|
||||
import app.termora.*
|
||||
import app.termora.terminal.PtyConnector
|
||||
import org.apache.commons.io.Charsets
|
||||
import org.apache.commons.io.FileUtils
|
||||
@@ -51,6 +48,10 @@ class WSLHostTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
||||
return ptyConnector
|
||||
}
|
||||
|
||||
override fun createReconnectTerminalTab(): TerminalTab {
|
||||
return WSLHostTerminalTab(windowScope, host)
|
||||
}
|
||||
|
||||
|
||||
override fun sendStartupCommand(ptyConnector: PtyConnector, bytes: ByteArray) {
|
||||
// Nothing
|
||||
|
||||
@@ -332,15 +332,16 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
||||
var top = sr.getOrElse(0) { 1 }
|
||||
var bottom = sr.getOrElse(1) { terminalModel.getRows() }
|
||||
|
||||
if (bottom <= top || top < 1) {
|
||||
if (bottom <= top) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn("Set Scrolling Region Error. top: $top , bottom: $bottom")
|
||||
}
|
||||
|
||||
top = 1
|
||||
bottom = terminalModel.getRows()
|
||||
}
|
||||
|
||||
top = max(1, top)
|
||||
bottom = min(terminalModel.getRows(), bottom)
|
||||
|
||||
|
||||
// 设置滚动区域
|
||||
terminal.getTerminalModel().setData(
|
||||
DataKey.ScrollingRegion,
|
||||
@@ -519,9 +520,13 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
||||
|
||||
val writer = terminalModel.getData(DataKey.TerminalWriter)
|
||||
|
||||
// VT102_RESPONSE
|
||||
val bytes = "${ControlCharacters.ESC}[?6c".toByteArray(writer.getCharset())
|
||||
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
|
||||
if (args.startsWith('>')) {
|
||||
val bytes = "${ControlCharacters.ESC}[>0;276;0c".toByteArray(writer.getCharset())
|
||||
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
|
||||
} else {
|
||||
val bytes = "${ControlCharacters.ESC}[?1;2c".toByteArray(writer.getCharset())
|
||||
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ class TerminalFindPanel(
|
||||
}
|
||||
} else {
|
||||
if (index - 1 <= 0) {
|
||||
index = 0
|
||||
index = kinds.size - 1
|
||||
} else {
|
||||
index--
|
||||
}
|
||||
|
||||
@@ -185,8 +185,9 @@ class TerminalPanel(val tab: TerminalTab?, val terminal: Terminal, private val w
|
||||
this.addMouseMotionListener(mouseAdapter)
|
||||
|
||||
// 超链接
|
||||
val hyperlinkAdapter = TerminalPanelMouseHyperlinkAdapter(this, terminal)
|
||||
val hyperlinkAdapter = TerminalPanelMouseHyperlinkAdapter(this, terminalDisplay, terminal)
|
||||
this.addMouseListener(hyperlinkAdapter)
|
||||
this.addMouseMotionListener(hyperlinkAdapter)
|
||||
|
||||
// 鼠标跟踪
|
||||
val trackingAdapter = TerminalPanelMouseTrackingAdapter(this, terminal, writer)
|
||||
|
||||
@@ -2,6 +2,8 @@ package app.termora.terminal.panel
|
||||
|
||||
import app.termora.terminal.ClickableHighlighter
|
||||
import app.termora.terminal.Terminal
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import java.awt.Cursor
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.SwingUtilities
|
||||
@@ -11,19 +13,42 @@ import javax.swing.SwingUtilities
|
||||
*/
|
||||
class TerminalPanelMouseHyperlinkAdapter(
|
||||
private val terminalPanel: TerminalPanel,
|
||||
private val terminalDisplay: TerminalDisplay,
|
||||
private val terminal: Terminal,
|
||||
) : MouseAdapter() {
|
||||
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (SwingUtilities.isLeftMouseButton(e)) {
|
||||
val position = terminalPanel.pointToPosition(e.point)
|
||||
for (highlighter in terminal.getMarkupModel().getHighlighters(position)) {
|
||||
if (highlighter is ClickableHighlighter) {
|
||||
highlighter.onClicked(position)
|
||||
}
|
||||
if (SwingUtilities.isLeftMouseButton(e).not()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (SystemInfo.isMacOS) {
|
||||
if (e.isMetaDown.not())
|
||||
return
|
||||
} else if (e.isControlDown.not()) {
|
||||
return
|
||||
}
|
||||
|
||||
val position = terminalPanel.pointToPosition(e.point)
|
||||
for (highlighter in terminal.getMarkupModel().getHighlighters(position)) {
|
||||
if (highlighter is ClickableHighlighter) {
|
||||
highlighter.onClicked(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseMoved(e: MouseEvent) {
|
||||
val position = terminalPanel.pointToPosition(e.point)
|
||||
var cursor = Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)
|
||||
for (highlighter in terminal.getMarkupModel().getHighlighters(position)) {
|
||||
if (highlighter is ClickableHighlighter) {
|
||||
cursor = if (SystemInfo.isMacOS) Cursor.getDefaultCursor()
|
||||
else Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
break
|
||||
}
|
||||
}
|
||||
terminalDisplay.cursor = cursor
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package app.termora.terminal.panel
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.TerminalCopyAction
|
||||
import app.termora.actions.TerminalPasteAction
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.terminal.*
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
@@ -27,6 +28,7 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
|
||||
private val isSelectCopy get() = terminalModel.getData(TerminalPanel.SelectCopy, false)
|
||||
private val selectionModel get() = terminal.getSelectionModel()
|
||||
private val wordBreakIterator = BreakIterator.getWordInstance()
|
||||
private val rightClickMode get() = DatabaseManager.getInstance().terminal.rightClick
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(TerminalPanelMouseSelectionAdapter::class.java)
|
||||
@@ -50,7 +52,7 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
|
||||
|
||||
if (SwingUtilities.isRightMouseButton(e)) {
|
||||
// 如果有选中并且开启了选中复制,那么右键直接是粘贴
|
||||
if (selectionModel.hasSelection() && !isSelectCopy) {
|
||||
if (selectionModel.hasSelection() && isSelectCopy.not()) {
|
||||
triggerCopyAction(
|
||||
KeyEvent(
|
||||
e.component,
|
||||
@@ -61,6 +63,20 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
|
||||
'C'
|
||||
)
|
||||
)
|
||||
|
||||
if (rightClickMode == "CopyAndPaste") {
|
||||
triggerPasteAction(
|
||||
KeyEvent(
|
||||
e.component,
|
||||
KeyEvent.KEY_PRESSED,
|
||||
e.`when`,
|
||||
e.modifiersEx,
|
||||
KeyEvent.VK_V,
|
||||
'V'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
} else {
|
||||
// paste
|
||||
triggerPasteAction(
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.actions.*
|
||||
import app.termora.plugin.internal.badge.Badge
|
||||
import app.termora.plugin.internal.ssh.SSHTerminalTab
|
||||
import app.termora.plugin.internal.ssh.SSHTerminalTab.Companion.SSHSession
|
||||
@@ -39,7 +37,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
||||
SSHVisualWindow(tab, "Transfer", visualWindowManager) {
|
||||
SSHVisualWindow(tab, "Transfer", visualWindowManager), DataProvider {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(TransferVisualWindow::class.java)
|
||||
@@ -65,7 +63,7 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
||||
private val downloadBtn = JButton(Icons.download)
|
||||
private val badgePresentation = Badge.getInstance(tab.windowScope)
|
||||
.addBadge(downloadBtn).apply { visible = false }
|
||||
|
||||
private val support = DataProviderSupport()
|
||||
|
||||
init {
|
||||
initViews()
|
||||
@@ -82,6 +80,8 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
||||
|
||||
|
||||
add(panel, BorderLayout.CENTER)
|
||||
|
||||
support.addData(TransportViewer.MyTransferManager, transferManager)
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
@@ -240,6 +240,10 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
||||
super.dispose()
|
||||
}
|
||||
|
||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||
return support.getData(dataKey)
|
||||
}
|
||||
|
||||
override fun toolbarButtons(): List<Pair<JButton, Position>> {
|
||||
return listOf(downloadBtn to Position.Left, questionBtn to Position.Right)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package app.termora.tlog
|
||||
|
||||
import app.termora.Host
|
||||
import app.termora.Icons
|
||||
import app.termora.PtyHostTerminalTab
|
||||
import app.termora.WindowScope
|
||||
import app.termora.*
|
||||
import app.termora.terminal.PtyConnector
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -71,6 +68,10 @@ class LogViewerTerminalTab(
|
||||
return false
|
||||
}
|
||||
|
||||
override fun createReconnectTerminalTab(): TerminalTab {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun canClone(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package app.termora.transfer
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.transfer.InternalTransferManager.TransferMode
|
||||
import app.termora.transfer.TransportPanel.Companion.isWindowsFileSystem
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -95,8 +96,12 @@ internal class DefaultInternalTransferManager(
|
||||
val context = AskTransferContext(TransferAction.Overwrite, false)
|
||||
for (pair in paths) {
|
||||
if (mode == TransferMode.Transfer && context.applyAll.not()) {
|
||||
var name = pair.first.name
|
||||
if (targetWorkdir.fileSystem.isWindowsFileSystem()) {
|
||||
name = name.replace(":", "-")
|
||||
}
|
||||
val action = withContext(Dispatchers.Swing) {
|
||||
getTransferAction(context, targetWorkdir.resolve(pair.first.name), pair.second)
|
||||
getTransferAction(context, targetWorkdir.resolve(name), pair.second)
|
||||
}
|
||||
if (action == null) {
|
||||
break
|
||||
@@ -272,8 +277,11 @@ internal class DefaultInternalTransferManager(
|
||||
val isDirectory = pair.second.isDirectory
|
||||
val path = pair.first
|
||||
if (isDirectory.not() || mode == TransferMode.Rmrf) {
|
||||
val transfer =
|
||||
createTransfer(path, workdir.resolve(path.name), isDirectory, StringUtils.EMPTY, mode, action)
|
||||
var name = path.name
|
||||
if (workdir.fileSystem.isWindowsFileSystem()) {
|
||||
name = name.replace(":", "-")
|
||||
}
|
||||
val transfer = createTransfer(path, workdir.resolve(name), isDirectory, StringUtils.EMPTY, mode, action)
|
||||
return if (transferManager.addTransfer(transfer)) FileVisitResult.CONTINUE else FileVisitResult.TERMINATE
|
||||
}
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ internal class TransportPanel(
|
||||
private val loadingPanel = LoadingPanel()
|
||||
private val model = TransportTableModel()
|
||||
private val table = JTable(model)
|
||||
private val tableScrollPane = JScrollPane(table)
|
||||
private val sorter = TableRowSorter(table.model)
|
||||
private var hasParent = false
|
||||
private val panel get() = this
|
||||
@@ -211,7 +212,7 @@ internal class TransportPanel(
|
||||
|
||||
|
||||
table.setDefaultRenderer(Any::class.java, MyDefaultTableCellRenderer())
|
||||
val scrollPane = JScrollPane(table)
|
||||
val scrollPane = tableScrollPane
|
||||
scrollPane.apply { border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor) }
|
||||
|
||||
layeredPane.add(scrollPane, JLayeredPane.DEFAULT_LAYER as Any)
|
||||
@@ -241,7 +242,11 @@ internal class TransportPanel(
|
||||
|
||||
Disposer.register(this, editTransferListener)
|
||||
|
||||
refreshBtn.addActionListener { reload(requestFocus = true) }
|
||||
refreshBtn.addActionListener {
|
||||
val filename = getSelectFilename()
|
||||
if (filename != null) registerSelectRow(filename)
|
||||
reload(requestFocus = true)
|
||||
}
|
||||
|
||||
prevBtn.addActionListener { navigator.back() }
|
||||
nextBtn.addActionListener { navigator.forward() }
|
||||
@@ -303,11 +308,16 @@ internal class TransportPanel(
|
||||
if (target.fileSystem != loader.getSyncTransportSupport().getFileSystem()) return
|
||||
}
|
||||
if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) {
|
||||
if (loading) {
|
||||
registerNextReloadCallback { reload(requestFocus = false) }
|
||||
} else {
|
||||
val c = {
|
||||
val filename = getSelectFilename()
|
||||
if (filename != null) registerSelectRow(filename)
|
||||
reload(requestFocus = false)
|
||||
}
|
||||
if (loading) {
|
||||
registerNextReloadCallback { c.invoke() }
|
||||
} else {
|
||||
c.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}).let { Disposer.register(this, it) }
|
||||
@@ -430,6 +440,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 +449,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)) {
|
||||
@@ -488,6 +507,28 @@ internal class TransportPanel(
|
||||
}
|
||||
})
|
||||
|
||||
table.actionMap.put("Delete", object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val rows = table.selectedRows.map { sorter.convertRowIndexToModel(it) }.toTypedArray()
|
||||
val files = rows.map { model.getPath(it) to model.getAttributes(it) }
|
||||
// 排除父目录
|
||||
val validFiles = files.filter { !it.second.isParent }
|
||||
if (validFiles.isNotEmpty()) {
|
||||
// 显示删除确认对话框
|
||||
if (OptionPane.showConfirmDialog(
|
||||
owner,
|
||||
I18n.getString("termora.keymgr.delete-warning"),
|
||||
messageType = JOptionPane.WARNING_MESSAGE
|
||||
) == JOptionPane.YES_OPTION
|
||||
) {
|
||||
// 直接执行删除操作
|
||||
val future = internalTransferManager.addTransfer(validFiles, InternalTransferManager.TransferMode.Delete)
|
||||
mountFuture(future)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 快速导航
|
||||
table.addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
@@ -511,12 +552,25 @@ internal class TransportPanel(
|
||||
}
|
||||
})
|
||||
|
||||
// 重写全选行为,排除".."父目录
|
||||
table.actionMap.put("selectAll", object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
table.clearSelection()
|
||||
val startRow = if (hasParent) 1 else 0 // 跳过".."行
|
||||
if (startRow < table.rowCount) {
|
||||
table.setRowSelectionInterval(startRow, table.rowCount - 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val inputMap = table.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
|
||||
if (SystemInfo.isMacOS.not()) {
|
||||
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), "Reload")
|
||||
}
|
||||
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "EnterSelectionFolder")
|
||||
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, toolkit.menuShortcutKeyMaskEx), "Reload")
|
||||
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "Delete")
|
||||
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), "Delete")
|
||||
}
|
||||
|
||||
private fun initTransferHandler() {
|
||||
@@ -649,6 +703,9 @@ internal class TransportPanel(
|
||||
}
|
||||
|
||||
fun registerSelectRow(name: String) {
|
||||
val verticalValue = tableScrollPane.verticalScrollBar.value
|
||||
val horizontalValue = tableScrollPane.horizontalScrollBar.value
|
||||
|
||||
registerNextReloadCallback {
|
||||
for (i in 0 until model.rowCount) {
|
||||
if (model.getAttributes(i).name == name) {
|
||||
@@ -656,12 +713,22 @@ internal class TransportPanel(
|
||||
table.clearSelection()
|
||||
table.setRowSelectionInterval(c, c)
|
||||
table.scrollRectToVisible(table.getCellRect(c, TransportTableModel.COLUMN_NAME, true))
|
||||
tableScrollPane.verticalScrollBar.value = verticalValue
|
||||
tableScrollPane.horizontalScrollBar.value = horizontalValue
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getSelectFilename(): String? {
|
||||
val row = table.selectedRow
|
||||
if (row < 0) return null
|
||||
val c = sorter.convertRowIndexToModel(row)
|
||||
if (c < 0) return null
|
||||
return model.getAttributes(c).name
|
||||
}
|
||||
|
||||
private fun registerNextReloadCallback(block: () -> Unit) {
|
||||
nextReloadCallbacks.computeIfAbsent(mod.get()) { mutableListOf() }
|
||||
.add(block)
|
||||
@@ -1074,6 +1141,8 @@ internal class TransportPanel(
|
||||
} else if (actionCommand == TransportPopupMenu.ActionCommand.Delete) {
|
||||
transfer(InternalTransferManager.TransferMode.Delete)
|
||||
} else if (actionCommand == TransportPopupMenu.ActionCommand.Refresh) {
|
||||
val filename = getSelectFilename()
|
||||
if (filename != null) registerSelectRow(filename)
|
||||
reload(requestFocus = true)
|
||||
} else if (actionCommand == TransportPopupMenu.ActionCommand.Edit) {
|
||||
edit()
|
||||
@@ -1130,7 +1199,9 @@ internal class TransportPanel(
|
||||
|
||||
private fun edit() {
|
||||
for (path in files.map { it.first }) {
|
||||
val target = Application.createSubTemporaryDir().resolve(path.name)
|
||||
var name = path.name
|
||||
if (SystemInfo.isWindows) name = name.replace(":", "-")
|
||||
val target = Application.createSubTemporaryDir().resolve(name)
|
||||
val transferId = internalTransferManager.addHighTransfer(path, target)
|
||||
editTransferListener.addListenTransfer(transferId)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package app.termora.transfer
|
||||
|
||||
import app.termora.Application
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.I18n
|
||||
import app.termora.OptionPane
|
||||
import app.termora.*
|
||||
import app.termora.plugin.ExtensionManager
|
||||
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import kotlinx.coroutines.launch
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
||||
@@ -149,7 +147,12 @@ internal class TransportPopupMenu(
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
transferMenu.addActionListener { fireActionPerformed(it, ActionCommand.Transfer) }
|
||||
transferMenu.addActionListener {
|
||||
swingCoroutineScope.launch {
|
||||
fireActionPerformed(it, ActionCommand.Transfer)
|
||||
}
|
||||
}
|
||||
|
||||
deleteMenu.addActionListener {
|
||||
if (OptionPane.showConfirmDialog(
|
||||
owner,
|
||||
|
||||
@@ -3,6 +3,8 @@ package app.termora.tree
|
||||
import app.termora.*
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.account.AccountManager
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.OpenHostAction
|
||||
import app.termora.database.DatabaseChangedExtension
|
||||
import app.termora.database.DatabaseManager
|
||||
@@ -32,6 +34,10 @@ import org.jdesktop.swingx.action.ActionManager
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.NodeList
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import java.awt.datatransfer.Transferable
|
||||
import java.awt.datatransfer.UnsupportedFlavorException
|
||||
import java.awt.event.*
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
@@ -52,7 +58,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 {
|
||||
// 基本信息
|
||||
@@ -140,6 +146,41 @@ class NewHostTree : SimpleTree(), Disposable {
|
||||
}
|
||||
})
|
||||
|
||||
actionMap.put("copy", object : AnAction() {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
toolkit.systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
|
||||
val nodes = getSelectionSimpleTreeNodes(false).toMutableList()
|
||||
nodes.removeIf { e -> e.getParents().any { nodes.contains(it) } }
|
||||
if (nodes.isEmpty() || nodes.any { it is TeamTreeNode }) return
|
||||
if (nodes.any { it.id == "0" || it.id.isBlank() }) return
|
||||
toolkit.systemClipboard.setContents(NodesTransferable(nodes), null)
|
||||
}
|
||||
})
|
||||
|
||||
actionMap.put("paste", object : AnAction() {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val lastNode = getLastSelectedPathNode() ?: return
|
||||
val folder = if (lastNode.isFolder) lastNode.parent ?: simpleTreeModel.root
|
||||
else lastNode.parent ?: return
|
||||
|
||||
if (toolkit.systemClipboard.isDataFlavorAvailable(NodesTransferable.FLAVOR).not()) return
|
||||
val nodes = (toolkit.systemClipboard.getData(NodesTransferable.FLAVOR) as? List<*>)
|
||||
?.filterIsInstance<HostTreeNode>() ?: return
|
||||
|
||||
for (node in nodes) {
|
||||
val newNode = copyNode(node, folder.id)
|
||||
// 复制的是文件夹,就在最后面
|
||||
if (newNode.isFolder) {
|
||||
simpleTreeModel.insertNodeInto(newNode, folder, folder.folderCount)
|
||||
} else if (lastNode.isFolder) { // 用户选的节点是文件夹,那就在最后一个child下面
|
||||
simpleTreeModel.insertNodeInto(newNode, folder, folder.childCount)
|
||||
} else { // 用户选的是主机并且复制的是主机
|
||||
simpleTreeModel.insertNodeInto(newNode, folder, folder.getIndex(lastNode) + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
fun restoreExpansions() {
|
||||
@@ -198,10 +239,12 @@ class NewHostTree : SimpleTree(), Disposable {
|
||||
val sshMenu = importMenu.add(".ssh/config")
|
||||
val mobaXtermMenu = importMenu.add("MobaXterm")
|
||||
|
||||
|
||||
// 为了避免误导,如果是 SSH 右键时显示 SFTP
|
||||
val sftpText = if (SSHProtocolProvider.PROTOCOL.equals(lastHost.protocol, true))
|
||||
"SFTP" else I18n.getString("termora.transport.sftp")
|
||||
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect"))
|
||||
val openWith = popupMenu.add(JMenu(I18n.getString("termora.welcome.contextmenu.connect-with"))) as JMenu
|
||||
val openWithSFTP = openWith.add(I18n.getString("termora.transport.sftp"))
|
||||
val openWithSFTP = openWith.add(sftpText)
|
||||
val openWithSFTPCommand = openWith.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
|
||||
val openInNewWindow = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open-in-new-window"))
|
||||
popupMenu.addSeparator()
|
||||
@@ -389,6 +432,20 @@ class NewHostTree : SimpleTree(), Disposable {
|
||||
|
||||
})
|
||||
|
||||
val mnemonics = mapOf(
|
||||
refresh to KeyEvent.VK_R,
|
||||
newMenu to KeyEvent.VK_W,
|
||||
newFolder to KeyEvent.VK_F,
|
||||
rename to KeyEvent.VK_M,
|
||||
remove to KeyEvent.VK_D,
|
||||
property to KeyEvent.VK_I,
|
||||
)
|
||||
|
||||
for ((item, mnemonic) in mnemonics) {
|
||||
item.text = "${item.text}(${KeyEvent.getKeyText(mnemonic)})"
|
||||
item.setMnemonic(mnemonic)
|
||||
}
|
||||
|
||||
popupMenu.show(this, evt.x, evt.y)
|
||||
}
|
||||
|
||||
@@ -577,26 +634,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 +743,11 @@ class NewHostTree : SimpleTree(), Disposable {
|
||||
printer.printRecord(
|
||||
groups.joinToString("/"),
|
||||
label,
|
||||
SSHProtocolProvider.PROTOCOL,
|
||||
target,
|
||||
port,
|
||||
StringUtils.EMPTY,
|
||||
SSHProtocolProvider.PROTOCOL
|
||||
StringUtils.EMPTY,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -703,10 +764,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 +808,11 @@ class NewHostTree : SimpleTree(), Disposable {
|
||||
printer.printRecord(
|
||||
folders.joinToString("/"),
|
||||
label,
|
||||
SSHProtocolProvider.PROTOCOL,
|
||||
hostname,
|
||||
port.toString(),
|
||||
username,
|
||||
SSHProtocolProvider.PROTOCOL
|
||||
StringUtils.EMPTY,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -776,10 +839,11 @@ class NewHostTree : SimpleTree(), Disposable {
|
||||
printer.printRecord(
|
||||
StringUtils.EMPTY,
|
||||
label,
|
||||
SSHProtocolProvider.PROTOCOL,
|
||||
hostname,
|
||||
port.toString(),
|
||||
username,
|
||||
SSHProtocolProvider.PROTOCOL
|
||||
StringUtils.EMPTY,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -819,7 +883,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 +920,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 +965,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 +1021,11 @@ class NewHostTree : SimpleTree(), Disposable {
|
||||
printer.printRecord(
|
||||
folderNames.joinToString("/"),
|
||||
StringUtils.defaultIfBlank(title, hostname),
|
||||
SSHProtocolProvider.PROTOCOL,
|
||||
hostname,
|
||||
port,
|
||||
username,
|
||||
SSHProtocolProvider.PROTOCOL
|
||||
StringUtils.EMPTY,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -975,6 +1057,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 +1100,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 {
|
||||
@@ -1040,5 +1132,23 @@ class NewHostTree : SimpleTree(), Disposable {
|
||||
electerm,
|
||||
}
|
||||
|
||||
private class NodesTransferable(val nodes: List<HostTreeNode>) : Transferable {
|
||||
companion object {
|
||||
val FLAVOR = DataFlavor("termora/host-tree", "Termora host tree transfers")
|
||||
}
|
||||
|
||||
override fun getTransferDataFlavors(): Array<out DataFlavor> {
|
||||
return arrayOf(FLAVOR)
|
||||
}
|
||||
|
||||
override fun isDataFlavorSupported(flavor: DataFlavor?): Boolean {
|
||||
return flavor == FLAVOR
|
||||
}
|
||||
|
||||
override fun getTransferData(flavor: DataFlavor?): Any {
|
||||
return if (flavor == FLAVOR) nodes else throw UnsupportedFlavorException(flavor)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package app.termora.tree
|
||||
|
||||
import javax.swing.Icon
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
import javax.swing.tree.TreeNode
|
||||
|
||||
abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -35,4 +36,15 @@ abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) {
|
||||
return children
|
||||
}
|
||||
|
||||
open fun getParents(): List<SimpleTreeNode<T>> {
|
||||
val parents = mutableListOf<SimpleTreeNode<T>>()
|
||||
var p = parent as TreeNode?
|
||||
while (p != null) {
|
||||
if (p is SimpleTreeNode<T>) {
|
||||
parents.add(p)
|
||||
}
|
||||
p = p.parent
|
||||
}
|
||||
return parents
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<Identity Name="TermoraDev.Termora"
|
||||
Publisher="CN=C804E131-4368-4BF7-9E7F-95C681AD0AAC"
|
||||
Version="@version@.0"
|
||||
Version="@version@.@betaVersion@"
|
||||
ProcessorArchitecture="@architecture@"/>
|
||||
|
||||
<Properties>
|
||||
|
||||
@@ -45,6 +45,7 @@ termora.settings.appearance.i-want-to-translate=I want to translate
|
||||
termora.settings.appearance.follow-system=Sync with OS
|
||||
termora.settings.appearance.opacity=Opacity
|
||||
termora.settings.appearance.background-running=Backgrounding
|
||||
termora.settings.appearance.tab-order=Tab Order
|
||||
termora.settings.appearance.confirm-tab-close=Confirm tab close
|
||||
|
||||
termora.settings.terminal=Terminal
|
||||
@@ -56,6 +57,9 @@ termora.settings.terminal.debug=Debug mode
|
||||
termora.settings.terminal.beep=Beep
|
||||
termora.settings.terminal.hyperlink=Hyperlink
|
||||
termora.settings.terminal.select-copy=Select copy
|
||||
termora.settings.terminal.right-click=Right click
|
||||
termora.settings.terminal.right-click.copy-and-paste=Copy and Paste
|
||||
termora.settings.terminal.right-click.copy=${termora.copy}
|
||||
termora.settings.terminal.cursor-style=Cursor type
|
||||
termora.settings.terminal.cursor-blink=Cursor blink
|
||||
termora.settings.terminal.local-shell=Local shell
|
||||
@@ -115,6 +119,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 +187,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
|
||||
@@ -233,6 +239,8 @@ termora.keymgr.table.name=Name
|
||||
termora.keymgr.table.type=Type
|
||||
termora.keymgr.table.length=Length
|
||||
termora.keymgr.table.remark=Description
|
||||
termora.keymgr.export-done=The export was successful
|
||||
termora.keymgr.export-done-open-folder=The export was successful. Do you want to open the folder?
|
||||
|
||||
termora.keymgr.ssh-copy-id.number=Number of hosts [{0}] Number of public keys [{1}]
|
||||
termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied}
|
||||
@@ -242,6 +250,7 @@ termora.keymgr.ssh-copy-id.end=End of public key copying
|
||||
|
||||
# Tabbed
|
||||
termora.tabbed.contextmenu.rename=Rename
|
||||
termora.tabbed.contextmenu.select-host=Select Host
|
||||
termora.tabbed.contextmenu.sftp-command=SFTP Command
|
||||
termora.tabbed.contextmenu.sftp-not-install=SFTP programme not found, please install and try again
|
||||
termora.tabbed.contextmenu.clone=Clone
|
||||
|
||||
@@ -40,6 +40,7 @@ termora.settings.appearance.follow-system=Как в системе
|
||||
termora.settings.appearance.opacity=Прозрачность
|
||||
termora.settings.appearance.background-image=Фоновое изображение
|
||||
termora.settings.appearance.background-running=Фон
|
||||
termora.settings.appearance.tab-order=Порядок вкладок
|
||||
termora.settings.appearance.confirm-tab-close=Подтверждение закрытия вкладки
|
||||
|
||||
termora.setting.security=Безопасность
|
||||
@@ -59,6 +60,8 @@ termora.settings.terminal.debug=Режим отладки
|
||||
termora.settings.terminal.beep=Сигнал
|
||||
termora.settings.terminal.hyperlink=Ссылки
|
||||
termora.settings.terminal.select-copy=Копировать выделенное
|
||||
termora.settings.terminal.right-click=правой кнопкой мыши
|
||||
termora.settings.terminal.right-click.copy-and-paste=Копировать и вставить
|
||||
termora.settings.terminal.cursor-style=Вид курсора
|
||||
termora.settings.terminal.cursor-blink=Мигать курсором
|
||||
termora.settings.terminal.local-shell=Локальный терминал
|
||||
@@ -66,27 +69,6 @@ termora.settings.terminal.floating-toolbar=Плавающая панель
|
||||
termora.settings.terminal.auto-close-tab=Автозакрытие вкладки
|
||||
termora.settings.terminal.auto-close-tab-description=Автоматически закрывать вкладку при обычном отключении терминала
|
||||
|
||||
termora.settings.sync=Синхронизация
|
||||
termora.settings.sync.done=Синхронизация успешна
|
||||
termora.settings.sync.export=${termora.keymgr.export}
|
||||
termora.settings.sync.import=${termora.keymgr.import}
|
||||
termora.settings.sync.import.file-too-large=Файл слишком большой
|
||||
termora.settings.sync.import.successful=Импортировано успешно
|
||||
termora.settings.sync.export-done=Экспортировано успешно
|
||||
termora.settings.sync.export-encrypt=Введите пароль для расшифровки файла (выборочно)
|
||||
termora.settings.sync.export-done-open-folder=Экспорт прошел успешно. Открыть папку?
|
||||
termora.settings.sync.range=Диапазон
|
||||
termora.settings.sync.range.keys=Мои ключи
|
||||
termora.settings.sync.range.keyword-highlights=${termora.highlight}
|
||||
termora.settings.sync.last-sync-time=Последняя синхронизация
|
||||
termora.settings.sync.gist=Gist
|
||||
termora.settings.sync.token=Токен
|
||||
termora.settings.sync.type=Сервис
|
||||
termora.settings.sync.webdav.help=WebDAV адрес, https://yourhost/webdav/termora.json
|
||||
termora.settings.sync.policy=Тип синхронизации
|
||||
termora.settings.sync.policy.manual=Вручную
|
||||
termora.settings.sync.policy.on-change=При изменениях
|
||||
|
||||
termora.settings.about=О программе
|
||||
termora.settings.about.author=Автор
|
||||
termora.settings.about.source=Ссылка
|
||||
@@ -101,6 +83,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 +145,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=переменные
|
||||
|
||||
@@ -199,6 +183,8 @@ termora.keymgr.table.name=Название
|
||||
termora.keymgr.table.type=Тип
|
||||
termora.keymgr.table.length=Длина
|
||||
termora.keymgr.table.remark=Описание
|
||||
termora.keymgr.export-done=Экспорт выполнен успешно
|
||||
termora.keymgr.export-done-open-folder=Экспорт выполнен успешно. Открыть папку?
|
||||
|
||||
termora.keymgr.ssh-copy-id.number=Кол-во хостов [{0}] Кол-во публичных ключей [{1}]
|
||||
termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied}
|
||||
@@ -207,6 +193,7 @@ termora.keymgr.ssh-copy-id.end=Копирования открытого клю
|
||||
|
||||
# Tabbed
|
||||
termora.tabbed.contextmenu.rename=Переименовать
|
||||
termora.tabbed.contextmenu.select-host=Выбрать хост
|
||||
termora.tabbed.contextmenu.sftp-command=SFTP Команда
|
||||
termora.tabbed.contextmenu.sftp-not-install=Программа SFTP не найдена, пожалуйста, установите и повторите попытку.
|
||||
termora.tabbed.contextmenu.clone=Дублировать
|
||||
|
||||
@@ -49,6 +49,7 @@ termora.settings.appearance.i-want-to-translate=我想要翻译
|
||||
termora.settings.appearance.follow-system=跟随系统
|
||||
termora.settings.appearance.opacity=透明度
|
||||
termora.settings.appearance.background-running=后台运行
|
||||
termora.settings.appearance.tab-order=标签序号
|
||||
termora.settings.appearance.confirm-tab-close=标签关闭前确认
|
||||
|
||||
# Find everywhere
|
||||
@@ -70,6 +71,8 @@ termora.settings.terminal.debug=调试模式
|
||||
termora.settings.terminal.beep=蜂鸣声
|
||||
termora.settings.terminal.hyperlink=超链接
|
||||
termora.settings.terminal.select-copy=选中复制
|
||||
termora.settings.terminal.right-click=右键点击
|
||||
termora.settings.terminal.right-click.copy-and-paste=复制 & 粘贴
|
||||
termora.settings.terminal.cursor-style=光标样式
|
||||
termora.settings.terminal.cursor-blink=光标闪烁
|
||||
termora.settings.terminal.local-shell=本地终端
|
||||
@@ -127,6 +130,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 +179,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 位字符
|
||||
@@ -227,6 +232,8 @@ termora.keymgr.table.name=名称
|
||||
termora.keymgr.table.type=类型
|
||||
termora.keymgr.table.length=长度
|
||||
termora.keymgr.table.remark=备注
|
||||
termora.keymgr.export-done=导出成功
|
||||
termora.keymgr.export-done-open-folder=导出成功,是否需要打开所在文件夹?
|
||||
|
||||
termora.keymgr.ssh-copy-id.number=主机数量 [{0}] 公钥数量 [{1}]
|
||||
termora.keymgr.ssh-copy-id.failed=复制失败
|
||||
@@ -239,6 +246,7 @@ termora.tools.multiple=将命令发送到当前窗口会话
|
||||
|
||||
# Tabbed
|
||||
termora.tabbed.contextmenu.rename=重命名
|
||||
termora.tabbed.contextmenu.select-host=选中主机
|
||||
termora.tabbed.contextmenu.sftp-command=SFTP 终端
|
||||
termora.tabbed.contextmenu.sftp-not-install=没有找到 SFTP 程序,请安装后重试
|
||||
termora.tabbed.contextmenu.clone=克隆
|
||||
|
||||
@@ -48,6 +48,7 @@ termora.settings.appearance.i-want-to-translate=我想要翻譯
|
||||
termora.settings.appearance.follow-system=跟隨系統
|
||||
termora.settings.appearance.opacity=透明度
|
||||
termora.settings.appearance.background-running=後台運行
|
||||
termora.settings.appearance.tab-order=標籤序號
|
||||
termora.settings.appearance.confirm-tab-close=關閉分頁確認
|
||||
|
||||
termora.settings.keymap=鍵盤
|
||||
@@ -56,6 +57,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=保留原始文件修改時間
|
||||
@@ -78,8 +80,10 @@ termora.settings.terminal.size=大小
|
||||
termora.settings.terminal.max-rows=最大行數
|
||||
termora.settings.terminal.debug=偵錯模式
|
||||
termora.settings.terminal.beep=超連結
|
||||
termora.settings.terminal.hyperlink=Hyperlink
|
||||
termora.settings.terminal.hyperlink=超連結
|
||||
termora.settings.terminal.select-copy=選取複製
|
||||
termora.settings.terminal.right-click=右鍵點擊
|
||||
termora.settings.terminal.right-click.copy-and-paste=複製 & 貼上
|
||||
termora.settings.terminal.cursor-style=遊標風格
|
||||
termora.settings.terminal.cursor-blink=遊標閃爍
|
||||
termora.settings.terminal.local-shell=本地端
|
||||
@@ -178,6 +182,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=預期
|
||||
@@ -223,6 +228,8 @@ termora.keymgr.table.name=名稱
|
||||
termora.keymgr.table.type=型別
|
||||
termora.keymgr.table.length=長度
|
||||
termora.keymgr.table.remark=備註
|
||||
termora.keymgr.export-done=匯出成功
|
||||
termora.keymgr.export-done-open-folder=匯出成功,是否需要打開所在資料夾?
|
||||
|
||||
termora.keymgr.ssh-copy-id.number=主機數量 [{0}] 公鑰數量 [{1}]
|
||||
termora.keymgr.ssh-copy-id.failed=複製失敗
|
||||
@@ -234,6 +241,7 @@ termora.tools.multiple=將命令傳送到目前視窗會話
|
||||
|
||||
# Tabbed
|
||||
termora.tabbed.contextmenu.rename=重新命名
|
||||
termora.tabbed.contextmenu.select-host=選取主機
|
||||
termora.tabbed.contextmenu.sftp-command=SFTP 終端
|
||||
termora.tabbed.contextmenu.sftp-not-install=沒有找到 SFTP 程序,請安裝後重試
|
||||
termora.tabbed.contextmenu.clone=克隆
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.33214 2.63203L13.3321 7.07539C13.4389 7.17028 13.5 7.3063 13.5 7.44914V13C13.5 13.2761 13.2761 13.5 13 13.5H10C9.72386 13.5 9.5 13.2761 9.5 13V11C9.5 10.1716 8.82843 9.5 8 9.5C7.17157 9.5 6.5 10.1716 6.5 11V13C6.5 13.2761 6.27614 13.5 6 13.5H3C2.72386 13.5 2.5 13.2761 2.5 13V7.44914C2.5 7.3063 2.56109 7.17028 2.66786 7.07539L7.66786 2.63203C7.85729 2.46369 8.14271 2.46369 8.33214 2.63203Z" fill="#EBECF0" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.33214 2.63203L13.3321 7.07539C13.4389 7.17028 13.5 7.3063 13.5 7.44914V13C13.5 13.2761 13.2761 13.5 13 13.5H10C9.72386 13.5 9.5 13.2761 9.5 13V11C9.5 10.1716 8.82843 9.5 8 9.5C7.17157 9.5 6.5 10.1716 6.5 11V13C6.5 13.2761 6.27614 13.5 6 13.5H3C2.72386 13.5 2.5 13.2761 2.5 13V7.44914C2.5 7.3063 2.56109 7.17028 2.66786 7.07539L7.66786 2.63203C7.85729 2.46369 8.14271 2.46369 8.33214 2.63203Z" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 716 B After Width: | Height: | Size: 701 B |
@@ -1,4 +1,4 @@
|
||||
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.33214 2.63203L13.3321 7.07539C13.4389 7.17028 13.5 7.3063 13.5 7.44914V13C13.5 13.2761 13.2761 13.5 13 13.5H10C9.72386 13.5 9.5 13.2761 9.5 13V11C9.5 10.1716 8.82843 9.5 8 9.5C7.17157 9.5 6.5 10.1716 6.5 11V13C6.5 13.2761 6.27614 13.5 6 13.5H3C2.72386 13.5 2.5 13.2761 2.5 13V7.44914C2.5 7.3063 2.56109 7.17028 2.66786 7.07539L7.66786 2.63203C7.85729 2.46369 8.14271 2.46369 8.33214 2.63203Z" fill="#43454A" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.33214 2.63203L13.3321 7.07539C13.4389 7.17028 13.5 7.3063 13.5 7.44914V13C13.5 13.2761 13.2761 13.5 13 13.5H10C9.72386 13.5 9.5 13.2761 9.5 13V11C9.5 10.1716 8.82843 9.5 8 9.5C7.17157 9.5 6.5 10.1716 6.5 11V13C6.5 13.2761 6.27614 13.5 6 13.5H3C2.72386 13.5 2.5 13.2761 2.5 13V7.44914C2.5 7.3063 2.56109 7.17028 2.66786 7.07539L7.66786 2.63203C7.85729 2.46369 8.14271 2.46369 8.33214 2.63203Z" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 716 B After Width: | Height: | Size: 701 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user