Compare commits

...

54 Commits

Author SHA1 Message Date
dependabot[bot]
de7a7c93b8 chore(deps): bump org.glassfish.jaxb:jaxb-runtime from 2.3.3 to 4.0.6
Bumps org.glassfish.jaxb:jaxb-runtime from 2.3.3 to 4.0.6.

---
updated-dependencies:
- dependency-name: org.glassfish.jaxb:jaxb-runtime
  dependency-version: 4.0.6
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-24 02:18:03 +00:00
hstyi
fcec30d70a chore: editor shows tooltip 2025-09-24 09:30:36 +08:00
dependabot[bot]
f6dc0098f7 chore(deps): bump com.github.oshi:oshi-core from 6.8.1 to 6.9.0
Bumps [com.github.oshi:oshi-core](https://github.com/oshi/oshi) from 6.8.1 to 6.9.0.
- [Release notes](https://github.com/oshi/oshi/releases)
- [Changelog](https://github.com/oshi/oshi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/oshi/oshi/compare/oshi-parent-6.8.1...oshi-parent-6.9.0)

---
updated-dependencies:
- dependency-name: com.github.oshi:oshi-core
  dependency-version: 6.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-23 09:07:17 +08:00
hstyi
ca7b30bdb0 chore: improve code 2025-09-19 14:48:08 +08:00
imblowsnow
f73e7f4214 feat: show remark on node hover 2025-09-18 09:04:59 +08:00
hstyi
613a1ca78a release: 2.0.0-beta.14 2025-09-15 17:27:41 +08:00
dependabot[bot]
bf9e3ea2e2 chore(deps): bump com.qcloud:cos_api from 5.6.253 to 5.6.255
Bumps [com.qcloud:cos_api](https://github.com/tencentyun/cos-java-sdk-v5) from 5.6.253 to 5.6.255.
- [Release notes](https://github.com/tencentyun/cos-java-sdk-v5/releases)
- [Changelog](https://github.com/tencentyun/cos-java-sdk-v5/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tencentyun/cos-java-sdk-v5/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-15 17:18:58 +08:00
dependabot[bot]
a4390c4c6d chore(deps): bump org.commonmark:commonmark from 0.25.1 to 0.26.0
Bumps [org.commonmark:commonmark](https://github.com/commonmark/commonmark-java) from 0.25.1 to 0.26.0.
- [Release notes](https://github.com/commonmark/commonmark-java/releases)
- [Changelog](https://github.com/commonmark/commonmark-java/blob/main/CHANGELOG.md)
- [Commits](https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.25.1...commonmark-parent-0.26.0)

---
updated-dependencies:
- dependency-name: org.commonmark:commonmark
  dependency-version: 0.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-15 17:18:49 +08:00
hstyi
9cf317e245 fix: macros cannot sync 2025-09-15 07:01:18 +08:00
hstyi
d000d73122 fix: SFTP connection may time out 2025-09-12 10:22:39 +08:00
dependabot[bot]
88613ed2f6 chore(deps): bump kotlin from 2.2.10 to 2.2.20
Bumps `kotlin` from 2.2.10 to 2.2.20.

Updates `org.jetbrains.kotlin.jvm` from 2.2.10 to 2.2.20
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.2.10...v2.2.20)

Updates `org.jetbrains.kotlin.plugin.serialization` from 2.2.10 to 2.2.20
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.2.10...v2.2.20)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlin.jvm
  dependency-version: 2.2.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.kotlin.plugin.serialization
  dependency-version: 2.2.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-12 08:55:38 +08:00
hstyi
2fc381caa5 fix: virtual window auto hiding 2025-09-08 11:07:06 +08:00
hstyi
30e245f7a3 chore: add tooltip to some buttons 2025-09-08 10:59:18 +08:00
hstyi
35cf92e685 fix: exposed compile 2025-09-08 09:25:39 +08:00
hstyi
522ee44ca2 chore: upgrade exposed version 2025-09-06 10:54:31 +08:00
hstyi
5cf03e1f1f fix: transfer text error 2025-09-05 15:18:11 +08:00
dependabot[bot]
afca4ddf0e chore(deps): bump com.qcloud:cos_api from 5.6.251 to 5.6.253
Bumps [com.qcloud:cos_api](https://github.com/tencentyun/cos-java-sdk-v5) from 5.6.251 to 5.6.253.
- [Release notes](https://github.com/tencentyun/cos-java-sdk-v5/releases)
- [Changelog](https://github.com/tencentyun/cos-java-sdk-v5/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tencentyun/cos-java-sdk-v5/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 19:39:08 +08:00
dependabot[bot]
ca757f975a chore(deps): bump cn.hutool:hutool-all from 5.8.39 to 5.8.40
Bumps [cn.hutool:hutool-all](https://github.com/looly/hutool) from 5.8.39 to 5.8.40.
- [Release notes](https://github.com/looly/hutool/releases)
- [Changelog](https://github.com/chinabugotech/hutool/blob/v5-master/CHANGELOG.md)
- [Commits](https://github.com/looly/hutool/compare/5.8.39...5.8.40)

---
updated-dependencies:
- dependency-name: cn.hutool:hutool-all
  dependency-version: 5.8.40
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 06:11:18 +08:00
dependabot[bot]
79c304ae3d chore(deps): bump com.maxmind.geoip2:geoip2 from 4.3.1 to 4.4.0
Bumps [com.maxmind.geoip2:geoip2](https://github.com/maxmind/GeoIP2-java) from 4.3.1 to 4.4.0.
- [Release notes](https://github.com/maxmind/GeoIP2-java/releases)
- [Changelog](https://github.com/maxmind/GeoIP2-java/blob/main/CHANGELOG.md)
- [Commits](https://github.com/maxmind/GeoIP2-java/compare/v4.3.1...v4.4.0)

---
updated-dependencies:
- dependency-name: com.maxmind.geoip2:geoip2
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-29 16:48:43 +08:00
dependabot[bot]
1848c869e7 chore(deps): bump com.github.hstyi:geolite2
Bumps [com.github.hstyi:geolite2](https://github.com/hstyi/GeoLite2) from v1.0-202508110059 to v1.0-202508180058.
- [Release notes](https://github.com/hstyi/GeoLite2/releases)
- [Commits](https://github.com/hstyi/GeoLite2/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-25 11:10:58 +08:00
hsurich
029e570551 chore: improve SFTP file selection and deletion UX 2025-08-21 14:54:13 +08:00
dependabot[bot]
905c570e4c chore(deps): bump com.github.mwiede:jsch from 2.27.2 to 2.27.3
Bumps [com.github.mwiede:jsch](https://github.com/mwiede/jsch) from 2.27.2 to 2.27.3.
- [Release notes](https://github.com/mwiede/jsch/releases)
- [Changelog](https://github.com/mwiede/jsch/blob/master/ChangeLog)
- [Commits](https://github.com/mwiede/jsch/compare/jsch-2.27.2...jsch-2.27.3)

---
updated-dependencies:
- dependency-name: com.github.mwiede:jsch
  dependency-version: 2.27.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-21 12:05:00 +08:00
hstyi
a3069229b8 chore: ssh supports modifying the backspace behaviour 2025-08-20 21:31:37 +08:00
hstyi
1e930d61c9 fix: xterm Send Device Attributes 2025-08-20 17:05:19 +08:00
hstyi
0015c3a7fb chore: select host 2025-08-20 17:04:58 +08:00
hstyi
4bfb87e5c7 fix: connection timeout 2025-08-20 14:59:12 +08:00
hstyi
4fbb626c42 chore: find panel circle navigation 2025-08-20 10:52:07 +08:00
hstyi
35b175d944 fix: settings dialog scale 2025-08-20 09:08:58 +08:00
hstyi
5939297550 fix: some i18n errors 2025-08-18 15:40:25 +08:00
hstyi
e6e5867742 chore: Windows pack 2025-08-18 15:18:39 +08:00
hstyi
bd9b73ad6a release: 2.0.0-beta.13 2025-08-17 11:46:43 +08:00
hstyi
dbea769994 fix: sftp command may cause bad key types 2025-08-17 08:54:42 +08:00
hstyi
9cd83c4025 feat: host tree supports copy and paste 2025-08-15 16:35:43 +08:00
hstyi
d4cc080e7b chore: transfer text 2025-08-15 16:35:32 +08:00
hstyi
a324bc3d96 fix: keyword highlighting causes crash 2025-08-15 13:12:55 +08:00
dependabot[bot]
36929e9ea3 chore(deps): bump kotlin from 2.2.0 to 2.2.10
Bumps `kotlin` from 2.2.0 to 2.2.10.

Updates `org.jetbrains.kotlin.jvm` from 2.2.0 to 2.2.10
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.2.0...v2.2.10)

Updates `org.jetbrains.kotlin.plugin.serialization` from 2.2.0 to 2.2.10
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.2.0...v2.2.10)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlin.jvm
  dependency-version: 2.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.kotlin.plugin.serialization
  dependency-version: 2.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-15 11:10:11 +08:00
hstyi
dd73b933d9 fix: some data cannot be pulled 2025-08-14 17:52:33 +08:00
hstyi
117a9ea692 fix: icons not displaying on some Linux systems 2025-08-13 09:36:59 +08:00
dependabot[bot]
2f932de295 chore(deps): bump com.huaweicloud:esdk-obs-java-bundle
Bumps [com.huaweicloud:esdk-obs-java-bundle](https://github.com/huaweicloud/huaweicloud-sdk-java-obs) from 3.25.5 to 3.25.7.
- [Release notes](https://github.com/huaweicloud/huaweicloud-sdk-java-obs/releases)
- [Commits](https://github.com/huaweicloud/huaweicloud-sdk-java-obs/compare/v3.25.5...v3.25.7)

---
updated-dependencies:
- dependency-name: com.huaweicloud:esdk-obs-java-bundle
  dependency-version: 3.25.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-13 09:36:18 +08:00
hstyi
679b24a74d chore: simplify host field input 2025-08-12 18:39:54 +08:00
hstyi
c6b33ea828 chore: improve settings action 2025-08-12 18:39:45 +08:00
dependabot[bot]
a4ea8f2491 chore(deps): bump com.github.hstyi:geolite2
Bumps [com.github.hstyi:geolite2](https://github.com/hstyi/GeoLite2) from v1.0-202508040102 to v1.0-202508110059.
- [Release notes](https://github.com/hstyi/GeoLite2/releases)
- [Commits](https://github.com/hstyi/GeoLite2/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 18:39:37 +08:00
hstyi
1c2315b5e9 fix: Linux unable to open local terminal 2025-08-12 10:22:08 +08:00
hstyi
d48e412580 release: 2.0.0-beta.12 2025-08-12 09:44:27 +08:00
dependabot[bot]
3b3fb41384 chore(deps): bump com.qcloud:cos_api from 5.6.249 to 5.6.251
Bumps [com.qcloud:cos_api](https://github.com/tencentyun/cos-java-sdk-v5) from 5.6.249 to 5.6.251.
- [Release notes](https://github.com/tencentyun/cos-java-sdk-v5/releases)
- [Changelog](https://github.com/tencentyun/cos-java-sdk-v5/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tencentyun/cos-java-sdk-v5/compare/v5.6.249...v5.6.251)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 08:17:38 +08:00
hstyi
190ac697fb chore: turn off the ApplePressAndHoldEnabled 2025-08-09 16:24:43 +08:00
hstyi
8cdbf24cdc fix: file name containing : cannot be transferred 2025-08-09 15:56:32 +08:00
hstyi
6e182b6813 chore: remember the colspan state of the fence layout 2025-08-09 15:33:22 +08:00
hstyi
3fa4064655 feat: hyperlinks require holding down the function key to open 2025-08-09 12:45:06 +08:00
hstyi
a77a03d8b3 fix: transfer causing repositioning after refresh 2025-08-09 12:29:19 +08:00
hstyi
5f8b9d36e2 chore: Agent Forwarding 2025-08-09 11:45:37 +08:00
hstyi
1ed5e164de feat: linux window opacity 2025-08-08 18:12:00 +08:00
hstyi
c67d5b0276 feat: ssh ForwardAgent 2025-08-08 18:11:51 +08:00
hstyi
9646a98f6d feat: background image supports fill mode 2025-08-08 15:06:13 +08:00
69 changed files with 836 additions and 162 deletions

View File

@@ -1 +1 @@
2.0.0-beta.11 2.0.0-beta.14

View File

@@ -233,9 +233,10 @@ tasks.register<Copy>("copy-dependencies") {
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") } exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) { } else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
val osName = if (os.isWindows) "win32" else if (os.isMacOsX) "darwin" else "linux" val osName = if (os.isWindows) "win32" else if (os.isMacOsX) "darwin" else "linux"
val targetDir = FileUtils.getFile(dylib, pty4j.name, osName)
FileUtils.forceMkdir(targetDir)
val myArchName = if (arch.isArm) "aarch64" else "x86-64" 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) { if (os.isWindows) {
// @formatter:off // @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/*win/${myArchName}/*", "-d", targetDir.absolutePath) } exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/*win/${myArchName}/*", "-d", targetDir.absolutePath) }
@@ -383,6 +384,7 @@ tasks.register<Exec>("jpackage") {
} }
if (os.isLinux) { if (os.isLinux) {
options.add("--add-opens=java.desktop/sun.awt.X11=ALL-UNNAMED")
if (isDeb) { if (isDeb) {
options.add("-Djpackage.app-layout=deb") options.add("-Djpackage.app-layout=deb")
} }
@@ -402,18 +404,6 @@ tasks.register<Exec>("jpackage") {
arguments.addAll(listOf("--copyright", "TermoraDev")) arguments.addAll(listOf("--copyright", "TermoraDev"))
arguments.addAll(listOf("--app-content", "$buildDir/plugins")) 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) { if (os.isMacOsX) {
arguments.addAll(listOf("--mac-package-name", project.name.uppercaseFirstChar())) arguments.addAll(listOf("--mac-package-name", project.name.uppercaseFirstChar()))
arguments.addAll(listOf("--mac-app-category", "developer-tools")) arguments.addAll(listOf("--mac-app-category", "developer-tools"))
@@ -680,17 +670,24 @@ fun packOnLinux(distributionDir: Directory, finalFilenameWithoutExtension: Strin
exec { commandLine("chmod", "+x", appimagetool.absolutePath) } exec { commandLine("chmod", "+x", appimagetool.absolutePath) }
} }
// Desktop file // Desktop file
val termoraName = project.name.uppercaseFirstChar() 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 val desktopFile = distributionDir.file(termoraName + File.separator + termoraName + ".desktop").asFile
desktopFile.writeText( desktopFile.writeText(
"""[Desktop Entry] """[Desktop Entry]
Type=Application Type=Application
Name=${termoraName} Name=${termoraName}
Comment=Terminal emulator and SSH client Comment=Terminal emulator and SSH client
Icon=/lib/${termoraName} Icon=${termoraName}
Categories=Development; Categories=Development;
StartupWMClass=${termoraName}
Terminal=false Terminal=false
""".trimIndent() """.trimIndent()
) )

View File

@@ -1,5 +1,5 @@
[versions] [versions]
kotlin = "2.2.0" kotlin = "2.2.20"
slf4j = "2.0.17" slf4j = "2.0.17"
pty4j = "0.13.10" pty4j = "0.13.10"
tinylog = "2.7.0" tinylog = "2.7.0"
@@ -16,19 +16,19 @@ commons-vfs2 = "2.10.0"
swingx = "1.6.5-1" swingx = "1.6.5-1"
jgoodies-forms = "1.9.0" jgoodies-forms = "1.9.0"
jfa = "1.2.0" jfa = "1.2.0"
oshi = "6.8.1" oshi = "6.9.0"
versioncompare = "1.4.1" versioncompare = "1.4.1"
jna = "5.17.0" jna = "5.17.0"
jSystemThemeDetector = "3.9.1" jSystemThemeDetector = "3.9.1"
commons-io = "2.20.0" commons-io = "2.20.0"
jbr-api = "17.1.10.1" jbr-api = "17.1.10.1"
hutool = "5.8.39" hutool = "5.8.40"
jsch = "2.27.2" jsch = "2.27.3"
okhttp = "5.1.0" okhttp = "5.1.0"
sshj = "0.39.0" sshj = "0.39.0"
sshd-core = "2.15.0" sshd-core = "2.15.0"
jgit = "7.2.0.202503040940-r" jgit = "7.2.0.202503040940-r"
commonmark = "0.25.1" commonmark = "0.26.0"
jnafilechooser = "1.1.2" jnafilechooser = "1.1.2"
xodus = "2.0.1" xodus = "2.0.1"
bip39 = "1.0.9" bip39 = "1.0.9"
@@ -41,7 +41,7 @@ jSerialComm = "2.11.2"
ini4j = "0.5.5-2" ini4j = "0.5.5-2"
restart4j = "0.0.1" restart4j = "0.0.1"
eddsa = "0.3.0" eddsa = "0.3.0"
exposed = "1.0.0-beta-5" exposed = "1.0.0-rc-1"
h2 = "2.3.232" h2 = "2.3.232"
sqlite = "3.50.3.0" sqlite = "3.50.3.0"
jug = "5.1.0" jug = "5.1.0"
@@ -106,7 +106,7 @@ eddsa = { module = "net.i2p.crypto:eddsa", version.ref = "eddsa" }
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
exposed-crypt = { module = "org.jetbrains.exposed:exposed-crypt", version.ref = "exposed" } exposed-crypt = { module = "org.jetbrains.exposed:exposed-crypt", version.ref = "exposed" }
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" } exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
exposed-migration = { module = "org.jetbrains.exposed:exposed-migration", version.ref = "exposed" } exposed-migration = { module = "org.jetbrains.exposed:exposed-migration-core", version.ref = "exposed" }
h2 = { module = "com.h2database:h2", version.ref = "h2" } h2 = { module = "com.h2database:h2", version.ref = "h2" }
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" } sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
jug = { module = "com.fasterxml.uuid:java-uuid-generator", version.ref = "jug" } jug = { module = "com.fasterxml.uuid:java-uuid-generator", version.ref = "jug" }

View File

@@ -3,7 +3,7 @@ plugins {
} }
project.version = "0.0.5" project.version = "0.0.6"

View File

@@ -18,4 +18,8 @@ object Appearance {
set(value) { set(value) {
enableManager.setFlag("Plugins.bg.interval", 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)
} }

View File

@@ -2,6 +2,8 @@ package app.termora.plugins.bg
import app.termora.GlassPaneExtension import app.termora.GlassPaneExtension
import app.termora.WindowScope import app.termora.WindowScope
import app.termora.restore
import app.termora.save
import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLaf
import java.awt.AlphaComposite import java.awt.AlphaComposite
import java.awt.Graphics2D import java.awt.Graphics2D
@@ -12,15 +14,52 @@ class BGGlassPaneExtension private constructor() : GlassPaneExtension {
val instance = BGGlassPaneExtension() val instance = BGGlassPaneExtension()
} }
override fun paint(scope: WindowScope, c: JComponent, g2d: Graphics2D) { override fun paint(scope: WindowScope, c: JComponent, g2d: Graphics2D) {
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
g2d.save()
g2d.composite = AlphaComposite.getInstance( g2d.composite = AlphaComposite.getInstance(
AlphaComposite.SRC_OVER, AlphaComposite.SRC_OVER,
if (FlatLaf.isLafDark()) 0.2f else 0.1f 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()
} }
} }

View File

@@ -10,6 +10,8 @@ import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.exception.ExceptionUtils import org.apache.commons.lang3.exception.ExceptionUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Component
import java.awt.event.ItemEvent
import java.io.File import java.io.File
import java.nio.file.StandardCopyOption import java.nio.file.StandardCopyOption
import javax.swing.* import javax.swing.*
@@ -23,6 +25,7 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
private val owner get() = SwingUtilities.getWindowAncestor(this) private val owner get() = SwingUtilities.getWindowAncestor(this)
val backgroundImageTextField = OutlineTextField() val backgroundImageTextField = OutlineTextField()
val fillModeComboBox = OutlineComboBox<FillMode>()
val intervalSpinner = NumberSpinner(360, minimum = 30, maximum = 86400) val intervalSpinner = NumberSpinner(360, minimum = 30, maximum = 86400)
private val backgroundButton = JButton(Icons.folder) private val backgroundButton = JButton(Icons.folder)
@@ -36,6 +39,38 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
private fun initView() { 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.isEditable = false
backgroundImageTextField.trailingComponent = backgroundButton backgroundImageTextField.trailingComponent = backgroundButton
backgroundImageTextField.text = Appearance.backgroundImage backgroundImageTextField.text = Appearance.backgroundImage
@@ -80,6 +115,15 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
Appearance.interval = value 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) { private fun onSelectedBackgroundImage(file: File) {
@@ -124,7 +168,7 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
private fun getFormPanel(): JPanel { private fun getFormPanel(): JPanel {
val layout = FormLayout( val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default", "left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default",
"pref, $FORM_MARGIN, pref" "pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
) )
var rows = 1 var rows = 1
@@ -138,6 +182,10 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
.add(bgClearBox).xy(5, rows) .add(bgClearBox).xy(5, rows)
.apply { rows += step } .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) builder.add("${BGI18n.getString("termora.plugins.bg.interval")}:").xy(1, rows)
.add(intervalSpinner).xy(3, rows) .add(intervalSpinner).xy(3, rows)
.apply { rows += step } .apply { rows += step }

View File

@@ -0,0 +1,8 @@
package app.termora.plugins.bg
enum class FillMode {
STRETCH, // 拉伸
FIT, // 等比例铺满
CENTER, // 居中
TILE, // 平铺
}

View File

@@ -1,2 +1,7 @@
termora.plugins.bg.interval=Interval 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 termora.plugins.bg.background-image=Background Image

View File

@@ -1,2 +1,8 @@
termora.plugins.bg.background-image=背景图 termora.plugins.bg.background-image=背景图
termora.plugins.bg.interval=切换间隔 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=平铺

View File

@@ -1,2 +1,8 @@
termora.plugins.bg.background-image=背景圖 termora.plugins.bg.background-image=背景圖
termora.plugins.bg.interval=切換間隔 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=平鋪

View File

@@ -8,7 +8,7 @@ project.version = "0.0.4"
dependencies { dependencies {
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
implementation("com.qcloud:cos_api:5.6.249") implementation("com.qcloud:cos_api:5.6.255")
compileOnly(project(":")) compileOnly(project(":"))
} }

View File

@@ -4,7 +4,7 @@ plugins {
project.version = "0.0.7" project.version = "0.0.8"
dependencies { dependencies {

View File

@@ -1,9 +1,6 @@
package app.termora.plugins.editor package app.termora.plugins.editor
import app.termora.DocumentAdaptor import app.termora.*
import app.termora.DynamicColor
import app.termora.EnableManager
import app.termora.Icons
import app.termora.database.DatabaseManager import app.termora.database.DatabaseManager
import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.components.FlatTextField import com.formdev.flatlaf.extras.components.FlatTextField
@@ -39,6 +36,10 @@ class EditorPanel(private val window: JFrame, private val file: File) : JPanel(B
companion object { companion object {
private val log = LoggerFactory.getLogger(EditorPanel::class.java) private val log = LoggerFactory.getLogger(EditorPanel::class.java)
private val saveIcon = DynamicIcon(
"icons/save.svg", "icons/save_dark.svg",
loader = EditorPlugin::class.java.classLoader
)
} }
private var text = file.readText(Charsets.UTF_8) private var text = file.readText(Charsets.UTF_8)
@@ -54,6 +55,7 @@ class EditorPanel(private val window: JFrame, private val file: File) : JPanel(B
private val prevBtn = JButton(Icons.up) private val prevBtn = JButton(Icons.up)
private val context = SearchContext() private val context = SearchContext()
private val softWrapBtn = JToggleButton(Icons.softWrap) private val softWrapBtn = JToggleButton(Icons.softWrap)
private val saveBtn = JButton(saveIcon)
private val scrollUpBtn = JButton(Icons.scrollUp) private val scrollUpBtn = JButton(Icons.scrollUp)
private val scrollEndBtn = JButton(Icons.scrollDown) private val scrollEndBtn = JButton(Icons.scrollDown)
private val prettyBtn = JButton(Icons.reformatCode) private val prettyBtn = JButton(Icons.reformatCode)
@@ -141,11 +143,18 @@ class EditorPanel(private val window: JFrame, private val file: File) : JPanel(B
) )
toolbar.orientation = VERTICAL toolbar.orientation = VERTICAL
toolbar.add(saveBtn)
toolbar.add(scrollUpBtn) toolbar.add(scrollUpBtn)
toolbar.add(prettyBtn) toolbar.add(prettyBtn)
toolbar.add(softWrapBtn) toolbar.add(softWrapBtn)
toolbar.add(scrollEndBtn) toolbar.add(scrollEndBtn)
saveBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.save")
scrollUpBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.first-line")
scrollEndBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.last-line")
softWrapBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.soft-wrap")
prettyBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.format")
val viewPanel = JPanel(BorderLayout()) val viewPanel = JPanel(BorderLayout())
viewPanel.add(scrollPane, BorderLayout.CENTER) viewPanel.add(scrollPane, BorderLayout.CENTER)
viewPanel.add(toolbar, BorderLayout.EAST) viewPanel.add(toolbar, BorderLayout.EAST)
@@ -211,6 +220,8 @@ class EditorPanel(private val window: JFrame, private val file: File) : JPanel(B
} }
}) })
saveBtn.addActionListener(textArea.actionMap.get("Save"))
textArea.actionMap.put("Format", object : AbstractAction() { textArea.actionMap.put("Format", object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(e: ActionEvent) {
format() format()

View File

@@ -1,2 +1,6 @@
termora.plugins.editor.not-save=The file has not been saved. Are you sure you want to exit? termora.plugins.editor.not-save=The file has not been saved. Are you sure you want to exit?
termora.plugins.editor.save=Save
termora.plugins.editor.first-line=Jump to first line
termora.plugins.editor.last-line=Jump to last line
termora.plugins.editor.soft-wrap=Soft-wrap
termora.plugins.editor.format=Format

View File

@@ -1 +1,6 @@
termora.plugins.editor.not-save=Файл не сохранён. Вы уверены, что хотите выйти? termora.plugins.editor.not-save=Файл не сохранён. Вы уверены, что хотите выйти?
termora.plugins.editor.save=Сохранить
termora.plugins.editor.first-line=Перейти на первую строку
termora.plugins.editor.last-line=Перейти на последнюю строку
termora.plugins.editor.soft-wrap=Мягкий перенос
termora.plugins.editor.format=Формат

View File

@@ -1 +1,6 @@
termora.plugins.editor.not-save=文件尚未保存,你确定要退出吗? termora.plugins.editor.not-save=文件尚未保存,你确定要退出吗?
termora.plugins.editor.save=保存
termora.plugins.editor.first-line=跳转到第一行
termora.plugins.editor.last-line=跳转到最后一行
termora.plugins.editor.soft-wrap=自动换行
termora.plugins.editor.format=格式化

View File

@@ -1 +1,6 @@
termora.plugins.editor.not-save=檔案尚未儲存,你確定要退出嗎? termora.plugins.editor.not-save=檔案尚未儲存,你確定要退出嗎?
termora.plugins.editor.save=儲存
termora.plugins.editor.first-line=跳到第一行
termora.plugins.editor.last-line=跳到最後一行
termora.plugins.editor.soft-wrap=自動換行
termora.plugins.editor.format=格式化

View File

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2022 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="M5.5 3V5.5H10.5V3M4.5 13V9.5H11.5V13M2.5 13.5V2.5H11.5L13.5 4.5V13.5H2.5Z" stroke="#6C707E" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 357 B

View File

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2022 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="M5.5 3V5.5H10.5V3M4.5 13V9.5H11.5V13M2.5 13.5V2.5H11.5L13.5 4.5V13.5H2.5Z" stroke="#CED0D6" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 357 B

View File

@@ -7,9 +7,9 @@ project.version = "0.0.8"
dependencies { dependencies {
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
compileOnly(project(":")) compileOnly(project(":"))
implementation("com.maxmind.geoip2:geoip2:4.3.1") implementation("com.maxmind.geoip2:geoip2:4.4.0")
// https://github.com/hstyi/geolite2 // https://github.com/hstyi/geolite2
implementation("com.github.hstyi:geolite2:v1.0-202508040102") implementation("com.github.hstyi:geolite2:v1.0-202508180058")
} }
apply(from = "$rootDir/plugins/common.gradle.kts") apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -8,7 +8,7 @@ project.version = "0.0.2"
dependencies { dependencies {
testImplementation(kotlin("test")) 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(":")) compileOnly(project(":"))
} }

View File

@@ -9,7 +9,7 @@ dependencies {
implementation("com.aliyun.oss:aliyun-sdk-oss:3.18.3") implementation("com.aliyun.oss:aliyun-sdk-oss:3.18.3")
implementation("javax.xml.bind:jaxb-api:2.3.1") implementation("javax.xml.bind:jaxb-api:2.3.1")
implementation("javax.activation:activation:1.1.1") implementation("javax.activation:activation:1.1.1")
implementation("org.glassfish.jaxb:jaxb-runtime:2.3.3") implementation("org.glassfish.jaxb:jaxb-runtime:4.0.6")
compileOnly(project(":")) compileOnly(project(":"))
} }

View File

@@ -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=Sync
termora.settings.sync.done=Synchronized data successfully 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=Range
termora.settings.sync.range.keys=My keys termora.settings.sync.range.keys=My keys
termora.settings.sync.range.keyword-highlights=${termora.highlight} termora.settings.sync.range.keyword-highlights=${termora.highlight}

View File

@@ -2,15 +2,10 @@ termora.plugins.sync.disabled-sync=你已登录,无法使用此功能
termora.settings.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=范围
termora.settings.sync.range.keys=我的密钥 termora.settings.sync.range.keys=我的密钥
termora.settings.sync.last-sync-time=最后同步时间 termora.settings.sync.last-sync-time=最后同步时间
termora.settings.sync.done=同步数据成功 termora.settings.sync.done=同步数据成功
termora.settings.sync.import.file-too-large=文件太大
termora.settings.sync.import.successful=导入数据成功
termora.settings.sync.gist=片段 termora.settings.sync.gist=片段
termora.settings.sync.token=令牌 termora.settings.sync.token=令牌
termora.settings.sync.type=类型 termora.settings.sync.type=类型

View File

@@ -1,9 +1,6 @@
termora.plugins.sync.disabled-sync=你已登錄,無法使用此功能 termora.plugins.sync.disabled-sync=你已登錄,無法使用此功能
termora.settings.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=範圍
termora.settings.sync.range.keys=我的密鑰 termora.settings.sync.range.keys=我的密鑰
termora.settings.sync.last-sync-time=最後同步時間 termora.settings.sync.last-sync-time=最後同步時間

View File

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

View File

@@ -9,6 +9,7 @@ import org.apache.commons.lang3.SystemUtils
import org.apache.commons.lang3.math.NumberUtils import org.apache.commons.lang3.math.NumberUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.tinylog.configuration.Configuration import org.tinylog.configuration.Configuration
import java.awt.Toolkit
import java.io.File import java.io.File
import kotlin.system.exitProcess import kotlin.system.exitProcess
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
@@ -35,10 +36,20 @@ class ApplicationInitializr {
// 检查是否单例 // 检查是否单例
checkSingleton() checkSingleton()
if (SystemUtils.IS_OS_MAC_OSX) { if (SystemInfo.isMacOS) {
System.setProperty("apple.awt.application.name", Application.getName()) 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 runtime = measureTimeMillis { ApplicationRunner().run() }
val log = LoggerFactory.getLogger(javaClass) val log = LoggerFactory.getLogger(javaClass)

View File

@@ -9,6 +9,7 @@ internal class FramePlugin : InternalPlugin() {
init { init {
support.addExtension(DatabasePropertiesChangedExtension::class.java) { KeymapRefresher.getInstance() } support.addExtension(DatabasePropertiesChangedExtension::class.java) { KeymapRefresher.getInstance() }
support.addExtension(DatabaseChangedExtension::class.java) { KeymapRefresher.getInstance() } support.addExtension(DatabaseChangedExtension::class.java) { KeymapRefresher.getInstance() }
support.addExtension(ApplicationRunnerExtension::class.java) { ApplePressAndHoldEnabledApplicationRunnerExtension.instance }
} }
override fun getName(): String { override fun getName(): String {

View File

@@ -1,6 +1,7 @@
package app.termora package app.termora
import app.termora.database.DatabaseManager import app.termora.database.DatabaseManager
import com.formdev.flatlaf.util.UIScale
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Dimension import java.awt.Dimension
import java.awt.Window import java.awt.Window
@@ -14,7 +15,10 @@ internal class SettingsDialog(owner: Window) : DialogWrapper(owner) {
private val properties get() = DatabaseManager.getInstance().properties private val properties get() = DatabaseManager.getInstance().properties
init { 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 isModal = true
title = I18n.getString("termora.setting") title = I18n.getString("termora.setting")
setLocationRelativeTo(null) setLocationRelativeTo(null)

View File

@@ -21,6 +21,7 @@ import com.jgoodies.forms.layout.FormLayout
import com.jthemedetecor.OsThemeDetector import com.jthemedetecor.OsThemeDetector
import com.sun.jna.LastErrorException import com.sun.jna.LastErrorException
import com.sun.jna.Native import com.sun.jna.Native
import com.sun.jna.platform.WindowUtils
import com.sun.jna.platform.win32.Shell32 import com.sun.jna.platform.win32.Shell32
import com.sun.jna.platform.win32.ShlObj import com.sun.jna.platform.win32.ShlObj
import com.sun.jna.platform.win32.WinDef import com.sun.jna.platform.win32.WinDef
@@ -169,7 +170,8 @@ class SettingsOptionsPane : OptionsPane() {
backgroundComBoBox.isEnabled = SystemInfo.isWindows || SystemInfo.isMacOS 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) { opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) {
override fun getNextValue(): Any { override fun getNextValue(): Any {
return super.getNextValue() ?: maximum return super.getNextValue() ?: maximum

View File

@@ -2,6 +2,7 @@ package app.termora
import app.termora.actions.AnAction import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
import app.termora.plugin.internal.extension.DynamicExtensionHandler
import app.termora.tree.NewHostTree import app.termora.tree.NewHostTree
import com.formdev.flatlaf.extras.components.FlatTabbedPane import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.extras.components.FlatToolBar import com.formdev.flatlaf.extras.components.FlatToolBar
@@ -9,15 +10,14 @@ import com.formdev.flatlaf.util.SystemInfo
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Dimension import java.awt.Dimension
import java.awt.Font import java.awt.Font
import java.awt.event.ComponentAdapter import java.awt.event.*
import java.awt.event.ComponentEvent
import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter
import javax.swing.* import javax.swing.*
import javax.swing.tree.TreePath
import kotlin.math.max import kotlin.math.max
class TermoraFencePanel( class TermoraFencePanel(
private val ws: WindowScope,
private val terminalTabbed: TerminalTabbed, private val terminalTabbed: TerminalTabbed,
private val tabbed: FlatTabbedPane, private val tabbed: FlatTabbedPane,
private val moveMouseAdapter: MouseAdapter, private val moveMouseAdapter: MouseAdapter,
@@ -72,10 +72,12 @@ class TermoraFencePanel(
leftTreePanel.addComponentListener(object : ComponentAdapter() { leftTreePanel.addComponentListener(object : ComponentAdapter() {
override fun componentHidden(e: ComponentEvent) { override fun componentHidden(e: ComponentEvent) {
toolbar.isVisible = true toolbar.isVisible = true
enableManager.setFlag("Termora.Fence.colspan", true)
} }
override fun componentShown(e: ComponentEvent) { override fun componentShown(e: ComponentEvent) {
toolbar.isVisible = false toolbar.isVisible = false
enableManager.setFlag("Termora.Fence.colspan", false)
} }
}) })
@@ -86,6 +88,50 @@ class TermoraFencePanel(
toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK
), "toggle" ), "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 { private inner class LeftTreePanel : JPanel(BorderLayout()), Disposable {
@@ -144,19 +190,19 @@ class TermoraFencePanel(
} }
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
if (leftTreePanel.isVisible) dividerLocation = splitPane.dividerLocation toggle()
leftTreePanel.isVisible = leftTreePanel.isVisible.not()
if (leftTreePanel.isVisible) splitPane.dividerLocation = dividerLocation
} }
} }
} }
private fun toggle() {
override fun dispose() { if (leftTreePanel.isVisible) dividerLocation = splitPane.dividerLocation
if (leftTreePanel.isVisible) leftTreePanel.isVisible = leftTreePanel.isVisible.not()
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10)) if (leftTreePanel.isVisible) splitPane.dividerLocation = dividerLocation
mySplitPane.doLayout()
} }
fun getHostTree(): NewHostTree { fun getHostTree(): NewHostTree {
return leftTreePanel.hostTree return leftTreePanel.hostTree
} }

View File

@@ -212,7 +212,7 @@ class TermoraFrame : JFrame(), DataProvider {
} }
if (layout == TermoraLayout.Fence) { if (layout == TermoraLayout.Fence) {
val fencePanel = TermoraFencePanel(terminalTabbed, tabbedPane, moveMouseAdapter) val fencePanel = TermoraFencePanel(windowScope, terminalTabbed, tabbedPane, moveMouseAdapter)
add(fencePanel, BorderLayout.CENTER) add(fencePanel, BorderLayout.CENTER)
dataProviderSupport.addData(DataProviders.Welcome.HostTree, fencePanel.getHostTree()) dataProviderSupport.addData(DataProviders.Welcome.HostTree, fencePanel.getHostTree())
Disposer.register(windowScope, fencePanel) Disposer.register(windowScope, fencePanel)

View File

@@ -5,6 +5,7 @@ import app.termora.plugin.ExtensionManager
import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import com.sun.jna.Pointer import com.sun.jna.Pointer
import com.sun.jna.platform.WindowUtils
import com.sun.jna.platform.win32.User32 import com.sun.jna.platform.win32.User32
import com.sun.jna.platform.win32.WinDef import com.sun.jna.platform.win32.WinDef
import com.sun.jna.platform.win32.WinUser.* import com.sun.jna.platform.win32.WinUser.*
@@ -206,7 +207,7 @@ class TermoraFrameManager : Disposable {
} }
fun setOpacity(opacity: Double) { fun setOpacity(opacity: Double) {
if (opacity < 0 || opacity > 1 || SystemInfo.isLinux) return if (opacity < 0 || opacity > 1) return
for (window in getWindows()) { for (window in getWindows()) {
setOpacity(window, opacity) setOpacity(window, opacity)
} }
@@ -227,6 +228,8 @@ class TermoraFrameManager : Disposable {
User32.INSTANCE.SetWindowLong(hwnd, GWL_EXSTYLE, exStyle or WS_EX_LAYERED) User32.INSTANCE.SetWindowLong(hwnd, GWL_EXSTYLE, exStyle or WS_EX_LAYERED)
} }
User32.INSTANCE.SetLayeredWindowAttributes(hwnd, 0, alpha, LWA_ALPHA) User32.INSTANCE.SetLayeredWindowAttributes(hwnd, 0, alpha, LWA_ALPHA)
} else if (SystemInfo.isLinux && WindowUtils.isWindowAlphaSupported()) {
WindowUtils.setWindowAlpha(window, opacity.toFloat())
} }
} }

View File

@@ -139,6 +139,7 @@ object AccountHttp {
} }
} catch (e: Exception) { } catch (e: Exception) {
if (cidr == "localhost" || cidr == "127.0.0.1") continue
if (log.isDebugEnabled) { if (log.isDebugEnabled) {
log.debug(e.message, e) log.debug(e.message, e)
} }

View File

@@ -126,7 +126,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
while (true) { while (true) {
val request = Request.Builder() val request = Request.Builder()
.get() .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() .build()
val text = AccountHttp.execute(request = request) val text = AccountHttp.execute(request = request)
val response = ohMyJson.decodeFromString<DataChangesResponse>(text) val response = ohMyJson.decodeFromString<DataChangesResponse>(text)

View File

@@ -7,6 +7,7 @@ import org.apache.commons.codec.binary.Base64
import org.apache.commons.codec.digest.DigestUtils import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.lang3.ObjectUtils import org.apache.commons.lang3.ObjectUtils
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.selectAll import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.jdbc.update import org.jetbrains.exposed.v1.jdbc.update

View File

@@ -1,9 +1,6 @@
package app.termora.actions package app.termora.actions
import app.termora.ApplicationScope import app.termora.*
import app.termora.I18n
import app.termora.Icons
import app.termora.SettingsDialog
import com.formdev.flatlaf.extras.FlatDesktop import com.formdev.flatlaf.extras.FlatDesktop
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import java.awt.KeyboardFocusManager import java.awt.KeyboardFocusManager
@@ -32,13 +29,13 @@ class SettingsAction private constructor() : AnAction(
private val action get() = this private val action get() = this
init { init {
FlatDesktop.setPreferencesHandler { FlatDesktop.setPreferencesHandler(object : Runnable {
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner override fun run() {
// Doorman 的情况下不允许打开 val focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow ?: return
if (owner != null && ApplicationScope.windowScopes().isNotEmpty()) { if (focusedWindow !is TermoraFrame) return
actionPerformed(ActionEvent(owner, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY)) actionPerformed(ActionEvent(focusedWindow, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
} }
} })
} }
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {

View File

@@ -16,8 +16,8 @@ import app.termora.snippet.SnippetManager
import app.termora.terminal.CursorStyle import app.termora.terminal.CursorStyle
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.jetbrains.exposed.v1.core.SqlExpressionBuilder.eq
import org.jetbrains.exposed.v1.core.and import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.statements.StatementType import org.jetbrains.exposed.v1.core.statements.StatementType
import org.jetbrains.exposed.v1.jdbc.* import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager

View File

@@ -101,6 +101,16 @@ internal class KeywordHighlightPaintListener private constructor() : TerminalPai
// -1 表示不使用高亮集 // -1 表示不使用高亮集
if (keywordHighlightSetId == "-1") return 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) { for (highlight in keywordHighlights) {
if (highlight.enabled.not()) continue if (highlight.enabled.not()) continue
if (highlight.type != KeywordHighlightType.Highlight) continue if (highlight.type != KeywordHighlightType.Highlight) continue
@@ -151,7 +161,6 @@ internal class KeywordHighlightPaintListener private constructor() : TerminalPai
} }
} }
} }
override fun after( override fun after(

View File

@@ -262,8 +262,8 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
OptionPane.openFileInFolder( OptionPane.openFileInFolder(
SwingUtilities.getWindowAncestor(this), SwingUtilities.getWindowAncestor(this),
file, I18n.getString("termora.settings.sync.export-done-open-folder"), file, I18n.getString("termora.keymgr.export-done-open-folder"),
I18n.getString("termora.settings.sync.export-done") I18n.getString("termora.keymgr.export-done")
) )
} }

View File

@@ -32,7 +32,7 @@ class MacroManager private constructor() {
val accountId = AccountManager.getInstance().getAccountId() val accountId = AccountManager.getInstance().getAccountId()
database.save( database.saveAndIncrementVersion(
Data( Data(
id = macro.id, id = macro.id,
ownerId = accountId, ownerId = accountId,

View File

@@ -103,9 +103,20 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
commands.add("Compression=yes") commands.add("Compression=yes")
// HostKeyAlgorithms 让 SFTP 命令的顺序和 sshd 的一致 这样可以避免 known_hosts 文件不一致问题 // 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("-o")
commands.add("HostKeyAlgorithms=${hostKeyAlgorithms}") commands.add("HostKeyAlgorithms=${hostKeyAlgorithms.joinToString(",")}")
// 不使用配置文件 // 不使用配置文件
commands.add("-F") commands.add("-F")
@@ -143,6 +154,15 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
return ptyConnector 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) { private fun setAuthentication(commands: MutableList<String>, host: Host) {
// 如果通过公钥连接 // 如果通过公钥连接
if (host.authentication.type == AuthenticationType.PublicKey) { if (host.authentication.type == AuthenticationType.PublicKey) {

View File

@@ -8,6 +8,7 @@ import app.termora.keymgr.KeyManagerDialog
import app.termora.plugin.internal.AltKeyModifier import app.termora.plugin.internal.AltKeyModifier
import app.termora.plugin.internal.BasicProxyOption import app.termora.plugin.internal.BasicProxyOption
import app.termora.plugin.internal.BasicTerminalOption import app.termora.plugin.internal.BasicTerminalOption
import app.termora.plugin.internal.telnet.TelnetHostOptionsPane.Backspace
import app.termora.tree.Filter import app.termora.tree.Filter
import app.termora.tree.HostTreeNode import app.termora.tree.HostTreeNode
import app.termora.tree.NewHostTreeDialog 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.*
import java.awt.event.* import java.awt.event.*
import javax.swing.* import javax.swing.*
import javax.swing.event.DocumentEvent
import javax.swing.table.DefaultTableCellRenderer import javax.swing.table.DefaultTableCellRenderer
import javax.swing.table.DefaultTableModel import javax.swing.table.DefaultTableModel
@@ -35,6 +37,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
private val terminalOption = BasicTerminalOption().apply { private val terminalOption = BasicTerminalOption().apply {
showCharsetComboBox = true showCharsetComboBox = true
showLoginScripts = true showLoginScripts = true
showBackspaceComboBox = true
showEnvironmentTextArea = true showEnvironmentTextArea = true
showStartupCommandTextField = true showStartupCommandTextField = true
showHeartbeatIntervalTextField = true showHeartbeatIntervalTextField = true
@@ -46,6 +49,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
private val jumpHostsOption = JumpHostsOption() private val jumpHostsOption = JumpHostsOption()
private val sftpOption = SFTPOption() private val sftpOption = SFTPOption()
private val owner: Window get() = SwingUtilities.getWindowAncestor(this) private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
private var setHostMode = false
init { init {
addOption(generalOption) addOption(generalOption)
@@ -110,11 +114,13 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
x11Forwarding = tunnelingOption.x11ServerTextField.text, x11Forwarding = tunnelingOption.x11ServerTextField.text,
loginScripts = terminalOption.loginScripts, loginScripts = terminalOption.loginScripts,
extras = mutableMapOf( extras = mutableMapOf(
"backspace" to (terminalOption.backspaceComboBox.selectedItem as Backspace).name,
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString() "altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
?: AltKeyModifier.EightBit.name), ?: AltKeyModifier.EightBit.name),
"keywordHighlightSetId" to ((terminalOption.highlightSetComboBox.selectedItem as? KeywordHighlight)?.id "keywordHighlightSetId" to ((terminalOption.highlightSetComboBox.selectedItem as? KeywordHighlight)?.id
?: "-1"), ?: "-1"),
"timeout" to (terminalOption.timeoutTextField.value ?: 60).toString() "timeout" to (terminalOption.timeoutTextField.value ?: 60).toString(),
"forwardAgent" to tunnelingOption.forwardAgentCheckBox.isSelected.toString(),
) )
) )
@@ -134,6 +140,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
} }
fun setHost(host: Host) { fun setHost(host: Host) {
setHostMode = true
generalOption.portTextField.value = host.port generalOption.portTextField.value = host.port
generalOption.nameTextField.text = host.name generalOption.nameTextField.text = host.name
generalOption.usernameTextField.text = host.username generalOption.usernameTextField.text = host.username
@@ -165,6 +172,9 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
.getOrNull() ?: AltKeyModifier.EightBit .getOrNull() ?: AltKeyModifier.EightBit
terminalOption.backspaceComboBox.selectedItem =
Backspace.valueOf(host.options.extras["backspace"] ?: Backspace.Delete.name)
val timeout = host.options.extras["timeout"] ?: "60" val timeout = host.options.extras["timeout"] ?: "60"
terminalOption.timeoutTextField.value = timeout.toIntOrNull() ?: 60 terminalOption.timeoutTextField.value = timeout.toIntOrNull() ?: 60
@@ -182,6 +192,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
tunnelingOption.tunnelings.addAll(host.tunnelings) tunnelingOption.tunnelings.addAll(host.tunnelings)
tunnelingOption.x11ForwardingCheckBox.isSelected = host.options.enableX11Forwarding tunnelingOption.x11ForwardingCheckBox.isSelected = host.options.enableX11Forwarding
tunnelingOption.x11ServerTextField.text = StringUtils.defaultIfBlank(host.options.x11Forwarding, "localhost:0") tunnelingOption.x11ServerTextField.text = StringUtils.defaultIfBlank(host.options.x11Forwarding, "localhost:0")
tunnelingOption.forwardAgentCheckBox.isSelected = host.options.extras["forwardAgent"]?.toBoolean() ?: false
if (host.options.jumpHosts.isNotEmpty()) { if (host.options.jumpHosts.isNotEmpty()) {
val hosts = HostManager.getInstance().hosts().associateBy { it.id } val hosts = HostManager.getInstance().hosts().associateBy { it.id }
@@ -296,6 +307,8 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
val remarkTextArea = FixedLengthTextArea(512) val remarkTextArea = FixedLengthTextArea(512)
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>() val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
private var hostFocused = false
init { init {
initView() initView()
initEvents() initEvents()
@@ -403,6 +416,26 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
removeComponentListener(this) 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() { private fun chooseKeyPair() {
@@ -570,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 tunnelings = mutableListOf<Tunneling>()
val x11ForwardingCheckBox = JCheckBox("X DISPLAY:") val x11ForwardingCheckBox = JCheckBox("X DISPLAY:")
val forwardAgentCheckBox = JCheckBox("Enable ForwardAgent")
val x11ServerTextField = OutlineTextField(255) val x11ServerTextField = OutlineTextField(255)
private val model = object : DefaultTableModel() { private val model = object : DefaultTableModel() {
@@ -649,6 +683,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
box.add(deleteBtn) box.add(deleteBtn)
x11ForwardingCheckBox.isFocusable = false x11ForwardingCheckBox.isFocusable = false
forwardAgentCheckBox.isFocusable = false
if (x11ServerTextField.text.isBlank()) { if (x11ServerTextField.text.isBlank()) {
x11ServerTextField.text = "localhost:0" x11ServerTextField.text = "localhost:0"
@@ -662,6 +697,13 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
x11Forwarding.add(x11ForwardingCheckBox) x11Forwarding.add(x11ForwardingCheckBox)
x11Forwarding.add(x11ServerTextField) 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 x11ServerTextField.isEnabled = x11ForwardingCheckBox.isSelected
val panel = JPanel(BorderLayout()) val panel = JPanel(BorderLayout())
@@ -670,8 +712,13 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
panel.add(box, BorderLayout.SOUTH) panel.add(box, BorderLayout.SOUTH)
panel.border = BorderFactory.createEmptyBorder(0, 0, 8, 0) 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(panel, BorderLayout.CENTER)
add(x11Forwarding, BorderLayout.SOUTH) add(forwardingBox, BorderLayout.SOUTH)
} }

View File

@@ -7,9 +7,8 @@ import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
import app.termora.database.DatabaseManager import app.termora.database.DatabaseManager
import app.termora.keymap.KeyShortcut import app.termora.keymap.KeyShortcut
import app.termora.keymap.KeymapManager import app.termora.keymap.KeymapManager
import app.termora.terminal.ControlCharacters import app.termora.plugin.internal.telnet.TelnetHostOptionsPane
import app.termora.terminal.DataKey import app.termora.terminal.*
import app.termora.terminal.PtyConnector
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.sync.Mutex 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.CloseFuture
import org.apache.sshd.common.future.SshFutureListener import org.apache.sshd.common.future.SshFutureListener
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.awt.event.KeyEvent
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import javax.swing.Icon import javax.swing.Icon
import javax.swing.JComponent import javax.swing.JComponent
@@ -110,7 +110,18 @@ class SSHTerminalTab(
// clear screen // clear screen
terminal.clearScreen() terminal.clearScreen()
// show cursor // 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( return ptyConnectorFactory.decorate(

View File

@@ -69,4 +69,7 @@ class SftpCommandTerminalTabbedContextMenuExtension private constructor() : Term
openHostAction.actionPerformed(OpenHostActionEvent(evt.source, host, evt)) openHostAction.actionPerformed(OpenHostActionEvent(evt.source, host, evt))
} }
override fun ordered(): Long {
return 1
}
} }

View File

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

View File

@@ -56,7 +56,6 @@ import org.apache.sshd.server.forward.AcceptAllForwardingFilter
import org.apache.sshd.server.forward.RejectAllForwardingFilter import org.apache.sshd.server.forward.RejectAllForwardingFilter
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession import org.eclipse.jgit.internal.transport.sshd.JGitClientSession
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient 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.PageantConnector
import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocketConnector import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocketConnector
import org.eclipse.jgit.internal.transport.sshd.proxy.AbstractClientProxyConnector import org.eclipse.jgit.internal.transport.sshd.proxy.AbstractClientProxyConnector
@@ -112,6 +111,8 @@ object SshClients {
env.putAll(host.options.envs()) env.putAll(host.options.envs())
val channel = session.createShellChannel(configuration, env) val channel = session.createShellChannel(configuration, env)
channel.isAgentForwarding = host.options.extras["forwardAgent"]?.toBoolean() == true
if (host.options.enableX11Forwarding) { if (host.options.enableX11Forwarding) {
if (channel is app.termora.x11.ChannelShell) { if (channel is app.termora.x11.ChannelShell) {
channel.xForwarding = true channel.xForwarding = true
@@ -386,7 +387,7 @@ object SshClients {
val channelFactories = mutableListOf<ChannelFactory>() val channelFactories = mutableListOf<ChannelFactory>()
channelFactories.addAll(ClientBuilder.DEFAULT_CHANNEL_FACTORIES) channelFactories.addAll(ClientBuilder.DEFAULT_CHANNEL_FACTORIES)
channelFactories.add(X11ChannelFactory.Companion.INSTANCE) channelFactories.add(X11ChannelFactory.INSTANCE)
builder.channelFactories(channelFactories) builder.channelFactories(channelFactories)
val sshClient = builder.build() as JGitSshClient val sshClient = builder.build() as JGitSshClient
@@ -395,12 +396,14 @@ object SshClients {
// JGit 会尝试读取本地的私钥或缓存的私钥 // JGit 会尝试读取本地的私钥或缓存的私钥
sshClient.keyIdentityProvider = KeyIdentityProvider { mutableListOf() } 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.PublicKey || host.authentication.type == AuthenticationType.SSHAgent) {
if (host.authentication.type == AuthenticationType.SSHAgent) {
// ssh-agent
sshClient.agentFactory = JGitSshAgentFactory(ConnectorFactory.getDefault(), null)
}
CoreModuleProperties.PREFERRED_AUTHS.set( CoreModuleProperties.PREFERRED_AUTHS.set(
sshClient, sshClient,
listOf( listOf(
@@ -422,8 +425,11 @@ object SshClients {
val heartbeatInterval = max(host.options.heartbeatInterval, 3) 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.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true) CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true)
CoreModuleProperties.IO_CONNECT_TIMEOUT.set(sshClient, timeout)
sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) } sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) }

View File

@@ -520,9 +520,13 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
val writer = terminalModel.getData(DataKey.TerminalWriter) val writer = terminalModel.getData(DataKey.TerminalWriter)
// VT102_RESPONSE if (args.startsWith('>')) {
val bytes = "${ControlCharacters.ESC}[?6c".toByteArray(writer.getCharset()) val bytes = "${ControlCharacters.ESC}[>0;276;0c".toByteArray(writer.getCharset())
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes)) writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
} else {
val bytes = "${ControlCharacters.ESC}[?1;2c".toByteArray(writer.getCharset())
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
}
} }

View File

@@ -1,16 +1,20 @@
package app.termora.terminal package app.termora.terminal
import org.slf4j.LoggerFactory
import java.util.* import java.util.*
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel { open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
private var startPosition = Position.unknown private var startPosition = Position.unknown
private var endPosition = Position.unknown private var endPosition = Position.unknown
private var block = false private var block = false
private val document = terminal.getDocument() private val document = terminal.getDocument()
internal companion object { internal companion object {
private val log = LoggerFactory.getLogger(SelectionModelImpl::class.java)
fun isPointInsideArea(start: Position, end: Position, x: Int, y: Int, cols: Int): Boolean { fun isPointInsideArea(start: Position, end: Position, x: Int, y: Int, cols: Int): Boolean {
val top = min(start.y, end.y) val top = min(start.y, end.y)
val bottom = max(start.y, end.y) val bottom = max(start.y, end.y)
@@ -49,7 +53,13 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
} }
// 设置新的选择区域 // 设置新的选择区域
setSelection(startPosition, endPosition) try {
setSelection(startPosition, endPosition)
} catch (e: Exception) {
if (log.isTraceEnabled) {
log.trace(e.message)
}
}
} }

View File

@@ -172,7 +172,7 @@ class TerminalFindPanel(
} }
} else { } else {
if (index - 1 <= 0) { if (index - 1 <= 0) {
index = 0 index = kinds.size - 1
} else { } else {
index-- index--
} }

View File

@@ -185,8 +185,9 @@ class TerminalPanel(val tab: TerminalTab?, val terminal: Terminal, private val w
this.addMouseMotionListener(mouseAdapter) this.addMouseMotionListener(mouseAdapter)
// 超链接 // 超链接
val hyperlinkAdapter = TerminalPanelMouseHyperlinkAdapter(this, terminal) val hyperlinkAdapter = TerminalPanelMouseHyperlinkAdapter(this, terminalDisplay, terminal)
this.addMouseListener(hyperlinkAdapter) this.addMouseListener(hyperlinkAdapter)
this.addMouseMotionListener(hyperlinkAdapter)
// 鼠标跟踪 // 鼠标跟踪
val trackingAdapter = TerminalPanelMouseTrackingAdapter(this, terminal, writer) val trackingAdapter = TerminalPanelMouseTrackingAdapter(this, terminal, writer)

View File

@@ -2,6 +2,8 @@ package app.termora.terminal.panel
import app.termora.terminal.ClickableHighlighter import app.termora.terminal.ClickableHighlighter
import app.termora.terminal.Terminal import app.termora.terminal.Terminal
import com.formdev.flatlaf.util.SystemInfo
import java.awt.Cursor
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
@@ -11,19 +13,42 @@ import javax.swing.SwingUtilities
*/ */
class TerminalPanelMouseHyperlinkAdapter( class TerminalPanelMouseHyperlinkAdapter(
private val terminalPanel: TerminalPanel, private val terminalPanel: TerminalPanel,
private val terminalDisplay: TerminalDisplay,
private val terminal: Terminal, private val terminal: Terminal,
) : MouseAdapter() { ) : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { override fun mouseClicked(e: MouseEvent) {
if (SwingUtilities.isLeftMouseButton(e)) { if (SwingUtilities.isLeftMouseButton(e).not()) {
val position = terminalPanel.pointToPosition(e.point) return
for (highlighter in terminal.getMarkupModel().getHighlighters(position)) { }
if (highlighter is ClickableHighlighter) {
highlighter.onClicked(position) 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
}
} }

View File

@@ -30,6 +30,8 @@ import java.nio.file.Path
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors import java.util.concurrent.Executors
import javax.swing.* import javax.swing.*
import javax.swing.event.PopupMenuEvent
import javax.swing.event.PopupMenuListener
import kotlin.io.path.absolutePathString import kotlin.io.path.absolutePathString
import kotlin.math.max import kotlin.math.max
import kotlin.reflect.cast import kotlin.reflect.cast
@@ -58,12 +60,24 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
private val connectFailedPanel = ConnectFailedPanel() private val connectFailedPanel = ConnectFailedPanel()
private val transferManager = TransferTableModel(coroutineScope) private val transferManager = TransferTableModel(coroutineScope)
private val disposable = Disposer.newDisposable() private val disposable = Disposer.newDisposable()
private val focusedWindow get() = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
private val owner get() = SwingUtilities.getWindowAncestor(this) private val owner get() = SwingUtilities.getWindowAncestor(this)
private val questionBtn = JButton(Icons.questionMark) private val questionBtn = JButton(Icons.questionMark)
private val downloadBtn = JButton(Icons.download) private val downloadBtn = JButton(Icons.download)
private val badgePresentation = Badge.getInstance(tab.windowScope) private val badgePresentation = Badge.getInstance(tab.windowScope)
.addBadge(downloadBtn).apply { visible = false } .addBadge(downloadBtn).apply { visible = false }
private val support = DataProviderSupport() private val support = DataProviderSupport()
private var isShowPopupMenu = false
override var isStickHover: Boolean
get() = super.isStickHover
set(value) {
if (isShowPopupMenu || owner != focusedWindow) {
super.isStickHover = true
} else {
super.isStickHover = value
}
}
init { init {
initViews() initViews()
@@ -135,6 +149,8 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
} }
}) })
questionBtn.toolTipText = I18n.getString("termora.visual-window.transport.question")
// 立即连接 // 立即连接
connect() connect()
} }
@@ -151,7 +167,7 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
val support = DefaultTransportSupport(fileSystem, fileSystem.defaultDir) val support = DefaultTransportSupport(fileSystem, fileSystem.defaultDir)
withContext(Dispatchers.Swing) { withContext(Dispatchers.Swing) {
val internalTransferManager = MyInternalTransferManager() val internalTransferManager = MyInternalTransferManager()
val transportPanel = TransportPanel( val transportPanel = object : TransportPanel(
internalTransferManager, tab.host, internalTransferManager, tab.host,
object : TransportSupportLoader { object : TransportSupportLoader {
override suspend fun getTransportSupport(): TransportSupport { override suspend fun getTransportSupport(): TransportSupport {
@@ -165,7 +181,27 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
override fun isLoaded(): Boolean { override fun isLoaded(): Boolean {
return true return true
} }
}) }) {
override fun customizeContextmenu(
rows: Array<Int>,
e: MouseEvent,
popupMenu: TransportPopupMenu
) {
popupMenu.addPopupMenuListener(object : PopupMenuListener {
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent?) {
isShowPopupMenu = true
}
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent?) {
isShowPopupMenu = false
}
override fun popupMenuCanceled(e: PopupMenuEvent?) {
isShowPopupMenu = false
}
})
}
}
internalTransferManager.setTransferPanel(transportPanel) internalTransferManager.setTransferPanel(transportPanel)
Disposer.register(transportPanel, object : Disposable { Disposer.register(transportPanel, object : Disposable {
override fun dispose() { override fun dispose() {
@@ -240,6 +276,10 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
super.dispose() super.dispose()
} }
override fun reassemble() {
super.reassemble()
}
override fun <T : Any> getData(dataKey: DataKey<T>): T? { override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return support.getData(dataKey) return support.getData(dataKey)
} }

View File

@@ -36,6 +36,7 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
private var dialog: VisualWindowDialog? = null private var dialog: VisualWindowDialog? = null
private var oldBounds = Rectangle() private var oldBounds = Rectangle()
private var toggleWindowBtn = JButton(Icons.openInNewWindow) private var toggleWindowBtn = JButton(Icons.openInNewWindow)
private val closeBtn = JButton(Icons.close)
private var isAlwaysTop private var isAlwaysTop
get() = properties.getString("VisualWindow.${id}.dialog.isAlwaysTop", "false").toBoolean() get() = properties.getString("VisualWindow.${id}.dialog.isAlwaysTop", "false").toBoolean()
set(value) = properties.putString("VisualWindow.${id}.dialog.isAlwaysTop", value.toString()) set(value) = properties.putString("VisualWindow.${id}.dialog.isAlwaysTop", value.toString())
@@ -47,8 +48,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
} }
} }
protected var isStickHover = false protected open var isStickHover = false
private set(value) { set(value) {
if (value == field) return if (value == field) return
field = value field = value
reassemble() reassemble()
@@ -92,6 +93,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
oldBounds = bounds oldBounds = bounds
alwaysTopBtn.isSelected = isAlwaysTop alwaysTopBtn.isSelected = isAlwaysTop
alwaysTopBtn.isVisible = false alwaysTopBtn.isVisible = false
closeBtn.toolTipText = I18n.getString("termora.tabbed.contextmenu.close")
} }
protected open fun toolbarButtons(): List<Pair<JButton, Position>> { protected open fun toolbarButtons(): List<Pair<JButton, Position>> {
@@ -134,6 +137,7 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
addMouseListener(object : MouseAdapter() {}) addMouseListener(object : MouseAdapter() {})
toggleWindowBtn.addActionListener { toggleWindow() } toggleWindowBtn.addActionListener { toggleWindow() }
toggleWindowBtn.toolTipText = I18n.getString("termora.visual-window.toggle-window")
addPropertyChangeListener("isWindow") { addPropertyChangeListener("isWindow") {
if (isWindow) { if (isWindow) {
@@ -165,6 +169,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
dialog?.isAlwaysOnTop = isAlwaysTop dialog?.isAlwaysOnTop = isAlwaysTop
} }
} }
closeBtn.addActionListener { if (beforeClose()) Disposer.dispose(visualWindow) }
} }
private fun initToolBar() { private fun initToolBar() {
@@ -180,7 +186,7 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
buttons.filter { it.second == Position.Right }.forEach { toolbar.add(it.first) } buttons.filter { it.second == Position.Right }.forEach { toolbar.add(it.first) }
toolbar.add(toggleWindowBtn) toolbar.add(toggleWindowBtn)
toolbar.add(JButton(Icons.close).apply { addActionListener { if (beforeClose()) Disposer.dispose(visualWindow) } }) toolbar.add(closeBtn)
toolbar.border = BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor) toolbar.border = BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor)
add(toolbar, BorderLayout.NORTH) add(toolbar, BorderLayout.NORTH)
} }

View File

@@ -66,6 +66,8 @@ class BookmarkButton : JButton(Icons.bookmarks) {
}) })
isBookmark = false isBookmark = false
toolTipText = I18n.getString("termora.transport.bookmarks")
} }
private fun showBookmarks(e: MouseEvent) { private fun showBookmarks(e: MouseEvent) {

View File

@@ -2,6 +2,7 @@ package app.termora.transfer
import app.termora.* import app.termora.*
import app.termora.transfer.InternalTransferManager.TransferMode import app.termora.transfer.InternalTransferManager.TransferMode
import app.termora.transfer.TransportPanel.Companion.isWindowsFileSystem
import com.jgoodies.forms.builder.FormBuilder import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout import com.jgoodies.forms.layout.FormLayout
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -95,8 +96,12 @@ internal class DefaultInternalTransferManager(
val context = AskTransferContext(TransferAction.Overwrite, false) val context = AskTransferContext(TransferAction.Overwrite, false)
for (pair in paths) { for (pair in paths) {
if (mode == TransferMode.Transfer && context.applyAll.not()) { if (mode == TransferMode.Transfer && context.applyAll.not()) {
var name = pair.first.name
if (targetWorkdir.fileSystem.isWindowsFileSystem()) {
name = name.replace(":", "-")
}
val action = withContext(Dispatchers.Swing) { val action = withContext(Dispatchers.Swing) {
getTransferAction(context, targetWorkdir.resolve(pair.first.name), pair.second) getTransferAction(context, targetWorkdir.resolve(name), pair.second)
} }
if (action == null) { if (action == null) {
break break
@@ -272,8 +277,11 @@ internal class DefaultInternalTransferManager(
val isDirectory = pair.second.isDirectory val isDirectory = pair.second.isDirectory
val path = pair.first val path = pair.first
if (isDirectory.not() || mode == TransferMode.Rmrf) { if (isDirectory.not() || mode == TransferMode.Rmrf) {
val transfer = var name = path.name
createTransfer(path, workdir.resolve(path.name), isDirectory, StringUtils.EMPTY, mode, action) 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 return if (transferManager.addTransfer(transfer)) FileVisitResult.CONTINUE else FileVisitResult.TERMINATE
} }

View File

@@ -63,7 +63,7 @@ import kotlin.io.path.*
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
internal class TransportPanel( internal open class TransportPanel(
private val internalTransferManager: InternalTransferManager, private val internalTransferManager: InternalTransferManager,
val host: Host, val host: Host,
val loader: TransportSupportLoader, val loader: TransportSupportLoader,
@@ -106,6 +106,7 @@ internal class TransportPanel(
private val loadingPanel = LoadingPanel() private val loadingPanel = LoadingPanel()
private val model = TransportTableModel() private val model = TransportTableModel()
private val table = JTable(model) private val table = JTable(model)
private val tableScrollPane = JScrollPane(table)
private val sorter = TableRowSorter(table.model) private val sorter = TableRowSorter(table.model)
private var hasParent = false private var hasParent = false
private val panel get() = this private val panel get() = this
@@ -130,10 +131,10 @@ internal class TransportPanel(
* 工作目录 * 工作目录
*/ */
override var workdir: Path? = null override var workdir: Path? = null
private set protected set
override var loading = false override var loading = false
private set(value) { protected set(value) {
val oldValue = field val oldValue = field
field = value field = value
if (oldValue != value) { if (oldValue != value) {
@@ -164,6 +165,14 @@ internal class TransportPanel(
toolbar.add(eyeBtn) toolbar.add(eyeBtn)
toolbar.add(refreshBtn) toolbar.add(refreshBtn)
prevBtn.toolTipText = I18n.getString("termora.transport.toolbar.prev")
homeBtn.toolTipText = I18n.getString("termora.transport.toolbar.home")
nextBtn.toolTipText = I18n.getString("termora.transport.toolbar.next")
parentBtn.toolTipText = I18n.getString("termora.transport.toolbar.parent")
eyeBtn.toolTipText = I18n.getString("termora.transport.toolbar.show-hide")
refreshBtn.toolTipText = I18n.getString("termora.transport.toolbar.refresh")
sorter.maxSortKeys = 1 sorter.maxSortKeys = 1
table.setRowSorter(sorter) table.setRowSorter(sorter)
table.setAutoCreateRowSorter(false) table.setAutoCreateRowSorter(false)
@@ -211,7 +220,7 @@ internal class TransportPanel(
table.setDefaultRenderer(Any::class.java, MyDefaultTableCellRenderer()) table.setDefaultRenderer(Any::class.java, MyDefaultTableCellRenderer())
val scrollPane = JScrollPane(table) val scrollPane = tableScrollPane
scrollPane.apply { border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor) } scrollPane.apply { border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor) }
layeredPane.add(scrollPane, JLayeredPane.DEFAULT_LAYER as Any) layeredPane.add(scrollPane, JLayeredPane.DEFAULT_LAYER as Any)
@@ -241,7 +250,11 @@ internal class TransportPanel(
Disposer.register(this, editTransferListener) 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() } prevBtn.addActionListener { navigator.back() }
nextBtn.addActionListener { navigator.forward() } nextBtn.addActionListener { navigator.forward() }
@@ -303,11 +316,16 @@ internal class TransportPanel(
if (target.fileSystem != loader.getSyncTransportSupport().getFileSystem()) return if (target.fileSystem != loader.getSyncTransportSupport().getFileSystem()) return
} }
if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) { if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) {
if (loading) { val c = {
registerNextReloadCallback { reload(requestFocus = false) } val filename = getSelectFilename()
} else { if (filename != null) registerSelectRow(filename)
reload(requestFocus = false) reload(requestFocus = false)
} }
if (loading) {
registerNextReloadCallback { c.invoke() }
} else {
c.invoke()
}
} }
} }
}).let { Disposer.register(this, it) } }).let { Disposer.register(this, it) }
@@ -401,7 +419,7 @@ internal class TransportPanel(
} }
}) })
addPropertyChangeListener("workdir") { evt -> reload() } addPropertyChangeListener("workdir") { _ -> reload() }
reload() reload()
} }
@@ -497,6 +515,29 @@ 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() { table.addKeyListener(object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) { override fun keyPressed(e: KeyEvent) {
@@ -520,12 +561,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) val inputMap = table.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
if (SystemInfo.isMacOS.not()) { if (SystemInfo.isMacOS.not()) {
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), "Reload") inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), "Reload")
} }
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "EnterSelectionFolder") 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_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() { private fun initTransferHandler() {
@@ -658,6 +712,9 @@ internal class TransportPanel(
} }
fun registerSelectRow(name: String) { fun registerSelectRow(name: String) {
val verticalValue = tableScrollPane.verticalScrollBar.value
val horizontalValue = tableScrollPane.horizontalScrollBar.value
registerNextReloadCallback { registerNextReloadCallback {
for (i in 0 until model.rowCount) { for (i in 0 until model.rowCount) {
if (model.getAttributes(i).name == name) { if (model.getAttributes(i).name == name) {
@@ -665,12 +722,22 @@ internal class TransportPanel(
table.clearSelection() table.clearSelection()
table.setRowSelectionInterval(c, c) table.setRowSelectionInterval(c, c)
table.scrollRectToVisible(table.getCellRect(c, TransportTableModel.COLUMN_NAME, true)) table.scrollRectToVisible(table.getCellRect(c, TransportTableModel.COLUMN_NAME, true))
tableScrollPane.verticalScrollBar.value = verticalValue
tableScrollPane.horizontalScrollBar.value = horizontalValue
break 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) { private fun registerNextReloadCallback(block: () -> Unit) {
nextReloadCallbacks.computeIfAbsent(mod.get()) { mutableListOf() } nextReloadCallbacks.computeIfAbsent(mod.get()) { mutableListOf() }
.add(block) .add(block)
@@ -858,13 +925,18 @@ internal class TransportPanel(
} }
} }
private fun showContextmenu(rows: Array<Int>, e: MouseEvent) { protected open fun showContextmenu(rows: Array<Int>, e: MouseEvent) {
val files = rows.map { model.getPath(it) to model.getAttributes(it) } val files = rows.map { model.getPath(it) to model.getAttributes(it) }
val popupMenu = TransportPopupMenu(owner, model, internalTransferManager, loader, files) val popupMenu = TransportPopupMenu(owner, model, internalTransferManager, loader, files)
popupMenu.addActionListener(PopupMenuActionListener(files)) popupMenu.addActionListener(PopupMenuActionListener(files))
customizeContextmenu(rows, e, popupMenu)
popupMenu.show(table, e.x, e.y) popupMenu.show(table, e.x, e.y)
} }
protected open fun customizeContextmenu(rows: Array<Int>, e: MouseEvent, popupMenu: TransportPopupMenu) {
}
override fun <T : Any> getData(dataKey: DataKey<T>): T? { override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return support.getData(dataKey) return support.getData(dataKey)
} }
@@ -1083,6 +1155,8 @@ internal class TransportPanel(
} else if (actionCommand == TransportPopupMenu.ActionCommand.Delete) { } else if (actionCommand == TransportPopupMenu.ActionCommand.Delete) {
transfer(InternalTransferManager.TransferMode.Delete) transfer(InternalTransferManager.TransferMode.Delete)
} else if (actionCommand == TransportPopupMenu.ActionCommand.Refresh) { } else if (actionCommand == TransportPopupMenu.ActionCommand.Refresh) {
val filename = getSelectFilename()
if (filename != null) registerSelectRow(filename)
reload(requestFocus = true) reload(requestFocus = true)
} else if (actionCommand == TransportPopupMenu.ActionCommand.Edit) { } else if (actionCommand == TransportPopupMenu.ActionCommand.Edit) {
edit() edit()
@@ -1139,7 +1213,9 @@ internal class TransportPanel(
private fun edit() { private fun edit() {
for (path in files.map { it.first }) { 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) val transferId = internalTransferManager.addHighTransfer(path, target)
editTransferListener.addListenTransfer(transferId) editTransferListener.addListenTransfer(transferId)
} }

View File

@@ -4,6 +4,7 @@ import app.termora.*
import app.termora.plugin.ExtensionManager import app.termora.plugin.ExtensionManager
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.util.SystemInfo
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
@@ -41,7 +42,14 @@ internal class TransportPopupMenu(
private val copyPathMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.copy-path")) private val copyPathMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.copy-path"))
private val copyMenu = JMenuItem(I18n.getString("termora.copy")) private val copyMenu = JMenuItem(I18n.getString("termora.copy"))
private val pasteMenu = JMenuItem(I18n.getString("termora.paste")) private val pasteMenu = JMenuItem(I18n.getString("termora.paste"))
private val openInFinderMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.open-in-folder")) private val openInFinderMenu = JMenuItem(
I18n.getString(
"termora.transport.table.contextmenu.open-in-folder",
if (SystemInfo.isMacOS) I18n.getString("termora.finder")
else if (SystemInfo.isWindows) I18n.getString("termora.explorer")
else I18n.getString("termora.folder")
)
)
private val renameMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.rename")) private val renameMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.rename"))
private val deleteMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.delete")) private val deleteMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.delete"))

View File

@@ -8,6 +8,8 @@ import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.apache.sshd.client.SshClient import org.apache.sshd.client.SshClient
import org.apache.sshd.client.session.ClientSession import org.apache.sshd.client.session.ClientSession
import org.apache.sshd.core.CoreModuleProperties
import org.apache.sshd.sftp.SftpModuleProperties
import org.apache.sshd.sftp.client.SftpClientFactory import org.apache.sshd.sftp.client.SftpClientFactory
internal class SFTPTransferProtocolProvider : TransferProtocolProvider { internal class SFTPTransferProtocolProvider : TransferProtocolProvider {
@@ -32,6 +34,11 @@ internal class SFTPTransferProtocolProvider : TransferProtocolProvider {
client = if (owner == null) SshClients.openClient(requester.host) client = if (owner == null) SshClients.openClient(requester.host)
else SshClients.openClient(requester.host, owner) else SshClients.openClient(requester.host, owner)
session = SshClients.openSession(requester.host, client) session = SshClients.openSession(requester.host, client)
CoreModuleProperties.IO_CONNECT_TIMEOUT.get(client).ifPresent { e ->
SftpModuleProperties.SFTP_CHANNEL_OPEN_TIMEOUT.set(session, e)
}
val fileSystem = SftpClientFactory.instance().createSftpFileSystem(session) val fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
val host = requester.host val host = requester.host

View File

@@ -3,6 +3,8 @@ package app.termora.tree
import app.termora.* import app.termora.*
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.account.AccountManager import app.termora.account.AccountManager
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.actions.OpenHostAction import app.termora.actions.OpenHostAction
import app.termora.database.DatabaseChangedExtension import app.termora.database.DatabaseChangedExtension
import app.termora.database.DatabaseManager import app.termora.database.DatabaseManager
@@ -32,6 +34,10 @@ import org.jdesktop.swingx.action.ActionManager
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.NodeList 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.awt.event.*
import java.io.* import java.io.*
import java.util.* import java.util.*
@@ -140,6 +146,61 @@ class NewHostTree : SimpleTree(), Disposable {
} }
}) })
// 开启 ToolTip 功能
ToolTipManager.sharedInstance().registerComponent(this)
// 设置鼠标移动提示
addMouseMotionListener(object : java.awt.event.MouseMotionAdapter() {
override fun mouseMoved(e: MouseEvent) {
val path: TreePath? = getPathForLocation(e.x, e.y)
if (path != null) {
val node: HostTreeNode = path.lastPathComponent as HostTreeNode
if (node.host.remark.isNotEmpty()){
toolTipText = node.host.remark
}else{
toolTipText = null
}
} else {
toolTipText = null
}
}
})
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() { fun restoreExpansions() {
@@ -152,6 +213,9 @@ class NewHostTree : SimpleTree(), Disposable {
} }
override fun dispose() { override fun dispose() {
// 销毁
ToolTipManager.sharedInstance().unregisterComponent(this)
val name = super.getName() val name = super.getName()
if (name.isNullOrBlank().not()) { if (name.isNullOrBlank().not()) {
properties.putString("${name}.state", TreeUtils.saveExpansionState(this)) properties.putString("${name}.state", TreeUtils.saveExpansionState(this))
@@ -198,10 +262,12 @@ class NewHostTree : SimpleTree(), Disposable {
val sshMenu = importMenu.add(".ssh/config") val sshMenu = importMenu.add(".ssh/config")
val mobaXtermMenu = importMenu.add("MobaXterm") 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 open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect"))
val openWith = popupMenu.add(JMenu(I18n.getString("termora.welcome.contextmenu.connect-with"))) as JMenu 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 openWithSFTPCommand = openWith.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
val openInNewWindow = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open-in-new-window")) val openInNewWindow = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open-in-new-window"))
popupMenu.addSeparator() popupMenu.addSeparator()
@@ -389,6 +455,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) popupMenu.show(this, evt.x, evt.y)
} }
@@ -1075,5 +1155,23 @@ class NewHostTree : SimpleTree(), Disposable {
electerm, 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)
}
}
}

View File

@@ -2,6 +2,7 @@ package app.termora.tree
import javax.swing.Icon import javax.swing.Icon
import javax.swing.tree.DefaultMutableTreeNode import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.TreeNode
abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) { abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@@ -35,4 +36,15 @@ abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) {
return children 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
}
} }

View File

@@ -239,6 +239,8 @@ termora.keymgr.table.name=Name
termora.keymgr.table.type=Type termora.keymgr.table.type=Type
termora.keymgr.table.length=Length termora.keymgr.table.length=Length
termora.keymgr.table.remark=Description 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.number=Number of hosts [{0}] Number of public keys [{1}]
termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied} termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied}
@@ -248,6 +250,7 @@ termora.keymgr.ssh-copy-id.end=End of public key copying
# Tabbed # Tabbed
termora.tabbed.contextmenu.rename=Rename termora.tabbed.contextmenu.rename=Rename
termora.tabbed.contextmenu.select-host=Select Host
termora.tabbed.contextmenu.sftp-command=SFTP Command 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.sftp-not-install=SFTP programme not found, please install and try again
termora.tabbed.contextmenu.clone=Clone termora.tabbed.contextmenu.clone=Clone
@@ -308,6 +311,14 @@ termora.tools.multiple=Send command to the current window sessions
termora.transport.local=Local termora.transport.local=Local
termora.transport.file-already-exists=The file {0} already exists termora.transport.file-already-exists=The file {0} already exists
termora.transport.toolbar.prev=Backward
termora.transport.toolbar.home=Home Folder
termora.transport.toolbar.next=Forward
termora.transport.toolbar.parent=Parent Folder
termora.transport.toolbar.show-hide=Show/Hide Folders
termora.transport.toolbar.refresh=Refresh Folder
termora.transport.bookmarks=Bookmarks Manager termora.transport.bookmarks=Bookmarks Manager
termora.transport.bookmarks.up=Up termora.transport.bookmarks.up=Up
termora.transport.bookmarks.down=Down termora.transport.bookmarks.down=Down
@@ -324,7 +335,7 @@ termora.transport.table.owner=Owner
termora.transport.table.contextmenu.transfer=Transfer termora.transport.table.contextmenu.transfer=Transfer
termora.transport.table.contextmenu.edit=${termora.keymgr.edit} termora.transport.table.contextmenu.edit=${termora.keymgr.edit}
termora.transport.table.contextmenu.copy-path=Copy Path termora.transport.table.contextmenu.copy-path=Copy Path
termora.transport.table.contextmenu.open-in-folder=Open in ${termora.finder} termora.transport.table.contextmenu.open-in-folder=Open in {0}
termora.transport.table.contextmenu.rename=${termora.welcome.contextmenu.rename} termora.transport.table.contextmenu.rename=${termora.welcome.contextmenu.rename}
termora.transport.table.contextmenu.delete=${termora.remove} termora.transport.table.contextmenu.delete=${termora.remove}
termora.transport.table.contextmenu.rm-warning=Using the rm -rf command to delete a file is very dangerous termora.transport.table.contextmenu.rm-warning=Using the rm -rf command to delete a file is very dangerous
@@ -430,6 +441,8 @@ termora.visual-window.system-information.mem=Mem
termora.visual-window.system-information.swap=Swap termora.visual-window.system-information.swap=Swap
termora.visual-window.system-information.filesystem=Filesystem termora.visual-window.system-information.filesystem=Filesystem
termora.visual-window.system-information.used-total=Used / Total termora.visual-window.system-information.used-total=Used / Total
termora.visual-window.toggle-window=Toggle window
termora.visual-window.transport.question=More Features
termora.visual-window.nvidia-smi=NVIDIA SMI termora.visual-window.nvidia-smi=NVIDIA SMI

View File

@@ -69,27 +69,6 @@ termora.settings.terminal.floating-toolbar=Плавающая панель
termora.settings.terminal.auto-close-tab=Автозакрытие вкладки termora.settings.terminal.auto-close-tab=Автозакрытие вкладки
termora.settings.terminal.auto-close-tab-description=Автоматически закрывать вкладку при обычном отключении терминала 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=О программе
termora.settings.about.author=Автор termora.settings.about.author=Автор
termora.settings.about.source=Ссылка termora.settings.about.source=Ссылка
@@ -204,6 +183,8 @@ termora.keymgr.table.name=Название
termora.keymgr.table.type=Тип termora.keymgr.table.type=Тип
termora.keymgr.table.length=Длина termora.keymgr.table.length=Длина
termora.keymgr.table.remark=Описание 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.number=Кол-во хостов [{0}] Кол-во публичных ключей [{1}]
termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied} termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied}
@@ -212,6 +193,7 @@ termora.keymgr.ssh-copy-id.end=Копирования открытого клю
# Tabbed # Tabbed
termora.tabbed.contextmenu.rename=Переименовать termora.tabbed.contextmenu.rename=Переименовать
termora.tabbed.contextmenu.select-host=Выбрать хост
termora.tabbed.contextmenu.sftp-command=SFTP Команда termora.tabbed.contextmenu.sftp-command=SFTP Команда
termora.tabbed.contextmenu.sftp-not-install=Программа SFTP не найдена, пожалуйста, установите и повторите попытку. termora.tabbed.contextmenu.sftp-not-install=Программа SFTP не найдена, пожалуйста, установите и повторите попытку.
termora.tabbed.contextmenu.clone=Дублировать termora.tabbed.contextmenu.clone=Дублировать
@@ -269,6 +251,14 @@ termora.transport.bookmarks=Менеджер закладок
termora.transport.bookmarks.up=Вверх termora.transport.bookmarks.up=Вверх
termora.transport.bookmarks.down=Вниз termora.transport.bookmarks.down=Вниз
termora.transport.toolbar.prev=Назад
termora.transport.toolbar.home=Домашняя папка
termora.transport.toolbar.next=Вперёд
termora.transport.toolbar.parent=Родительская папка
termora.transport.toolbar.show-hide=Показать/Скрыть папки
termora.transport.toolbar.refresh=Обновить
termora.transport.table.filename=Имя файла termora.transport.table.filename=Имя файла
termora.transport.table.type=Тип termora.transport.table.type=Тип
termora.transport.table.type.symbolic-link=Символьная Ссылка termora.transport.table.type.symbolic-link=Символьная Ссылка
@@ -376,6 +366,8 @@ termora.visual-window.system-information.mem=Память
termora.visual-window.system-information.swap=Подкачка termora.visual-window.system-information.swap=Подкачка
termora.visual-window.system-information.filesystem=Файловая система termora.visual-window.system-information.filesystem=Файловая система
termora.visual-window.system-information.used-total=Использовано / Всего termora.visual-window.system-information.used-total=Использовано / Всего
termora.visual-window.toggle-window=Переключить окно
termora.visual-window.transport.question=Больше возможностей
termora.visual-window.nvidia-smi=NVIDIA SMI termora.visual-window.nvidia-smi=NVIDIA SMI

View File

@@ -232,6 +232,8 @@ termora.keymgr.table.name=名称
termora.keymgr.table.type=类型 termora.keymgr.table.type=类型
termora.keymgr.table.length=长度 termora.keymgr.table.length=长度
termora.keymgr.table.remark=备注 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.number=主机数量 [{0}] 公钥数量 [{1}]
termora.keymgr.ssh-copy-id.failed=复制失败 termora.keymgr.ssh-copy-id.failed=复制失败
@@ -244,6 +246,7 @@ termora.tools.multiple=将命令发送到当前窗口会话
# Tabbed # Tabbed
termora.tabbed.contextmenu.rename=重命名 termora.tabbed.contextmenu.rename=重命名
termora.tabbed.contextmenu.select-host=选中主机
termora.tabbed.contextmenu.sftp-command=SFTP 终端 termora.tabbed.contextmenu.sftp-command=SFTP 终端
termora.tabbed.contextmenu.sftp-not-install=没有找到 SFTP 程序,请安装后重试 termora.tabbed.contextmenu.sftp-not-install=没有找到 SFTP 程序,请安装后重试
termora.tabbed.contextmenu.clone=克隆 termora.tabbed.contextmenu.clone=克隆
@@ -309,6 +312,15 @@ termora.transport.bookmarks=书签管理
termora.transport.bookmarks.up=上移 termora.transport.bookmarks.up=上移
termora.transport.bookmarks.down=下移 termora.transport.bookmarks.down=下移
termora.transport.toolbar.prev=返回
termora.transport.toolbar.home=默认目录
termora.transport.toolbar.next=前进
termora.transport.toolbar.parent=父目录
termora.transport.toolbar.show-hide=显示/隐藏目录
termora.transport.toolbar.refresh=刷新
termora.transport.table.filename=文件名 termora.transport.table.filename=文件名
termora.transport.table.type=类型 termora.transport.table.type=类型
termora.transport.table.size=大小 termora.transport.table.size=大小
@@ -320,7 +332,7 @@ termora.transport.table.owner=所有者
# contextmenu # contextmenu
termora.transport.table.contextmenu.transfer=传输 termora.transport.table.contextmenu.transfer=传输
termora.transport.table.contextmenu.copy-path=复制路径 termora.transport.table.contextmenu.copy-path=复制路径
termora.transport.table.contextmenu.open-in-folder=${termora.finder}中打开 termora.transport.table.contextmenu.open-in-folder={0}中打开
termora.transport.table.contextmenu.change-permissions=更改权限... termora.transport.table.contextmenu.change-permissions=更改权限...
termora.transport.table.contextmenu.refresh=刷新 termora.transport.table.contextmenu.refresh=刷新
termora.transport.table.contextmenu.compress=压缩 termora.transport.table.contextmenu.compress=压缩
@@ -426,6 +438,8 @@ termora.visual-window.system-information.mem=内存
termora.visual-window.system-information.swap=交换 termora.visual-window.system-information.swap=交换
termora.visual-window.system-information.filesystem=文件系统 termora.visual-window.system-information.filesystem=文件系统
termora.visual-window.system-information.used-total=使用 / 大小 termora.visual-window.system-information.used-total=使用 / 大小
termora.visual-window.toggle-window=切换窗口
termora.visual-window.transport.question=更多功能
termora.floating-toolbar.close-in-current-tab=在当前标签页关闭 termora.floating-toolbar.close-in-current-tab=在当前标签页关闭

View File

@@ -228,6 +228,8 @@ termora.keymgr.table.name=名稱
termora.keymgr.table.type=型別 termora.keymgr.table.type=型別
termora.keymgr.table.length=長度 termora.keymgr.table.length=長度
termora.keymgr.table.remark=備註 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.number=主機數量 [{0}] 公鑰數量 [{1}]
termora.keymgr.ssh-copy-id.failed=複製失敗 termora.keymgr.ssh-copy-id.failed=複製失敗
@@ -239,6 +241,7 @@ termora.tools.multiple=將命令傳送到目前視窗會話
# Tabbed # Tabbed
termora.tabbed.contextmenu.rename=重新命名 termora.tabbed.contextmenu.rename=重新命名
termora.tabbed.contextmenu.select-host=選取主機
termora.tabbed.contextmenu.sftp-command=SFTP 終端 termora.tabbed.contextmenu.sftp-command=SFTP 終端
termora.tabbed.contextmenu.sftp-not-install=沒有找到 SFTP 程序,請安裝後重試 termora.tabbed.contextmenu.sftp-not-install=沒有找到 SFTP 程序,請安裝後重試
termora.tabbed.contextmenu.clone=克隆 termora.tabbed.contextmenu.clone=克隆
@@ -304,6 +307,13 @@ termora.transport.bookmarks=書籤管理
termora.transport.bookmarks.up=上移 termora.transport.bookmarks.up=上移
termora.transport.bookmarks.down=下移 termora.transport.bookmarks.down=下移
termora.transport.toolbar.prev=返回
termora.transport.toolbar.home=預設目錄
termora.transport.toolbar.next=前進
termora.transport.toolbar.parent=父目錄
termora.transport.toolbar.show-hide=顯示/隱藏目錄
termora.transport.toolbar.refresh=重新整理
termora.transport.table.filename=檔名 termora.transport.table.filename=檔名
termora.transport.table.type=類型 termora.transport.table.type=類型
termora.transport.table.size=大小 termora.transport.table.size=大小
@@ -315,7 +325,7 @@ termora.transport.table.owner=所有者
# contextmenu # contextmenu
termora.transport.table.contextmenu.transfer=傳輸 termora.transport.table.contextmenu.transfer=傳輸
termora.transport.table.contextmenu.copy-path=複製路徑 termora.transport.table.contextmenu.copy-path=複製路徑
termora.transport.table.contextmenu.open-in-folder=${termora.finder}中打開 termora.transport.table.contextmenu.open-in-folder={0}中打開
termora.transport.table.contextmenu.change-permissions=更改權限... termora.transport.table.contextmenu.change-permissions=更改權限...
termora.transport.table.contextmenu.refresh=刷新 termora.transport.table.contextmenu.refresh=刷新
termora.transport.table.contextmenu.compress=壓縮 termora.transport.table.contextmenu.compress=壓縮
@@ -413,6 +423,8 @@ termora.visual-window.system-information.mem=內存
termora.visual-window.system-information.swap=交換 termora.visual-window.system-information.swap=交換
termora.visual-window.system-information.filesystem=檔案系統 termora.visual-window.system-information.filesystem=檔案系統
termora.visual-window.system-information.used-total=使用 / 大小 termora.visual-window.system-information.used-total=使用 / 大小
termora.visual-window.toggle-window=切換視窗
termora.visual-window.transport.question=更多功能
termora.floating-toolbar.close-in-current-tab=在目前標籤頁關閉 termora.floating-toolbar.close-in-current-tab=在目前標籤頁關閉

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -34,6 +34,7 @@ DisableProgramGroupPage=yes
;PrivilegesRequired=lowest ;PrivilegesRequired=lowest
OutputDir={#MyOutputDir} OutputDir={#MyOutputDir}
OutputBaseFilename={#MyAppName}-{#MyAppVersion} OutputBaseFilename={#MyAppName}-{#MyAppVersion}
Compression=lzma2/max
SolidCompression=yes SolidCompression=yes
WizardStyle=classic WizardStyle=classic
;WizardStyle=modern ;WizardStyle=modern

View File

@@ -0,0 +1,12 @@
FROM debian:bookworm
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Shanghai
RUN sed -i 's|http://deb.debian.org/debian|http://mirrors.aliyun.com/debian|g' /etc/apt/sources.list.d/debian.sources \
&& sed -i 's|http://security.debian.org/debian-security|http://mirrors.aliyun.com/debian-security|g' /etc/apt/sources.list.d/debian.sources
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates autoconf libevent-dev bison automake libtool pkg-config build-essential libncurses-dev
RUN git clone https://github.com/tmux/tmux.git && cd tmux && sh autogen.sh && ./configure && make && make install