Compare commits

..

75 Commits

Author SHA1 Message Date
dependabot[bot]
a5c524f761 chore(deps): bump com.github.mwiede:jsch from 2.27.3 to 2.27.7
Bumps [com.github.mwiede:jsch](https://github.com/mwiede/jsch) from 2.27.3 to 2.27.7.
- [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.3...jsch-2.27.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-28 02:17:27 +00:00
dependabot[bot]
5b165ed587 chore(deps): bump com.maxmind.geoip2:geoip2 from 4.4.0 to 5.0.0
Bumps [com.maxmind.geoip2:geoip2](https://github.com/maxmind/GeoIP2-java) from 4.4.0 to 5.0.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.4.0...v5.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 18:16:48 +08:00
dependabot[bot]
d73b3b706e chore(deps): bump com.qcloud:cos_api from 5.6.257 to 5.6.259
Bumps [com.qcloud:cos_api](https://github.com/tencentyun/cos-java-sdk-v5) from 5.6.257 to 5.6.259.
- [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.259
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-22 07:04:52 +08:00
dependabot[bot]
2928b35585 chore(deps): bump org.apache.commons:commons-lang3 from 3.19.0 to 3.20.0
Bumps org.apache.commons:commons-lang3 from 3.19.0 to 3.20.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 09:04:14 +08:00
hstyi
04bece21ff feat: insert new terminal tab at the correct index in terminal tab manager 2025-11-17 08:12:55 +08:00
hstyi
9e2e104baa chore: enhance terminal tab closing behavior to support reconnect option 2025-11-13 09:32:36 +08:00
hstyi
0615378a17 fix: selected terminal tab when transferring to a host 2025-11-11 10:52:28 +08:00
dependabot[bot]
013b03f9ef chore(deps): bump commons-codec:commons-codec from 1.19.0 to 1.20.0
Bumps [commons-codec:commons-codec](https://github.com/apache/commons-codec) from 1.19.0 to 1.20.0.
- [Changelog](https://github.com/apache/commons-codec/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-codec/compare/rel/commons-codec-1.19.0...rel/commons-codec-1.20.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 10:17:06 +08:00
hstyi
026b13ba05 feat: add support for UI scaling via TERMORA_SCALE environment variable 2025-11-07 09:59:37 +08:00
dependabot[bot]
6ec526eeeb chore(deps): bump com.fazecast:jSerialComm from 2.11.2 to 2.11.4
Bumps [com.fazecast:jSerialComm](https://github.com/Fazecast/jSerialComm) from 2.11.2 to 2.11.4.
- [Release notes](https://github.com/Fazecast/jSerialComm/releases)
- [Commits](https://github.com/Fazecast/jSerialComm/commits)

---
updated-dependencies:
- dependency-name: com.fazecast:jSerialComm
  dependency-version: 2.11.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-04 11:00:57 +08:00
dependabot[bot]
e064bb9bb5 chore(deps): bump com.github.hstyi:geolite2
Bumps [com.github.hstyi:geolite2](https://github.com/hstyi/GeoLite2) from v1.0-202510200054 to v1.0-202510270056.
- [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-202510270056
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-03 15:58:19 +08:00
dependabot[bot]
1f3fb5e2c0 chore(deps): bump okhttp from 5.2.1 to 5.3.0
Bumps `okhttp` from 5.2.1 to 5.3.0.

Updates `com.squareup.okhttp3:okhttp` from 5.2.1 to 5.3.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.2.1...parent-5.3.0)

Updates `com.squareup.okhttp3:logging-interceptor` from 5.2.1 to 5.3.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.2.1...parent-5.3.0)

---
updated-dependencies:
- dependency-name: com.squareup.okhttp3:okhttp
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.squareup.okhttp3:logging-interceptor
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-03 10:37:23 +08:00
hstyi
5984f3e856 chore: update JBR version to 21.0.8-b1163.69 2025-11-03 10:24:58 +08:00
hstyi
572c381e90 release: 2.0.0-beta.15 2025-11-03 09:23:44 +08:00
hstyi
7a8ecb06bf feat: add help message for removing shortcuts in keymap settings 2025-10-31 09:37:25 +08:00
hstyi
4c928ac826 feat: add domain field to SMB host options 2025-10-30 15:15:41 +08:00
dependabot[bot]
d07f9ede8c chore(deps): bump org.testcontainers:testcontainers-bom
Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/testcontainers/testcontainers-java/releases)
- [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testcontainers/testcontainers-java/compare/2.0.0...2.0.1)

---
updated-dependencies:
- dependency-name: org.testcontainers:testcontainers-bom
  dependency-version: 2.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-30 09:35:24 +08:00
dependabot[bot]
21a015bf8c chore(deps): bump org.javadelight:delight-rhino-sandbox
Bumps [org.javadelight:delight-rhino-sandbox](https://github.com/javadelight/delight-rhino-sandbox) from 0.0.17 to 0.2.1.
- [Release notes](https://github.com/javadelight/delight-rhino-sandbox/releases)
- [Commits](https://github.com/javadelight/delight-rhino-sandbox/compare/v0.0.17...v0.2.1)

---
updated-dependencies:
- dependency-name: org.javadelight:delight-rhino-sandbox
  dependency-version: 0.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-30 09:33:18 +08:00
hstyi
71a1f5db4b fix: replace WindowScope with Window in context menu extensions 2025-10-29 11:25:34 +08:00
hstyi
96fd07a6ff chore: update Alpine repository mirror and adjust SSH configuration 2025-10-28 10:18:23 +08:00
hstyi
733e062a7b chore(deps): correct module reference for testcontainers-junit-jupiter 2025-10-27 17:41:29 +08:00
hstyi
e87a779adc chore: refactor Mixpanel integration and add event tracking 2025-10-27 17:41:29 +08:00
dependabot[bot]
9c6aa4dcb6 chore(deps): bump com.mixpanel:mixpanel-java from 1.5.3 to 1.5.4
Bumps [com.mixpanel:mixpanel-java](https://github.com/mixpanel/mixpanel-java) from 1.5.3 to 1.5.4.
- [Release notes](https://github.com/mixpanel/mixpanel-java/releases)
- [Commits](https://github.com/mixpanel/mixpanel-java/compare/mixpanel-java-1.5.3...v1.5.4)

---
updated-dependencies:
- dependency-name: com.mixpanel:mixpanel-java
  dependency-version: 1.5.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 16:06:57 +08:00
dependabot[bot]
566b087eb1 chore(deps): bump com.github.oshi:oshi-core from 6.9.0 to 6.9.1
Bumps [com.github.oshi:oshi-core](https://github.com/oshi/oshi) from 6.9.0 to 6.9.1.
- [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.9.0...oshi-parent-6.9.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-26 21:08:45 +08:00
dependabot[bot]
2235e4c2a4 chore(deps): bump org.commonmark:commonmark from 0.26.0 to 0.27.0
Bumps [org.commonmark:commonmark](https://github.com/commonmark/commonmark-java) from 0.26.0 to 0.27.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.26.0...commonmark-parent-0.27.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-24 17:27:28 +08:00
dependabot[bot]
3b9d1f277b chore(deps): bump kotlin from 2.2.20 to 2.2.21
Bumps `kotlin` from 2.2.20 to 2.2.21.

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-24 11:03:42 +08:00
dependabot[bot]
5110595404 chore(deps): bump com.fasterxml.uuid:java-uuid-generator
Bumps [com.fasterxml.uuid:java-uuid-generator](https://github.com/cowtowncoder/java-uuid-generator) from 5.1.0 to 5.1.1.
- [Commits](https://github.com/cowtowncoder/java-uuid-generator/compare/java-uuid-generator-5.1.0...java-uuid-generator-5.1.1)

---
updated-dependencies:
- dependency-name: com.fasterxml.uuid:java-uuid-generator
  dependency-version: 5.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-24 09:16:25 +08:00
dependabot[bot]
034e0939be chore(deps): bump exposed from 1.0.0-rc-1 to 1.0.0-rc-2
Bumps `exposed` from 1.0.0-rc-1 to 1.0.0-rc-2.

Updates `org.jetbrains.exposed:exposed-core` from 1.0.0-rc-1 to 1.0.0-rc-2
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/compare/1.0.0-rc-1...1.0.0-rc-2)

Updates `org.jetbrains.exposed:exposed-crypt` from 1.0.0-rc-1 to 1.0.0-rc-2
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/compare/1.0.0-rc-1...1.0.0-rc-2)

Updates `org.jetbrains.exposed:exposed-jdbc` from 1.0.0-rc-1 to 1.0.0-rc-2
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/compare/1.0.0-rc-1...1.0.0-rc-2)

Updates `org.jetbrains.exposed:exposed-migration-core` from 1.0.0-rc-1 to 1.0.0-rc-2
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/compare/1.0.0-rc-1...1.0.0-rc-2)

---
updated-dependencies:
- dependency-name: org.jetbrains.exposed:exposed-core
  dependency-version: 1.0.0-rc-2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.exposed:exposed-crypt
  dependency-version: 1.0.0-rc-2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.exposed:exposed-jdbc
  dependency-version: 1.0.0-rc-2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.exposed:exposed-migration-core
  dependency-version: 1.0.0-rc-2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-24 09:16:17 +08:00
dependabot[bot]
4ccfa82c8a chore(deps): bump okhttp from 5.2.0 to 5.2.1
Bumps `okhttp` from 5.2.0 to 5.2.1.

Updates `com.squareup.okhttp3:okhttp` from 5.2.0 to 5.2.1
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.2.0...parent-5.2.1)

Updates `com.squareup.okhttp3:logging-interceptor` from 5.2.0 to 5.2.1
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.2.0...parent-5.2.1)

---
updated-dependencies:
- dependency-name: com.squareup.okhttp3:okhttp
  dependency-version: 5.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.squareup.okhttp3:logging-interceptor
  dependency-version: 5.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-24 09:15:59 +08:00
hstyi
d21ae5499a feat: HTTP server for authentication 2025-10-23 17:23:01 +08:00
hstyi
d80a9d48ab fix: data pull may not be possible 2025-10-23 16:51:04 +08:00
dependabot[bot]
b305d6fd34 chore(deps): bump com.github.hstyi:geolite2
Bumps [com.github.hstyi:geolite2](https://github.com/hstyi/GeoLite2) from v1.0-202510060050 to v1.0-202510200054.
- [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-202510200054
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-22 13:48:30 +08:00
dependabot[bot]
756fd305d1 chore(deps): bump com.qcloud:cos_api from 5.6.255.1 to 5.6.257
Bumps [com.qcloud:cos_api](https://github.com/tencentyun/cos-java-sdk-v5) from 5.6.255.1 to 5.6.257.
- [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.257
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 09:31:18 +08:00
dependabot[bot]
f9549fbb7d chore(deps): bump org.testcontainers:testcontainers-bom
Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 1.21.3 to 2.0.0.
- [Release notes](https://github.com/testcontainers/testcontainers-java/releases)
- [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.21.3...2.0.0)

---
updated-dependencies:
- dependency-name: org.testcontainers:testcontainers-bom
  dependency-version: 2.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-16 09:54:22 +08:00
hstyi
e18b454fcc fix: Nord Dark selected color 2025-10-14 11:06:59 +08:00
dependabot[bot]
4f4ccfa7d4 chore(deps): bump flatlaf from 3.6.1 to 3.6.2
Bumps `flatlaf` from 3.6.1 to 3.6.2.

Updates `com.formdev:flatlaf` from 3.6.1 to 3.6.2
- [Release notes](https://github.com/JFormDesigner/FlatLaf/releases)
- [Changelog](https://github.com/JFormDesigner/FlatLaf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JFormDesigner/FlatLaf/compare/3.6.1...3.6.2)

Updates `com.formdev:flatlaf-extras` from 3.6.1 to 3.6.2
- [Release notes](https://github.com/JFormDesigner/FlatLaf/releases)
- [Changelog](https://github.com/JFormDesigner/FlatLaf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JFormDesigner/FlatLaf/compare/3.6.1...3.6.2)

Updates `com.formdev:flatlaf-swingx` from 3.6.1 to 3.6.2
- [Release notes](https://github.com/JFormDesigner/FlatLaf/releases)
- [Changelog](https://github.com/JFormDesigner/FlatLaf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JFormDesigner/FlatLaf/compare/3.6.1...3.6.2)

---
updated-dependencies:
- dependency-name: com.formdev:flatlaf
  dependency-version: 3.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.formdev:flatlaf-extras
  dependency-version: 3.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.formdev:flatlaf-swingx
  dependency-version: 3.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 11:14:29 +08:00
dependabot[bot]
5dfd5fefb2 chore(deps): bump jgit from 7.2.0.202503040940-r to 7.4.0.202509020913-r
Bumps `jgit` from 7.2.0.202503040940-r to 7.4.0.202509020913-r.

Updates `org.eclipse.jgit:org.eclipse.jgit` from 7.2.0.202503040940-r to 7.4.0.202509020913-r
- [Commits](https://github.com/eclipse-jgit/jgit/compare/v7.2.0.202503040940-r...v7.4.0.202509020913-r)

Updates `org.eclipse.jgit:org.eclipse.jgit.ssh.apache` from 7.2.0.202503040940-r to 7.4.0.202509020913-r
- [Commits](https://github.com/eclipse-jgit/jgit/compare/v7.2.0.202503040940-r...v7.4.0.202509020913-r)

Updates `org.eclipse.jgit:org.eclipse.jgit.ssh.apache.agent` from 7.2.0.202503040940-r to 7.4.0.202509020913-r
- [Commits](https://github.com/eclipse-jgit/jgit/compare/v7.2.0.202503040940-r...v7.4.0.202509020913-r)

---
updated-dependencies:
- dependency-name: org.eclipse.jgit:org.eclipse.jgit
  dependency-version: 7.4.0.202509020913-r
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.eclipse.jgit:org.eclipse.jgit.ssh.apache
  dependency-version: 7.4.0.202509020913-r
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.eclipse.jgit:org.eclipse.jgit.ssh.apache.agent
  dependency-version: 7.4.0.202509020913-r
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-10 10:21:56 +08:00
dependabot[bot]
a7ea4c70d2 chore(deps): bump com.qcloud:cos_api from 5.6.255 to 5.6.255.1
Bumps [com.qcloud:cos_api](https://github.com/tencentyun/cos-java-sdk-v5) from 5.6.255 to 5.6.255.1.
- [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.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-10 10:21:31 +08:00
dependabot[bot]
b7796f58f0 chore(deps): bump org.apache.commons:commons-lang3 from 3.18.0 to 3.19.0
Bumps org.apache.commons:commons-lang3 from 3.18.0 to 3.19.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-10 10:21:25 +08:00
dependabot[bot]
c7bedc57e0 chore(deps): bump io.minio:minio from 8.5.17 to 8.6.0
Bumps [io.minio:minio](https://github.com/minio/minio-java) from 8.5.17 to 8.6.0.
- [Release notes](https://github.com/minio/minio-java/releases)
- [Commits](https://github.com/minio/minio-java/compare/8.5.17...8.6.0)

---
updated-dependencies:
- dependency-name: io.minio:minio
  dependency-version: 8.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-10 10:21:14 +08:00
dependabot[bot]
935f305ada chore(deps): bump com.github.hstyi:geolite2
Bumps [com.github.hstyi:geolite2](https://github.com/hstyi/GeoLite2) from v1.0-202508180058 to v1.0-202510060050.
- [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-202510060050
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-10 10:21:01 +08:00
dependabot[bot]
8cf47a7ca1 chore(deps): bump okhttp from 5.1.0 to 5.2.0
Bumps `okhttp` from 5.1.0 to 5.2.0.

Updates `com.squareup.okhttp3:okhttp` from 5.1.0 to 5.2.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.1.0...parent-5.2.0)

Updates `com.squareup.okhttp3:logging-interceptor` from 5.1.0 to 5.2.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.1.0...parent-5.2.0)

---
updated-dependencies:
- dependency-name: com.squareup.okhttp3:okhttp
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.squareup.okhttp3:logging-interceptor
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-09 14:56:29 +08:00
dependabot[bot]
c6c5ad711d chore(deps): bump jna from 5.17.0 to 5.18.1
Bumps `jna` from 5.17.0 to 5.18.1.

Updates `net.java.dev.jna:jna` from 5.17.0 to 5.18.1
- [Changelog](https://github.com/java-native-access/jna/blob/master/CHANGES.md)
- [Commits](https://github.com/java-native-access/jna/compare/5.17.0...5.18.1)

Updates `net.java.dev.jna:jna-platform` from 5.17.0 to 5.18.1
- [Changelog](https://github.com/java-native-access/jna/blob/master/CHANGES.md)
- [Commits](https://github.com/java-native-access/jna/compare/5.17.0...5.18.1)

---
updated-dependencies:
- dependency-name: net.java.dev.jna:jna
  dependency-version: 5.18.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: net.java.dev.jna:jna-platform
  dependency-version: 5.18.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-04 14:08:52 +08:00
hstyi
5fc76d955a feat: supports ECDSA keypair 2025-10-01 15:25:31 +08:00
hstyi
0aabe1b0dc chore: jbrsdk-21.0.8 2025-09-28 14:23:16 +08:00
hsurich
820c4274e7 chore: allow case-insensitive search including remarks 2025-09-28 07:02:19 +08: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
80 changed files with 860 additions and 408 deletions

View File

@@ -3,8 +3,8 @@ name: Linux
on: [ push, pull_request ] on: [ push, pull_request ]
env: env:
JBR_MAJOR: 21.0.7 JBR_MAJOR: 21.0.8
JBR_PATCH: b1038.58 JBR_PATCH: b1163.69
jobs: jobs:
build: build:

View File

@@ -8,15 +8,15 @@ env:
# 只有发布版本时才需要公证 # 只有发布版本时才需要公证
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}" TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }} TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
JBR_MAJOR: 21.0.7 JBR_MAJOR: 21.0.8
JBR_PATCH: b1038.58 JBR_PATCH: b1163.69
jobs: jobs:
build: build:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ macos-15, macos-13 ] os: [ macos-15-intel, macos-latest ]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:

View File

@@ -3,8 +3,8 @@ name: Windows
on: [ push, pull_request ] on: [ push, pull_request ]
env: env:
JBR_MAJOR: 21.0.7 JBR_MAJOR: 21.0.8
JBR_PATCH: b1038.58 JBR_PATCH: b1163.69
jobs: jobs:
build: build:

View File

@@ -1 +1 @@
2.0.0-beta.13 2.0.0-beta.15

View File

@@ -339,6 +339,7 @@ tasks.register<Exec>("jlink") {
"java.security.jgss", "java.security.jgss",
"jdk.crypto.ec", "jdk.crypto.ec",
"jdk.unsupported", "jdk.unsupported",
"jdk.httpserver",
) )
commandLine( commandLine(
@@ -404,18 +405,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"))

View File

@@ -1,13 +1,13 @@
[versions] [versions]
kotlin = "2.2.10" kotlin = "2.2.21"
slf4j = "2.0.17" slf4j = "2.0.17"
pty4j = "0.13.10" pty4j = "0.13.10"
tinylog = "2.7.0" tinylog = "2.7.0"
kotlinx-coroutines = "1.10.2" kotlinx-coroutines = "1.10.2"
flatlaf = "3.6.1" flatlaf = "3.6.2"
kotlinx-serialization-json = "1.9.0" kotlinx-serialization-json = "1.9.0"
commons-codec = "1.19.0" commons-codec = "1.20.0"
commons-lang3 = "3.18.0" commons-lang3 = "3.20.0"
commons-csv = "1.14.1" commons-csv = "1.14.1"
commons-net = "3.12.0" commons-net = "3.12.0"
commons-text = "1.14.0" commons-text = "1.14.0"
@@ -16,35 +16,35 @@ 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.1"
versioncompare = "1.4.1" versioncompare = "1.4.1"
jna = "5.17.0" jna = "5.18.1"
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.7"
okhttp = "5.1.0" okhttp = "5.3.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.4.0.202509020913-r"
commonmark = "0.25.1" commonmark = "0.27.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"
colorpicker = "2.0.1" colorpicker = "2.0.1"
rhino = "1.8.0" rhino = "1.8.0"
delight-rhino-sandbox = "0.0.17" delight-rhino-sandbox = "0.2.1"
testcontainers = "1.21.3" testcontainers = "2.0.1"
mixpanel = "1.5.3" mixpanel = "1.5.4"
jSerialComm = "2.11.2" jSerialComm = "2.11.4"
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-2"
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.1"
semver4j = "6.0.0" semver4j = "6.0.0"
jsvg = "2.0.0" jsvg = "2.0.0"
dom4j = "2.2.0" dom4j = "2.2.0"
@@ -70,7 +70,7 @@ flatlafextras = { group = "com.formdev", name = "flatlaf-extras", version.ref =
flatlafswingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" } flatlafswingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "testcontainers" } testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "testcontainers" }
testcontainers = { module = "org.testcontainers:testcontainers" } testcontainers = { module = "org.testcontainers:testcontainers" }
testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter" } testcontainers-junit-jupiter = { module = "org.testcontainers:testcontainers-junit-jupiter" }
swingx = { module = "org.swinglabs.swingx:swingx-all", version.ref = "swingx" } swingx = { module = "org.swinglabs.swingx:swingx-all", version.ref = "swingx" }
jgoodies-forms = { module = "com.jgoodies:jgoodies-forms", version.ref = "jgoodies-forms" } jgoodies-forms = { module = "com.jgoodies:jgoodies-forms", version.ref = "jgoodies-forms" }
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" } jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
@@ -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

@@ -8,7 +8,7 @@ project.version = "0.0.4"
dependencies { dependencies {
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
implementation("com.qcloud:cos_api:5.6.251") implementation("com.qcloud:cos_api:5.6.259")
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:5.0.0")
// https://github.com/hstyi/geolite2 // https://github.com/hstyi/geolite2
implementation("com.github.hstyi:geolite2:v1.0-202508110059") implementation("com.github.hstyi:geolite2:v1.0-202510270056")
} }
apply(from = "$rootDir/plugins/common.gradle.kts") apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -13,7 +13,7 @@ dependencies {
testImplementation(libs.testcontainers.junit.jupiter) testImplementation(libs.testcontainers.junit.jupiter)
testImplementation(project(":")) testImplementation(project(":"))
implementation("io.minio:minio:8.5.17") implementation("io.minio:minio:8.6.0")
compileOnly(project(":")) compileOnly(project(":"))
} }

View File

@@ -10,7 +10,7 @@ project.version = "0.0.5"
dependencies { dependencies {
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
compileOnly(project(":")) compileOnly(project(":"))
implementation("com.fazecast:jSerialComm:2.11.2") implementation("com.fazecast:jSerialComm:2.11.4")
} }
apply(from = "$rootDir/plugins/common.gradle.kts") apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -2,7 +2,7 @@ plugins {
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.jvm)
} }
project.version = "0.0.3" project.version = "0.0.4"
dependencies { dependencies {
testImplementation(kotlin("test")) testImplementation(kotlin("test"))

View File

@@ -42,6 +42,7 @@ class SMBHostOptionsPane : OptionsPane() {
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text, sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
extras = mutableMapOf( extras = mutableMapOf(
"smb.share" to generalOption.shareTextField.text, "smb.share" to generalOption.shareTextField.text,
"smb.domain" to generalOption.domainTextField.text,
) )
) )
@@ -66,6 +67,7 @@ class SMBHostOptionsPane : OptionsPane() {
generalOption.remarkTextArea.text = host.remark generalOption.remarkTextArea.text = host.remark
generalOption.passwordTextField.text = host.authentication.password generalOption.passwordTextField.text = host.authentication.password
generalOption.shareTextField.text = host.options.extras["smb.share"] ?: StringUtils.EMPTY generalOption.shareTextField.text = host.options.extras["smb.share"] ?: StringUtils.EMPTY
generalOption.domainTextField.text = host.options.extras["smb.domain"] ?: StringUtils.EMPTY
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
} }
@@ -114,6 +116,7 @@ class SMBHostOptionsPane : OptionsPane() {
val nameTextField = OutlineTextField(128) val nameTextField = OutlineTextField(128)
val shareTextField = OutlineTextField(256) val shareTextField = OutlineTextField(256)
val usernameTextField = OutlineComboBox<String>() val usernameTextField = OutlineComboBox<String>()
val domainTextField = OutlineTextField(128)
val hostTextField = OutlineTextField(255) val hostTextField = OutlineTextField(255)
val passwordTextField = OutlinePasswordField(255) val passwordTextField = OutlinePasswordField(255)
val remarkTextArea = FixedLengthTextArea(512) val remarkTextArea = FixedLengthTextArea(512)
@@ -188,7 +191,9 @@ class SMBHostOptionsPane : OptionsPane() {
.add(portTextField).xy(7, rows).apply { rows += step } .add(portTextField).xy(7, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows) .add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step } .add(usernameTextField).xy(3, rows)
.add("${SMBI18n.getString("termora.plugins.smb.domain")}:").xy(5, rows)
.add(domainTextField).xy(7, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows) .add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step } .add(passwordTextField).xyw(3, rows, 5).apply { rows += step }

View File

@@ -30,6 +30,7 @@ class SMBProtocolProvider private constructor() : TransferProtocolProvider {
val client = SMBClient() val client = SMBClient()
val host = requester.host val host = requester.host
val connection = client.connect(host.host, host.port) val connection = client.connect(host.host, host.port)
val domain = host.options.extras["smb.domain"] ?: StringUtils.EMPTY
val session = when (host.username) { val session = when (host.username) {
"Guest" -> connection.authenticate(AuthenticationContext.guest()) "Guest" -> connection.authenticate(AuthenticationContext.guest())
"Anonymous" -> connection.authenticate(AuthenticationContext.anonymous()) "Anonymous" -> connection.authenticate(AuthenticationContext.anonymous())
@@ -37,7 +38,7 @@ class SMBProtocolProvider private constructor() : TransferProtocolProvider {
AuthenticationContext( AuthenticationContext(
host.username, host.username,
host.authentication.password.toCharArray(), host.authentication.password.toCharArray(),
null domain.ifBlank { null }
) )
) )
} }

View File

@@ -1 +1,2 @@
termora.plugins.smb.share=Share name termora.plugins.smb.share=Share name
termora.plugins.smb.domain=Domain

View File

@@ -1 +1,3 @@
termora.plugins.smb.share=共享名称 termora.plugins.smb.share=共享名称
termora.plugins.smb.domain=域名

View File

@@ -1 +1,3 @@
termora.plugins.smb.share=共享名稱 termora.plugins.smb.share=共享名稱
termora.plugins.smb.domain=網域

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

@@ -50,6 +50,17 @@ class ApplicationInitializr {
} }
} }
// https://github.com/TermoraDev/termora/issues/1254
if (System.getProperty(FlatSystemProperties.UI_SCALE).isNullOrBlank()) {
val scale = System.getenv("TERMORA_SCALE")
if (scale.isNullOrBlank().not()) {
if (NumberUtils.toDouble(scale, -1.0) > 0) {
System.setProperty(FlatSystemProperties.UI_SCALE_ENABLED, "true")
System.setProperty(FlatSystemProperties.UI_SCALE, scale)
}
}
}
// 启动 // 启动
val runtime = measureTimeMillis { ApplicationRunner().run() } val runtime = measureTimeMillis { ApplicationRunner().run() }
val log = LoggerFactory.getLogger(javaClass) val log = LoggerFactory.getLogger(javaClass)

View File

@@ -10,15 +10,11 @@ import com.formdev.flatlaf.extras.FlatInspector
import com.formdev.flatlaf.ui.FlatTableCellBorder import com.formdev.flatlaf.ui.FlatTableCellBorder
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import com.jthemedetecor.OsThemeDetector import com.jthemedetecor.OsThemeDetector
import com.mixpanel.mixpanelapi.ClientDelivery
import com.mixpanel.mixpanelapi.MessageBuilder
import com.mixpanel.mixpanelapi.MixpanelAPI
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.LocaleUtils import org.apache.commons.lang3.LocaleUtils
import org.apache.commons.lang3.SystemUtils import org.apache.commons.lang3.SystemUtils
import org.json.JSONObject
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.awt.* import java.awt.*
import java.awt.desktop.AppReopenedEvent import java.awt.desktop.AppReopenedEvent
@@ -369,61 +365,8 @@ class ApplicationRunner {
if (Application.isUnknownVersion()) { if (Application.isUnknownVersion()) {
return return
} }
MixpanelService.getInstance().push("launch")
swingCoroutineScope.launch(Dispatchers.IO) {
try {
val properties = JSONObject()
properties.put("os", SystemUtils.OS_NAME)
if (SystemInfo.isLinux) {
properties.put("platform", "Linux")
} else if (SystemInfo.isWindows) {
properties.put("platform", "Windows")
} else if (SystemInfo.isMacOS) {
properties.put("platform", "macOS")
}
properties.put("version", Application.getVersion())
properties.put("language", Locale.getDefault().toString())
val message = MessageBuilder("0871335f59ee6d0eb246b008a20f9d1c")
.event(getAnalyticsUserID(), "launch", properties)
val delivery = ClientDelivery()
delivery.addMessage(message)
val endpoints = listOf(
"https://api-eu.mixpanel.com",
"https://api-in.mixpanel.com",
"https://api.mixpanel.com",
"http://api.mixpanel.com",
)
for (endpoint in endpoints) {
try {
MixpanelAPI(
"$endpoint/track",
"$endpoint/engage",
"$endpoint/groups"
).deliver(delivery, true)
break
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
continue
}
}
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
}
} }
private fun getAnalyticsUserID(): String {
val properties = DatabaseManager.getInstance().properties
var id = properties.getString("AnalyticsUserID")
if (id.isNullOrBlank()) {
id = randomUUID()
properties.putString("AnalyticsUserID", id)
}
return id
}
} }

View File

@@ -874,6 +874,8 @@ class NordDarkLaf : FlatPropertiesLaf("Nord Dark", Properties().apply {
TerminalColor.Basic.SELECTION_BACKGROUND, TerminalColor.Basic.SELECTION_BACKGROUND,
TerminalColor.Cursor.BACKGROUND -> 0xeceff4 TerminalColor.Cursor.BACKGROUND -> 0xeceff4
TerminalColor.Basic.SELECTION_FOREGROUND -> 0x3b4252
TerminalColor.Basic.FOREGROUND -> 0xd8dee9 TerminalColor.Basic.FOREGROUND -> 0xd8dee9

View File

@@ -0,0 +1,85 @@
package app.termora
import app.termora.database.DatabaseManager
import com.formdev.flatlaf.util.SystemInfo
import com.mixpanel.mixpanelapi.ClientDelivery
import com.mixpanel.mixpanelapi.MessageBuilder
import com.mixpanel.mixpanelapi.MixpanelAPI
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.apache.commons.lang3.SystemUtils
import org.json.JSONObject
import org.slf4j.LoggerFactory
import java.util.*
internal class MixpanelService private constructor() {
companion object {
private val log = LoggerFactory.getLogger(MixpanelService::class.java)
fun getInstance(): MixpanelService {
return ApplicationScope.forApplicationScope().getOrCreate(MixpanelService::class) { MixpanelService() }
}
}
fun push(event: String, extras: Map<String, String> = emptyMap()) {
swingCoroutineScope.launch(Dispatchers.IO) {
try {
val properties = JSONObject()
for (entry in extras) {
properties.put(entry.key, entry.value)
}
properties.put("os", SystemUtils.OS_NAME)
if (SystemInfo.isLinux) {
properties.put("platform", "Linux")
} else if (SystemInfo.isWindows) {
properties.put("platform", "Windows")
} else if (SystemInfo.isMacOS) {
properties.put("platform", "macOS")
}
properties.put("version", Application.getVersion())
properties.put("language", Locale.getDefault().toString())
val message = MessageBuilder("0871335f59ee6d0eb246b008a20f9d1c")
.event(getAnalyticsUserID(), event, properties)
val delivery = ClientDelivery()
delivery.addMessage(message)
val endpoints = listOf(
"https://api-eu.mixpanel.com",
"https://api-in.mixpanel.com",
"https://api.mixpanel.com",
"http://api.mixpanel.com",
)
for (endpoint in endpoints) {
try {
MixpanelAPI(
"$endpoint/track",
"$endpoint/engage",
"$endpoint/groups"
).deliver(delivery, true)
break
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
continue
}
}
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
}
}
private fun getAnalyticsUserID(): String {
val properties = DatabaseManager.getInstance().properties
var id = properties.getString("AnalyticsUserID")
if (id.isNullOrBlank()) {
id = randomUUID()
properties.putString("AnalyticsUserID", id)
}
return id
}
}

View File

@@ -3,5 +3,5 @@ package app.termora
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
import java.util.* import java.util.*
class OpenHostActionEvent(source: Any, val host: Host, event: EventObject) : class OpenHostActionEvent(source: Any, val host: Host, event: EventObject, val tabIndex: Int = -1) :
AnActionEvent(source, String(), event) AnActionEvent(source, String(), event)

View File

@@ -179,7 +179,7 @@ abstract class PtyHostTerminalTab(
val tab = createReconnectTerminalTab() val tab = createReconnectTerminalTab()
manager.addTerminalTab(index, tab, true) manager.addTerminalTab(index, tab, true)
manager.closeTerminalTab(this, true) manager.closeTerminalTab(this, disposable = true, reconnect = true)
if (tab is HostTerminalTab) { if (tab is HostTerminalTab) {
tab.start() tab.start()

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

@@ -141,25 +141,28 @@ class TerminalTabbed(
} }
private fun removeTabAt(index: Int, disposable: Boolean = true) { private fun removeTabAt(index: Int, disposable: Boolean = true, reconnect: Boolean = false) {
if (tabbedPane.isTabClosable(index)) { if (tabbedPane.isTabClosable(index)) {
val tab = tabs[index] val tab = tabs[index]
// 询问是否可以关闭 // 询问是否可以关闭
if (disposable) { if (disposable) {
// 如果开启了关闭确认,那么直接询问用户 // 如果是重连接,那么直接关闭不进行任何形式的询问
if (appearance.confirmTabClose) { if (reconnect.not()) {
if (OptionPane.showConfirmDialog( // 如果开启了关闭确认,那么直接询问用户
windowScope.window, if (appearance.confirmTabClose) {
I18n.getString("termora.tabbed.tab.close-prompt"), if (OptionPane.showConfirmDialog(
messageType = JOptionPane.QUESTION_MESSAGE, windowScope.window,
optionType = JOptionPane.OK_CANCEL_OPTION I18n.getString("termora.tabbed.tab.close-prompt"),
) != JOptionPane.OK_OPTION messageType = JOptionPane.QUESTION_MESSAGE,
) { optionType = JOptionPane.OK_CANCEL_OPTION
) != JOptionPane.OK_OPTION
) {
return
}
} else if (!tab.willBeClose()) { // 如果没有开启则询问用户
return return
} }
} else if (!tab.willBeClose()) { // 如果没有开启则询问用户
return
} }
} }
@@ -233,7 +236,7 @@ class TerminalTabbed(
if (tab is HostTerminalTab) { if (tab is HostTerminalTab) {
actionManager actionManager
.getAction(OpenHostAction.OPEN_HOST) .getAction(OpenHostAction.OPEN_HOST)
.actionPerformed(OpenHostActionEvent(this, tab.host, evt)) .actionPerformed(OpenHostActionEvent(this, tab.host, evt, tabIndex + 1))
} }
} }
@@ -361,7 +364,7 @@ class TerminalTabbed(
} }
} }
override fun indexOfTerminalTab(tab: TerminalTab):Int { override fun indexOfTerminalTab(tab: TerminalTab): Int {
return tabbedPane.indexOfComponent(tab.getJComponent()) return tabbedPane.indexOfComponent(tab.getJComponent())
} }
@@ -451,10 +454,10 @@ class TerminalTabbed(
} }
} }
override fun closeTerminalTab(tab: TerminalTab, disposable: Boolean) { override fun closeTerminalTab(tab: TerminalTab, disposable: Boolean, reconnect: Boolean) {
for (i in 0 until tabs.size) { for (i in 0 until tabs.size) {
if (tabs[i] == tab) { if (tabs[i] == tab) {
removeTabAt(i, disposable) removeTabAt(i, disposable, reconnect)
break break
} }
} }

View File

@@ -6,7 +6,7 @@ interface TerminalTabbedManager {
fun getSelectedTerminalTab(): TerminalTab? fun getSelectedTerminalTab(): TerminalTab?
fun getTerminalTabs(): List<TerminalTab> fun getTerminalTabs(): List<TerminalTab>
fun setSelectedTerminalTab(tab: TerminalTab) fun setSelectedTerminalTab(tab: TerminalTab)
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true) fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true, reconnect: Boolean = false)
fun refreshTerminalTabs() fun refreshTerminalTabs()
fun indexOfTerminalTab(tab: TerminalTab): Int fun indexOfTerminalTab(tab: TerminalTab): Int
} }

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,
@@ -98,6 +98,40 @@ class TermoraFencePanel(
toggle() 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 {

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

@@ -170,16 +170,18 @@ class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProv
filterableTreeModel.addFilter(object : Filter { filterableTreeModel.addFilter(object : Filter {
override fun filter(node: Any): Boolean { override fun filter(node: Any): Boolean {
val text = searchTextField.text val text = searchTextField.text.trim()
if (text.isBlank()) return true if (text.isBlank()) return true
if (node !is HostTreeNode) return false if (node !is HostTreeNode) return false
if (node is TeamTreeNode || node.id == "0") return true if (node is TeamTreeNode || node.id == "0") return true
return node.host.name.contains(text) || node.host.host.contains(text) return node.host.name.contains(text, ignoreCase = true)
|| node.host.username.contains(text) || node.host.host.contains(text, ignoreCase = true)
|| node.host.username.contains(text, ignoreCase = true)
|| node.host.remark.contains(text, ignoreCase = true)
} }
override fun canFilter(): Boolean { override fun canFilter(): Boolean {
return searchTextField.text.isNotBlank() return searchTextField.text.trim().isNotBlank()
} }
}) })

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

@@ -1,6 +1,7 @@
package app.termora.account package app.termora.account
import app.termora.* import app.termora.*
import app.termora.Application.ohMyJson
import app.termora.OptionsPane.Companion.FORM_MARGIN import app.termora.OptionsPane.Companion.FORM_MARGIN
import app.termora.actions.AnAction import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
@@ -8,21 +9,36 @@ import app.termora.database.DatabaseManager
import app.termora.plugin.internal.extension.DynamicExtensionHandler import app.termora.plugin.internal.extension.DynamicExtensionHandler
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 com.sun.net.httpserver.HttpServer
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import org.apache.commons.codec.binary.Hex
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.time.DateFormatUtils import org.apache.commons.lang3.time.DateFormatUtils
import org.jdesktop.swingx.JXBusyLabel
import org.jdesktop.swingx.JXHyperlink import org.jdesktop.swingx.JXHyperlink
import org.slf4j.LoggerFactory
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.CardLayout
import java.net.InetSocketAddress
import java.net.URI import java.net.URI
import java.net.URLEncoder
import java.util.* import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import javax.swing.* import javax.swing.*
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable { class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
companion object {
private val log = LoggerFactory.getLogger(AccountOption::class.java)
}
private val owner get() = SwingUtilities.getWindowAncestor(this) private val owner get() = SwingUtilities.getWindowAncestor(this)
private val databaseManager get() = DatabaseManager.getInstance() private val databaseManager get() = DatabaseManager.getInstance()
@@ -30,18 +46,31 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
private val accountProperties get() = AccountProperties.getInstance() private val accountProperties get() = AccountProperties.getInstance()
private val userInfoPanel = JPanel(BorderLayout()) private val userInfoPanel = JPanel(BorderLayout())
private val lastSynchronizationOnLabel = JLabel() private val lastSynchronizationOnLabel = JLabel()
private val serverManager get() = ServerManager.getInstance()
private val cardLayout = CardLayout()
private val contentPanel = JPanel(cardLayout)
private val loginPanel = JPanel(BorderLayout())
private val busyLabel = JXBusyLabel()
private var httpServer: HttpServer? = null
init { init {
initView() initView()
initEvents() initEvents()
} }
private fun initView() { private fun initView() {
refreshUserInfoPanel() refreshUserInfoPanel()
add(userInfoPanel, BorderLayout.CENTER) refreshLoginPanel()
}
contentPanel.add(userInfoPanel, "UserInfo")
contentPanel.add(loginPanel, "Login")
cardLayout.show(contentPanel, "UserInfo")
add(contentPanel, BorderLayout.CENTER)
}
private fun initEvents() { private fun initEvents() {
// 服务器签名发生变更 // 服务器签名发生变更
@@ -99,11 +128,7 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
planBox.add(Box.createHorizontalStrut(16)) planBox.add(Box.createHorizontalStrut(16))
val upgrade = JXHyperlink(object : AnAction(I18n.getString("termora.settings.account.upgrade")) { val upgrade = JXHyperlink(object : AnAction(I18n.getString("termora.settings.account.upgrade")) {
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
if (I18n.isChinaMainland()) { Application.browse(URI.create("${accountManager.getServer()}/v1/client/redirect?to=upgrade&version=${Application.getVersion()}"))
Application.browse(URI.create("https://www.termora.cn/pricing?version=${Application.getVersion()}"))
} else {
Application.browse(URI.create("https://www.termora.app/pricing?version=${Application.getVersion()}"))
}
} }
}) })
upgrade.isFocusable = false upgrade.isFocusable = false
@@ -145,6 +170,29 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
.build() .build()
} }
private fun getLoginComponent(): JComponent {
val layout = FormLayout(
"default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
val cancelBtn = JXHyperlink(object : AnAction(I18n.getString("termora.cancel")) {
override fun actionPerformed(evt: AnActionEvent) {
httpServer?.stop(0)
cardLayout.show(contentPanel, "UserInfo")
}
})
val tipLabel = JLabel(I18n.getString("termora.settings.account.wait-login"))
tipLabel.foreground = UIManager.getColor("TextField.placeholderForeground")
return FormBuilder.create().layout(layout).debug(false).padding("10dlu,0,0,0")
.add(busyLabel).xy(1, 1, "center, fill")
.add(tipLabel).xy(1, 3, "center, fill")
.add(cancelBtn).xy(1, 5, "center, fill")
.build()
}
private fun createActionPanel(isFreePlan: Boolean): JComponent { private fun createActionPanel(isFreePlan: Boolean): JComponent {
val actionBox = Box.createHorizontalBox() val actionBox = Box.createHorizontalBox()
actionBox.add(Box.createHorizontalGlue()) actionBox.add(Box.createHorizontalGlue())
@@ -219,11 +267,139 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
return actionBox return actionBox
} }
private fun showLoginPanel() {
refreshLoginPanel()
busyLabel.isBusy = true
cardLayout.show(contentPanel, "Login")
}
private fun onLogin() { private fun onLogin() {
httpServer?.stop(0)
val dialog = LoginServerDialog(owner) val dialog = LoginServerDialog(owner)
dialog.isVisible = true dialog.isVisible = true
val server = dialog.server ?: return
showLoginPanel()
onLogin(server)
} }
private fun onLogin(server: Server) {
val httpServer = HttpServer.create(InetSocketAddress("127.0.0.1", 0), 0)
.apply { httpServer = this }
val future = processLogin(server, httpServer)
val loginJob = swingCoroutineScope.launch(Dispatchers.IO) {
try {
val loginResult = future.get(5, TimeUnit.MINUTES)
serverManager.login(server, loginResult.refreshToken, loginResult.password)
} catch (e: Exception) {
if (log.isErrorEnabled) log.error(e.message, e)
withContext(Dispatchers.Swing) {
OptionPane.showMessageDialog(
owner,
StringUtils.defaultIfBlank(
e.message ?: StringUtils.EMPTY,
I18n.getString("termora.settings.account.login-failed")
),
messageType = JOptionPane.ERROR_MESSAGE,
)
}
} finally {
withContext(Dispatchers.Swing) { cardLayout.show(contentPanel, "UserInfo") }
httpServer.stop(0)
}
}
Disposer.register(this, object : Disposable {
override fun dispose() {
loginJob.cancel()
httpServer.stop(0)
}
})
}
override fun dispose() {
busyLabel.isBusy = false
super.dispose()
}
private fun processLogin(server: Server, httpServer: HttpServer): CompletableFuture<LoginResult> {
val keypair = RSA.generateKeyPair(2048)
val future = CompletableFuture<LoginResult>()
httpServer.createContext("/callback") { exchange ->
val method = exchange.requestMethod
if (method.equals("OPTIONS", ignoreCase = true)) {
exchange.responseHeaders.add("Access-Control-Allow-Origin", "*")
exchange.responseHeaders.add("Access-Control-Allow-Methods", "POST, OPTIONS")
exchange.responseHeaders.add("Access-Control-Allow-Headers", "Content-Type")
exchange.sendResponseHeaders(204, -1)
} else {
var loginResult: LoginResult? = null
if (method.equals("POST", ignoreCase = true)) {
try {
val text = String(exchange.requestBody.readAllBytes())
loginResult = ohMyJson.decodeFromString<LoginResult>(text)
val secretKey = RSA.decrypt(keypair.private, Hex.decodeHex(loginResult.secretKey))
val secretIv = RSA.decrypt(keypair.private, Hex.decodeHex(loginResult.secretIv))
val password = AES.CBC.decrypt(secretKey, secretIv, Hex.decodeHex(loginResult.password))
val refreshToken = AES.CBC.decrypt(
secretKey, secretIv, Hex.decodeHex(loginResult.refreshToken)
)
loginResult = loginResult.copy(
password = String(password),
refreshToken = String(refreshToken)
)
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
}
val response = "OK".toByteArray()
exchange.responseHeaders.add("Access-Control-Allow-Origin", "*")
exchange.sendResponseHeaders(200, response.size.toLong())
exchange.responseBody.use { it.write(response) }
if (loginResult != null) {
future.complete(loginResult)
}
}
IOUtils.closeQuietly { exchange.close() }
}
httpServer.start()
val sb = StringBuilder()
val redirect = StringBuilder()
redirect.append("/device?callback=").append("http://127.0.0.1:${httpServer.address.port}/callback")
redirect.append("&from=device&publicKey=").append(keypair.public.encoded.toHexString())
redirect.append("&format=hex&device=termora&device-version=").append(Application.getVersion())
sb.append(server.server)
sb.append("/v1/client/redirect?to=login&from=device")
sb.append("&redirect=").append(URLEncoder.encode(redirect.toString(), Charsets.UTF_8))
Application.browse(URI.create(sb.toString()))
return future
}
@Serializable
private data class LoginResult(
val password: String,
val refreshToken: String,
val secretKey: String,
val secretIv: String,
)
private fun refreshUserInfoPanel() { private fun refreshUserInfoPanel() {
userInfoPanel.removeAll() userInfoPanel.removeAll()
userInfoPanel.add(getCenterComponent(), BorderLayout.CENTER) userInfoPanel.add(getCenterComponent(), BorderLayout.CENTER)
@@ -231,6 +407,13 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
userInfoPanel.repaint() userInfoPanel.repaint()
} }
private fun refreshLoginPanel() {
loginPanel.removeAll()
loginPanel.add(getLoginComponent(), BorderLayout.CENTER)
loginPanel.revalidate()
loginPanel.repaint()
}
override fun getIcon(isSelected: Boolean): Icon { override fun getIcon(isSelected: Boolean): Icon {
return Icons.user return Icons.user
} }

View File

@@ -9,12 +9,6 @@ import app.termora.database.DatabaseManager
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
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.*
import kotlinx.coroutines.swing.Swing
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Request
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.JXHyperlink import org.jdesktop.swingx.JXHyperlink
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -24,12 +18,10 @@ import java.awt.Window
import java.awt.event.WindowAdapter import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent import java.awt.event.WindowEvent
import java.net.URI import java.net.URI
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.* import javax.swing.*
import javax.swing.event.ListDataEvent import javax.swing.event.ListDataEvent
import javax.swing.event.ListDataListener import javax.swing.event.ListDataListener
import kotlin.math.max import kotlin.math.max
import kotlin.time.Duration.Companion.milliseconds
class LoginServerDialog(owner: Window) : DialogWrapper(owner) { class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
companion object { companion object {
@@ -37,18 +29,14 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
} }
private val serverComboBox = OutlineComboBox<Server>() private val serverComboBox = OutlineComboBox<Server>()
private val usernameTextField = OutlineTextField(128)
private val passwordField = OutlinePasswordField()
private val mfaTextField = OutlineTextField(128)
private val okAction = OkAction(I18n.getString("termora.settings.account.login")) private val okAction = OkAction(I18n.getString("termora.settings.account.login"))
private val cancelAction = super.createCancelAction() private val cancelAction = super.createCancelAction()
private val cancelButton = super.createJButtonForAction(cancelAction) private val cancelButton = super.createJButtonForAction(cancelAction)
private val isLoggingIn = AtomicBoolean(false)
private val singaporeServer = private val singaporeServer =
Server(I18n.getString("termora.settings.account.server-singapore"), "https://account.termora.app") Server(I18n.getString("termora.settings.account.server-singapore"), "https://account.termora.app")
private val chinaServer = private val chinaServer =
Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn") Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn")
private val serverManager get() = ServerManager.getInstance() var server: Server? = null
init { init {
isModal = true isModal = true
@@ -60,12 +48,10 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
size = Dimension(max(preferredSize.width, UIManager.getInt("Dialog.width") - 250), preferredSize.height) size = Dimension(max(preferredSize.width, UIManager.getInt("Dialog.width") - 250), preferredSize.height)
setLocationRelativeTo(owner) setLocationRelativeTo(owner)
passwordField.putClientProperty(FlatClientProperties.STYLE, mapOf("showCapsLock" to true))
addWindowListener(object : WindowAdapter() { addWindowListener(object : WindowAdapter() {
override fun windowOpened(e: WindowEvent) { override fun windowOpened(e: WindowEvent) {
removeWindowListener(this) removeWindowListener(this)
usernameTextField.requestFocus()
} }
}) })
} }
@@ -73,7 +59,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
override fun createCenterPanel(): JComponent { override fun createCenterPanel(): JComponent {
val layout = FormLayout( val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref", "left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN" "pref, $FORM_MARGIN"
) )
var rows = 1 var rows = 1
@@ -90,7 +76,6 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
serverComboBox.addItem(Server(server.name, server.server)) serverComboBox.addItem(Server(server.name, server.server))
} }
mfaTextField.placeholderText = I18n.getString("termora.settings.account.mfa")
serverComboBox.renderer = object : DefaultListCellRenderer() { serverComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent( override fun getListCellRendererComponent(
@@ -153,40 +138,6 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
} }
} }
val registerAction = object : AnAction(I18n.getString("termora.settings.account.register")) {
override fun actionPerformed(evt: AnActionEvent) {
val server = serverComboBox.selectedItem as Server?
if (server == null) {
serverComboBox.outline = FlatClientProperties.OUTLINE_ERROR
serverComboBox.requestFocusInWindow()
return
}
try {
val text = AccountHttp.execute(
AccountHttp.client, Request.Builder()
.get().url("${server.server}/v1/client/system").build()
)
val json = runCatching { ohMyJson.decodeFromString<JsonObject>(text) }.getOrNull()
val allowRegister = json?.get("register")?.jsonPrimitive?.boolean ?: false
if (allowRegister.not()) {
throw IllegalStateException(I18n.getString("termora.settings.account.not-support-register"))
}
Application.browse(URI.create("${server.server}/v1/client/redirect?to=register&from=${Application.getName()}"))
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
OptionPane.showMessageDialog(
dialog,
e.message ?: I18n.getString("termora.settings.account.not-support-register"),
messageType = JOptionPane.ERROR_MESSAGE
)
}
}
}
fun refreshButton() { fun refreshButton() {
if (serverComboBox.selectedItem == singaporeServer || serverComboBox.selectedItem == chinaServer || serverComboBox.itemCount < 1) { if (serverComboBox.selectedItem == singaporeServer || serverComboBox.selectedItem == chinaServer || serverComboBox.itemCount < 1) {
newAction.name = I18n.getString("termora.welcome.contextmenu.new") newAction.name = I18n.getString("termora.welcome.contextmenu.new")
@@ -214,21 +165,11 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
}) })
val registerLink = JXHyperlink(registerAction)
registerLink.isFocusable = false
return FormBuilder.create().layout(layout).debug(false).padding("0dlu, $FORM_MARGIN, 0dlu, $FORM_MARGIN") return FormBuilder.create().layout(layout).debug(false).padding("0dlu, $FORM_MARGIN, 0dlu, $FORM_MARGIN")
.add("${I18n.getString("termora.settings.account.server")}:").xy(1, rows) .add("${I18n.getString("termora.settings.account.server")}:").xy(1, rows)
.add(serverComboBox).xy(3, rows) .add(serverComboBox).xy(3, rows)
.add(newServer).xy(5, rows).apply { rows += step } .add(newServer).xy(5, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.account")}:").xy(1, rows)
.add(usernameTextField).xy(3, rows)
.add(registerLink).xy(5, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
.add(passwordField).xy(3, rows).apply { rows += step }
.add("MFA:").xy(1, rows)
.add(mfaTextField).xy(3, rows).apply { rows += step }
.build() .build()
} }
@@ -315,95 +256,21 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
} }
override fun doOKAction() { override fun doOKAction() {
if (isLoggingIn.get()) return
val server = serverComboBox.selectedItem as? Server server = serverComboBox.selectedItem as? Server
if (server == null) { if (server == null) {
serverComboBox.outline = FlatClientProperties.OUTLINE_ERROR serverComboBox.outline = FlatClientProperties.OUTLINE_ERROR
serverComboBox.requestFocusInWindow() serverComboBox.requestFocusInWindow()
return return
} }
if (usernameTextField.text.isBlank()) {
usernameTextField.outline = FlatClientProperties.OUTLINE_ERROR
usernameTextField.requestFocusInWindow()
return
} else if (passwordField.password.isEmpty()) {
passwordField.outline = FlatClientProperties.OUTLINE_ERROR
passwordField.requestFocusInWindow()
return
}
if (isLoggingIn.compareAndSet(false, true)) {
okAction.isEnabled = false
usernameTextField.isEnabled = false
passwordField.isEnabled = false
mfaTextField.isEnabled = false
serverComboBox.isEnabled = false
cancelButton.isVisible = false
onLogin(server)
return
}
super.doOKAction() super.doOKAction()
} }
private fun onLogin(server: Server) {
val job = swingCoroutineScope.launch(Dispatchers.IO) {
var c = 0
while (isActive) {
if (++c > 3) c = 0
okAction.name = I18n.getString("termora.settings.account.login") + ".".repeat(c)
delay(350.milliseconds)
}
}
val loginJob = swingCoroutineScope.launch(Dispatchers.IO) {
try {
serverManager.login(
server, usernameTextField.text,
String(passwordField.password), mfaTextField.text.trim()
)
withContext(Dispatchers.Swing) {
super.doOKAction()
}
} catch (e: Exception) {
if (log.isErrorEnabled) log.error(e.message, e)
withContext(Dispatchers.Swing) {
OptionPane.showMessageDialog(
this@LoginServerDialog,
StringUtils.defaultIfBlank(
e.message ?: StringUtils.EMPTY,
I18n.getString("termora.settings.account.login-failed")
),
messageType = JOptionPane.ERROR_MESSAGE,
)
}
} finally {
job.cancel()
withContext(Dispatchers.Swing) {
okAction.name = I18n.getString("termora.settings.account.login")
okAction.isEnabled = true
usernameTextField.isEnabled = true
passwordField.isEnabled = true
serverComboBox.isEnabled = true
cancelButton.isVisible = true
mfaTextField.isEnabled = true
}
isLoggingIn.compareAndSet(true, false)
}
}
Disposer.register(disposable, object : Disposable {
override fun dispose() {
if (loginJob.isActive)
loginJob.cancel()
}
})
}
override fun doCancelAction() { override fun doCancelAction() {
if (isLoggingIn.get()) return server = null
super.doCancelAction() super.doCancelAction()
} }
} }

View File

@@ -67,7 +67,6 @@ class PullService private constructor() : SyncService(), Disposable, Application
private var lastChangeHash = StringUtils.EMPTY private var lastChangeHash = StringUtils.EMPTY
private fun pullChanges() { private fun pullChanges() {
if (isFreePlan) return
val hash: String val hash: String
try { try {

View File

@@ -1,7 +1,10 @@
package app.termora.account package app.termora.account
import app.termora.* import app.termora.AES
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.ApplicationScope
import app.termora.PBKDF2
import app.termora.RSA
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
@@ -9,7 +12,6 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import org.apache.commons.codec.binary.Base64 import org.apache.commons.codec.binary.Base64
import org.apache.commons.codec.digest.DigestUtils
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
class ServerManager private constructor() { class ServerManager private constructor() {
@@ -28,7 +30,7 @@ class ServerManager private constructor() {
/** /**
* 登录,不报错就是登录成功 * 登录,不报错就是登录成功
*/ */
fun login(server: Server, username: String, password: String, mfa: String) { fun login(server: Server, refreshToken: String, password: String) {
if (accountManager.isLocally().not()) { if (accountManager.isLocally().not()) {
throw IllegalStateException("Already logged in") throw IllegalStateException("Already logged in")
@@ -39,25 +41,25 @@ class ServerManager private constructor() {
} }
try { try {
doLogin(server, username, password, mfa) doLogin(server, refreshToken, password)
} finally { } finally {
isLoggingIn.compareAndSet(true, false) isLoggingIn.compareAndSet(true, false)
} }
} }
private fun doLogin(server: Server, username: String, password: String, mfa: String) { private fun doLogin(server: Server, refreshToken: String, password: String) {
// 服务器信息 // 服务器信息
val serverInfo = getServerInfo(server) val serverInfo = getServerInfo(server)
// call login // call login
val loginResponse = callLogin(serverInfo, server, username, password, mfa) val loginResponse = callToken(server, refreshToken)
// call me // call me
val meResponse = callMe(server.server, loginResponse.accessToken) val meResponse = callMe(server.server, loginResponse.accessToken)
// 解密 // 解密
val salt = "${serverInfo.salt}:${username}".toByteArray() val salt = "${serverInfo.salt}:${meResponse.email}".toByteArray()
val privateKeySecureKey = PBKDF2.hash(salt, password.toCharArray(), 1024, 256) val privateKeySecureKey = PBKDF2.hash(salt, password.toCharArray(), 1024, 256)
val privateKeySecureIv = PBKDF2.hash(salt, password.toCharArray(), 1024, 128) val privateKeySecureIv = PBKDF2.hash(salt, password.toCharArray(), 1024, 128)
val privateKeyEncoded = AES.CBC.decrypt( val privateKeyEncoded = AES.CBC.decrypt(
@@ -106,29 +108,19 @@ class ServerManager private constructor() {
return ohMyJson.decodeFromString<ServerInfo>(AccountHttp.execute(request = request)) return ohMyJson.decodeFromString<ServerInfo>(AccountHttp.execute(request = request))
} }
private fun callLogin( private fun callToken(
serverInfo: ServerInfo,
server: Server, server: Server,
username: String, refreshToken: String,
password: String,
mfa: String
): LoginResponse { ): LoginResponse {
val body = ohMyJson.encodeToString(mapOf("refreshToken" to refreshToken))
val passwordHex = DigestUtils.sha256Hex("${serverInfo.salt}:${username}:${password}")
val requestBody = ohMyJson.encodeToString(mapOf("email" to username, "password" to passwordHex, "mfa" to mfa))
.toRequestBody("application/json".toMediaType()) .toRequestBody("application/json".toMediaType())
val request = Request.Builder().url("${server.server}/v1/token")
val request = Request.Builder() .header("Authorization", "Bearer $refreshToken")
.url("${server.server}/v1/login") .post(body)
.post(requestBody)
.build() .build()
val response = AccountHttp.client.newCall(request).execute() val response = AccountHttp.client.newCall(request).execute()
val text = response.use { response.body.use { it?.string() } } val text = response.use { response.body.use { it.string() } }
if (text == null) {
throw ResponseException(response.code, response)
}
if (response.isSuccessful.not()) { if (response.isSuccessful.not()) {
val message = ohMyJson.parseToJsonElement(text).jsonObject["message"]?.jsonPrimitive?.content val message = ohMyJson.parseToJsonElement(text).jsonObject["message"]?.jsonPrimitive?.content

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

@@ -56,7 +56,12 @@ class OpenHostAction : AnAction() {
if (tab == null) return if (tab == null) return
terminalTabbedManager.addTerminalTab(tab) if (evt.tabIndex >= 0) {
terminalTabbedManager.addTerminalTab(evt.tabIndex, tab)
} else {
terminalTabbedManager.addTerminalTab(tab)
}
if (tab is PtyHostTerminalTab) { if (tab is PtyHostTerminalTab) {
tab.start() tab.start()
} }

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

@@ -29,8 +29,10 @@ class KeymapPanel : JPanel(BorderLayout()) {
private val copyBtn = JButton(Icons.copy) private val copyBtn = JButton(Icons.copy)
private val renameBtn = JButton(Icons.edit) private val renameBtn = JButton(Icons.edit)
private val deleteBtn = JButton(Icons.delete) private val deleteBtn = JButton(Icons.delete)
private val infoBtn = JButton(Icons.questionMark)
private val database get() = DatabaseManager.getInstance() private val database get() = DatabaseManager.getInstance()
private val allowKeyCodes = mutableSetOf<Int>() private val allowKeyCodes = mutableSetOf<Int>()
private val owner get() = SwingUtilities.getWindowAncestor(this)
init { init {
initView() initView()
@@ -89,8 +91,8 @@ class KeymapPanel : JPanel(BorderLayout()) {
box.add(copyBtn) box.add(copyBtn)
box.add(renameBtn) box.add(renameBtn)
box.add(deleteBtn) box.add(deleteBtn)
box.add(infoBtn)
box.add(Box.createHorizontalGlue()) box.add(Box.createHorizontalGlue())
box.border = BorderFactory.createEmptyBorder(0, 0, 6, 0)
add(box, BorderLayout.NORTH) add(box, BorderLayout.NORTH)
add(scrollPane, BorderLayout.CENTER) add(scrollPane, BorderLayout.CENTER)
@@ -105,6 +107,12 @@ class KeymapPanel : JPanel(BorderLayout()) {
} }
}) })
infoBtn.addActionListener {
val color = UIManager.getColor("TextField.placeholderForeground")
val msg = I18n.getString("termora.settings.keymap.question", color.red, color.green, color.blue)
OptionPane.showMessageDialog(owner, msg)
}
copyBtn.addActionListener { copyBtn.addActionListener {
val keymap = getCurrentKeymap() val keymap = getCurrentKeymap()
if (keymap != null) { if (keymap != null) {

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")
) )
} }
@@ -287,6 +287,9 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
typeComboBox.addItem("RSA") typeComboBox.addItem("RSA")
typeComboBox.addItem("ED25519") typeComboBox.addItem("ED25519")
typeComboBox.addItem("ECDSA-SHA2-NISTP256")
typeComboBox.addItem("ECDSA-SHA2-NISTP384")
typeComboBox.addItem("ECDSA-SHA2-NISTP521")
// 默认 RSA // 默认 RSA
lengthComboBox.addItem(1024) lengthComboBox.addItem(1024)
@@ -396,6 +399,12 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
lengthComboBox.addItem(1024 * 4) lengthComboBox.addItem(1024 * 4)
lengthComboBox.addItem(1024 * 8) lengthComboBox.addItem(1024 * 8)
lengthComboBox.selectedItem = 1024 * 2 lengthComboBox.selectedItem = 1024 * 2
} else if (typeComboBox.selectedItem == "ECDSA-SHA2-NISTP256") {
lengthComboBox.addItem(256)
} else if (typeComboBox.selectedItem == "ECDSA-SHA2-NISTP384") {
lengthComboBox.addItem(384)
} else if (typeComboBox.selectedItem == "ECDSA-SHA2-NISTP521") {
lengthComboBox.addItem(521)
} }
} }
} }
@@ -413,6 +422,17 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
super.doCancelAction() super.doCancelAction()
} }
private fun genKeyPair(): KeyPair {
val keyType = when (typeComboBox.selectedItem) {
"ED25519" -> KeyPairProvider.SSH_ED25519
"ECDSA-SHA2-NISTP256" -> KeyPairProvider.ECDSA_SHA2_NISTP256
"ECDSA-SHA2-NISTP384" -> KeyPairProvider.ECDSA_SHA2_NISTP384
"ECDSA-SHA2-NISTP521" -> KeyPairProvider.ECDSA_SHA2_NISTP521
else -> KeyPairProvider.SSH_RSA
}
return KeyUtils.generateKeyPair(keyType, lengthComboBox.selectedItem as Int)
}
override fun doOKAction() { override fun doOKAction() {
if (ohKeyPair == OhKeyPair.empty) { if (ohKeyPair == OhKeyPair.empty) {
@@ -422,9 +442,7 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
return return
} }
val keyType = if (typeComboBox.selectedItem == "RSA") val keyPair = genKeyPair()
KeyPairProvider.SSH_RSA else KeyPairProvider.SSH_ED25519
val keyPair = KeyUtils.generateKeyPair(keyType, lengthComboBox.selectedItem as Int)
ohKeyPair = OhKeyPair( ohKeyPair = OhKeyPair(
id = randomUUID(), id = randomUUID(),
name = nameTextField.text, name = nameTextField.text,

View File

@@ -2,6 +2,7 @@ package app.termora.keymgr
import app.termora.AES.decodeBase64 import app.termora.AES.decodeBase64
import app.termora.RSA import app.termora.RSA
import org.apache.sshd.common.config.keys.impl.ECDSAPublicKeyEntryDecoder
import org.apache.sshd.common.keyprovider.AbstractResourceKeyPairProvider import org.apache.sshd.common.keyprovider.AbstractResourceKeyPairProvider
import org.apache.sshd.common.session.SessionContext import org.apache.sshd.common.session.SessionContext
import org.apache.sshd.common.util.security.eddsa.Ed25519PublicKeyDecoder import org.apache.sshd.common.util.security.eddsa.Ed25519PublicKeyDecoder
@@ -25,6 +26,8 @@ class OhKeyPairKeyPairProvider(private val id: String) : AbstractResourceKeyPair
when (ohKeyPair.type) { when (ohKeyPair.type) {
"RSA" -> RSA.generatePublic(ohKeyPair.publicKey.decodeBase64()) "RSA" -> RSA.generatePublic(ohKeyPair.publicKey.decodeBase64())
"ED25519" -> Ed25519PublicKeyDecoder.INSTANCE.generatePublicKey((X509EncodedKeySpec(ohKeyPair.publicKey.decodeBase64()))) "ED25519" -> Ed25519PublicKeyDecoder.INSTANCE.generatePublicKey((X509EncodedKeySpec(ohKeyPair.publicKey.decodeBase64())))
"ECDSA-SHA2-NISTP256","ECDSA-SHA2-NISTP384","ECDSA-SHA2-NISTP521" ->
ECDSAPublicKeyEntryDecoder.INSTANCE.generatePublicKey(X509EncodedKeySpec(ohKeyPair.publicKey.decodeBase64()))
else -> throw UnsupportedOperationException("${ohKeyPair.type} is not supported") else -> throw UnsupportedOperationException("${ohKeyPair.type} is not supported")
} }
} as PublicKey } as PublicKey
@@ -33,6 +36,8 @@ class OhKeyPairKeyPairProvider(private val id: String) : AbstractResourceKeyPair
when (ohKeyPair.type) { when (ohKeyPair.type) {
"RSA" -> RSA.generatePrivate(ohKeyPair.privateKey.decodeBase64()) "RSA" -> RSA.generatePrivate(ohKeyPair.privateKey.decodeBase64())
"ED25519" -> Ed25519PublicKeyDecoder.INSTANCE.generatePrivateKey(PKCS8EncodedKeySpec(ohKeyPair.privateKey.decodeBase64())) "ED25519" -> Ed25519PublicKeyDecoder.INSTANCE.generatePrivateKey(PKCS8EncodedKeySpec(ohKeyPair.privateKey.decodeBase64()))
"ECDSA-SHA2-NISTP256","ECDSA-SHA2-NISTP384","ECDSA-SHA2-NISTP521" ->
ECDSAPublicKeyEntryDecoder.INSTANCE.generatePrivateKey(PKCS8EncodedKeySpec(ohKeyPair.privateKey.decodeBase64()))
else -> throw UnsupportedOperationException("${ohKeyPair.type} is not supported") else -> throw UnsupportedOperationException("${ohKeyPair.type} is not supported")
} }
} as PrivateKey } as PrivateKey

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

@@ -185,6 +185,13 @@ class PluginPanel(val descriptor: PluginPluginDescriptor) : JPanel(), Disposable
} }
} }
MixpanelService.getInstance().push(
"uninstall-plugin", mapOf(
"pluginName" to descriptor.plugin.getName(),
"pluginVersion" to descriptor.version.toString(),
)
)
// 询问是否重启 // 询问是否重启
TermoraRestarter.getInstance().scheduleRestart(owner) TermoraRestarter.getInstance().scheduleRestart(owner)
} else { } else {
@@ -227,6 +234,13 @@ class PluginPanel(val descriptor: PluginPluginDescriptor) : JPanel(), Disposable
} }
}, button == updateButton) }, button == updateButton)
MixpanelService.getInstance().push(
"${if (button == installButton) "install" else "update"}-plugin", mapOf(
"pluginName" to descriptor.plugin.getName(),
"pluginVersion" to descriptor.version.toString(),
)
)
withContext(Dispatchers.Swing) { withContext(Dispatchers.Swing) {
installed.add(descriptor.id) installed.add(descriptor.id)

View File

@@ -27,9 +27,14 @@ class CloneSessionTerminalTabbedContextMenuExtension private constructor() : Ter
cloneSession.addActionListener(object : AnAction() { cloneSession.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
val index = terminalTabbedManager.indexOfTerminalTab(tab)
val handler = c.copy(channel = null) val handler = c.copy(channel = null)
val newTab = SSHTerminalTab(windowScope, tab.host, handler) val newTab = SSHTerminalTab(windowScope, tab.host, handler)
terminalTabbedManager.addTerminalTab(newTab) if (index >= 0) {
terminalTabbedManager.addTerminalTab(index + 1, newTab)
} else {
terminalTabbedManager.addTerminalTab(newTab)
}
newTab.start() newTab.start()
} }
}) })

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
@@ -36,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
@@ -112,6 +114,7 @@ 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
@@ -169,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

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

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

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

@@ -46,6 +46,7 @@ class TransferAnAction : AnAction(I18n.getString("termora.transport.sftp"), Icon
val panel = tabbed.getTransportPanel(i) ?: continue val panel = tabbed.getTransportPanel(i) ?: continue
if (panel.host.id == host.id) { if (panel.host.id == host.id) {
tabbed.selectedIndex = i tabbed.selectedIndex = i
terminalTabbedManager.setSelectedTerminalTab(sftpTab)
return return
} }
} }

View File

@@ -1,7 +1,7 @@
package app.termora.transfer package app.termora.transfer
import app.termora.WindowScope
import app.termora.plugin.Extension import app.termora.plugin.Extension
import java.awt.Window
import java.nio.file.FileSystem import java.nio.file.FileSystem
import java.nio.file.Path import java.nio.file.Path
import javax.swing.JMenuItem import javax.swing.JMenuItem
@@ -14,7 +14,7 @@ internal interface TransportContextMenuExtension : Extension {
* @param fileSystem 为 null 表示可能已经断线,处于不可用状态 * @param fileSystem 为 null 表示可能已经断线,处于不可用状态
*/ */
fun createJMenuItem( fun createJMenuItem(
windowScope: WindowScope, window: Window,
fileSystem: FileSystem?, fileSystem: FileSystem?,
popupMenu: TransportPopupMenu, popupMenu: TransportPopupMenu,
files: List<Pair<Path, TransportTableModel.Attributes>> files: List<Pair<Path, TransportTableModel.Attributes>>

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,
@@ -131,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) {
@@ -165,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)
@@ -411,7 +419,7 @@ internal class TransportPanel(
} }
}) })
addPropertyChangeListener("workdir") { evt -> reload() } addPropertyChangeListener("workdir") { _ -> reload() }
reload() reload()
} }
@@ -507,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) {
@@ -530,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() {
@@ -881,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)
} }

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"))
@@ -100,7 +108,7 @@ internal class TransportPopupMenu(
for (extension in extensionManager.getExtensions(TransportContextMenuExtension::class.java)) { for (extension in extensionManager.getExtensions(TransportContextMenuExtension::class.java)) {
try { try {
val menu = extension.createJMenuItem( val menu = extension.createJMenuItem(
ApplicationScope.forWindowScope(owner), owner,
fileSystem, fileSystem,
this, this,
files files

View File

@@ -1,7 +1,6 @@
package app.termora.transfer.internal.sftp package app.termora.transfer.internal.sftp
import app.termora.I18n import app.termora.I18n
import app.termora.WindowScope
import app.termora.actions.AnAction import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
import app.termora.randomUUID import app.termora.randomUUID
@@ -9,6 +8,7 @@ import app.termora.transfer.*
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.apache.sshd.common.file.util.MockPath import org.apache.sshd.common.file.util.MockPath
import org.apache.sshd.sftp.client.fs.SftpFileSystem import org.apache.sshd.sftp.client.fs.SftpFileSystem
import java.awt.Window
import java.nio.file.FileSystem import java.nio.file.FileSystem
import java.nio.file.Path import java.nio.file.Path
import javax.swing.JMenu import javax.swing.JMenu
@@ -23,7 +23,7 @@ internal class CompressTransportContextMenuExtension private constructor() : Tra
} }
override fun createJMenuItem( override fun createJMenuItem(
windowScope: WindowScope, window: Window,
fileSystem: FileSystem?, fileSystem: FileSystem?,
popupMenu: TransportPopupMenu, popupMenu: TransportPopupMenu,
files: List<Pair<Path, TransportTableModel.Attributes>> files: List<Pair<Path, TransportTableModel.Attributes>>

View File

@@ -1,13 +1,13 @@
package app.termora.transfer.internal.sftp package app.termora.transfer.internal.sftp
import app.termora.I18n import app.termora.I18n
import app.termora.WindowScope
import app.termora.actions.AnAction import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
import app.termora.randomUUID import app.termora.randomUUID
import app.termora.transfer.* import app.termora.transfer.*
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.apache.sshd.sftp.client.fs.SftpFileSystem import org.apache.sshd.sftp.client.fs.SftpFileSystem
import java.awt.Window
import java.nio.file.FileSystem import java.nio.file.FileSystem
import java.nio.file.Path import java.nio.file.Path
import javax.swing.JMenu import javax.swing.JMenu
@@ -21,7 +21,7 @@ internal class ExtractTransportContextMenuExtension private constructor() : Tran
} }
override fun createJMenuItem( override fun createJMenuItem(
windowScope: WindowScope, window: Window,
fileSystem: FileSystem?, fileSystem: FileSystem?,
popupMenu: TransportPopupMenu, popupMenu: TransportPopupMenu,
files: List<Pair<Path, TransportTableModel.Attributes>> files: List<Pair<Path, TransportTableModel.Attributes>>

View File

@@ -3,11 +3,11 @@ package app.termora.transfer.internal.sftp
import app.termora.I18n import app.termora.I18n
import app.termora.Icons import app.termora.Icons
import app.termora.OptionPane import app.termora.OptionPane
import app.termora.WindowScope
import app.termora.transfer.TransportContextMenuExtension import app.termora.transfer.TransportContextMenuExtension
import app.termora.transfer.TransportPopupMenu import app.termora.transfer.TransportPopupMenu
import app.termora.transfer.TransportTableModel import app.termora.transfer.TransportTableModel
import org.apache.sshd.sftp.client.fs.SftpFileSystem import org.apache.sshd.sftp.client.fs.SftpFileSystem
import java.awt.Window
import java.nio.file.FileSystem import java.nio.file.FileSystem
import java.nio.file.Path import java.nio.file.Path
import javax.swing.JMenuItem import javax.swing.JMenuItem
@@ -19,7 +19,7 @@ internal class RmrfTransportContextMenuExtension private constructor() : Transpo
} }
override fun createJMenuItem( override fun createJMenuItem(
windowScope: WindowScope, window: Window,
fileSystem: FileSystem?, fileSystem: FileSystem?,
popupMenu: TransportPopupMenu, popupMenu: TransportPopupMenu,
files: List<Pair<Path, TransportTableModel.Attributes>> files: List<Pair<Path, TransportTableModel.Attributes>>
@@ -31,7 +31,7 @@ internal class RmrfTransportContextMenuExtension private constructor() : Transpo
val rmrfMenu = JMenuItem("rm -rf", Icons.warningIntroduction) val rmrfMenu = JMenuItem("rm -rf", Icons.warningIntroduction)
rmrfMenu.addActionListener { rmrfMenu.addActionListener {
if (OptionPane.showConfirmDialog( if (OptionPane.showConfirmDialog(
windowScope.window, window,
I18n.getString("termora.transport.table.contextmenu.rm-warning"), I18n.getString("termora.transport.table.contextmenu.rm-warning"),
messageType = JOptionPane.ERROR_MESSAGE messageType = JOptionPane.ERROR_MESSAGE
) == JOptionPane.YES_OPTION ) == JOptionPane.YES_OPTION

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

@@ -146,6 +146,26 @@ 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() { actionMap.put("copy", object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
toolkit.systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null) toolkit.systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
@@ -193,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))

View File

@@ -89,11 +89,9 @@ termora.settings.plugin.install-from-disk-warning=<b>{0}</b> plugin will have ac
termora.settings.plugin.not-compatible=The plugin <b>{0}</b> is incompatible with the current version. Please reinstall <b>{0}</b> termora.settings.plugin.not-compatible=The plugin <b>{0}</b> is incompatible with the current version. Please reinstall <b>{0}</b>
termora.settings.account=Account termora.settings.account=Account
termora.settings.account.register=Register
termora.settings.account.not-support-register=This server does not support account registration
termora.settings.account.login=Log in termora.settings.account.login=Log in
termora.settings.account.server=Server termora.settings.account.server=Server
termora.settings.account.mfa=MFA is optional termora.settings.account.wait-login=Waiting for login in the default browser...
termora.settings.account.locally=locally termora.settings.account.locally=locally
termora.settings.account.lifetime=Lifetime termora.settings.account.lifetime=Lifetime
termora.settings.account.upgrade=Upgrade termora.settings.account.upgrade=Upgrade
@@ -116,7 +114,7 @@ termora.settings.keymap=Keymap
termora.settings.keymap.shortcut=Shortcut termora.settings.keymap.shortcut=Shortcut
termora.settings.keymap.action=Action termora.settings.keymap.action=Action
termora.settings.keymap.already-exists=The shortcut [{0}] is already in use by [{1}] termora.settings.keymap.already-exists=The shortcut [{0}] is already in use by [{1}]
termora.settings.keymap.question=Select a line, press the <font color=rgb({0},{1},{2})> ⌫ (Backspace)</font> key to remove the shortcut
termora.settings.sftp.edit-command=Edit Command termora.settings.sftp.edit-command=Edit Command
termora.settings.sftp.db-click-behavior=Double-click termora.settings.sftp.db-click-behavior=Double-click
@@ -239,6 +237,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 +248,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 +309,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 +333,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 +439,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

@@ -49,7 +49,26 @@ termora.setting.security.enter-password-again=Повторите пароль
termora.setting.security.password-is-different=Пароли отличаются termora.setting.security.password-is-different=Пароли отличаются
termora.setting.security.mnemonic-note=Сохраните мнемоническую фразу в надежном месте, она может помочь восстановить данные, если вы забудете пароль termora.setting.security.mnemonic-note=Сохраните мнемоническую фразу в надежном месте, она может помочь восстановить данные, если вы забудете пароль
termora.settings.account=Учётная запись
termora.settings.account.login=Войти
termora.settings.account.server=Сервер
termora.settings.account.wait-login=Ожидание входа в браузере по умолчанию...
termora.settings.account.locally=локально
termora.settings.account.lifetime=Время действия
termora.settings.account.upgrade=Обновить
termora.settings.account.verify=Подтвердить
termora.settings.account.subscription=Подписка
termora.settings.account.valid-to=Действительна до
termora.settings.account.synchronization-on=Синхронизация вкл
termora.settings.account.sync-now = Синхронизировать сейчас
termora.settings.account.logout = Выйти
termora.settings.account.logout-confirm = Вы уверены, что хотите выйти?
termora.settings.account.unsynced-logout-confirm = Несинхронизировано Вы уверены, что хотите выйти?
termora.settings.account.server-singapore = Сингапур
termora.settings.account.server-china = Материковый Китай
termora.settings.account.new-server = Новый сервер
termora.settings.account.deploy-server = Развернуть
termora.settings.account.login-failed = Не удалось войти, повторите попытку позже
termora.settings.terminal=Терминал termora.settings.terminal=Терминал
@@ -69,27 +88,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=Ссылка
@@ -101,7 +99,7 @@ termora.settings.keymap=Горяиче клавиши
termora.settings.keymap.shortcut=Комбинация termora.settings.keymap.shortcut=Комбинация
termora.settings.keymap.action=Команда termora.settings.keymap.action=Команда
termora.settings.keymap.already-exists=Комбинация [{0}] уже используется [{1}] termora.settings.keymap.already-exists=Комбинация [{0}] уже используется [{1}]
termora.settings.keymap.question=Выберите строку и нажмите <font color=rgb({0},{1},{2})> ⌫ (клавишу Backspace)</font>, чтобы удалить сочетание клавиш
termora.settings.sftp.edit-command=Редактировать команду termora.settings.sftp.edit-command=Редактировать команду
termora.settings.sftp.db-click-behavior=двойной щелчок termora.settings.sftp.db-click-behavior=двойной щелчок
@@ -204,6 +202,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 +212,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 +270,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 +385,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

@@ -103,10 +103,8 @@ termora.settings.plugin.not-compatible=插件 <b>{0}</b> 与当前版本不兼
termora.settings.account=账号 termora.settings.account=账号
termora.settings.account.login=登录 termora.settings.account.login=登录
termora.settings.account.register=注册
termora.settings.account.not-support-register=该服务器不支持注册账号
termora.settings.account.server=服务器 termora.settings.account.server=服务器
termora.settings.account.mfa=多因素验证是可选的 termora.settings.account.wait-login=正在等待默认浏览器中登录...
termora.settings.account.locally=本地的 termora.settings.account.locally=本地的
termora.settings.account.lifetime=长期 termora.settings.account.lifetime=长期
termora.settings.account.verify=验证 termora.settings.account.verify=验证
@@ -127,7 +125,7 @@ termora.settings.keymap=键盘
termora.settings.keymap.shortcut=快捷键 termora.settings.keymap.shortcut=快捷键
termora.settings.keymap.action=操作 termora.settings.keymap.action=操作
termora.settings.keymap.already-exists=快捷键 [{0}] 已经被 [{1}] 占用 termora.settings.keymap.already-exists=快捷键 [{0}] 已经被 [{1}] 占用
termora.settings.keymap.question=选中一行,按下 <font color=rgb({0},{1},{2})> ⌫ (退格键)</font> 移除快捷键
termora.settings.sftp.edit-command=编辑命令 termora.settings.sftp.edit-command=编辑命令
termora.settings.sftp.db-click-behavior=双击行为 termora.settings.sftp.db-click-behavior=双击行为
@@ -232,6 +230,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 +244,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 +310,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 +330,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 +436,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

@@ -55,6 +55,7 @@ termora.settings.keymap=鍵盤
termora.settings.keymap.shortcut=快捷鍵 termora.settings.keymap.shortcut=快捷鍵
termora.settings.keymap.action=操作 termora.settings.keymap.action=操作
termora.settings.keymap.already-exists=快捷鍵 [{0}] 已經被 [{1}] 占用 termora.settings.keymap.already-exists=快捷鍵 [{0}] 已經被 [{1}] 占用
termora.settings.keymap.question=選取一行,按下 <font color=rgb({0},{1},{2})> ⌫ (退格鍵)</font> 移除快速鍵
termora.settings.sftp.edit-command=編輯命令 termora.settings.sftp.edit-command=編輯命令
termora.settings.sftp.db-click-behavior=按兩下行為 termora.settings.sftp.db-click-behavior=按兩下行為
@@ -114,10 +115,8 @@ termora.settings.plugin.not-compatible=插件 <b>{0}</b> 與目前版本不相
termora.settings.account=帳號 termora.settings.account=帳號
termora.settings.account.login=登入 termora.settings.account.login=登入
termora.settings.account.register=註冊
termora.settings.account.not-support-register=此伺服器不支援註冊帳號
termora.settings.account.server=伺服器 termora.settings.account.server=伺服器
termora.settings.account.mfa=多因素驗證是可選的 termora.settings.account.wait-login=正在等待預設瀏覽器登入...
termora.settings.account.locally=本地的 termora.settings.account.locally=本地的
termora.settings.account.lifetime=長期 termora.settings.account.lifetime=長期
termora.settings.account.verify=驗證 termora.settings.account.verify=驗證
@@ -228,6 +227,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 +240,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 +306,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 +324,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 +422,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

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

@@ -15,6 +15,19 @@ class KeyUtilsTest {
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair("ssh-rsa", 1024).public), 1024) assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair("ssh-rsa", 1024).public), 1024)
} }
@Test
fun test_ECDSA() {
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP256, 256).private), 256)
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP256, 256).public), 256)
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP384, 384).private), 384)
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP384, 384).public), 384)
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP521, 521).private), 521)
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP521, 521).public), 521)
}
@Test @Test
fun test_ed25519() { fun test_ed25519() {
val keyPair = KeyUtils.generateKeyPair(KeyPairProvider.SSH_ED25519, 256) val keyPair = KeyUtils.generateKeyPair(KeyPairProvider.SSH_ED25519, 256)

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

View File

@@ -1,5 +1,5 @@
FROM linuxserver/openssh-server FROM linuxserver/openssh-server:9.3_p2-r1-ls147
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
&& apk update && apk add wget tmux gcc zip p7zip g++ git make zsh htop stress-ng inetutils-telnet xclock xcalc xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \ && apk update && apk add wget tmux gcc zip p7zip g++ git make zsh htop stress-ng inetutils-telnet xclock xcalc xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
&& tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \ && tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \
&& ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz && ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz
@@ -7,3 +7,6 @@ RUN sed -i 's/#AllowAgentForwarding yes/AllowAgentForwarding yes/g' /etc/ssh/ssh
RUN sed -i 's/AllowTcpForwarding no/AllowTcpForwarding yes/g' /etc/ssh/sshd_config RUN sed -i 's/AllowTcpForwarding no/AllowTcpForwarding yes/g' /etc/ssh/sshd_config
RUN sed -i 's/GatewayPorts no/GatewayPorts yes/g' /etc/ssh/sshd_config RUN sed -i 's/GatewayPorts no/GatewayPorts yes/g' /etc/ssh/sshd_config
RUN sed -i 's/X11Forwarding no/X11Forwarding yes/g' /etc/ssh/sshd_config RUN sed -i 's/X11Forwarding no/X11Forwarding yes/g' /etc/ssh/sshd_config
# docker build -t sshd .
# docker run --rm -it -e SUDO_ACCESS=true -e PASSWORD_ACCESS=true -e USER_PASSWORD=123456 -p 2222:2222 sshd