Compare commits

...

134 Commits

Author SHA1 Message Date
dependabot[bot]
23aea95d8b chore(deps): bump org.javadelight:delight-rhino-sandbox
Bumps [org.javadelight:delight-rhino-sandbox](https://github.com/javadelight/delight-rhino-sandbox) from 0.2.1 to 0.2.2.
- [Release notes](https://github.com/javadelight/delight-rhino-sandbox/releases)
- [Commits](https://github.com/javadelight/delight-rhino-sandbox/compare/v0.2.1...v0.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 00:32:57 +00:00
dependabot[bot]
3fa2659d59 chore(deps): bump org.mozilla:rhino from 1.8.0 to 1.9.0
Bumps [org.mozilla:rhino](https://github.com/mozilla/rhino) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/mozilla/rhino/releases)
- [Changelog](https://github.com/mozilla/rhino/blob/master/RELEASE-NOTES.md)
- [Commits](https://github.com/mozilla/rhino/commits)

---
updated-dependencies:
- dependency-name: org.mozilla:rhino
  dependency-version: 1.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 08:31:50 +08:00
hstyi
3ece32e427 chore: handle IPv6 addresses correctly in RDP connection string 2025-12-22 09:13:23 +08:00
hstyi
dad4d26fd8 chore: add "Nothing" option for right-click 2025-12-22 09:12:27 +08:00
dependabot[bot]
9b387c71fc chore(deps): bump org.testcontainers:testcontainers-bom
Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 2.0.1 to 2.0.3.
- [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.1...2.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-17 19:28:10 +08:00
dependabot[bot]
7ac833b53b chore(deps): bump kotlin from 2.2.21 to 2.3.0
Bumps `kotlin` from 2.2.21 to 2.3.0.

Updates `org.jetbrains.kotlin.jvm` from 2.2.21 to 2.3.0
- [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.21...v2.3.0)

Updates `org.jetbrains.kotlin.plugin.serialization` from 2.2.21 to 2.3.0
- [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.21...v2.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-17 19:27:59 +08:00
dependabot[bot]
2327c5fd48 chore(deps): bump org.apache.commons:commons-pool2 from 2.12.1 to 2.13.0
Bumps org.apache.commons:commons-pool2 from 2.12.1 to 2.13.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 07:55:39 +08:00
Srar
bfc63a3983 fix: copy hotkey conflicts with ctrlc
(cherry picked from commit b499667cbb)
2025-12-12 09:32:32 +08:00
dependabot[bot]
c727925791 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.1 to 5.2.0.
- [Commits](https://github.com/cowtowncoder/java-uuid-generator/compare/java-uuid-generator-5.1.1...java-uuid-generator-5.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 09:27:41 +08:00
dependabot[bot]
cae1173180 chore(deps): bump flatlaf from 3.6.2 to 3.7
Bumps `flatlaf` from 3.6.2 to 3.7.

Updates `com.formdev:flatlaf` from 3.6.2 to 3.7
- [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.2...3.7)

Updates `com.formdev:flatlaf-extras` from 3.6.2 to 3.7
- [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.2...3.7)

Updates `com.formdev:flatlaf-swingx` from 3.6.2 to 3.7
- [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.2...3.7)

---
updated-dependencies:
- dependency-name: com.formdev:flatlaf
  dependency-version: '3.7'
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.formdev:flatlaf-extras
  dependency-version: '3.7'
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.formdev:flatlaf-swingx
  dependency-version: '3.7'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 09:27:29 +08:00
dependabot[bot]
10d2736232 chore(deps): bump commons-io:commons-io from 2.20.0 to 2.21.0
Bumps [commons-io:commons-io](https://github.com/apache/commons-io) from 2.20.0 to 2.21.0.
- [Changelog](https://github.com/apache/commons-io/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-io/compare/rel/commons-io-2.20.0...rel/commons-io-2.21.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-09 14:37:56 +08:00
dependabot[bot]
97f01b7e3f chore(deps): bump org.apache.commons:commons-text from 1.14.0 to 1.15.0
Bumps [org.apache.commons:commons-text](https://github.com/apache/commons-text) from 1.14.0 to 1.15.0.
- [Changelog](https://github.com/apache/commons-text/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-text/compare/rel/commons-text-1.14.0...rel/commons-text-1.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-09 14:37:42 +08:00
hstyi
21c7dd7a42 fix: prevent pulling changes for locally managed accounts 2025-12-08 16:26:24 +08:00
hstyi
bbc64043ed fix: correct scrolling region handling in ControlSequenceIntroducerProcessor 2025-12-08 16:26:15 +08:00
dependabot[bot]
79842f4625 chore(deps): bump exposed from 1.0.0-rc-2 to 1.0.0-rc-4
Bumps `exposed` from 1.0.0-rc-2 to 1.0.0-rc-4.

Updates `org.jetbrains.exposed:exposed-core` from 1.0.0-rc-2 to 1.0.0-rc-4
- [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-2...1.0.0-rc-4)

Updates `org.jetbrains.exposed:exposed-crypt` from 1.0.0-rc-2 to 1.0.0-rc-4
- [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-2...1.0.0-rc-4)

Updates `org.jetbrains.exposed:exposed-jdbc` from 1.0.0-rc-2 to 1.0.0-rc-4
- [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-2...1.0.0-rc-4)

Updates `org.jetbrains.exposed:exposed-migration-core` from 1.0.0-rc-2 to 1.0.0-rc-4
- [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-2...1.0.0-rc-4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-04 08:49:46 +08:00
hstyi
626b344088 fix: ensure dialog title is set correctly in KeyboardInteractiveDialog 2025-11-30 12:21:09 +08: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
hstyi
bd9b73ad6a release: 2.0.0-beta.13 2025-08-17 11:46:43 +08:00
hstyi
dbea769994 fix: sftp command may cause bad key types 2025-08-17 08:54:42 +08:00
hstyi
9cd83c4025 feat: host tree supports copy and paste 2025-08-15 16:35:43 +08:00
hstyi
d4cc080e7b chore: transfer text 2025-08-15 16:35:32 +08:00
hstyi
a324bc3d96 fix: keyword highlighting causes crash 2025-08-15 13:12:55 +08:00
dependabot[bot]
36929e9ea3 chore(deps): bump kotlin from 2.2.0 to 2.2.10
Bumps `kotlin` from 2.2.0 to 2.2.10.

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 08:17:38 +08:00
hstyi
190ac697fb chore: turn off the ApplePressAndHoldEnabled 2025-08-09 16:24:43 +08:00
hstyi
8cdbf24cdc fix: file name containing : cannot be transferred 2025-08-09 15:56:32 +08:00
hstyi
6e182b6813 chore: remember the colspan state of the fence layout 2025-08-09 15:33:22 +08:00
hstyi
3fa4064655 feat: hyperlinks require holding down the function key to open 2025-08-09 12:45:06 +08:00
hstyi
a77a03d8b3 fix: transfer causing repositioning after refresh 2025-08-09 12:29:19 +08:00
hstyi
5f8b9d36e2 chore: Agent Forwarding 2025-08-09 11:45:37 +08:00
hstyi
1ed5e164de feat: linux window opacity 2025-08-08 18:12:00 +08:00
hstyi
c67d5b0276 feat: ssh ForwardAgent 2025-08-08 18:11:51 +08:00
hstyi
9646a98f6d feat: background image supports fill mode 2025-08-08 15:06:13 +08:00
hstyi
aee34415a7 release: 2.0.0-beta.11 2025-08-08 13:41:31 +08:00
hstyi
e4e70cc72c chore: osx dist 2025-08-08 13:37:27 +08:00
hstyi
49779fe8f2 feat: tab order settings 2025-08-08 10:02:16 +08:00
hstyi
969ddc3662 feat: add tab index 2025-08-07 23:47:52 +08:00
hstyi
de9b418c75 feat: editor support maximization 2025-08-07 16:44:31 +08:00
hstyi
f8588745cd fix: compression not working & popup not closing 2025-08-07 16:44:19 +08:00
hstyi
7c0cbab187 chore: improve subversion 2025-08-07 13:17:31 +08:00
hstyi
176fa64de0 chore: improve osx pack 2025-08-07 11:29:13 +08:00
hstyi
495ab69195 chore: improve ssh loading 2025-08-07 11:28:59 +08:00
hstyi
93c28242fb chore: App Store 2025-08-06 19:56:06 +08:00
hstyi
57662f717b chore: README 2025-08-06 18:56:38 +08:00
dependabot[bot]
3669bd1f88 chore(deps): bump com.github.hstyi:geolite2
Bumps com.github.hstyi:geolite2 from v1.0-202507280101 to v1.0-202508040102.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-05 21:43:25 +08:00
dependabot[bot]
00e695b7d5 chore(deps): bump org.commonmark:commonmark from 0.25.0 to 0.25.1
Bumps [org.commonmark:commonmark](https://github.com/commonmark/commonmark-java) from 0.25.0 to 0.25.1.
- [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.0...commonmark-parent-0.25.1)

---
updated-dependencies:
- dependency-name: org.commonmark:commonmark
  dependency-version: 0.25.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-04 12:13:31 +08:00
dependabot[bot]
02c92e6019 chore(deps): bump commons-net:commons-net from 3.11.1 to 3.12.0
Bumps [commons-net:commons-net](https://github.com/apache/commons-net) from 3.11.1 to 3.12.0.
- [Changelog](https://github.com/apache/commons-net/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-net/compare/rel/commons-net-3.11.1...rel/commons-net-3.12.0)

---
updated-dependencies:
- dependency-name: commons-net:commons-net
  dependency-version: 3.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-04 12:13:09 +08:00
hstyi
8ba74f0846 fix: terminal exception caused by reconnection 2025-08-01 18:10:34 +08:00
hstyi
79ed6d3858 feat: support right click copy and paste 2025-08-01 17:39:19 +08:00
hstyi
8a66606275 chore: match case tooltip 2025-08-01 11:44:24 +08:00
dependabot[bot]
3ebdf73fbf chore(deps): bump exposed from 1.0.0-beta-4 to 1.0.0-beta-5
Bumps `exposed` from 1.0.0-beta-4 to 1.0.0-beta-5.

Updates `org.jetbrains.exposed:exposed-core` from 1.0.0-beta-4 to 1.0.0-beta-5
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/commits)

Updates `org.jetbrains.exposed:exposed-crypt` from 1.0.0-beta-4 to 1.0.0-beta-5
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/commits)

Updates `org.jetbrains.exposed:exposed-jdbc` from 1.0.0-beta-4 to 1.0.0-beta-5
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/commits)

Updates `org.jetbrains.exposed:exposed-migration` from 1.0.0-beta-4 to 1.0.0-beta-5
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-01 11:09:44 +08:00
hstyi
d249e5da5a fix: XTerm Set Scrolling Region 2025-07-31 16:57:33 +08:00
hstyi
7243e933e6 fix: aura selection text color 2025-07-31 16:22:32 +08:00
130 changed files with 1856 additions and 690 deletions

View File

@@ -3,8 +3,8 @@ name: Linux
on: [ push, pull_request ]
env:
JBR_MAJOR: 21.0.7
JBR_PATCH: b1038.58
JBR_MAJOR: 21.0.8
JBR_PATCH: b1163.69
jobs:
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_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
JBR_MAJOR: 21.0.7
JBR_PATCH: b1038.58
JBR_MAJOR: 21.0.8
JBR_PATCH: b1163.69
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ macos-15, macos-13 ]
os: [ macos-15-intel, macos-latest ]
steps:
- uses: actions/checkout@v4
with:
@@ -81,6 +81,10 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
- name: Install create-dmg
shell: bash
run: brew install create-dmg
- name: Compile
shell: bash
run: ./gradlew :check-license && ./gradlew classes -x test
@@ -93,13 +97,6 @@ jobs:
shell: bash
run: ./gradlew :jpackage && ./gradlew :dist
- name: Upload zip artifact
uses: actions/upload-artifact@v4
with:
name: termora-osx-zip-${{ runner.arch }}
path: |
build/distributions/*.zip
- name: Upload dmg artifact
uses: actions/upload-artifact@v4
with:

View File

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

View File

@@ -82,6 +82,7 @@ Termora is developed using [**Kotlin/JVM**](https://kotlinlang.org/) and partial
- 🧾 [Latest Release](https://github.com/TermoraDev/termora/releases/latest)
- 🍺 **Homebrew**: `brew install --cask termora`
- 🔨 **WinGet**: `winget install termora`
- <img src="https://apps.microsoft.com/assets/icons/logo-16x16.png" alt="microsoft logo"/> <b>Microsoft Store</b>: <a href="https://apps.microsoft.com/store/detail/9NRZBHG43SB9?cid=DevShareMCLPCS">Visit Termora in the Microsoft Store</a>

View File

@@ -80,6 +80,7 @@ Termora 使用 [**Kotlin/JVM**](https://kotlinlang.org/) 开发,支持(正
- 🧾 [Latest release](https://github.com/TermoraDev/termora/releases/latest)
- 🍺 **Homebrew**`brew install --cask termora`
- 🪟 **WinGet**`winget install termora`
- <img src="https://apps.microsoft.com/assets/icons/logo-16x16.png" alt="microsoft logo"/> <b>Microsoft Store</b>: <a href="https://apps.microsoft.com/store/detail/9NRZBHG43SB9?cid=DevShareMCLPCS">Termora</a>

View File

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

View File

@@ -32,6 +32,7 @@ val appVersion = project.version.toString().split("-")[0]
val makeAppx = if (os.isWindows) StringUtils.defaultString(System.getenv("MAKEAPPX_PATH")) else StringUtils.EMPTY
val isDeb = os.isLinux && System.getenv("TERMORA_TYPE") == "deb"
val isAppx = os.isWindows && makeAppx.isNotBlank() && System.getenv("TERMORA_TYPE") == "appx"
val isBeta = project.version.toString().contains("beta", ignoreCase = true)
// macOS 签名信息
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
@@ -173,10 +174,12 @@ publishing {
}
tasks.processResources {
val betaVersion = project.version.toString().substringAfterLast('.')
filesMatching("**/AppxManifest.xml") {
filter<ReplaceTokens>(
"tokens" to mapOf(
"version" to appVersion,
"betaVersion" to if (isBeta) betaVersion else "0",
"architecture" to if (arch.isArm64) "arm64" else "x64",
"projectDir" to project.projectDir.absolutePath,
)
@@ -230,9 +233,10 @@ tasks.register<Copy>("copy-dependencies") {
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
val osName = if (os.isWindows) "win32" else if (os.isMacOsX) "darwin" else "linux"
val targetDir = FileUtils.getFile(dylib, pty4j.name, osName)
FileUtils.forceMkdir(targetDir)
val myArchName = if (arch.isArm) "aarch64" else "x86-64"
val targetDir = if (os.isMacOsX) FileUtils.getFile(dylib, pty4j.name, osName)
else FileUtils.getFile(dylib, pty4j.name, osName, myArchName)
FileUtils.forceMkdir(targetDir)
if (os.isWindows) {
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/*win/${myArchName}/*", "-d", targetDir.absolutePath) }
@@ -335,6 +339,7 @@ tasks.register<Exec>("jlink") {
"java.security.jgss",
"jdk.crypto.ec",
"jdk.unsupported",
"jdk.httpserver",
)
commandLine(
@@ -380,6 +385,7 @@ tasks.register<Exec>("jpackage") {
}
if (os.isLinux) {
options.add("--add-opens=java.desktop/sun.awt.X11=ALL-UNNAMED")
if (isDeb) {
options.add("-Djpackage.app-layout=deb")
}
@@ -399,18 +405,6 @@ tasks.register<Exec>("jpackage") {
arguments.addAll(listOf("--copyright", "TermoraDev"))
arguments.addAll(listOf("--app-content", "$buildDir/plugins"))
if (os.isWindows) {
arguments.addAll(
listOf(
"--description",
"${project.name.uppercaseFirstChar()}: A terminal emulator and SSH client"
)
)
} else {
arguments.addAll(listOf("--description", "A terminal emulator and SSH client."))
}
if (os.isMacOsX) {
arguments.addAll(listOf("--mac-package-name", project.name.uppercaseFirstChar()))
arguments.addAll(listOf("--mac-app-category", "developer-tools"))
@@ -442,7 +436,7 @@ tasks.register<Exec>("jpackage") {
throw UnsupportedOperationException()
}
if (os.isMacOsX && macOSSign) {
if (macOSSign) {
arguments.add("--mac-sign")
arguments.add("--mac-signing-key-user-name")
arguments.add(macOSSignUsername)
@@ -677,17 +671,24 @@ fun packOnLinux(distributionDir: Directory, finalFilenameWithoutExtension: Strin
exec { commandLine("chmod", "+x", appimagetool.absolutePath) }
}
// Desktop file
val termoraName = project.name.uppercaseFirstChar()
// copy icon
FileUtils.copyFile(
File("${projectDir.absolutePath}/src/main/resources/icons/termora_256x256.png"),
distributionDir.file(termoraName + File.separator + termoraName + ".png").asFile
)
val desktopFile = distributionDir.file(termoraName + File.separator + termoraName + ".desktop").asFile
desktopFile.writeText(
"""[Desktop Entry]
Type=Application
Name=${termoraName}
Comment=Terminal emulator and SSH client
Icon=/lib/${termoraName}
Icon=${termoraName}
Categories=Development;
StartupWMClass=${termoraName}
Terminal=false
""".trimIndent()
)

View File

@@ -1,50 +1,50 @@
[versions]
kotlin = "2.2.0"
kotlin = "2.3.0"
slf4j = "2.0.17"
pty4j = "0.13.10"
tinylog = "2.7.0"
kotlinx-coroutines = "1.10.2"
flatlaf = "3.6.1"
flatlaf = "3.7"
kotlinx-serialization-json = "1.9.0"
commons-codec = "1.19.0"
commons-lang3 = "3.18.0"
commons-codec = "1.20.0"
commons-lang3 = "3.20.0"
commons-csv = "1.14.1"
commons-net = "3.11.1"
commons-text = "1.14.0"
commons-net = "3.12.0"
commons-text = "1.15.0"
commons-compress = "1.28.0"
commons-vfs2 = "2.10.0"
swingx = "1.6.5-1"
jgoodies-forms = "1.9.0"
jfa = "1.2.0"
oshi = "6.8.1"
oshi = "6.9.1"
versioncompare = "1.4.1"
jna = "5.17.0"
jna = "5.18.1"
jSystemThemeDetector = "3.9.1"
commons-io = "2.20.0"
commons-io = "2.21.0"
jbr-api = "17.1.10.1"
hutool = "5.8.39"
jsch = "2.27.2"
okhttp = "5.1.0"
hutool = "5.8.40"
jsch = "2.27.3"
okhttp = "5.3.0"
sshj = "0.39.0"
sshd-core = "2.15.0"
jgit = "7.2.0.202503040940-r"
commonmark = "0.25.0"
jgit = "7.4.0.202509020913-r"
commonmark = "0.27.0"
jnafilechooser = "1.1.2"
xodus = "2.0.1"
bip39 = "1.0.9"
colorpicker = "2.0.1"
rhino = "1.8.0"
delight-rhino-sandbox = "0.0.17"
testcontainers = "1.21.3"
mixpanel = "1.5.3"
jSerialComm = "2.11.2"
rhino = "1.9.0"
delight-rhino-sandbox = "0.2.2"
testcontainers = "2.0.3"
mixpanel = "1.5.4"
jSerialComm = "2.11.4"
ini4j = "0.5.5-2"
restart4j = "0.0.1"
eddsa = "0.3.0"
exposed = "1.0.0-beta-4"
exposed = "1.0.0-rc-4"
h2 = "2.3.232"
sqlite = "3.50.3.0"
jug = "5.1.0"
jug = "5.2.0"
semver4j = "6.0.0"
jsvg = "2.0.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" }
testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "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" }
jgoodies-forms = { module = "com.jgoodies:jgoodies-forms", version.ref = "jgoodies-forms" }
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-crypt = { module = "org.jetbrains.exposed:exposed-crypt", 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" }
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
jug = { module = "com.fasterxml.uuid:java-uuid-generator", version.ref = "jug" }

View File

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

View File

@@ -18,4 +18,8 @@ object Appearance {
set(value) {
enableManager.setFlag("Plugins.bg.interval", value)
}
var fillMode: String
get() = enableManager.getFlag("Plugins.bg.fillMode", FillMode.STRETCH.name)
set(value) = enableManager.setFlag("Plugins.bg.fillMode", value)
}

View File

@@ -2,6 +2,8 @@ package app.termora.plugins.bg
import app.termora.GlassPaneExtension
import app.termora.WindowScope
import app.termora.restore
import app.termora.save
import com.formdev.flatlaf.FlatLaf
import java.awt.AlphaComposite
import java.awt.Graphics2D
@@ -12,15 +14,52 @@ class BGGlassPaneExtension private constructor() : GlassPaneExtension {
val instance = BGGlassPaneExtension()
}
override fun paint(scope: WindowScope, c: JComponent, g2d: Graphics2D) {
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
g2d.save()
g2d.composite = AlphaComposite.getInstance(
AlphaComposite.SRC_OVER,
if (FlatLaf.isLafDark()) 0.2f else 0.1f
)
g2d.drawImage(img, 0, 0, c.width, c.height, null)
g2d.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER)
when (Appearance.fillMode) {
FillMode.STRETCH.name -> {
g2d.drawImage(img, 0, 0, c.width, c.height, null)
}
FillMode.CENTER.name -> {
val x = (c.width - img.width) / 2
val y = (c.height - img.height) / 2
g2d.drawImage(img, x, y, null)
}
FillMode.TILE.name -> {
val iw = img.width
val ih = img.height
var y = 0
while (y < c.height) {
var x = 0
while (x < c.width) {
g2d.drawImage(img, x, y, null)
x += iw
}
y += ih
}
}
FillMode.FIT.name -> {
val scale = maxOf(c.width.toDouble() / img.width, c.height.toDouble() / img.height)
val newW = (img.width * scale).toInt()
val newH = (img.height * scale).toInt()
val x = (c.width - newW) / 2
val y = (c.height - newH) / 2
g2d.drawImage(img, x, y, newW, newH, null)
}
}
g2d.restore()
}
}

View File

@@ -10,6 +10,8 @@ import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.exception.ExceptionUtils
import org.slf4j.LoggerFactory
import java.awt.BorderLayout
import java.awt.Component
import java.awt.event.ItemEvent
import java.io.File
import java.nio.file.StandardCopyOption
import javax.swing.*
@@ -23,6 +25,7 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
private val owner get() = SwingUtilities.getWindowAncestor(this)
val backgroundImageTextField = OutlineTextField()
val fillModeComboBox = OutlineComboBox<FillMode>()
val intervalSpinner = NumberSpinner(360, minimum = 30, maximum = 86400)
private val backgroundButton = JButton(Icons.folder)
@@ -36,6 +39,38 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
private fun initView() {
fillModeComboBox.addItem(FillMode.STRETCH)
fillModeComboBox.addItem(FillMode.FIT)
fillModeComboBox.addItem(FillMode.CENTER)
fillModeComboBox.addItem(FillMode.TILE)
fillModeComboBox.selectedItem = runCatching { FillMode.valueOf(Appearance.fillMode) }
.getOrNull() ?: FillMode.STRETCH
fillModeComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component? {
var text = value?.toString()
if (value == FillMode.STRETCH) {
text = BGI18n.getString("termora.plugins.bg.fill-mode.stretch")
} else if (value == FillMode.FIT) {
text = BGI18n.getString("termora.plugins.bg.fill-mode.fit")
} else if (value == FillMode.CENTER) {
text = BGI18n.getString("termora.plugins.bg.fill-mode.center")
} else if (value == FillMode.TILE) {
text = BGI18n.getString("termora.plugins.bg.fill-mode.tile")
}
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
}
}
backgroundImageTextField.isEditable = false
backgroundImageTextField.trailingComponent = backgroundButton
backgroundImageTextField.text = Appearance.backgroundImage
@@ -80,6 +115,15 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
Appearance.interval = value
}
}
fillModeComboBox.addItemListener {
if (it.stateChange == ItemEvent.SELECTED) {
Appearance.fillMode = fillModeComboBox.selectedItem?.toString() ?: FillMode.STRETCH.name
for (frame in TermoraFrameManager.getInstance().getWindows()) {
SwingUtilities.invokeLater { SwingUtilities.updateComponentTreeUI(frame) }
}
}
}
}
private fun onSelectedBackgroundImage(file: File) {
@@ -124,7 +168,7 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
private fun getFormPanel(): JPanel {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default",
"pref, $FORM_MARGIN, pref"
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
var rows = 1
@@ -138,6 +182,10 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
.add(bgClearBox).xy(5, rows)
.apply { rows += step }
builder.add("${BGI18n.getString("termora.plugins.bg.fill-mode")}:").xy(1, rows)
.add(fillModeComboBox).xy(3, rows)
.apply { rows += step }
builder.add("${BGI18n.getString("termora.plugins.bg.interval")}:").xy(1, rows)
.add(intervalSpinner).xy(3, rows)
.apply { rows += step }

View File

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

View File

@@ -1,2 +1,7 @@
termora.plugins.bg.interval=Interval
termora.plugins.bg.fill-mode=Fill Mode
termora.plugins.bg.fill-mode.stretch=Stretch
termora.plugins.bg.fill-mode.fit=Fit
termora.plugins.bg.fill-mode.center=Center
termora.plugins.bg.fill-mode.tile=Tile
termora.plugins.bg.background-image=Background Image

View File

@@ -1,2 +1,8 @@
termora.plugins.bg.background-image=背景图
termora.plugins.bg.interval=切换间隔
termora.plugins.bg.fill-mode=填充模式
termora.plugins.bg.fill-mode.stretch=拉伸
termora.plugins.bg.fill-mode.fit=适合
termora.plugins.bg.fill-mode.center=居中
termora.plugins.bg.fill-mode.tile=平铺

View File

@@ -1,2 +1,8 @@
termora.plugins.bg.background-image=背景圖
termora.plugins.bg.interval=切換間隔
termora.plugins.bg.fill-mode=填充模式
termora.plugins.bg.fill-mode.stretch=拉伸
termora.plugins.bg.fill-mode.fit=適合
termora.plugins.bg.fill-mode.center=居中
termora.plugins.bg.fill-mode.tile=平鋪

View File

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

View File

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

View File

@@ -1,94 +0,0 @@
package app.termora.plugins.editor
import app.termora.DialogWrapper
import app.termora.Disposable
import app.termora.Disposer
import app.termora.OptionPane
import java.awt.Dimension
import java.awt.Window
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import java.io.File
import java.nio.file.Path
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.JComponent
import javax.swing.JOptionPane
import javax.swing.UIManager
import kotlin.io.path.absolutePathString
import kotlin.io.path.name
class EditorDialog(file: Path, owner: Window, private val myDisposable: Disposable) : DialogWrapper(null) {
private val filename = file.name
private val filepath = File(file.absolutePathString())
private val editorPanel = EditorPanel(this, filepath)
private val disposed = AtomicBoolean()
init {
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
isModal = false
controlsVisible = true
isResizable = true
title = filename
iconImages = owner.iconImages
escapeDispose = false
defaultCloseOperation = DO_NOTHING_ON_CLOSE
initEvents()
setLocationRelativeTo(owner)
init()
}
private fun initEvents() {
addWindowListener(object : WindowAdapter() {
override fun windowClosing(e: WindowEvent?) {
if (disposed.compareAndSet(false, true)) {
doCancelAction()
}
}
})
Disposer.register(myDisposable, object : Disposable {
override fun dispose() {
if (disposed.compareAndSet(false, true)) {
doCancelAction()
}
}
})
Disposer.register(disposable, object : Disposable {
override fun dispose() {
if (disposed.compareAndSet(false, true)) {
Disposer.dispose(myDisposable)
}
}
})
}
override fun doCancelAction() {
if (editorPanel.changes()) {
if (OptionPane.showConfirmDialog(
this,
"文件尚未保存,你确定要退出吗?",
optionType = JOptionPane.OK_CANCEL_OPTION,
) != JOptionPane.OK_OPTION
) {
return
}
}
super.doCancelAction()
}
override fun createCenterPanel(): JComponent {
return editorPanel
}
override fun createSouthPanel(): JComponent? {
return null
}
}

View File

@@ -0,0 +1,91 @@
package app.termora.plugins.editor
import app.termora.Disposable
import app.termora.Disposer
import app.termora.EnableManager
import app.termora.OptionPane
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.Window
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import java.io.File
import java.nio.file.Path
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.JFrame
import javax.swing.JOptionPane
import javax.swing.UIManager
import kotlin.io.path.absolutePathString
import kotlin.io.path.name
import kotlin.math.max
class EditorFrame(private val file: Path, private val owner: Window, private val disposable: Disposable) : JFrame() {
private val enableManager get() = EnableManager.getInstance()
private val disposed = AtomicBoolean()
private val filepath = File(file.absolutePathString())
private val frame get() = this
private val editorPanel = EditorPanel(this, filepath)
init {
initView()
initEvent()
}
private fun initEvent() {
Disposer.register(disposable, object : Disposable {
override fun dispose() {
if (disposed.compareAndSet(false, true)) frame.dispose()
}
})
addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
if (disposed.compareAndSet(false, true)) Disposer.dispose(disposable)
enableManager.setFlag("Plugins.editor.dialog.width", width)
enableManager.setFlag("Plugins.editor.dialog.height", height)
enableManager.setFlag("Plugins.editor.dialog.extendedState", extendedState)
}
override fun windowClosing(e: WindowEvent?) {
if (editorPanel.changes()) {
if (OptionPane.showConfirmDialog(
frame,
EditorI18n.getString("termora.plugins.editor.not-save"),
optionType = JOptionPane.OK_CANCEL_OPTION,
) == JOptionPane.OK_OPTION
) {
frame.dispose()
}
} else {
frame.dispose()
}
}
})
}
private fun initView() {
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
val state = enableManager.getFlag("Plugins.editor.dialog.extendedState", 0)
if ((state and MAXIMIZED_BOTH) == MAXIMIZED_BOTH) {
frame.setLocationRelativeTo(null)
frame.extendedState = state
} else {
val mySize = size
mySize.width = max(enableManager.getFlag("Plugins.editor.dialog.width", mySize.width), mySize.width)
mySize.height = max(enableManager.getFlag("Plugins.editor.dialog.height", mySize.height), mySize.height)
size = mySize
setLocationRelativeTo(owner)
}
title = file.name
iconImages = owner.iconImages
defaultCloseOperation = DO_NOTHING_ON_CLOSE
rootPane.contentPane.layout = BorderLayout()
rootPane.contentPane.add(editorPanel, BorderLayout.CENTER)
}
}

View File

@@ -0,0 +1,13 @@
package app.termora.plugins.editor
import app.termora.NamedI18n
import org.slf4j.Logger
import org.slf4j.LoggerFactory
object EditorI18n : NamedI18n("i18n/messages") {
private val log = LoggerFactory.getLogger(EditorI18n::class.java)
override fun getLogger(): Logger {
return log
}
}

View File

@@ -1,9 +1,6 @@
package app.termora.plugins.editor
import app.termora.DocumentAdaptor
import app.termora.DynamicColor
import app.termora.EnableManager
import app.termora.Icons
import app.termora.*
import app.termora.database.DatabaseManager
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.components.FlatTextField
@@ -35,10 +32,14 @@ import javax.swing.event.DocumentEvent
import kotlin.math.max
import kotlin.math.min
class EditorPanel(private val window: JDialog, private val file: File) : JPanel(BorderLayout()) {
class EditorPanel(private val window: JFrame, private val file: File) : JPanel(BorderLayout()) {
companion object {
private val log = LoggerFactory.getLogger(EditorPanel::class.java)
private val saveIcon = DynamicIcon(
"icons/save.svg", "icons/save_dark.svg",
loader = EditorPlugin::class.java.classLoader
)
}
private var text = file.readText(Charsets.UTF_8)
@@ -54,6 +55,7 @@ class EditorPanel(private val window: JDialog, private val file: File) : JPanel(
private val prevBtn = JButton(Icons.up)
private val context = SearchContext()
private val softWrapBtn = JToggleButton(Icons.softWrap)
private val saveBtn = JButton(saveIcon)
private val scrollUpBtn = JButton(Icons.scrollUp)
private val scrollEndBtn = JButton(Icons.scrollDown)
private val prettyBtn = JButton(Icons.reformatCode)
@@ -141,11 +143,18 @@ class EditorPanel(private val window: JDialog, private val file: File) : JPanel(
)
toolbar.orientation = VERTICAL
toolbar.add(saveBtn)
toolbar.add(scrollUpBtn)
toolbar.add(prettyBtn)
toolbar.add(softWrapBtn)
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())
viewPanel.add(scrollPane, BorderLayout.CENTER)
viewPanel.add(toolbar, BorderLayout.EAST)
@@ -211,6 +220,8 @@ class EditorPanel(private val window: JDialog, private val file: File) : JPanel(
}
})
saveBtn.addActionListener(textArea.actionMap.get("Save"))
textArea.actionMap.put("Format", object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
format()

View File

@@ -14,7 +14,7 @@ class MyTransportEditFileExtension private constructor() : TransportEditFileExte
override fun edit(owner: Window, path: Path): Disposable {
val disposable = Disposer.newDisposable()
SwingUtilities.invokeLater { EditorDialog(path, owner, disposable).isVisible = true }
SwingUtilities.invokeLater { EditorFrame(path, owner, disposable).isVisible = true }
return disposable
}
}

View File

@@ -0,0 +1,6 @@
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

@@ -0,0 +1,6 @@
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,6 @@
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,6 @@
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,7 +7,7 @@ project.version = "0.0.2"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
implementation("org.apache.commons:commons-pool2:2.12.1")
implementation("org.apache.commons:commons-pool2:2.13.0")
testImplementation(project(":"))
}

View File

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

View File

@@ -8,7 +8,7 @@ project.version = "0.0.2"
dependencies {
testImplementation(kotlin("test"))
implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.5")
implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.7")
compileOnly(project(":"))
}

View File

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

View File

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

View File

@@ -1,9 +1,6 @@
package app.termora.plugins.serial
import app.termora.Host
import app.termora.Icons
import app.termora.PtyHostTerminalTab
import app.termora.WindowScope
import app.termora.*
import app.termora.terminal.PtyConnector
import org.apache.commons.io.Charsets
import java.nio.charset.StandardCharsets
@@ -11,6 +8,8 @@ import javax.swing.Icon
class SerialTerminalTab(windowScope: WindowScope, host: Host) :
PtyHostTerminalTab(windowScope, host) {
override suspend fun openPtyConnector(): PtyConnector {
val serialPort = Serials.openPort(host)
return SerialPortPtyConnector(
@@ -19,6 +18,10 @@ class SerialTerminalTab(windowScope: WindowScope, host: Host) :
)
}
override fun createReconnectTerminalTab(): TerminalTab {
return SerialTerminalTab(windowScope, host)
}
override fun getIcon(): Icon {
return Icons.plugin
}

View File

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

View File

@@ -42,6 +42,7 @@ class SMBHostOptionsPane : OptionsPane() {
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
extras = mutableMapOf(
"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.passwordTextField.text = host.authentication.password
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
}
@@ -114,6 +116,7 @@ class SMBHostOptionsPane : OptionsPane() {
val nameTextField = OutlineTextField(128)
val shareTextField = OutlineTextField(256)
val usernameTextField = OutlineComboBox<String>()
val domainTextField = OutlineTextField(128)
val hostTextField = OutlineTextField(255)
val passwordTextField = OutlinePasswordField(255)
val remarkTextArea = FixedLengthTextArea(512)
@@ -188,7 +191,9 @@ class SMBHostOptionsPane : OptionsPane() {
.add(portTextField).xy(7, rows).apply { rows += step }
.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(passwordTextField).xyw(3, rows, 5).apply { rows += step }

View File

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

View File

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

View File

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

View File

@@ -1 +1,3 @@
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.done=Synchronized data successfully
termora.settings.sync.export=${termora.keymgr.export}
termora.settings.sync.import=${termora.keymgr.import}
termora.settings.sync.import.file-too-large=The file is too large
termora.settings.sync.import.successful=Import data successfully
termora.settings.sync.export-done=The export was successful
termora.settings.sync.export-encrypt=Enter password to encrypt file (optional)
termora.settings.sync.export-done-open-folder=The export was successful. Do you want to open the folder?
termora.settings.sync.range=Range
termora.settings.sync.range.keys=My keys
termora.settings.sync.range.keyword-highlights=${termora.highlight}

View File

@@ -2,15 +2,10 @@ termora.plugins.sync.disabled-sync=你已登录,无法使用此功能
termora.settings.sync=同步
termora.settings.sync.export-done=导出成功
termora.settings.sync.export-encrypt=输入密码加密文件 (可选)
termora.settings.sync.export-done-open-folder=导出成功,是否需要打开所在文件夹?
termora.settings.sync.range=范围
termora.settings.sync.range.keys=我的密钥
termora.settings.sync.last-sync-time=最后同步时间
termora.settings.sync.done=同步数据成功
termora.settings.sync.import.file-too-large=文件太大
termora.settings.sync.import.successful=导入数据成功
termora.settings.sync.gist=片段
termora.settings.sync.token=令牌
termora.settings.sync.type=类型

View File

@@ -1,9 +1,6 @@
termora.plugins.sync.disabled-sync=你已登錄,無法使用此功能
termora.settings.sync=同步
termora.settings.sync.export-done=匯出成功
termora.settings.sync.export-encrypt=輸入密碼加密檔案 (可選)
termora.settings.sync.export-done-open-folder=匯出成功,是否需要打開所在資料夾?
termora.settings.sync.range=範圍
termora.settings.sync.range.keys=我的密鑰
termora.settings.sync.last-sync-time=最後同步時間

View File

@@ -12,6 +12,7 @@ enum class AppLayout {
* macOS
*/
App,
AppStore,
/**
* Linux

View File

@@ -0,0 +1,21 @@
package app.termora
import com.formdev.flatlaf.util.SystemInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
internal class ApplePressAndHoldEnabledApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
companion object {
val instance = ApplePressAndHoldEnabledApplicationRunnerExtension()
}
override fun ready() {
if (SystemInfo.isMacOS.not()) return
swingCoroutineScope.launch(Dispatchers.IO) {
Runtime.getRuntime()
.exec(arrayOf("defaults", "write", "app.termora", "ApplePressAndHoldEnabled", "-bool", "false"))
.waitFor()
}
}
}

View File

@@ -9,6 +9,7 @@ import org.apache.commons.lang3.SystemUtils
import org.apache.commons.lang3.math.NumberUtils
import org.slf4j.LoggerFactory
import org.tinylog.configuration.Configuration
import java.awt.Toolkit
import java.io.File
import kotlin.system.exitProcess
import kotlin.system.measureTimeMillis
@@ -35,10 +36,31 @@ class ApplicationInitializr {
// 检查是否单例
checkSingleton()
if (SystemUtils.IS_OS_MAC_OSX) {
if (SystemInfo.isMacOS) {
System.setProperty("apple.awt.application.name", Application.getName())
}
if (SystemInfo.isLinux) {
// https://stackoverflow.com/questions/10593075
runCatching {
val toolkit = Toolkit.getDefaultToolkit()
val awtAppClassNameField = toolkit.javaClass.getDeclaredField("awtAppClassName")
awtAppClassNameField.setAccessible(true)
awtAppClassNameField.set(toolkit, Application.getName())
}
}
// 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 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.util.SystemInfo
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.launch
import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.LocaleUtils
import org.apache.commons.lang3.SystemUtils
import org.json.JSONObject
import org.slf4j.LoggerFactory
import java.awt.*
import java.awt.desktop.AppReopenedEvent
@@ -369,61 +365,8 @@ class ApplicationRunner {
if (Application.isUnknownVersion()) {
return
}
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)
}
}
}
MixpanelService.getInstance().push("launch")
}
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

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

View File

@@ -3,14 +3,16 @@ package app.termora
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProvider
import app.termora.actions.DataProviders
import app.termora.terminal.*
import app.termora.terminal.ControlCharacters
import app.termora.terminal.DataKey
import app.termora.terminal.DataListener
import app.termora.terminal.Terminal
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.swing.Swing
import org.apache.commons.lang3.StringUtils
import java.beans.PropertyChangeEvent
import java.util.*
import javax.swing.Icon
@@ -27,13 +29,8 @@ abstract class HostTerminalTab(
protected val terminalTabbedManager
get() = AnActionEvent(getJComponent(), StringUtils.EMPTY, EventObject(getJComponent()))
.getData(DataProviders.TerminalTabbedManager)
protected val coroutineScope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.Swing) }
protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Swing)
protected val terminalModel get() = terminal.getTerminalModel()
protected var unread = false
set(value) {
field = value
firePropertyChange(PropertyChangeEvent(this, "icon", null, null))
}
/* visualTerminal */
@@ -45,15 +42,6 @@ abstract class HostTerminalTab(
terminal.getTerminalModel().setData(Host, host)
terminal.getTerminalModel().addDataListener(object : DataListener {
override fun onChanged(key: DataKey<*>, data: Any) {
if (key == VisualTerminal.Written) {
if (hasFocus || unread) {
return
}
// 如果当前选中的不是这个 Tab那么设置成未读
if (terminalTabbedManager?.getSelectedTerminalTab() != this@HostTerminalTab) {
unread = true
}
}
}
})
}
@@ -75,8 +63,6 @@ abstract class HostTerminalTab(
override fun onGrabFocus() {
super.onGrabFocus()
if (!unread) return
unread = false
}
@Suppress("UNCHECKED_CAST")

View File

@@ -533,7 +533,7 @@ class AuraLaf : FlatPropertiesLaf("Aura", Properties().apply {
TerminalColor.Bright.WHITE -> 0xffffff
TerminalColor.Basic.SELECTION_BACKGROUND,
TerminalColor.Cursor.BACKGROUND -> 0xedecee
TerminalColor.Cursor.BACKGROUND -> 0xacacac
else -> Int.MAX_VALUE
}
@@ -874,6 +874,8 @@ class NordDarkLaf : FlatPropertiesLaf("Nord Dark", Properties().apply {
TerminalColor.Basic.SELECTION_BACKGROUND,
TerminalColor.Cursor.BACKGROUND -> 0xeceff4
TerminalColor.Basic.SELECTION_FOREGROUND -> 0x3b4252
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

@@ -2,19 +2,21 @@ package app.termora
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
import app.termora.actions.SwitchTabAction
import app.termora.database.DatabaseManager
import app.termora.keymap.KeyShortcut
import app.termora.keymap.KeymapManager
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.ui.FlatTabbedPaneUI
import org.apache.commons.lang3.StringUtils
import java.awt.*
import java.awt.event.*
import java.awt.image.BufferedImage
import java.util.*
import javax.swing.ImageIcon
import javax.swing.JDialog
import javax.swing.JLabel
import javax.swing.SwingUtilities
import javax.swing.*
import kotlin.math.abs
class MyTabbedPane : FlatTabbedPane() {
internal class MyTabbedPane : FlatTabbedPane(), Disposable {
private val dragMouseAdaptor = DragMouseAdaptor()
private val terminalTabbedManager
@@ -23,6 +25,30 @@ class MyTabbedPane : FlatTabbedPane() {
private val owner
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
.getData(DataProviders.TermoraFrame) as TermoraFrame
private val keymap get() = KeymapManager.getInstance().getActiveKeymap()
private val tabOrder get() = DatabaseManager.getInstance().appearance.tabOrder
private val isScreen get() = TermoraLayout.Layout == TermoraLayout.Screen
private var isSwitchTabMode = false
set(value) {
if (tabOrder == TabOrder.Always.name) {
if (field.not()) {
field = true
repaint()
}
return
} else if (tabOrder == TabOrder.Hide.name) {
if (field) {
field = false
repaint()
}
return
} else if (tabOrder == TabOrder.AsNeed.name) {
if (field != value) {
field = value
repaint()
}
}
}
init {
isFocusable = false
@@ -38,6 +64,16 @@ class MyTabbedPane : FlatTabbedPane() {
private fun initEvents() {
addMouseListener(dragMouseAdaptor)
addMouseMotionListener(dragMouseAdaptor)
val awtEventListener = MyAWTEventListener()
toolkit.addAWTEventListener(awtEventListener, AWTEvent.KEY_EVENT_MASK or AWTEvent.WINDOW_EVENT_MASK)
Disposer.register(this, object : Disposable {
override fun dispose() {
toolkit.removeAWTEventListener(awtEventListener)
}
})
}
override fun processMouseEvent(e: MouseEvent) {
@@ -70,6 +106,32 @@ class MyTabbedPane : FlatTabbedPane() {
firePropertyChange("selectedIndex", oldIndex, index)
}
override fun updateUI() {
super.updateUI()
setUI(MyMyTabbedPaneUI())
}
private inner class MyAWTEventListener : AWTEventListener {
override fun eventDispatched(event: AWTEvent) {
if (event is KeyEvent) {
if (isSwitchTabMode) isSwitchTabMode = false
val shortcuts = keymap.getShortcut(SwitchTabAction.SWITCH_TAB)
if (shortcuts.isEmpty()) return
val shortcut = shortcuts.first() as KeyShortcut
val modifiers = KeyStroke.getKeyStroke(event.keyCode, event.modifiersEx).modifiers
if (shortcut.keyStroke.modifiers != modifiers) return
if (SwingUtilities.getWindowAncestor(event.component) != owner) return
if (isSwitchTabMode.not()) isSwitchTabMode = true
} else if (event is WindowEvent) {
if (event.id == WindowEvent.WINDOW_LOST_FOCUS || event.id == WindowEvent.WINDOW_DEACTIVATED) {
if (isSwitchTabMode) isSwitchTabMode = false
} else if (event.id == WindowEvent.WINDOW_GAINED_FOCUS || event.id == WindowEvent.WINDOW_ACTIVATED) {
// 触发一次刷新
isSwitchTabMode = isSwitchTabMode
}
}
}
}
private inner class DragMouseAdaptor : MouseAdapter(), KeyEventDispatcher {
private var mousePressedPoint = Point()
@@ -267,5 +329,81 @@ class MyTabbedPane : FlatTabbedPane() {
}
}
private inner class MyMyTabbedPaneUI : FlatTabbedPaneUI() {
override fun paintIcon(
g: Graphics,
tabPlacement: Int,
tabIndex: Int,
icon: Icon,
iconRect: Rectangle?,
isSelected: Boolean
) {
super.paintIcon(g, tabPlacement, tabIndex, MyIcon(icon, tabIndex, isSelected), iconRect, isSelected)
}
override fun createMoreTabsButton(): JButton {
return MyMoreTabsButton()
}
private inner class MyMoreTabsButton : FlatMoreTabsButton() {
override fun createTabMenuItem(tabIndex: Int): JMenuItem? {
val item = super.createTabMenuItem(tabIndex)
if (tabIndex == 0 && isScreen) {
item.text = Application.getName()
}
return item
}
}
}
override fun getIconAt(index: Int): Icon? {
if (isSwitchTabMode) {
return MyIcon(super.getIconAt(index), index, selectedIndex == index)
}
return super.getIconAt(index)
}
private inner class MyIcon(private val icon: Icon, private val tabIndex: Int, private val isSelected: Boolean) :
Icon {
override fun paintIcon(c: Component, g: Graphics, x: Int, y: Int) {
if (isScreen && tabIndex == 0) {
icon.paintIcon(c, g, x, y)
return
}
if (isSwitchTabMode.not()) {
icon.paintIcon(c, g, x, y)
return
}
if (g !is Graphics2D) return
g.save()
setupAntialiasing(g)
val fm = g.getFontMetrics(g.font)
val text = "${tabIndex + 1}"
val textWidth = fm.stringWidth(text)
val textHeight = fm.ascent
val centerX = x + (icon.iconWidth - textWidth) / 2
val centerY = y + (icon.iconHeight + textHeight) / 2 - 1
g.color = c.getForeground()
g.drawString(text, centerX, centerY)
g.restore()
}
override fun getIconWidth(): Int {
return icon.iconWidth
}
override fun getIconHeight(): Int {
return icon.iconHeight
}
}
}

View File

@@ -3,5 +3,5 @@ package app.termora
import app.termora.actions.AnActionEvent
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)

View File

@@ -173,10 +173,21 @@ abstract class PtyHostTerminalTab(
}
override fun reconnect() {
stop()
start()
val manager = terminalTabbedManager ?: return
val index = manager.indexOfTerminalTab(this)
if (index < 0) return
val tab = createReconnectTerminalTab()
manager.addTerminalTab(index, tab, true)
manager.closeTerminalTab(this, disposable = true, reconnect = true)
if (tab is HostTerminalTab) {
tab.start()
}
}
protected abstract fun createReconnectTerminalTab(): TerminalTab
override fun getJComponent(): JComponent {
return terminalPanel
}

View File

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

View File

@@ -21,6 +21,7 @@ import com.jgoodies.forms.layout.FormLayout
import com.jthemedetecor.OsThemeDetector
import com.sun.jna.LastErrorException
import com.sun.jna.Native
import com.sun.jna.platform.WindowUtils
import com.sun.jna.platform.win32.Shell32
import com.sun.jna.platform.win32.ShlObj
import com.sun.jna.platform.win32.WinDef
@@ -114,6 +115,7 @@ class SettingsOptionsPane : OptionsPane() {
val languageComboBox = FlatComboBox<String>()
val backgroundComBoBox = YesOrNoComboBox()
val confirmTabCloseComBoBox = YesOrNoComboBox()
val tabOrderComboBox = FlatComboBox<TabOrder>()
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
val preferredThemeBtn = JButton(Icons.settings)
val opacitySpinner = NumberSpinner(100, 0, 100)
@@ -128,6 +130,12 @@ class SettingsOptionsPane : OptionsPane() {
private fun initView() {
tabOrderComboBox.addItem(TabOrder.Hide)
tabOrderComboBox.addItem(TabOrder.AsNeed)
tabOrderComboBox.addItem(TabOrder.Always)
tabOrderComboBox.selectedItem = runCatching { TabOrder.valueOf(appearance.tabOrder) }
.getOrNull() ?: TabOrder.Hide
layoutComboBox.addItem(TermoraLayout.Screen)
layoutComboBox.addItem(TermoraLayout.Fence)
layoutComboBox.renderer = object : DefaultListCellRenderer() {
@@ -162,7 +170,8 @@ class SettingsOptionsPane : OptionsPane() {
backgroundComBoBox.isEnabled = SystemInfo.isWindows || SystemInfo.isMacOS
opacitySpinner.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows
opacitySpinner.isEnabled = (SystemInfo.isMacOS || SystemInfo.isWindows)
|| (SystemInfo.isLinux && WindowUtils.isWindowAlphaSupported())
opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) {
override fun getNextValue(): Any {
return super.getNextValue() ?: maximum
@@ -226,6 +235,14 @@ class SettingsOptionsPane : OptionsPane() {
}
})
tabOrderComboBox.addItemListener(object : ItemListener {
override fun itemStateChanged(e: ItemEvent) {
if (e.stateChange == ItemEvent.SELECTED) {
appearance.tabOrder = tabOrderComboBox.selectedItem?.toString() ?: return
}
}
})
opacitySpinner.addChangeListener {
val opacity = opacitySpinner.value
if (opacity is Double) {
@@ -349,7 +366,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun getFormPanel(): JPanel {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
val box = FlatToolBar()
box.add(followSystemCheckBox)
@@ -380,6 +397,9 @@ class SettingsOptionsPane : OptionsPane() {
builder.add("${I18n.getString("termora.settings.appearance.background-running")}:").xy(1, rows)
.add(backgroundComBoBox).xy(3, rows).apply { rows += step }
builder.add("${I18n.getString("termora.settings.appearance.tab-order")}:").xy(1, rows)
.add(tabOrderComboBox).xy(3, rows).apply { rows += step }
val confirmTabCloseBox = Box.createHorizontalBox()
confirmTabCloseBox.add(JLabel("${I18n.getString("termora.settings.appearance.confirm-tab-close")}:"))
confirmTabCloseBox.add(Box.createHorizontalStrut(8))
@@ -404,6 +424,7 @@ class SettingsOptionsPane : OptionsPane() {
private val fontSizeTextField = IntSpinner(0, 9, 99)
private val terminalSetting get() = DatabaseManager.getInstance().terminal
private val selectCopyComboBox = YesOrNoComboBox()
private val rightClickComboBox = OutlineComboBox<String>()
private val autoCloseTabComboBox = YesOrNoComboBox()
private val floatingToolbarComboBox = YesOrNoComboBox()
private val hyperlinkComboBox = YesOrNoComboBox()
@@ -417,6 +438,12 @@ class SettingsOptionsPane : OptionsPane() {
}
}
rightClickComboBox.addItemListener {
if (it.stateChange == ItemEvent.SELECTED) {
terminalSetting.rightClick = rightClickComboBox.selectedItem as String
}
}
fallbackFontComboBox.addItemListener {
if (it.stateChange == ItemEvent.SELECTED) {
terminalSetting.fallbackFont = fallbackFontComboBox.selectedItem as String
@@ -518,6 +545,11 @@ class SettingsOptionsPane : OptionsPane() {
fontSizeTextField.value = terminalSetting.fontSize
maxRowsTextField.value = terminalSetting.maxRows
rightClickComboBox.addItem("Copy")
rightClickComboBox.addItem("CopyAndPaste")
rightClickComboBox.addItem("Nothing")
rightClickComboBox.selectedItem = terminalSetting.rightClick
cursorStyleComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
@@ -532,6 +564,26 @@ class SettingsOptionsPane : OptionsPane() {
}
}
rightClickComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component {
var text = value?.toString()
if (value == "Copy") {
text = I18n.getString("termora.settings.terminal.right-click.copy")
} else if (value == "CopyAndPaste") {
text = I18n.getString("termora.settings.terminal.right-click.copy-and-paste")
}else if (value == "Nothing") {
text = I18n.getString("termora.settings.terminal.right-click.nothing")
}
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
}
}
cursorStyleComboBox.addItem(CursorStyle.Block)
cursorStyleComboBox.addItem(CursorStyle.Bar)
cursorStyleComboBox.addItem(CursorStyle.Underline)
@@ -595,7 +647,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, left:pref, $FORM_MARGIN, pref, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
val beepBtn = JButton(Icons.run)
@@ -624,6 +676,8 @@ class SettingsOptionsPane : OptionsPane() {
.add(hyperlinkComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.terminal.select-copy")}:").xy(1, rows)
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.terminal.right-click")}:").xy(1, rows)
.add(rightClickComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
.add(cursorStyleComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.terminal.cursor-blink")}:").xy(1, rows)

View File

@@ -0,0 +1,7 @@
package app.termora
internal enum class TabOrder {
Hide,
AsNeed,
Always,
}

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)) {
val tab = tabs[index]
// 询问是否可以关闭
if (disposable) {
// 如果开启了关闭确认,那么直接询问用户
if (appearance.confirmTabClose) {
if (OptionPane.showConfirmDialog(
windowScope.window,
I18n.getString("termora.tabbed.tab.close-prompt"),
messageType = JOptionPane.QUESTION_MESSAGE,
optionType = JOptionPane.OK_CANCEL_OPTION
) != JOptionPane.OK_OPTION
) {
// 如果是重连接,那么直接关闭不进行任何形式的询问
if (reconnect.not()) {
// 如果开启了关闭确认,那么直接询问用户
if (appearance.confirmTabClose) {
if (OptionPane.showConfirmDialog(
windowScope.window,
I18n.getString("termora.tabbed.tab.close-prompt"),
messageType = JOptionPane.QUESTION_MESSAGE,
optionType = JOptionPane.OK_CANCEL_OPTION
) != JOptionPane.OK_OPTION
) {
return
}
} else if (!tab.willBeClose()) { // 如果没有开启则询问用户
return
}
} else if (!tab.willBeClose()) { // 如果没有开启则询问用户
return
}
}
@@ -233,7 +236,7 @@ class TerminalTabbed(
if (tab is HostTerminalTab) {
actionManager
.getAction(OpenHostAction.OPEN_HOST)
.actionPerformed(OpenHostActionEvent(this, tab.host, evt))
.actionPerformed(OpenHostActionEvent(this, tab.host, evt, tabIndex + 1))
}
}
@@ -337,13 +340,7 @@ class TerminalTabbed(
val c = tab.getJComponent()
val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
tabbedPane.insertTab(
title,
tab.getIcon(),
c,
StringUtils.EMPTY,
index
)
tabbedPane.insertTab(title, tab.getIcon(), c, StringUtils.EMPTY, index)
// 设置标题
c.putClientProperty(titleProperty, title)
@@ -367,6 +364,10 @@ class TerminalTabbed(
}
}
override fun indexOfTerminalTab(tab: TerminalTab): Int {
return tabbedPane.indexOfComponent(tab.getJComponent())
}
private inner class SwitchFindEverywhereResult(
private val title: String,
private val icon: Icon?,
@@ -453,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) {
if (tabs[i] == tab) {
removeTabAt(i, disposable)
removeTabAt(i, disposable, reconnect)
break
}
}

View File

@@ -6,6 +6,7 @@ interface TerminalTabbedManager {
fun getSelectedTerminalTab(): TerminalTab?
fun getTerminalTabs(): List<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 indexOfTerminalTab(tab: TerminalTab): Int
}

View File

@@ -2,6 +2,7 @@ package app.termora
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.plugin.internal.extension.DynamicExtensionHandler
import app.termora.tree.NewHostTree
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.extras.components.FlatToolBar
@@ -9,15 +10,14 @@ import com.formdev.flatlaf.util.SystemInfo
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.Font
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter
import java.awt.event.*
import javax.swing.*
import javax.swing.tree.TreePath
import kotlin.math.max
class TermoraFencePanel(
private val ws: WindowScope,
private val terminalTabbed: TerminalTabbed,
private val tabbed: FlatTabbedPane,
private val moveMouseAdapter: MouseAdapter,
@@ -72,10 +72,12 @@ class TermoraFencePanel(
leftTreePanel.addComponentListener(object : ComponentAdapter() {
override fun componentHidden(e: ComponentEvent) {
toolbar.isVisible = true
enableManager.setFlag("Termora.Fence.colspan", true)
}
override fun componentShown(e: ComponentEvent) {
toolbar.isVisible = false
enableManager.setFlag("Termora.Fence.colspan", false)
}
})
@@ -86,6 +88,50 @@ class TermoraFencePanel(
toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK
), "toggle"
)
splitPane.addPropertyChangeListener("dividerLocation") {
if (leftTreePanel.isVisible)
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
}
if (enableManager.getFlag("Termora.Fence.colspan", false)) {
toggle()
}
DynamicExtensionHandler.getInstance()
.register(TerminalTabbedContextMenuExtension::class.java, object : TerminalTabbedContextMenuExtension {
override fun createJMenuItem(
windowScope: WindowScope,
tab: TerminalTab
): JMenuItem {
if (windowScope != ws) throw UnsupportedOperationException()
if (tab !is HostTerminalTab) throw UnsupportedOperationException()
if (tab.host.isTemporary) throw UnsupportedOperationException()
if (tab.host.id == "local") throw UnsupportedOperationException()
val item = JMenuItem(I18n.getString("termora.tabbed.contextmenu.select-host"))
item.addActionListener(object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
val tree = getHostTree()
for (node in tree.simpleTreeModel.root.getAllChildren()) {
if (node.id == tab.host.id) {
tree.selectionPath = TreePath(tree.simpleTreeModel.getPathToRoot(node))
tree.requestFocusInWindow()
break
}
}
}
})
return item
}
override fun ordered(): Long {
return Long.MAX_VALUE
}
}).let { Disposer.register(this, it) }
}
private inner class LeftTreePanel : JPanel(BorderLayout()), Disposable {
@@ -144,19 +190,19 @@ class TermoraFencePanel(
}
override fun actionPerformed(evt: AnActionEvent) {
if (leftTreePanel.isVisible) dividerLocation = splitPane.dividerLocation
leftTreePanel.isVisible = leftTreePanel.isVisible.not()
if (leftTreePanel.isVisible) splitPane.dividerLocation = dividerLocation
toggle()
}
}
}
override fun dispose() {
if (leftTreePanel.isVisible)
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
private fun toggle() {
if (leftTreePanel.isVisible) dividerLocation = splitPane.dividerLocation
leftTreePanel.isVisible = leftTreePanel.isVisible.not()
if (leftTreePanel.isVisible) splitPane.dividerLocation = dividerLocation
mySplitPane.doLayout()
}
fun getHostTree(): NewHostTree {
return leftTreePanel.hostTree
}

View File

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

View File

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

View File

@@ -170,16 +170,18 @@ class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProv
filterableTreeModel.addFilter(object : Filter {
override fun filter(node: Any): Boolean {
val text = searchTextField.text
val text = searchTextField.text.trim()
if (text.isBlank()) return true
if (node !is HostTreeNode) return false
if (node is TeamTreeNode || node.id == "0") return true
return node.host.name.contains(text) || node.host.host.contains(text)
|| node.host.username.contains(text)
return node.host.name.contains(text, ignoreCase = true)
|| 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 {
return searchTextField.text.isNotBlank()
return searchTextField.text.trim().isNotBlank()
}
})
@@ -224,7 +226,7 @@ class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProv
override fun getTitle(): String {
return I18n.getString("termora.title")
return StringUtils.EMPTY
}
override fun getIcon(): Icon {

View File

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

View File

@@ -1,6 +1,7 @@
package app.termora.account
import app.termora.*
import app.termora.Application.ohMyJson
import app.termora.OptionsPane.Companion.FORM_MARGIN
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
@@ -8,21 +9,36 @@ import app.termora.database.DatabaseManager
import app.termora.plugin.internal.extension.DynamicExtensionHandler
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import com.sun.net.httpserver.HttpServer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.swing.Swing
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.jdesktop.swingx.JXBusyLabel
import org.jdesktop.swingx.JXHyperlink
import org.slf4j.LoggerFactory
import java.awt.BorderLayout
import java.awt.CardLayout
import java.net.InetSocketAddress
import java.net.URI
import java.net.URLEncoder
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import javax.swing.*
import kotlin.time.Duration.Companion.milliseconds
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 databaseManager get() = DatabaseManager.getInstance()
@@ -30,18 +46,31 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
private val accountProperties get() = AccountProperties.getInstance()
private val userInfoPanel = JPanel(BorderLayout())
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 {
initView()
initEvents()
}
private fun initView() {
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() {
// 服务器签名发生变更
@@ -99,11 +128,7 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
planBox.add(Box.createHorizontalStrut(16))
val upgrade = JXHyperlink(object : AnAction(I18n.getString("termora.settings.account.upgrade")) {
override fun actionPerformed(evt: AnActionEvent) {
if (I18n.isChinaMainland()) {
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()}"))
}
Application.browse(URI.create("${accountManager.getServer()}/v1/client/redirect?to=upgrade&version=${Application.getVersion()}"))
}
})
upgrade.isFocusable = false
@@ -145,6 +170,29 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
.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 {
val actionBox = Box.createHorizontalBox()
actionBox.add(Box.createHorizontalGlue())
@@ -219,11 +267,139 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
return actionBox
}
private fun showLoginPanel() {
refreshLoginPanel()
busyLabel.isBusy = true
cardLayout.show(contentPanel, "Login")
}
private fun onLogin() {
httpServer?.stop(0)
val dialog = LoginServerDialog(owner)
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() {
userInfoPanel.removeAll()
userInfoPanel.add(getCenterComponent(), BorderLayout.CENTER)
@@ -231,6 +407,13 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
userInfoPanel.repaint()
}
private fun refreshLoginPanel() {
loginPanel.removeAll()
loginPanel.add(getLoginComponent(), BorderLayout.CENTER)
loginPanel.revalidate()
loginPanel.repaint()
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.user
}

View File

@@ -9,12 +9,6 @@ import app.termora.database.DatabaseManager
import com.formdev.flatlaf.FlatClientProperties
import com.jgoodies.forms.builder.FormBuilder
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.jdesktop.swingx.JXHyperlink
import org.slf4j.LoggerFactory
@@ -24,12 +18,10 @@ import java.awt.Window
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import java.net.URI
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.*
import javax.swing.event.ListDataEvent
import javax.swing.event.ListDataListener
import kotlin.math.max
import kotlin.time.Duration.Companion.milliseconds
class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
companion object {
@@ -37,18 +29,14 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
}
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 cancelAction = super.createCancelAction()
private val cancelButton = super.createJButtonForAction(cancelAction)
private val isLoggingIn = AtomicBoolean(false)
private val singaporeServer =
Server(I18n.getString("termora.settings.account.server-singapore"), "https://account.termora.app")
private val chinaServer =
Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn")
private val serverManager get() = ServerManager.getInstance()
var server: Server? = null
init {
isModal = true
@@ -60,12 +48,10 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
size = Dimension(max(preferredSize.width, UIManager.getInt("Dialog.width") - 250), preferredSize.height)
setLocationRelativeTo(owner)
passwordField.putClientProperty(FlatClientProperties.STYLE, mapOf("showCapsLock" to true))
addWindowListener(object : WindowAdapter() {
override fun windowOpened(e: WindowEvent) {
removeWindowListener(this)
usernameTextField.requestFocus()
}
})
}
@@ -73,7 +59,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
override fun createCenterPanel(): JComponent {
val layout = FormLayout(
"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
@@ -90,7 +76,6 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
serverComboBox.addItem(Server(server.name, server.server))
}
mfaTextField.placeholderText = I18n.getString("termora.settings.account.mfa")
serverComboBox.renderer = object : DefaultListCellRenderer() {
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() {
if (serverComboBox.selectedItem == singaporeServer || serverComboBox.selectedItem == chinaServer || serverComboBox.itemCount < 1) {
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")
.add("${I18n.getString("termora.settings.account.server")}:").xy(1, rows)
.add(serverComboBox).xy(3, rows)
.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()
}
@@ -315,95 +256,21 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
}
override fun doOKAction() {
if (isLoggingIn.get()) return
val server = serverComboBox.selectedItem as? Server
server = serverComboBox.selectedItem as? Server
if (server == null) {
serverComboBox.outline = FlatClientProperties.OUTLINE_ERROR
serverComboBox.requestFocusInWindow()
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()
}
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() {
if (isLoggingIn.get()) return
server = null
super.doCancelAction()
}
}

View File

@@ -67,7 +67,11 @@ class PullService private constructor() : SyncService(), Disposable, Application
private var lastChangeHash = StringUtils.EMPTY
private fun pullChanges() {
if (isFreePlan) return
if (accountManager.isLocally()) {
return
}
val hash: String
try {
@@ -126,7 +130,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
while (true) {
val request = Request.Builder()
.get()
.url("${accountManager.getServer()}/v1/data/changes?since=${since}&after=${after}&limit=${limit}")
.url("${accountManager.getServer()}/v1/data/changes?since=${nextSince}&after=${after}&limit=${limit}")
.build()
val text = AccountHttp.execute(request = request)
val response = ohMyJson.decodeFromString<DataChangesResponse>(text)

View File

@@ -1,7 +1,10 @@
package app.termora.account
import app.termora.*
import app.termora.AES
import app.termora.Application.ohMyJson
import app.termora.ApplicationScope
import app.termora.PBKDF2
import app.termora.RSA
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
@@ -9,7 +12,6 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.apache.commons.codec.binary.Base64
import org.apache.commons.codec.digest.DigestUtils
import java.util.concurrent.atomic.AtomicBoolean
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()) {
throw IllegalStateException("Already logged in")
@@ -39,25 +41,25 @@ class ServerManager private constructor() {
}
try {
doLogin(server, username, password, mfa)
doLogin(server, refreshToken, password)
} finally {
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)
// call login
val loginResponse = callLogin(serverInfo, server, username, password, mfa)
val loginResponse = callToken(server, refreshToken)
// call me
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 privateKeySecureIv = PBKDF2.hash(salt, password.toCharArray(), 1024, 128)
val privateKeyEncoded = AES.CBC.decrypt(
@@ -106,29 +108,19 @@ class ServerManager private constructor() {
return ohMyJson.decodeFromString<ServerInfo>(AccountHttp.execute(request = request))
}
private fun callLogin(
serverInfo: ServerInfo,
private fun callToken(
server: Server,
username: String,
password: String,
mfa: String
refreshToken: String,
): LoginResponse {
val passwordHex = DigestUtils.sha256Hex("${serverInfo.salt}:${username}:${password}")
val requestBody = ohMyJson.encodeToString(mapOf("email" to username, "password" to passwordHex, "mfa" to mfa))
val body = ohMyJson.encodeToString(mapOf("refreshToken" to refreshToken))
.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("${server.server}/v1/login")
.post(requestBody)
val request = Request.Builder().url("${server.server}/v1/token")
.header("Authorization", "Bearer $refreshToken")
.post(body)
.build()
val response = AccountHttp.client.newCall(request).execute()
val text = response.use { response.body.use { it?.string() } }
if (text == null) {
throw ResponseException(response.code, response)
}
val text = response.use { response.body.use { it.string() } }
if (response.isSuccessful.not()) {
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.lang3.ObjectUtils
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.transactions.transaction
import org.jetbrains.exposed.v1.jdbc.update

View File

@@ -8,7 +8,7 @@ object DataProviders {
val Terminal = DataKey(app.termora.terminal.Terminal::class)
val TerminalWriter get() = DataKey.TerminalWriter
val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
internal val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
val TerminalTab = DataKey(app.termora.TerminalTab::class)
val TerminalTabbedManager = DataKey(app.termora.TerminalTabbedManager::class)

View File

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

View File

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

View File

@@ -20,6 +20,10 @@ class TerminalCopyAction : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
val terminalPanel = evt.getData(DataProviders.TerminalPanel) ?: return
val selectionModel = terminalPanel.terminal.getSelectionModel()
if (!selectionModel.hasSelection()) {
return
}
val text = terminalPanel.copy()
val systemClipboard = terminalPanel.toolkit.systemClipboard

View File

@@ -16,8 +16,8 @@ import app.termora.snippet.SnippetManager
import app.termora.terminal.CursorStyle
import org.apache.commons.io.FileUtils
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.eq
import org.jetbrains.exposed.v1.core.statements.StatementType
import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
@@ -666,6 +666,11 @@ class DatabaseManager private constructor() : Disposable {
*/
var selectCopy by BooleanPropertyDelegate(false)
/**
* 右键点击Copy、CopyAndPaste
*/
var rightClick by StringPropertyDelegate("Copy")
/**
* 光标样式
*/
@@ -716,6 +721,11 @@ class DatabaseManager private constructor() : Disposable {
*/
var layout by StringPropertyDelegate(TermoraLayout.Screen.name)
/**
* 标签序号
*/
var tabOrder by StringPropertyDelegate(TabOrder.Hide.name)
/**
* 跟随系统
*/

View File

@@ -101,6 +101,16 @@ internal class KeywordHighlightPaintListener private constructor() : TerminalPai
// -1 表示不使用高亮集
if (keywordHighlightSetId == "-1") return
try {
doFind(offset, count, terminal, keywordHighlightSetId)
} catch (e: Exception) {
if (log.isDebugEnabled) {
log.debug(e.message, e)
}
}
}
private fun doFind(offset: Int, count: Int, terminal: Terminal, keywordHighlightSetId: String) {
for (highlight in keywordHighlights) {
if (highlight.enabled.not()) continue
if (highlight.type != KeywordHighlightType.Highlight) continue
@@ -151,7 +161,6 @@ internal class KeywordHighlightPaintListener private constructor() : TerminalPai
}
}
}
override fun after(

View File

@@ -82,7 +82,7 @@ class NewKeywordHighlightDialog(
FlatClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON
)
matchCaseBtn.toolTipText = "Match case"
matchCaseBtn.toolTipText = I18n.getString("termora.match-case")
val box = FlatToolBar()

View File

@@ -27,12 +27,14 @@ class KeyboardInteractiveDialog(
isModal = true
isResizable = true
controlsVisible = false
title = I18n.getString("termora.new-host.title")
init()
pack()
size = Dimension(max(300, size.width), size.height)
// fix https://github.com/TermoraDev/termora/issues/1311
pack()
setLocationRelativeTo(null)
}

View File

@@ -30,6 +30,7 @@ class TerminalUserInteraction(
)
dialog.setLocationRelativeTo(owner)
dialog.title = instruction ?: name ?: "OTP"
dialog.title = StringUtils.defaultIfBlank(dialog.title, "OTP")
passwords[i] = dialog.getText()
if (passwords[i].isBlank()) {
break

View File

@@ -29,8 +29,10 @@ class KeymapPanel : JPanel(BorderLayout()) {
private val copyBtn = JButton(Icons.copy)
private val renameBtn = JButton(Icons.edit)
private val deleteBtn = JButton(Icons.delete)
private val infoBtn = JButton(Icons.questionMark)
private val database get() = DatabaseManager.getInstance()
private val allowKeyCodes = mutableSetOf<Int>()
private val owner get() = SwingUtilities.getWindowAncestor(this)
init {
initView()
@@ -89,8 +91,8 @@ class KeymapPanel : JPanel(BorderLayout()) {
box.add(copyBtn)
box.add(renameBtn)
box.add(deleteBtn)
box.add(infoBtn)
box.add(Box.createHorizontalGlue())
box.border = BorderFactory.createEmptyBorder(0, 0, 6, 0)
add(box, BorderLayout.NORTH)
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 {
val keymap = getCurrentKeymap()
if (keymap != null) {

View File

@@ -262,8 +262,8 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
OptionPane.openFileInFolder(
SwingUtilities.getWindowAncestor(this),
file, I18n.getString("termora.settings.sync.export-done-open-folder"),
I18n.getString("termora.settings.sync.export-done")
file, I18n.getString("termora.keymgr.export-done-open-folder"),
I18n.getString("termora.keymgr.export-done")
)
}
@@ -287,6 +287,9 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
typeComboBox.addItem("RSA")
typeComboBox.addItem("ED25519")
typeComboBox.addItem("ECDSA-SHA2-NISTP256")
typeComboBox.addItem("ECDSA-SHA2-NISTP384")
typeComboBox.addItem("ECDSA-SHA2-NISTP521")
// 默认 RSA
lengthComboBox.addItem(1024)
@@ -396,6 +399,12 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
lengthComboBox.addItem(1024 * 4)
lengthComboBox.addItem(1024 * 8)
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()
}
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() {
if (ohKeyPair == OhKeyPair.empty) {
@@ -422,9 +442,7 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
return
}
val keyType = if (typeComboBox.selectedItem == "RSA")
KeyPairProvider.SSH_RSA else KeyPairProvider.SSH_ED25519
val keyPair = KeyUtils.generateKeyPair(keyType, lengthComboBox.selectedItem as Int)
val keyPair = genKeyPair()
ohKeyPair = OhKeyPair(
id = randomUUID(),
name = nameTextField.text,

View File

@@ -2,6 +2,7 @@ package app.termora.keymgr
import app.termora.AES.decodeBase64
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.session.SessionContext
import org.apache.sshd.common.util.security.eddsa.Ed25519PublicKeyDecoder
@@ -25,6 +26,8 @@ class OhKeyPairKeyPairProvider(private val id: String) : AbstractResourceKeyPair
when (ohKeyPair.type) {
"RSA" -> RSA.generatePublic(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")
}
} as PublicKey
@@ -33,6 +36,8 @@ class OhKeyPairKeyPairProvider(private val id: String) : AbstractResourceKeyPair
when (ohKeyPair.type) {
"RSA" -> RSA.generatePrivate(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")
}
} as PrivateKey

View File

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

View File

@@ -31,7 +31,7 @@ class LocalTerminalTab(windowScope: WindowScope, host: Host) :
}
override fun getIcon(): Icon {
return if (unread) Icons.terminalUnread else Icons.terminal
return Icons.terminal
}
override fun willBeClose(): Boolean {
@@ -62,6 +62,9 @@ class LocalTerminalTab(windowScope: WindowScope, host: Host) :
) == JOptionPane.OK_OPTION
}
override fun createReconnectTerminalTab(): TerminalTab {
return LocalTerminalTab(windowScope, host)
}
private fun getPtyProcessConnector(): PtyProcessConnector? {
var p = getPtyConnector() as PtyConnector?

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)
} else {
@@ -227,6 +234,13 @@ class PluginPanel(val descriptor: PluginPluginDescriptor) : JPanel(), Disposable
}
}, 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) {
installed.add(descriptor.id)

View File

@@ -10,6 +10,8 @@ import kotlinx.coroutines.launch
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.Strings
import org.apache.commons.lang3.SystemUtils
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
import java.net.URI
@@ -64,7 +66,15 @@ internal class RDPProtocolProvider private constructor() : GenericProtocolProvid
}
val sb = StringBuilder()
sb.append("full address:s:").append(host.host).append(':').append(host.port).appendLine()
sb.append("full address:s:")
if (SystemUtils.IS_OS_WINDOWS && Strings.CI.contains(host.host, ":")) {
var newHost = Strings.CI.removeStart(host.host, "[")
newHost = Strings.CI.removeEnd(newHost, "]")
sb.append('[').append(newHost).append(']')
} else {
sb.append(host.host)
}
sb.append(':').append(host.port).appendLine()
sb.append("username:s:").append(host.username).appendLine()
val desktop = host.options.extras["desktop"]
if (desktop.isNullOrBlank().not()) {

View File

@@ -103,9 +103,20 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
commands.add("Compression=yes")
// HostKeyAlgorithms 让 SFTP 命令的顺序和 sshd 的一致 这样可以避免 known_hosts 文件不一致问题
val hostKeyAlgorithms = ClientBuilder.setUpDefaultSignatureFactories(true).joinToString(",") { it.name }
val hostKeyAlgorithms = ClientBuilder.setUpDefaultSignatureFactories(true).map { it.name }.toMutableList()
val localHostKeyAlgorithms = getLocalSSHHostKeyAlgorithms()
// 删除本地 ssh 不存在的算法
hostKeyAlgorithms.removeIf { localHostKeyAlgorithms.contains(it).not() }
// 把本地支持的再添加进去
for (algorithm in localHostKeyAlgorithms) {
if (hostKeyAlgorithms.contains(algorithm).not()) {
hostKeyAlgorithms.add(algorithm)
}
}
commands.add("-o")
commands.add("HostKeyAlgorithms=${hostKeyAlgorithms}")
commands.add("HostKeyAlgorithms=${hostKeyAlgorithms.joinToString(",")}")
// 不使用配置文件
commands.add("-F")
@@ -143,6 +154,15 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
return ptyConnector
}
private fun getLocalSSHHostKeyAlgorithms(): Set<String> {
val pb = ProcessBuilder("ssh", "-Q", "key")
val process = pb.start()
if (process.waitFor() != 0) {
return emptySet()
}
return String(process.inputStream.readAllBytes()).lines().filter { it.isNotBlank() }.toSet()
}
private fun setAuthentication(commands: MutableList<String>, host: Host) {
// 如果通过公钥连接
if (host.authentication.type == AuthenticationType.PublicKey) {
@@ -185,6 +205,10 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
return Icons.fileFormat
}
override fun createReconnectTerminalTab(): TerminalTab {
return SFTPPtyTerminalTab(windowScope, host)
}
override fun sendStartupCommand(ptyConnector: PtyConnector, bytes: ByteArray) {
// Nothing
}

View File

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

View File

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

View File

@@ -7,9 +7,8 @@ import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
import app.termora.database.DatabaseManager
import app.termora.keymap.KeyShortcut
import app.termora.keymap.KeymapManager
import app.termora.terminal.ControlCharacters
import app.termora.terminal.DataKey
import app.termora.terminal.PtyConnector
import app.termora.plugin.internal.telnet.TelnetHostOptionsPane
import app.termora.terminal.*
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.sync.Mutex
@@ -20,6 +19,7 @@ import org.apache.sshd.client.session.ClientSession
import org.apache.sshd.common.future.CloseFuture
import org.apache.sshd.common.future.SshFutureListener
import org.slf4j.LoggerFactory
import java.awt.event.KeyEvent
import java.nio.charset.StandardCharsets
import javax.swing.Icon
import javax.swing.JComponent
@@ -54,6 +54,10 @@ class SSHTerminalTab(
return mutex.isLocked.not()
}
override fun createReconnectTerminalTab(): TerminalTab {
return SSHTerminalTab(windowScope, host)
}
override suspend fun openPtyConnector(): PtyConnector {
if (mutex.tryLock()) {
try {
@@ -79,15 +83,14 @@ class SSHTerminalTab(
}
val loading = coroutineScope.launch(Dispatchers.Swing) {
val braille = "⡿⣟⣯⣷⣾⣽⣻⢿".reversed().toCharArray()
// val braille = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏".toCharArray()
var c = 0
while (isActive) {
if (++c > 6) c = 1
terminal.write("${ControlCharacters.ESC}[1;32m")
terminal.write(".".repeat(c))
terminal.write(" ".repeat(6 - c))
terminal.write("${ControlCharacters.ESC}[0m")
delay(350.milliseconds)
terminal.write("${ControlCharacters.BS}".repeat(6))
if (++c >= braille.size) c = 0
terminal.write("${braille[c]}")
delay(100.milliseconds)
terminal.write("${ControlCharacters.BS}")
}
}
@@ -107,7 +110,18 @@ class SSHTerminalTab(
// clear screen
terminal.clearScreen()
// show cursor
terminalModel.setData(DataKey.Companion.ShowCursor, true)
terminalModel.setData(DataKey.ShowCursor, true)
val encoder = terminal.getKeyEncoder()
if (encoder is KeyEncoderImpl) {
val backspace = host.options.extras["backspace"]
if (backspace == TelnetHostOptionsPane.Backspace.Backspace.name) {
encoder.putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_BACK_SPACE), String(byteArrayOf(0x08)))
} else if (backspace == TelnetHostOptionsPane.Backspace.VT220.name) {
encoder.putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_BACK_SPACE), "${ControlCharacters.ESC}[3~")
}
}
}
return ptyConnectorFactory.decorate(
@@ -211,17 +225,6 @@ class SSHTerminalTab(
return super.getData(dataKey)
}
override fun reconnect() {
stop()
// 重新连接时就等于重新打开了一个标签handler 重置
handler.client = null
handler.session = null
handler.client = null
start()
}
override fun stop() {
if (mutex.tryLock()) {
try {
@@ -234,7 +237,7 @@ class SSHTerminalTab(
}
override fun getIcon(): Icon {
return if (unread) Icons.terminalUnread else Icons.terminal
return Icons.terminal
}
override fun beforeClose() {

View File

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

View File

@@ -0,0 +1,14 @@
package app.termora.plugin.internal.ssh
import org.apache.sshd.agent.local.ChannelAgentForwardingFactory
import org.apache.sshd.common.FactoryManager
import org.apache.sshd.common.channel.ChannelFactory
import org.eclipse.jgit.internal.transport.sshd.agent.JGitSshAgentFactory
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory
import java.io.File
internal class SshAgentFactory(factory: ConnectorFactory, homeDir: File?) : JGitSshAgentFactory(factory, homeDir) {
override fun getChannelForwardingFactories(manager: FactoryManager?): List<ChannelFactory> {
return listOf(ChannelAgentForwardingFactory.OPENSSH, ChannelAgentForwardingFactory.IETF)
}
}

View File

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

View File

@@ -1,9 +1,6 @@
package app.termora.plugin.internal.telnet
import app.termora.Host
import app.termora.ProxyType
import app.termora.PtyHostTerminalTab
import app.termora.WindowScope
import app.termora.*
import app.termora.terminal.ControlCharacters
import app.termora.terminal.KeyEncoderImpl
import app.termora.terminal.PtyConnector
@@ -71,5 +68,9 @@ class TelnetTerminalTab(
return ptyConnectorFactory.decorate(TelnetStreamPtyConnector(telnet, telnet.charset, characterMode))
}
override fun createReconnectTerminalTab(): TerminalTab {
return TelnetTerminalTab(windowScope, host)
}
}

View File

@@ -1,8 +1,11 @@
package app.termora.plugin.internal.updater
import app.termora.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext
import org.semver4j.Semver
import org.slf4j.LoggerFactory
import java.awt.KeyboardFocusManager
@@ -15,12 +18,12 @@ internal class MyApplicationRunnerExtension private constructor() : ApplicationR
private val log = LoggerFactory.getLogger(MyApplicationRunnerExtension::class.java)
}
private val disabledUpdater get() = Application.getLayout() == AppLayout.Appx
private val disabledUpdater get() = Application.getLayout() == AppLayout.Appx || Application.getLayout() == AppLayout.AppStore
private val updaterManager get() = UpdaterManager.getInstance()
override fun ready() {
swingCoroutineScope.launch {
swingCoroutineScope.launch(Dispatchers.IO) {
try {
delay(3.seconds)
scheduleUpdate()
@@ -31,7 +34,7 @@ internal class MyApplicationRunnerExtension private constructor() : ApplicationR
}
private fun scheduleUpdate() {
private suspend fun scheduleUpdate() {
if (disabledUpdater) return
val latestVersion = updaterManager.fetchLatestVersion()
@@ -45,13 +48,15 @@ internal class MyApplicationRunnerExtension private constructor() : ApplicationR
return
}
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
?: TermoraFrameManager.getInstance().getWindows().firstOrNull()
if (owner == null) return
val dialog = UpdaterDialog(owner, latestVersion)
dialog.isModal = true
dialog.isVisible = true
withContext(Dispatchers.Swing) {
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
?: TermoraFrameManager.getInstance().getWindows().firstOrNull()
if (owner != null) {
val dialog = UpdaterDialog(owner, latestVersion)
dialog.isModal = true
dialog.isVisible = true
}
}
}
}

View File

@@ -1,9 +1,6 @@
package app.termora.plugin.internal.wsl
import app.termora.Host
import app.termora.PtyConnectorFactory
import app.termora.PtyHostTerminalTab
import app.termora.WindowScope
import app.termora.*
import app.termora.terminal.PtyConnector
import org.apache.commons.io.Charsets
import org.apache.commons.io.FileUtils
@@ -51,6 +48,10 @@ class WSLHostTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
return ptyConnector
}
override fun createReconnectTerminalTab(): TerminalTab {
return WSLHostTerminalTab(windowScope, host)
}
override fun sendStartupCommand(ptyConnector: PtyConnector, bytes: ByteArray) {
// Nothing

View File

@@ -332,15 +332,22 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
var top = sr.getOrElse(0) { 1 }
var bottom = sr.getOrElse(1) { terminalModel.getRows() }
if (bottom <= top || top < 1) {
// ";r" https://vt100.net/docs/vt510-rm/DECSTBM.html
if (sr.size == 1 && args.startsWith(';')) {
bottom = top
top = 1
}
if (bottom <= top) {
if (log.isWarnEnabled) {
log.warn("Set Scrolling Region Error. top: $top , bottom: $bottom")
}
top = 1
bottom = terminalModel.getRows()
}
top = max(1, top)
bottom = min(terminalModel.getRows(), bottom)
// 设置滚动区域
terminal.getTerminalModel().setData(
DataKey.ScrollingRegion,
@@ -519,9 +526,13 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
val writer = terminalModel.getData(DataKey.TerminalWriter)
// VT102_RESPONSE
val bytes = "${ControlCharacters.ESC}[?6c".toByteArray(writer.getCharset())
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
if (args.startsWith('>')) {
val bytes = "${ControlCharacters.ESC}[>0;276;0c".toByteArray(writer.getCharset())
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
} else {
val bytes = "${ControlCharacters.ESC}[?1;2c".toByteArray(writer.getCharset())
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
}
}

View File

@@ -1,16 +1,20 @@
package app.termora.terminal
import org.slf4j.LoggerFactory
import java.util.*
import kotlin.math.max
import kotlin.math.min
open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
private var startPosition = Position.unknown
private var endPosition = Position.unknown
private var block = false
private val document = terminal.getDocument()
internal companion object {
private val log = LoggerFactory.getLogger(SelectionModelImpl::class.java)
fun isPointInsideArea(start: Position, end: Position, x: Int, y: Int, cols: Int): Boolean {
val top = min(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 {
if (index - 1 <= 0) {
index = 0
index = kinds.size - 1
} else {
index--
}

View File

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

Some files were not shown because too many files have changed in this diff Show More