Compare commits

..

293 Commits

Author SHA1 Message Date
hstyi
1c90fb4e18 release: 2.0.0-beta.4 2025-07-04 14:35:50 +08:00
dependabot[bot]
c1f1d5185e chore(deps): bump com.fazecast:jSerialComm from 2.11.0 to 2.11.2
Bumps [com.fazecast:jSerialComm](https://github.com/Fazecast/jSerialComm) from 2.11.0 to 2.11.2.
- [Release notes](https://github.com/Fazecast/jSerialComm/releases)
- [Commits](https://github.com/Fazecast/jSerialComm/compare/v2.11.0...v2.11.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-04 12:20:45 +08:00
dependabot[bot]
aa4863712d chore(deps): bump org.jetbrains.kotlinx:kotlinx-serialization-json
Bumps [org.jetbrains.kotlinx:kotlinx-serialization-json](https://github.com/Kotlin/kotlinx.serialization) from 1.8.1 to 1.9.0.
- [Release notes](https://github.com/Kotlin/kotlinx.serialization/releases)
- [Changelog](https://github.com/Kotlin/kotlinx.serialization/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Kotlin/kotlinx.serialization/compare/v1.8.1...v1.9.0)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlinx:kotlinx-serialization-json
  dependency-version: 1.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-04 12:20:34 +08:00
dependabot[bot]
247640f2e5 chore(deps): bump org.dom4j:dom4j from 2.1.4 to 2.2.0
Bumps [org.dom4j:dom4j](https://github.com/dom4j/dom4j) from 2.1.4 to 2.2.0.
- [Release notes](https://github.com/dom4j/dom4j/releases)
- [Commits](https://github.com/dom4j/dom4j/compare/version-2.1.4...version/2.2.0)

---
updated-dependencies:
- dependency-name: org.dom4j:dom4j
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-04 12:20:24 +08:00
dependabot[bot]
5b1f803fa8 chore(deps): bump com.aliyun.oss:aliyun-sdk-oss from 3.18.2 to 3.18.3
Bumps [com.aliyun.oss:aliyun-sdk-oss](https://github.com/aliyun/aliyun-oss-java-sdk) from 3.18.2 to 3.18.3.
- [Release notes](https://github.com/aliyun/aliyun-oss-java-sdk/releases)
- [Commits](https://github.com/aliyun/aliyun-oss-java-sdk/commits)

---
updated-dependencies:
- dependency-name: com.aliyun.oss:aliyun-sdk-oss
  dependency-version: 3.18.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-04 12:20:15 +08:00
hstyi
accf590c17 feat: support fallback font 2025-07-04 12:19:56 +08:00
hstyi
19fbeab817 fix: shortcut keys cannot be saved 2025-07-04 12:13:01 +08:00
hstyi
a785ab4680 chore: disable opengl 2025-07-04 09:58:08 +08:00
hstyi
5ee23cb379 fix: cursor style not working 2025-07-04 09:14:58 +08:00
hstyi
145d2de001 chore: tiny images 2025-07-03 15:54:38 +08:00
hstyi
8d3f5fe622 chore: README 2025-07-03 15:47:19 +08:00
hstyi
9ce4a88041 feat: use extension for floating toolbar 2025-07-03 12:00:02 +08:00
dependabot[bot]
c0ecc9fa7d chore(deps): bump exposed from 1.0.0-beta-2 to 1.0.0-beta-3
Bumps `exposed` from 1.0.0-beta-2 to 1.0.0-beta-3.

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 11:38:43 +08:00
hstyi
cb33a4468a chore: show close button 2025-07-03 10:48:15 +08:00
hstyi
168c4c5c64 chore: improve team sync 2025-07-03 08:49:00 +08:00
hstyi
9916edbd13 feat: support custom layout 2025-07-02 12:15:28 +08:00
hstyi
ab6b6a2127 chore: improve Windows task icon 2025-07-01 13:42:50 +08:00
hstyi
c45f5f4c92 fix: plugins compatibility 2025-07-01 13:21:53 +08:00
hstyi
92ee2d72f2 chore: only show flags on macOS 2025-07-01 12:03:27 +08:00
hstyi
a4364bcd6a release: 2.0.0-beta.3 2025-07-01 11:00:55 +08:00
hstyi
d0827c3b0c chore: improve connect-with 2025-07-01 10:52:45 +08:00
hstyi
036a04b0b3 feat: support SMB 2025-07-01 10:49:27 +08:00
hstyi
eee016c643 chore: supports retaining file modification date 2025-07-01 10:46:17 +08:00
hstyi
472bf6e81f feat: support login scripts 2025-07-01 10:40:06 +08:00
hstyi
21229e352f chore: do not refresh during installation 2025-06-30 17:34:02 +08:00
hstyi
1138f48a6e fix: host deletion query error 2025-06-30 17:18:05 +08:00
hstyi
f044e0480e chore: password show caps lock 2025-06-30 17:16:52 +08:00
hstyi
7047f17783 fix: quick open transfer failure 2025-06-30 14:33:55 +08:00
dependabot[bot]
9308f15abb chore(deps): bump com.qcloud:cos_api from 5.6.245 to 5.6.247
Bumps [com.qcloud:cos_api](https://github.com/tencentyun/cos-java-sdk-v5) from 5.6.245 to 5.6.247.
- [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.247
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 14:29:41 +08:00
dependabot[bot]
b2672f11fc chore(deps): bump org.testcontainers:testcontainers-bom
Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 1.21.2 to 1.21.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/1.21.2...1.21.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 14:29:29 +08:00
dependabot[bot]
f92c6586b2 chore(deps): bump org.semver4j:semver4j from 5.8.0 to 6.0.0
Bumps [org.semver4j:semver4j](https://github.com/semver4j/semver4j) from 5.8.0 to 6.0.0.
- [Release notes](https://github.com/semver4j/semver4j/releases)
- [Commits](https://github.com/semver4j/semver4j/compare/v5.8.0...v6.0.0)

---
updated-dependencies:
- dependency-name: org.semver4j:semver4j
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 14:29:20 +08:00
dependabot[bot]
69e07a9bd9 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.4 to 3.25.5.
- [Release notes](https://github.com/huaweicloud/huaweicloud-sdk-java-obs/releases)
- [Commits](https://github.com/huaweicloud/huaweicloud-sdk-java-obs/compare/v3.25.4...v3.25.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 14:29:07 +08:00
dependabot[bot]
cdec60fd25 chore(deps): bump org.xerial:sqlite-jdbc from 3.50.1.0 to 3.50.2.0
Bumps [org.xerial:sqlite-jdbc](https://github.com/xerial/sqlite-jdbc) from 3.50.1.0 to 3.50.2.0.
- [Release notes](https://github.com/xerial/sqlite-jdbc/releases)
- [Changelog](https://github.com/xerial/sqlite-jdbc/blob/master/CHANGELOG)
- [Commits](https://github.com/xerial/sqlite-jdbc/compare/3.50.1.0...3.50.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 14:28:47 +08:00
hstyi
7c30933794 fix: wsl reg exception 2025-06-30 13:36:35 +08:00
hstyi
b892d2fe13 release: 2.0.0-beta.2 2025-06-30 12:50:10 +08:00
hstyi
91ee463d41 fix: data migration not working 2025-06-30 12:43:31 +08:00
hstyi
169b66334c release: 2.0.0-beta.1 2025-06-30 10:02:04 +08:00
hstyi
729eb99730 fix: extension name retrieval failure 2025-06-30 09:40:40 +08:00
hstyi
b1e62952f5 fix: data sync delay 2025-06-30 00:36:10 +08:00
hstyi
e21e9f9ed9 fix: saved data causes tags to be lost 2025-06-30 00:36:01 +08:00
hstyi
885c0a6337 fix: tree root node not displaying 2025-06-30 00:12:57 +08:00
hstyi
09d837f5b8 chore: improve code 2025-06-29 13:45:07 +08:00
hstyi
b1e1f38b50 chore: improve plugin settings 2025-06-29 13:01:37 +08:00
hstyi
efa9613d26 fix: editor plugin popup 2025-06-29 12:37:44 +08:00
hstyi
287f6973f0 chore: improve editor 2025-06-29 12:12:30 +08:00
hstyi
70fc5e3228 chore: improve plugin restart prompt 2025-06-28 17:04:21 +08:00
hstyi
4bca15dbb0 fix: wsl reg 2025-06-28 17:04:12 +08:00
hstyi
ef2c57bb29 fix: geo class cast 2025-06-28 16:14:05 +08:00
hstyi
1135ecc5a3 chore: improve code 2025-06-28 15:55:40 +08:00
hstyi
e1b2e7b4db chore: upgrade geo version 2025-06-28 11:32:12 +08:00
hstyi
eec9154aeb chore: remove transfer Append 2025-06-28 11:30:17 +08:00
hstyi
fff2dd89c7 chore: improve geo 2025-06-28 11:13:26 +08:00
hstyi
54116a4bf5 feat: support Huawei OBS 2025-06-27 15:42:00 +08:00
hstyi
f28e785301 feat: support WebDAV 2025-06-27 12:18:01 +08:00
hstyi
39b9bba9cf feat: support tencent OSS 2025-06-27 10:36:43 +08:00
hstyi
d7120cabe0 chore: classes plugins 2025-06-26 18:18:27 +08:00
hstyi
007318dae3 feat: support tencent cos 2025-06-26 18:05:39 +08:00
hstyi
e25bd485ac chore: detection installation type 2025-06-26 15:40:40 +08:00
hstyi
7ba8e177b1 chore: account MFA 2025-06-26 10:07:06 +08:00
hstyi
17082c5fb8 chore: transfer supports custom type 2025-06-26 09:46:25 +08:00
hstyi
00dfb4ce39 chore: support s3 proxy 2025-06-25 18:12:50 +08:00
hstyi
cee0c863f8 feat: support S3 transfer protocol 2025-06-25 16:47:56 +08:00
hstyi
a2a02c0bad chore: improve wsl 2025-06-25 12:05:24 +08:00
hstyi
891688d0ca fix: displaying WSL on other platforms 2025-06-25 12:05:24 +08:00
hstyi
3d47840aa8 feat: WSL support on Windows 2025-06-25 12:05:24 +08:00
hstyi
01d0f9d4bd chore: improve account 2025-06-24 17:02:20 +08:00
hstyi
1c8abf9cba chore: improve badge 2025-06-24 15:50:23 +08:00
hstyi
efd01da6f1 chore: transfer support contextmenu 2025-06-24 12:15:41 +08:00
dependabot[bot]
c929a794d5 chore(deps): bump org.semver4j:semver4j from 5.7.1 to 5.8.0
Bumps [org.semver4j:semver4j](https://github.com/semver4j/semver4j) from 5.7.1 to 5.8.0.
- [Release notes](https://github.com/semver4j/semver4j/releases)
- [Commits](https://github.com/semver4j/semver4j/compare/v5.7.1...v5.8.0)

---
updated-dependencies:
- dependency-name: org.semver4j:semver4j
  dependency-version: 5.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-24 12:15:18 +08:00
dependabot[bot]
85b2f222f4 chore(deps): bump kotlin from 2.1.21 to 2.2.0
Bumps `kotlin` from 2.1.21 to 2.2.0.

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-24 11:51:21 +08:00
hstyi
899bd5a356 fix: i18n typo 2025-06-23 12:04:03 +08:00
dependabot[bot]
ded16873b0 chore(deps): bump org.testcontainers:testcontainers-bom
---
updated-dependencies:
- dependency-name: org.testcontainers:testcontainers-bom
  dependency-version: 1.21.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-23 11:35:04 +08:00
dependabot[bot]
501013ba31 chore(deps): bump org.commonmark:commonmark from 0.24.0 to 0.25.0
Bumps [org.commonmark:commonmark](https://github.com/commonmark/commonmark-java) from 0.24.0 to 0.25.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.24.0...commonmark-parent-0.25.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-23 11:18:41 +08:00
dependabot[bot]
76b1a2f4f8 chore(deps): bump com.fifesoft:languagesupport from 3.3.0 to 3.4.0
Bumps [com.fifesoft:languagesupport](https://github.com/bobbylight/RSTALanguageSupport) from 3.3.0 to 3.4.0.
- [Release notes](https://github.com/bobbylight/RSTALanguageSupport/releases)
- [Commits](https://github.com/bobbylight/RSTALanguageSupport/compare/3.3.0...3.4.0)

---
updated-dependencies:
- dependency-name: com.fifesoft:languagesupport
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-23 11:18:29 +08:00
hstyi
e34fea0ecb chore: linux deb 2025-06-22 18:55:48 +08:00
hstyi
7532546c77 fix: abnormal operation of transfer dialog 2025-06-22 14:37:37 +08:00
hstyi
187d5be658 feat: support transfer virtual window 2025-06-22 14:37:37 +08:00
hstyi
2bf4d277be chore: transfer shows error message 2025-06-22 12:42:33 +08:00
hstyi
389c243ada feat: transfer supports side button 2025-06-21 18:25:16 +08:00
hstyi
bb39178b88 fix: OTP dialog width 2025-06-21 18:03:18 +08:00
hstyi
e1eab9db06 refactor: transfer 2025-06-21 17:01:15 +08:00
hstyi
e6a45d25cd chore: improve sync plugin 2025-06-16 17:45:48 +08:00
hstyi
a64aef24b2 chore: build before dist 2025-06-16 15:31:10 +08:00
hstyi
c9c8aa8f2a chore: improve code 2025-06-16 15:23:02 +08:00
hstyi
d3cfde5238 fix: host filtering may not work 2025-06-16 15:22:43 +08:00
hstyi
b4e82a4a0e chore: add a badge to the plugin update button 2025-06-16 10:13:39 +08:00
hstyi
40b4848cc8 chore: bg 0.0.3 2025-06-16 09:41:54 +08:00
hstyi
0c4268a194 feat: support account registration 2025-06-16 09:32:22 +08:00
hstyi
866c823c30 fix: title not showing on Linux
(cherry picked from commit 071a091347)
2025-06-16 09:26:19 +08:00
hstyi
6f9cfc3b32 chore: improve native 2025-06-15 18:00:31 +08:00
hstyi
3717393ad9 chore: rename ssh-agent.sock 2025-06-15 17:00:08 +08:00
hstyi
10e7681ac2 chore: improve SSH_AUTH_SOCK reading on macOS 2025-06-15 16:45:20 +08:00
hstyi
51047e60f4 chore: FindEverywhere using extension 2025-06-15 12:19:25 +08:00
hstyi
02984bb2a2 fix: marketplace retry not working 2025-06-15 11:47:26 +08:00
hstyi
ea6b2d6a66 feat: support plugin repository 2025-06-15 08:39:01 +08:00
hstyi
95bf08b0da chore: user agent 2025-06-15 08:38:50 +08:00
hstyi
15131fefd1 chore: kotlin reflect 2025-06-14 16:47:30 +08:00
hstyi
26a06b1c91 chore: improve host tree filtering 2025-06-14 16:26:31 +08:00
dependabot[bot]
3cca89b917 chore(deps): bump org.semver4j:semver4j from 5.7.0 to 5.7.1
Bumps [org.semver4j:semver4j](https://github.com/semver4j/semver4j) from 5.7.0 to 5.7.1.
- [Release notes](https://github.com/semver4j/semver4j/releases)
- [Commits](https://github.com/semver4j/semver4j/compare/v5.7.0...v5.7.1)

---
updated-dependencies:
- dependency-name: org.semver4j:semver4j
  dependency-version: 5.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-13 16:54:03 +08:00
dependabot[bot]
d31510eb86 chore(deps): bump org.xerial:sqlite-jdbc from 3.49.1.0 to 3.50.1.0
Bumps [org.xerial:sqlite-jdbc](https://github.com/xerial/sqlite-jdbc) from 3.49.1.0 to 3.50.1.0.
- [Release notes](https://github.com/xerial/sqlite-jdbc/releases)
- [Changelog](https://github.com/xerial/sqlite-jdbc/blob/master/CHANGELOG)
- [Commits](https://github.com/xerial/sqlite-jdbc/compare/3.49.1.0...3.50.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-13 16:45:22 +08:00
dependabot[bot]
feaec3e223 chore(deps): bump exposed from 1.0.0-beta-1 to 1.0.0-beta-2
Bumps `exposed` from 1.0.0-beta-1 to 1.0.0-beta-2.

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

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

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

Updates `org.jetbrains.exposed:exposed-migration` from 1.0.0-beta-1 to 1.0.0-beta-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-beta-1...1.0.0-beta-2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-13 16:45:09 +08:00
hstyi
97886e019f fix: mixpanel endpoint 2025-06-13 16:35:52 +08:00
hstyi
7cb6696bba chore: improve geo 2025-06-13 16:26:41 +08:00
hstyi
ab017be855 chore: improve database secret 2025-06-13 16:08:16 +08:00
hstyi
6177bbdc68 chore!: migrate to version 2.x 2025-06-13 15:16:56 +08:00
hstyi
ca484618c7 chore: upgrade jdk 21.0.7b1034.51 2025-06-12 17:38:48 +08:00
hstyi
1f68f8a112 fix: text cursor not working (#637) 2025-06-11 10:52:43 +08:00
hstyi
0cd5670bd3 chore: winget.yml 2025-06-11 08:47:47 +08:00
hstyi
8e9c6bcb68 fix: macOS background running (#633) 2025-06-10 17:19:28 +08:00
hstyi
6c1fa0fc53 fix: custom toolbar action missing (#630) 2025-06-10 11:34:31 +08:00
hstyi
5145cfa8a5 release: 1.0.16 2025-06-10 08:34:03 +08:00
hstyi
87b1a5e315 fix: snippet \ character escape (#625) 2025-06-09 14:17:37 +08:00
hstyi
fa59869f2c fix: authentication username not being saved (#622) 2025-06-09 09:47:00 +08:00
kanoshiou
1ae64fe0db perf: lazy loading OptionsPane and Fonts (#619) 2025-06-07 12:07:55 +08:00
hstyi
f8d363836e chore: improve the host text field (#617) 2025-06-05 23:40:20 +08:00
dependabot[bot]
38dccb1d22 chore(deps): bump org.jetbrains.pty4j:pty4j from 0.13.5 to 0.13.6 (#613)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-04 11:11:50 +08:00
hstyi
3e31a89b92 chore: SFTP edit command supports manual file selection (#612) 2025-06-03 16:55:39 +08:00
kanoshiou
d8f892cc02 fix: missing remark when importing keys (#611) 2025-06-03 13:42:09 +08:00
hstyi
873deb55aa fix: SSH authentication causing IP and port changes (#610) 2025-06-03 12:55:41 +08:00
hstyi
c08712d79b fix: Xterm Send Device Attributes (Primary DA) (#607) 2025-05-30 10:44:53 +08:00
dependabot[bot]
61bc905727 chore(deps): bump org.testcontainers:testcontainers-bom from 1.21.0 to 1.21.1 (#606)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-30 10:28:44 +08:00
hstyi
17859be3c5 feat: confirm tab close (#605) 2025-05-30 09:48:48 +08:00
hstyi
7a24e34695 fix: delete leftover files before installing Windows (#604) 2025-05-30 09:11:57 +08:00
dependabot[bot]
58638eaad8 chore(deps): bump org.jetbrains.pty4j:pty4j from 0.13.4 to 0.13.5 (#603)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-29 18:26:50 +08:00
hstyi
09d2f2d193 chore: dialog location (#602) 2025-05-28 13:16:42 +08:00
hstyi
9121eff8d8 feat: support importing RDP protocol from CSV (#600) 2025-05-27 09:57:12 +08:00
dependabot[bot]
8b090b0526 chore(deps): bump org.gradle.toolchains.foojay-resolver-convention from 0.10.0 to 1.0.0 (#595) 2025-05-21 12:45:45 +08:00
hstyi
15a0d642ff feat: support block selection (#594) 2025-05-19 18:31:51 +08:00
hstyi
dc4333da21 release: 1.0.15 2025-05-19 11:34:23 +08:00
hstyi
184f6d46dc fix: snippet scroll (#587) 2025-05-16 13:17:02 +08:00
hstyi
68788905fe chore: improve sftp tab (#583) 2025-05-14 23:24:52 +08:00
dependabot[bot]
fc46216a3f chore(deps): bump kotlin from 2.1.20 to 2.1.21 (#580)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-14 11:43:24 +08:00
hstyi
563143645e fix: SFTP drag and drop upload (#578) 2025-05-13 13:50:08 +08:00
hstyi
891ccb901b chore: maven-publish 2025-05-12 16:56:01 +08:00
hstyi
928a866fe7 feat: improve SFTP (#572) 2025-05-12 15:37:39 +08:00
hstyi
ea25b5b46f feat: modify permissions to support recursion (#571) 2025-05-12 15:26:29 +08:00
hstyi
1de10e6129 fix: process Device Status Report (DSR) (#570) 2025-05-12 11:33:39 +08:00
hstyi
aaf9c2e8d2 feat: support for disabling hyperlink (#568) 2025-05-12 11:05:39 +08:00
hstyi
b8196b5730 fix: snippet unescape (#567) 2025-05-12 10:50:48 +08:00
hstyi
0a83e8beb4 fix: double-click to open the host (#566) 2025-05-12 10:49:35 +08:00
hstyi
bdf29b27e7 release: 1.0.14 2025-05-07 12:01:46 +08:00
hstyi
96da7eac41 chore: scroll to the bottom after pressed any key (#553) 2025-05-01 08:36:51 +08:00
hstyi
71c0751692 fix: test connect (#551) 2025-04-30 15:13:11 +08:00
dependabot[bot]
442f334af2 chore(deps): bump com.github.mwiede:jsch from 0.2.25 to 0.2.26 (#546)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-30 09:15:06 +08:00
hstyi
48302a519f fix: snippet i18n (#549) 2025-04-29 15:10:01 +08:00
hstyi
c00f759f15 fix: xterm CBT (#543) 2025-04-28 15:47:55 +08:00
hstyi
1736dd909e chore: folder count (#542) 2025-04-28 09:11:07 +08:00
hstyi
1f01e368dd feat: support for signature algorithms (#539) 2025-04-27 09:54:22 +08:00
hstyi
bfba958b7e feat: support for compression algorithms (#538) 2025-04-26 10:00:54 +08:00
dependabot[bot]
758121b523 chore(deps): bump org.testcontainers:testcontainers-bom from 1.20.6 to 1.21.0 (#528)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-26 09:46:37 +08:00
hstyi
06e9a89e82 fix: double-click to open the host (#529) 2025-04-26 09:45:42 +08:00
hstyi
0ba6ac3305 chore: correct typos (#537) 2025-04-26 09:44:57 +08:00
hstyi
993f220b8b feat: support RDP protocol (#524) 2025-04-20 15:33:09 +08:00
hstyi
8755c4ad23 chore: tmux 2025-04-16 16:35:03 +08:00
dependabot[bot]
77cb102dd6 chore(deps): bump com.github.oshi:oshi-core from 6.6.5 to 6.8.1 (#517)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-16 11:08:23 +08:00
hstyi
89cfb0b451 fix: snippet \ characters (#513) 2025-04-15 17:17:24 +08:00
hstyi
6bdd83f208 fix: highlighter CJK characters (#511) 2025-04-15 15:51:45 +08:00
hstyi
8f86057dcc chore: KeyShortcut toHuman text (#510) 2025-04-15 09:19:16 +08:00
hstyi
a7d7ffa2cc chore: improve dialog 2025-04-15 08:52:02 +08:00
hstyi
d51cbeee13 feat: Highlighter keywords support regex (#507) 2025-04-14 14:29:00 +08:00
hstyi
deb2a0151e fix: Linux moving window jitter 2025-04-14 13:22:25 +08:00
dependabot[bot]
e1c4e9312d chore(deps): bump org.jetbrains.pty4j:pty4j from 0.13.3 to 0.13.4
Bumps [org.jetbrains.pty4j:pty4j](https://github.com/JetBrains/pty4j) from 0.13.3 to 0.13.4.
- [Commits](https://github.com/JetBrains/pty4j/commits)

---
updated-dependencies:
- dependency-name: org.jetbrains.pty4j:pty4j
  dependency-version: 0.13.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 10:44:08 +08:00
dependabot[bot]
c7233357bd chore(deps): bump commons-io:commons-io from 2.18.0 to 2.19.0
Bumps commons-io:commons-io from 2.18.0 to 2.19.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 10:43:58 +08:00
hstyi
eff8d565d0 chore: upgrade flatlaf version 2025-04-14 10:42:30 +08:00
hstyi
932db49868 release: 1.0.13 2025-04-14 09:34:55 +08:00
hstyi
4d71c6cd05 chore: improve exit 2025-04-12 16:43:03 +08:00
hstyi
96133e5abf fix: default directory for SFTP Windows (#496) 2025-04-12 08:56:26 +08:00
hstyi
f06e5d7dc1 fix: Keymap sync override (#493) 2025-04-11 11:37:29 +08:00
dependabot[bot]
d4b96edccf chore(deps): bump org.gradle.toolchains.foojay-resolver-convention from 0.9.0 to 0.10.0 (#491)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-11 10:22:18 +08:00
dependabot[bot]
e9876d5b91 chore(deps): bump org.apache.commons:commons-text from 1.13.0 to 1.13.1 (#490)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-11 10:22:06 +08:00
hstyi
8b9a78a7bd chore: sync icon 2025-04-11 08:25:57 +08:00
hstyi
6b48f577e9 feat: macOS supports running in the background (#487) 2025-04-10 14:47:01 +08:00
hstyi
da9b6c21d6 fix: windows sftp path (#486) 2025-04-10 13:23:44 +08:00
hstyi
f1f889df14 chore: improve terminal close (#484) 2025-04-10 11:45:27 +08:00
hstyi
ed65853ebe fix: SFTP path not jumping on Windows (#483) 2025-04-10 11:08:51 +08:00
hstyi
5ffdd219d9 fix: Escape key (#482) 2025-04-10 09:37:05 +08:00
hstyi
4f84d6741c fix: JPopupMenu overlapping with background 2025-04-10 08:59:55 +08:00
hstyi
2568e7fcc8 fix: background image selection failure 2025-04-09 17:32:27 +08:00
hstyi
dddbb49084 feat: support setting background image (#475) 2025-04-09 16:03:38 +08:00
hstyi
95846ab135 fix: snippet unescape (#474) 2025-04-09 13:30:57 +08:00
dependabot[bot]
b5207e56c1 chore(deps): bump org.jetbrains.pty4j:pty4j from 0.13.2 to 0.13.3 (#471)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 10:42:50 +08:00
dependabot[bot]
160771e912 chore(deps): bump com.github.mwiede:jsch from 0.2.21 to 0.2.25 (#472)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 10:42:03 +08:00
dependabot[bot]
0fbe180f3f chore(deps): bump kotlinx-coroutines from 1.10.1 to 1.10.2 (#470)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 10:41:40 +08:00
hstyi
41a0409e9e fix: return to parent folder failure (#468) 2025-04-08 14:43:58 +08:00
hstyi
79e59143fb fix: last sync time (#467) 2025-04-08 14:40:20 +08:00
hstyi
54e0f621ce feat: support for restoring virtual windows 2025-04-07 11:51:55 +08:00
hstyi
4c8944d248 release: 1.0.12 2025-04-07 09:52:57 +08:00
hstyi
64bd95d8a8 chore: improve tree icon 2025-04-03 15:44:41 +08:00
hstyi
1d88942e8e feat: support automatic sync (#455) 2025-04-03 15:33:30 +08:00
hstyi
129e1b149a fix: vfs2 cache memory leaks 2025-04-03 00:55:26 +08:00
hstyi
01aac98437 feat: vfs2 2025-04-03 00:49:18 +08:00
dependabot[bot]
f9aaf7143f chore(deps): bump cn.hutool:hutool-all from 5.8.34 to 5.8.37
Bumps [cn.hutool:hutool-all](https://github.com/looly/hutool) from 5.8.34 to 5.8.37.
- [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/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-02 11:54:17 +08:00
dependabot[bot]
28174483f4 chore(deps): bump org.jetbrains.kotlinx:kotlinx-serialization-json
Bumps [org.jetbrains.kotlinx:kotlinx-serialization-json](https://github.com/Kotlin/kotlinx.serialization) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/Kotlin/kotlinx.serialization/releases)
- [Changelog](https://github.com/Kotlin/kotlinx.serialization/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Kotlin/kotlinx.serialization/compare/v1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlinx:kotlinx-serialization-json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-02 11:54:06 +08:00
hstyi
46412054c4 chore: improve x11 2025-04-01 17:53:13 +08:00
hstyi
1ab0d26bab chore: coroutine SupervisorJob 2025-04-01 15:57:32 +08:00
hstyi
d90fb9aa35 chore: swing CoroutineScope 2025-04-01 15:55:20 +08:00
hstyi
744e64b359 feat: support to set transparency (#446) 2025-04-01 14:57:57 +08:00
hstyi
2c5442f1f3 chore: improve x11 2025-04-01 10:38:33 +08:00
hstyi
054c4701d2 feat: support X11 forwarding (#443) 2025-04-01 00:54:02 +08:00
hstyi
54044625ea chore: remove proxy when session closes 2025-03-31 10:54:53 +08:00
hstyi
ca82704738 chore: improve proxy 2025-03-30 17:57:31 +08:00
hstyi
e98ec3fa8e chore: upgrade sshd version 2025-03-30 16:35:17 +08:00
hstyi
6a4abf7e50 fix: SSH proxy not working in jump hosts (#435) 2025-03-30 16:34:51 +08:00
hstyi
e2a6cceafd chore: upgrade jgit version 2025-03-30 15:58:42 +08:00
hstyi
283404b6b9 feat: SSH support ssh-agent (#433) 2025-03-30 12:48:14 +08:00
hstyi
c714f33a44 chore: telnetd Dockerfile 2025-03-29 22:45:44 +08:00
hstyi
30fe047e5c feat: authentication support fallback (#431) 2025-03-29 20:37:19 +08:00
hstyi
827d814c7b feat: improve sync (#429) 2025-03-29 19:09:04 +08:00
hstyi
ccb2c6daa0 fix: SFTP transport file 2025-03-29 14:40:42 +08:00
hstyi
1516d6d81e fix: SFTP add transport NPE 2025-03-29 14:34:50 +08:00
hstyi
09b3655c4e feat: SFTP file exists and prompts to overwrite (#426) 2025-03-29 13:41:02 +08:00
dependabot[bot]
614514c87e chore(deps): bump cash.z.ecc.android:kotlin-bip39 from 1.0.8 to 1.0.9
Bumps [cash.z.ecc.android:kotlin-bip39](https://github.com/zcash/kotlin-bip39) from 1.0.8 to 1.0.9.
- [Changelog](https://github.com/Electric-Coin-Company/kotlin-bip39/blob/main/CHANGELOG.md)
- [Commits](https://github.com/zcash/kotlin-bip39/compare/v1.0.8...v1.0.9)

---
updated-dependencies:
- dependency-name: cash.z.ecc.android:kotlin-bip39
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-28 11:39:38 +08:00
dependabot[bot]
30cba6720d chore(deps): bump org.mozilla:rhino from 1.7.15 to 1.8.0
Bumps [org.mozilla:rhino](https://github.com/mozilla/rhino) from 1.7.15 to 1.8.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-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-28 10:16:37 +08:00
dependabot[bot]
dce6551de2 chore(deps): bump org.testcontainers:testcontainers-bom
Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 1.20.4 to 1.20.6.
- [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.20.4...1.20.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-28 10:16:27 +08:00
dependabot[bot]
95943cdeec chore(deps): bump org.apache.commons:commons-csv from 1.13.0 to 1.14.0
Bumps [org.apache.commons:commons-csv](https://github.com/apache/commons-csv) from 1.13.0 to 1.14.0.
- [Changelog](https://github.com/apache/commons-csv/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-csv/compare/rel/commons-csv-1.13.0...rel/commons-csv-1.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-28 10:13:50 +08:00
dependabot[bot]
18a26ee6bf chore(deps): bump jna from 5.16.0 to 5.17.0
Bumps `jna` from 5.16.0 to 5.17.0.

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-28 10:13:42 +08:00
dependabot[bot]
f23aae371a chore(deps): bump org.slf4j:slf4j-api from 2.0.16 to 2.0.17
Bumps org.slf4j:slf4j-api from 2.0.16 to 2.0.17.

---
updated-dependencies:
- dependency-name: org.slf4j:slf4j-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-28 10:12:10 +08:00
hstyi
757bc1c001 chore: improve dependencies 2025-03-28 10:10:48 +08:00
hstyi
a19222dc60 chore: add dependabot 2025-03-27 17:40:18 +08:00
hstyi
24677ca4a6 feat: Welcome search supports keyboard navigation (#404) 2025-03-27 17:33:24 +08:00
hstyi
0c5b6f8112 feat: support system tray (#403) 2025-03-27 17:22:13 +08:00
hstyi
7c26e3d08a release: 1.0.11 2025-03-27 12:03:11 +08:00
hstyi
9b84fb4ec8 chore: ignore verify server key (#398) 2025-03-27 11:52:36 +08:00
hstyi
d8ec7b6d4a chore: automatically jump to the bottom (#397) 2025-03-27 11:44:10 +08:00
hstyi
769c0d990b fix: max row selection 2025-03-17 08:48:46 +08:00
hstyi
3f1ae38b61 chore: improve tick 2025-03-17 08:48:34 +08:00
hstyi
e10fce21a2 fix: flat inspector key shortcut 2025-03-16 17:04:12 +08:00
hstyi
a00557bb9d feat: process lock (#380) 2025-03-16 17:02:40 +08:00
hstyi
e478535ae5 chore: visual window stick 2025-03-16 10:21:33 +08:00
hstyi
7756758738 fix: SFTP path not working 2025-03-16 10:05:32 +08:00
hstyi
e0ea42faee feat: floating window supports stick (#374) 2025-03-16 08:42:25 +08:00
hstyi
e72c6b77b5 chore: Dockerfile x11 2025-03-15 23:15:09 +08:00
hstyi
bcd3aacd6f fix: emacs alt x 2025-03-15 20:49:55 +08:00
hstyi
570b0e08ad fix: AWTEventListener memory leaks 2025-03-15 15:11:50 +08:00
hstyi
d703850e87 chore: sftp failed message 2025-03-15 14:57:25 +08:00
hstyi
4bb1a411e8 feat: without jbr 2025-03-15 13:20:08 +08:00
hstyi
9884ed19fa chore: macOS dispatch_async 2025-03-15 08:29:42 +08:00
hstyi
1ffaed3f36 fix: sftp ui 2025-03-14 12:25:25 +08:00
hstyi
4cb42953ad feat: sftp contextmenu (#366) 2025-03-14 11:47:41 +08:00
hstyi
0248992dc3 chore: Command + Q will not trigger a popup 2025-03-14 09:36:17 +08:00
hstyi
9bab9db875 chore: hide copied toast 2025-03-14 09:28:45 +08:00
hstyi
b283a3ea38 feat: supports importing hosts from SSH config (#359) 2025-03-14 00:03:02 +08:00
hstyi
98ac2928b4 fix: xterm-256 foreground & background color (#358) 2025-03-13 23:39:18 +08:00
hstyi
a0a6f43c10 fix: arrow keys 2025-03-13 23:39:01 +08:00
hstyi
0c158acbe0 fix: sftp symbolic link 2025-03-13 22:21:39 +08:00
hstyi
9a97b3a304 feat: send command to the current window sessions 2025-03-13 22:17:01 +08:00
hstyi
aef44bd0da chore: improve factories 2025-03-13 20:45:49 +08:00
hstyi
75c65d9ba8 feat: support edit host (#352) 2025-03-13 20:45:31 +08:00
hstyi
93755db77f fix: nano bg color 2025-03-13 17:10:17 +08:00
hstyi
79d0a9a348 refactor: SFTP (#351) 2025-03-13 16:33:57 +08:00
hstyi
422e9aac84 release: 1.0.10 2025-03-05 11:11:41 +08:00
hstyi
9915c373b7 chore: remind me next time 2025-02-28 12:43:12 +08:00
hstyi
eba85e6348 fix: emacs shift key 2025-02-27 20:43:51 +08:00
hstyi
483a7772f4 feat: support snippet (#321) 2025-02-27 16:48:25 +08:00
hstyi
dcc96358f6 chore: remind me next time 2025-02-26 16:05:19 +08:00
hstyi
b5c30d505b feat: improve FlatTabbedPaneUI (#314) 2025-02-25 15:45:48 +08:00
hstyi
1f3ef5f3f0 chore: upgrade jdk 21.0.6b895.91 2025-02-25 13:27:41 +08:00
hstyi
d388bcfc92 chore: improve floating toolbar 2025-02-24 18:38:33 +08:00
hstyi
562c1f98fe feat: support to open host by enter 2025-02-24 17:11:12 +08:00
hstyi
f3c5009a45 feat: supports remembering window positions 2025-02-24 16:27:53 +08:00
hstyi
09a1d9f51e chore: osx GitHub actions 2025-02-24 14:31:09 +08:00
hstyi
84b48278ad feat: support sftp status (#307) 2025-02-24 14:14:44 +08:00
hstyi
ef9caf2578 release: 1.0.9 2025-02-24 13:23:13 +08:00
hstyi
b85bdf840e feat: support automatic download of update packages (#305) 2025-02-24 12:38:30 +08:00
hstyi
a2d7f3b5bb chore: Inno Setup 2025-02-24 00:51:53 +08:00
hstyi
02a96d73c8 fix: linux won't restart 2025-02-23 22:33:15 +08:00
hstyi
9fb12c7a71 feat: SFTP command add key shortcut 2025-02-23 21:32:25 +08:00
hstyi
145d8fc802 chore: automatically notarise macOS releases when released 2025-02-23 15:00:42 +08:00
hstyi
72c9dba806 feat: support restart (#299) 2025-02-23 11:32:44 +08:00
hstyi
de20bd654c feat: supports importing hosts from PuTTY (#297) 2025-02-22 16:47:50 +08:00
hstyi
35b3a10746 feat: supports importing hosts from electerm (#296) 2025-02-22 15:59:43 +08:00
hstyi
05fe6a0eb1 feat: supports importing hosts from FinalShell (#295) 2025-02-22 15:32:48 +08:00
hstyi
0552917c26 feat: supports importing hosts from SecureCRT (#294) 2025-02-22 14:51:32 +08:00
hstyi
51c355c113 feat: supports importing hosts from MobaXterm (#293) 2025-02-22 14:04:31 +08:00
hstyi
034ee3791d feat: supports importing hosts from Xshell (#292) 2025-02-22 13:15:45 +08:00
hstyi
adabaf8f2d feat: supports importing hosts from CSV (#291) 2025-02-22 12:23:31 +08:00
hstyi
1f392c52a1 chore: win 7z 2025-02-21 22:31:40 +08:00
hstyi
28fe4c725f feat: supports importing hosts from WindTerm (#289) 2025-02-21 21:44:51 +08:00
hstyi
18fe92cb11 chore: upgrade dependency versions 2025-02-21 19:42:08 +08:00
hstyi
c49acf7b51 feat: support fixed SFTP tab (#286) 2025-02-21 17:04:50 +08:00
hstyi
7df317a1b9 feat: refactoring HostTree & support sorting (#285) 2025-02-21 16:24:45 +08:00
hstyi
219e5420f5 fix: memory parsing error (#284) 2025-02-20 21:24:38 +08:00
hstyi
aefb7c3014 chore: exclude sshd-osgi 2025-02-20 20:53:12 +08:00
hstyi
f0c7f06ff5 chore: optimize package size 2025-02-20 20:41:34 +08:00
hstyi
604e07b43a fix: memory leaks 2025-02-20 17:17:23 +08:00
hstyi
0000e4610a feat: nvidia smi (#280) 2025-02-20 16:45:53 +08:00
hstyi
510324d7c4 fix: tunnels causes connection failure (#279) 2025-02-20 12:13:27 +08:00
hstyi
33a359fcbf feat: system information (#278) 2025-02-20 12:05:45 +08:00
hstyi
0b84d3271c feat: FindEverywhere show more info 2025-02-19 15:24:28 +08:00
hstyi
57547c95cb feat: blink (#273) 2025-02-19 13:17:59 +08:00
hstyi
503cfa9a4e fix: terminal cursor error (#272) 2025-02-19 10:29:31 +08:00
hstyi
af1f979e31 feat: ⌘ + Q to exit prompt 2025-02-18 23:29:04 +08:00
hstyi
3cd9f92ea9 feat: support setting sftp path (#267) 2025-02-18 18:09:33 +08:00
735 changed files with 39904 additions and 8749 deletions

12
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 25

View File

@@ -11,7 +11,7 @@ jobs:
fetch-depth: 0
# download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-aarch64-b825.69.tar.gz
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-aarch64-b1034.51.tar.gz
# appimagetool
- run: sudo apt install libfuse2
@@ -22,7 +22,7 @@ jobs:
with:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.6'
java-version: '21.0.7'
architecture: aarch64
- uses: actions/cache@v4
@@ -34,6 +34,11 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
# test build
- run: |
./gradlew classes -x test --no-daemon
./gradlew clean --no-daemon
# dist
- run: |
./gradlew dist --no-daemon

View File

@@ -11,7 +11,7 @@ jobs:
fetch-depth: 0
# download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-x64-b825.69.tar.gz
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-x64-b1034.51.tar.gz
# appimagetool
- run: sudo apt install libfuse2
@@ -22,7 +22,7 @@ jobs:
with:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.6'
java-version: '21.0.7'
architecture: x64
- uses: actions/cache@v4
@@ -34,6 +34,11 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
# test build
- run: |
./gradlew classes -x test --no-daemon
./gradlew clean --no-daemon
# dist
- run: |
./gradlew dist --no-daemon

View File

@@ -33,8 +33,18 @@ jobs:
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Setup the Notary information
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
STORE_CREDENTIALS: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
run: |
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
# download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b825.69.tar.gz
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-osx-aarch64-b1034.51.tar.gz
# install jdk
- name: Installing Java
@@ -42,7 +52,7 @@ jobs:
with:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.6'
java-version: '21.0.7'
architecture: aarch64
- uses: actions/cache@v4
@@ -54,11 +64,19 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
# test build
- run: |
./gradlew classes -x test --no-daemon
./gradlew clean --no-daemon
# dist
- name: Dist
env:
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' }}
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
# 只有发布版本时才需要公证
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 }}
run: |
./gradlew dist --no-daemon
@@ -66,4 +84,6 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: termora-osx-aarch64
path: build/distributions/*.dmg
path: |
build/distributions/*.zip
build/distributions/*.dmg

View File

@@ -33,8 +33,18 @@ jobs:
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Setup the Notary information
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
STORE_CREDENTIALS: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
run: |
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
# download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b825.69.tar.gz
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-osx-x64-b1034.51.tar.gz
# install jdk
- name: Installing Java
@@ -42,7 +52,7 @@ jobs:
with:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.6'
java-version: '21.0.7'
architecture: x64
@@ -56,11 +66,19 @@ jobs:
${{ runner.os }}-${{ runner.arch }}-gradle-
# test build
- run: |
./gradlew classes -x test --no-daemon
./gradlew clean --no-daemon
# dist
- name: Dist
env:
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' }}
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
# 只有发布版本时才需要公证
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 }}
run: |
./gradlew dist --no-daemon
@@ -68,4 +86,6 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: termora-osx-x86-64
path: build/distributions/*.dmg
path: |
build/distributions/*.zip
build/distributions/*.dmg

View File

@@ -10,11 +10,20 @@ jobs:
with:
fetch-depth: 0
- name: Install zip
run: |
$system32 = [System.Environment]::GetEnvironmentVariable("WINDIR") + "\System32"
Invoke-WebRequest -Uri "http://stahlworks.com/dev/zip.exe" -OutFile "$system32\zip.exe"
Invoke-WebRequest -Uri "http://stahlworks.com/dev/unzip.exe" -OutFile "$system32\unzip.exe"
- name: Install 7z
uses: milliewalky/setup-7-zip@v2
- name: Installing Java
uses: actions/setup-java@v4
with:
distribution: 'jetbrains'
java-version: '21'
run: |
curl -s --output ${{ runner.temp }}\java_package.zip -L https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-windows-x64-b1034.51.zip
unzip -q ${{ runner.temp }}\java_package.zip -d ${{ runner.temp }}\jbr
echo "JAVA_HOME=${{ runner.temp }}\jbr\jbrsdk-21.0.7-windows-x64-b1034.51" >> $env:GITHUB_ENV
- uses: actions/cache@v4
with:
@@ -25,6 +34,11 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
# test build
- run: |
.\gradlew classes -x test --no-daemon
.\gradlew clean --no-daemon
# dist
- run: |
.\gradlew.bat dist --no-daemon
@@ -36,4 +50,4 @@ jobs:
name: termora-windows-x86-64
path: |
build/distributions/*.zip
build/distributions/*.msi
build/distributions/*.exe

View File

@@ -7,7 +7,8 @@ jobs:
runs-on: windows-latest
steps:
- uses: vedantmgoyal9/winget-releaser@main
if: github.repository == 'TermoraDev/termora'
with:
identifier: TermoraDev.Termora
installers-regex: 'x86-64\.msi$' # Only x86-64.msi files
installers-regex: '\.exe$'
token: ${{ secrets.WINGET_TOKEN }}

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@ certs/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
.vs
### IntelliJ IDEA ###
.idea

125
README.md
View File

@@ -1,51 +1,102 @@
<div align="center">
<a href="./README.zh_CN.md">🇨🇳 简体中文</a>
<a href="./README.zh_CN.md">简体中文</a>
</div>
# Termora
**Termora** is a terminal emulator and SSH client for Windows, macOS and Linux.
**Termora** is a cross-platform terminal emulator and SSH client, available on **Windows, macOS, and Linux**.
<div align="center">
<img src="./docs/readme.png" alt="termora" />
<img src="docs/readme.png" alt="Readme" />
</div>
**Termora** is developed using [Kotlin/JVM](https://kotlinlang.org) and partially implements the [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) protocol (with ongoing improvements). Its ultimate vision is to achieve full platform support (including Android, iOS, and iPadOS) through [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html).
## Features
- SSH and local terminal support
- Serial port protocol support
- [SFTP](./docs/sftp.png?raw=1) & [Command](./docs/sftp-command.png?raw=1) file transfer support
- Compatible with Windows, macOS, and Linux
- Zmodem protocol support
- SSH port forwarding & Jump hosts
- Terminal log
- Configuration synchronization via [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
- Macro support (record and replay scripts)
- Keyword highlighting
- Key management
- Broadcast commands to multiple sessions
- [Find Everywhere](./docs/findeverywhere.png?raw=1) quick navigation
- Data encryption
- ...
## Download
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
## Development
It is recommended to use the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) version of the JDK and run the program via `./gradlew :run` to run the program.
The program can be run via `./gradlew dist` to automatically build the local version. On macOS: `dmg`, on Windows: `zip`, on Linux: `tar.gz`.
Termora is developed using [**Kotlin/JVM**](https://kotlinlang.org/) and partially implements the [**XTerm control sequence protocol**](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html). Its long-term goal is to achieve **full platform support** (including Android, iOS, and iPadOS) via [**Kotlin Multiplatform**](https://kotlinlang.org/docs/multiplatform.html).
## LICENSE
## ✨ Features
- 🧬 Cross-platform support
- 🔐 Built-in key manager
- 🖼️ X11 forwarding
- 🧑‍💻 SSH-Agent integration
- 💻 System information display
- 📁 GUI-based SFTP file management
- 📊 Nvidia GPU usage monitoring
- ⚡ Quick command shortcuts
## 🚀 File Transfer
- Direct transfers between server A ↔ B
- Recursive folder support
- Up to **6 concurrent transfer tasks**
<div align="center">
<img src="docs/transfer.png" alt="Transfer" />
</div>
## 📝 File Editing
- Auto-upload after editing and saving
- Rename files and folders
- Quick deletion of large folders (`rm -rf` supported)
- Visual permission editing
- Create new files and folders
<div align="center">
<img src="docs/transfer-edit.png" alt="Transfer Edit" />
</div>
## 💻 Hosts
- Tree-like hierarchical structure, similar to folders
- Assign tags to individual hosts
- Import hosts from other tools
- Open with the transfer tool
<div align="center">
<img src="docs/host.png" alt="Transfer Edit" />
</div>
## 🧩 Plugins
- 🌍 Geo: Display geolocation of hosts
- 🔄 Sync: Sync settings to Gist or WebDAV
- 🗂️ WebDAV: Connect to WebDAV storage
- 📝 Editor: Built-in SFTP file editor
- 📡 SMB: Connect to [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
- ☁️ S3: Connect to S3 object storage
- ☁️ Huawei OBS: Connect to Huawei Cloud OBS
- ☁️ Tencent COS: Connect to Tencent Cloud COS
- ☁️ Alibaba OSS: Connect to Alibaba Cloud OSS
- 👉 [View all plugins...](https://www.termora.app/plugins)
## 📦 Download
- 🧾 [Latest Release](https://github.com/TermoraDev/termora/releases/latest)
- 🍺 **Homebrew**: `brew install --cask termora`
- 🔨 **WinGet**: `winget install termora`
## 🛠️ Development
We recommend using the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK for development.
- Run locally: `./gradlew :run`
- Build for current OS: `./gradlew :dist`
## 📄 License
This software is distributed under a dual-license model. You may choose one of the following options:
- AGPL-3.0: Use, distribute, and modify the software under the terms of the [AGPL-3.0](https://opensource.org/license/agpl-v3).
- Proprietary License: For closed-source or proprietary use, please contact the author to obtain a commercial license.
- **AGPL-3.0**: Use, distribute, and modify the software under the terms of the [AGPL-3.0](https://opensource.org/license/agpl-v3).
- **Proprietary License**: For closed-source or proprietary use, please contact the author to obtain a commercial license.

View File

@@ -1,46 +1,100 @@
# Termora
**Termora** 是一终端模拟器和 SSH 客户端,支持 WindowsmacOSLinux。
**Termora** 是一款跨平台终端模拟器和 SSH 客户端,支持 **WindowsmacOSLinux**
<div align="center">
<img src="./docs/readme-zh_CN.png" alt="termora" />
<img src="docs/readme-zh_CN.png" alt="Readme" />
</div>
**Termora**用 [Kotlin/JVM](https://kotlinlang.org/) 开发并实现了 [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) 协议(尚未完全实现),它的最终目标是通过 [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) 实现全平台(含 Android、iOS、iPadOS 等
Termora 使用 [**Kotlin/JVM**](https://kotlinlang.org/) 开发,支持(正在实现中) [**XTerm 控制序列协议**](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)。未来目标是借助 [**Kotlin Multiplatform**](https://kotlinlang.org/docs/multiplatform.html) 实现 **全平台支持**,包括 Android、iOS、iPadOS 等。
## 功能特性
- 支持 SSH 和本地终端
- 支持串口协议
- 支持 [SFTP](./docs/sftp-zh_CN.png?raw=1) & [命令行](./docs/sftp-command.png?raw=1) 文件传输
- 支持 Windows、macOS、Linux 平台
- 支持 Zmodem 协议
- 支持 SSH 端口转发和跳板机
- 终端日志记录
- 支持配置同步到 [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
- 支持宏(录制脚本并回放)
- 支持关键词高亮
- 支持密钥管理器
- 支持将命令发送到多个会话
- 支持 [Find Everywhere](./docs/findeverywhere-zh_CN.png?raw=1) 快速跳转
- 支持数据加密
- ...
## 下载
## ✨ 功能特性
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
- 🧬 跨平台运行
- 🔐 内建密钥管理器
- 🖼️ 支持 X11 转发
- 🧑‍💻 SSH-Agent 集成
- 💻 系统信息展示
- 📁 图形化 SFTP 文件管理
- 📊 Nvidia 显卡使用率查看
- ⚡ 快捷指令支持
## 开发
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) 的 JDK 版本,通过 `./gradlew :run` 即可运行程序。
## 🚀 文件传输
通过 `./gradlew dist` 可以自动构建适用于本机的版本。在 macOS 上是:`dmg`,在 Windows 上是:`zip`,在 Linux 上是:`tar.gz`
- 支持 A ↔ B 服务器间直接传输
- 文件夹递归复制支持
- 最多可同时运行 **6 个传输任务**
## 协议
<div align="center">
<img src="docs/transfer-zh_CN.png" alt="Transfer" />
</div>
本软件采用双重许可模式,您可以选择以下任意一种许可方式:
- AGPL-3.0:根据 [AGPL-3.0](https://opensource.org/license/agpl-v3) 的条款,您可以自由使用、分发和修改本软件。
- 专有许可:如果希望在闭源或专有环境中使用,请联系作者获取许可。
## 📝 文件编辑功能
- 保存后自动上传修改内容
- 文件 / 文件夹 重命名
- 快速删除大文件夹:`rm -rf` 支持
- 可视化更改权限
- 支持新建文件 / 文件夹
<div align="center">
<img src="docs/transfer-edit-zh_CN.png" alt="Transfer Edit" />
</div>
## 💻 主机
- 类似文件夹树形结构
- 给主机添加标签
- 从其它软件导入
- 使用传输工具打开
<div align="center">
<img src="docs/host-zh_CN.png" alt="Transfer Edit" />
</div>
## 🧩 插件
- 🌍 Geo显示主机位置信息
- 🔄 Sync将配置同步至 Gist 或 WebDAV
- 🗂️ WebDAV连接 WebDAV 对象存储
- 📝 Editor内置 SFTP 文件编辑器
- 📡 SMB: 连接 [SMB](https://baike.baidu.com/item/smb/4750512) 文件共享协议
- ☁️ S3连接 S3 对象存储
- ☁️ Huawei OBS连接华为云对象存储
- ☁️ Tencent COS连接腾讯云 COS
- ☁️ Alibaba OSS连接阿里云 OSS
- 👉 [查看所有插件...](https://www.termora.cn/plugins)
## 📦 下载
- 🧾 [Latest release](https://github.com/TermoraDev/termora/releases/latest)
- 🍺 **Homebrew**`brew install --cask termora`
- 🪟 **WinGet**`winget install termora`
## 🛠️ 开发指南
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK 运行环境。
- 本地运行:`./gradlew :run`
- 构建当前系统安装包:`./gradlew :dist`
## 📄 授权协议
Termora 采用双重许可方式,您可以选择:
- **AGPL-3.0**:自由使用、修改、分发(遵循 [AGPL 条款](https://opensource.org/license/agpl-v3)
- **专有许可**:如需闭源或商业用途,请联系作者获取授权

View File

@@ -1,240 +1,232 @@
annotations 24.0.1
annotations
Apache License 2.0
https://github.com/JetBrains/java-annotations/blob/master/LICENSE.txt
bip39-lib-jvm 1.0.8
MIT License
https://github.com/Electric-Coin-Company/kotlin-bip39/blob/main/LICENSE
colorpicker 2.0.1
colorpicker
BSD 3-Clause "New" or "Revised" License
https://github.com/dheid/colorpicker/blob/main/LICENSE
commonmark 0.24.0
commonmark
BSD 2-Clause "Simplified" License
https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt
commons-codec 1.17.1
commons-codec
Apache License 2.0
https://github.com/apache/commons-codec/blob/master/LICENSE.txt
commons-compress 1.27.1
commons-vfs2
Apache License 2.0
https://github.com/apache/commons-compress/blob/master/LICENSE.txt
https://github.com/apache/commons-vfs/blob/master/LICENSE.txt
commons-io 2.18.0
commons-io
Apache License 2.0
https://github.com/apache/commons-io/blob/master/LICENSE.txt
commons-lang3 3.17.0
commons-lang3
Apache License 2.0
https://github.com/apache/commons-lang/blob/master/LICENSE.txt
commons-net 3.11.1
commons-net
Apache License 2.0
https://github.com/apache/commons-net/blob/master/LICENSE.txt
commons-text 1.12.0
commons-text
Apache License 2.0
https://github.com/apache/commons-text/blob/master/LICENSE.txt
eddsa 0.3.0
commons-csv
Apache License 2.0
https://github.com/apache/commons-csv/blob/master/LICENSE.txt
ini4j
Apache License 2.0
http://www.apache.org/licenses/LICENSE-2.0.txt
eddsa
Creative Commons Zero v1.0 Universal
https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt
flatlaf 3.5.4
flatlaf
Apache License 2.0
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
flatlaf 3.5.4-no-natives
flatlaf-no-natives
Apache License 2.0
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
flatlaf-extras 3.5.4
flatlaf-extras
Apache License 2.0
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
flatlaf-swingx 3.5.4
flatlaf-swingx
Apache License 2.0
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
JavaEWAH 1.2.3
JavaEWAH
Apache License 2.0
https://github.com/lemire/javaewah/blob/master/LICENSE
jbr-api 17.1.10.1
jbr-api
Apache License 2.0
https://github.com/JetBrains/JetBrainsRuntimeApi/blob/main/LICENSE
jcl-over-slf4j 1.7.36
jcl-over-slf4j
Apache License 2.0
https://www.apache.org/licenses/LICENSE-2.0.txt
jfa 1.2.0
jfa
Apache License 2.0
https://github.com/0x4a616e/jfa/blob/main/LICENSE
jgoodies-common 1.8.1
jgoodies-common
BSD-2-Clause License
http://www.opensource.org/licenses/bsd-license.html
jgoodies-forms 1.9.0
jgoodies-forms
BSD-2-Clause License
http://www.opensource.org/licenses/bsd-license.html
jna 5.16.0
jna
Apache License 2.0
https://github.com/java-native-access/jna/blob/master/AL2.0
jna-platform 5.16.0
jna-platform
Apache License 2.0
https://github.com/java-native-access/jna/blob/master/AL2.0
jnafilechooser-api 1.1.2
jnafilechooser-api
BSD 3-Clause "New" or "Revised" License
https://github.com/steos/jnafilechooser/blob/master/LICENSE
jnafilechooser-win32 1.1.2
jnafilechooser-win32
BSD 3-Clause "New" or "Revised" License
https://github.com/steos/jnafilechooser/blob/master/LICENSE
jsvg 1.4.0
jsvg
MIT License
https://github.com/weisJ/jsvg/blob/master/LICENSE
jSystemThemeDetector 3.9.1
jSystemThemeDetector
Apache License 2.0
https://github.com/Dansoftowner/jSystemThemeDetector/blob/master/LICENSE
kotlin-logging 1.7.9
kotlin-logging
Apache License 2.0
https://github.com/oshai/kotlin-logging/blob/master/LICENSE
kotlin-stdlib 2.1.0
kotlin-stdlib
Apache License 2.0
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
kotlin-stdlib-jdk7 1.9.10
kotlin-reflect
Apache License 2.0
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
kotlin-stdlib-jdk8 1.9.10
kotlin-stdlib-jdk7
Apache License 2.0
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
kotlin-stdlib-jdk8 1.9.10
kotlin-stdlib-jdk8
Apache License 2.0
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
kotlinx-coroutines-core-jvm 1.10.1
kotlin-stdlib-jdk8
Apache License 2.0
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
restart4j
Apache License 2.0
https://github.com/hstyi/restart4j/blob/main/LICENSE
kotlinx-coroutines-core
Apache License 2.0
https://www.apache.org/licenses/LICENSE-2.0
kotlinx-coroutines-swing 1.10.1
kotlinx-coroutines-swing
Apache License 2.0
https://www.apache.org/licenses/LICENSE-2.0
kotlinx-serialization-core-jvm 1.7.3
kotlinx-serialization-json
Apache License 2.0
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
kotlinx-serialization-json-jvm 1.7.3
Apache License 2.0
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
logging-interceptor 4.12.0
logging-interceptor
Apache License 2.0
https://www.apache.org/licenses/LICENSE-2.0
okhttp 4.12.0
okhttp
Apache License 2.0
https://www.apache.org/licenses/LICENSE-2.0
okio-jvm 3.6.0
okio-jvm
Apache License 2.0
https://www.apache.org/licenses/LICENSE-2.0
org.eclipse.jgit.ssh.apache 7.1.0.202411261347-r
org.eclipse.jgit.ssh.apache
Eclipse Distribution License
https://www.eclipse.org/org/documents/edl-v10.php
org.eclipse.jgit 7.1.0.202411261347-r
org.eclipse.jgit.ssh.apache.agent
Eclipse Distribution License
https://www.eclipse.org/org/documents/edl-v10.php
oshi-core 6.6.5
org.eclipse.jgit
Eclipse Distribution License
https://www.eclipse.org/org/documents/edl-v10.php
oshi-core
MIT License
https://github.com/oshi/oshi/blob/master/LICENSE
pty4j 0.13.2
pty4j
Eclipse Public License 1.0
https://github.com/JetBrains/pty4j/blob/master/LICENSE
slf4j-api 2.0.16
slf4j-api
MIT License
https://github.com/qos-ch/slf4j/blob/master/LICENSE.txt
slf4j-tinylog 2.7.0
slf4j-tinylog
Apache License 2.0
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
sshd-common 2.14.0
sshd-common
Apache License 2.0
https://www.apache.org/licenses/LICENSE-2.0
sshd-core 2.14.0
sshd-core
Apache License 2.0
https://www.apache.org/licenses/LICENSE-2.0
sshd-osgi 2.14.0
sshd-osgi
Apache License 2.0
https://www.apache.org/licenses/LICENSE-2.0
sshd-sftp 2.14.0
sshd-sftp
Apache License 2.0
https://www.apache.org/licenses/LICENSE-2.0
swingx-all 1.6.5-1
swingx-all
GNU LESSER GENERAL PUBLIC LICENSE v3
https://www.gnu.org/licenses/lgpl-3.0
tinylog-api 2.7.0
tinylog-api
Apache License 2.0
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
tinylog-impl 2.7.0
tinylog-impl
Apache License 2.0
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
versioncompare 1.4.1
versioncompare
Apache License 2.0
https://github.com/G00fY2/version-compare/blob/main/LICENSE
xodus-compress 2.0.1
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
xodus-environment 2.0.1
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
xodus-openAPI 2.0.1
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
xodus-utils 2.0.1
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
xodus-vfs 2.0.1
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
jediterm
Apache License 2.0
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
mixpanel-java 1.5.3
mixpanel-java
Apache License 2.0
https://github.com/mixpanel/mixpanel-java/blob/master/LICENSE
@@ -242,6 +234,34 @@ json-20231013
Public Domain.
https://github.com/stleary/JSON-java/blob/master/LICENSE
jSerialComm 2.11.0
jSerialComm
Apache License 2.0
https://github.com/Fazecast/jSerialComm/blob/master/LICENSE-APACHE-2.0
https://github.com/Fazecast/jSerialComm/blob/master/LICENSE-APACHE-2.0
exposed-core
Apache License 2.0
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
exposed-crypt
Apache License 2.0
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
exposed-jdbc
Apache License 2.0
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
sqlite-jdbc
Apache License 2.0
https://www.apache.org/licenses/LICENSE-2.0.txt
java-uuid-generator
Apache License 2.0
https://github.com/cowtowncoder/java-uuid-generator/blob/master/LICENSE
semver4j
MIT
https://github.com/semver4j/semver4j/blob/main/LICENSE
dom4j
Plexus (https://dom4j.github.io)
https://github.com/dom4j/dom4j/blob/master/LICENSE

1
VERSION Normal file
View File

@@ -0,0 +1 @@
2.0.0-beta.4

View File

@@ -3,23 +3,32 @@ import org.gradle.kotlin.dsl.support.uppercaseFirstChar
import org.gradle.nativeplatform.platform.internal.ArchitectureInternal
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
import org.jetbrains.kotlin.org.apache.commons.io.filefilter.FileFilterUtils
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
import org.jetbrains.kotlin.org.apache.commons.lang3.time.DateFormatUtils
import java.io.FileNotFoundException
import java.nio.file.Files
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.Future
plugins {
java
idea
application
`maven-publish`
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlinx.serialization)
}
group = "app.termora"
version = "1.0.8"
version = rootProject.projectDir.resolve("VERSION").readText().trim()
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
val appVersion = project.version.toString().split("-")[0]
val isDeb = os.isLinux && System.getProperty("type") == "deb"
// macOS 签名信息
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
@@ -31,15 +40,16 @@ val macOSNotaryKeychainProfile = System.getenv("TERMORA_MAC_NOTARY_KEYCHAIN_PROF
val macOSNotary = macOSSign && macOSNotaryKeychainProfile.isNotBlank()
&& System.getenv("TERMORA_MAC_NOTARY").toBoolean()
repositories {
mavenCentral()
maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies")
maven("https://www.jitpack.io")
allprojects {
repositories {
mavenCentral()
maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies")
maven("https://www.jitpack.io")
maven("https://central.sonatype.com/repository/maven-snapshots")
}
}
dependencies {
// 由于签名和公证macOS 不携带 natives
val useNoNativesFlatLaf = os.isMacOsX && System.getenv("ENABLE_BUILD").toBoolean()
testImplementation(kotlin("test"))
testImplementation(libs.hutool)
@@ -49,92 +59,120 @@ dependencies {
testImplementation(libs.delight.rhino.sandbox)
testImplementation(platform(libs.testcontainers.bom))
testImplementation(libs.testcontainers)
testImplementation(libs.h2)
testImplementation(libs.exposed.migration)
// implementation(platform(libs.koin.bom))
// implementation(libs.koin.core)
implementation(libs.slf4j.api)
implementation(libs.pty4j)
implementation(libs.slf4j.tinylog)
implementation(libs.tinylog.impl)
implementation(libs.commons.codec)
implementation(libs.commons.io)
implementation(libs.commons.lang3)
implementation(libs.commons.net)
implementation(libs.commons.text)
implementation(libs.commons.compress)
implementation(libs.kotlinx.coroutines.swing)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.flatlaf) {
artifact {
if (useNoNativesFlatLaf) {
classifier = "no-natives"
}
}
}
implementation(libs.flatlaf.extras) {
if (useNoNativesFlatLaf) {
exclude(group = "com.formdev", module = "flatlaf")
}
}
implementation(libs.flatlaf.swingx) {
if (useNoNativesFlatLaf) {
exclude(group = "com.formdev", module = "flatlaf")
}
}
api(kotlin("reflect"))
api(libs.slf4j.api)
api(libs.pty4j)
api(libs.slf4j.tinylog)
api(libs.tinylog.impl)
api(libs.commons.codec)
api(libs.commons.io)
api(libs.commons.lang3)
api(libs.commons.csv)
api(libs.commons.net)
api(libs.commons.text)
api(libs.kotlinx.coroutines.swing)
api(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json)
implementation(libs.swingx)
implementation(libs.jgoodies.forms)
implementation(libs.jna)
implementation(libs.jna.platform)
implementation(libs.versioncompare)
implementation(libs.oshi.core)
implementation(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
implementation(libs.jfa) { exclude(group = "*", module = "*") }
implementation(libs.jbr.api)
implementation(libs.okhttp)
implementation(libs.okhttp.logging)
implementation(libs.sshd.core)
implementation(libs.commonmark)
implementation(libs.jgit)
implementation(libs.jgit.sshd)
implementation(libs.jnafilechooser)
implementation(libs.xodus.vfs)
implementation(libs.xodus.openAPI)
implementation(libs.xodus.environment)
implementation(libs.bip39)
implementation(libs.colorpicker)
implementation(libs.mixpanel)
implementation(libs.jSerialComm)
api(libs.flatlaf)
api(libs.flatlafextras)
api(libs.flatlafswingx)
api(libs.kotlinx.serialization.json)
api(libs.swingx)
api(libs.jgoodies.forms)
api(libs.jna)
api(libs.jna.platform)
api(libs.versioncompare)
api(libs.oshi.core)
api(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
api(libs.jfa) { exclude(group = "*", module = "*") }
api(libs.jbr.api)
api(libs.okhttp)
api(libs.okhttp.logging)
api(libs.sshd.core)
api(libs.commonmark)
api(libs.jgit)
api(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
api(libs.eddsa)
api(libs.jnafilechooser)
api(libs.colorpicker)
api(libs.mixpanel)
api(libs.jSerialComm)
api(libs.ini4j)
api(libs.restart4j)
api(libs.exposed.core)
api(libs.exposed.crypt)
api(libs.exposed.jdbc)
api(libs.sqlite)
api(libs.jug)
api(libs.semver4j)
api(libs.jsvg)
api(libs.dom4j) { exclude(group = "*", module = "*") }
}
application {
val args = mutableListOf(
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
"-Xmx2g",
"-XX:+UseZGC",
"-XX:+ZUncommit",
"-XX:+ZGenerational",
"-XX:ZUncommitDelay=60",
"-Xmx2048m",
"-Drelease-date=${DateFormatUtils.format(Date(), "yyyy-MM-dd")}",
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
)
if (os.isMacOsX) {
args.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
// macOS NSWindow
args.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
args.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
args.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
args.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
args.add("-Dsun.java2d.metal=true")
args.add("-Dapple.awt.application.appearance=system")
}
args.add("-Dapp-version=${project.version}")
if (os.isLinux) {
args.add("-Dsun.java2d.opengl=true")
}
args.add("-DTERMORA_PLUGIN_DIRECTORY=${layout.buildDirectory.get().asFile.absolutePath}${File.separator}plugins")
applicationDefaultJvmArgs = args
mainClass = "app.termora.MainKt"
}
publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
pom {
name = project.name
description = "Termora is a terminal emulator and SSH client for Windows, macOS and Linux"
url = "https://github.com/TermoraDev/termora"
licenses {
license {
name = "AGPL-3.0"
url = "https://opensource.org/license/agpl-v3"
}
}
developers {
developer {
name = "hstyi"
url = "https://github.com/hstyi"
}
}
scm {
url = "https://github.com/TermoraDev/termora"
}
}
}
}
}
tasks.test {
useJUnitPlatform()
}
@@ -142,16 +180,19 @@ tasks.test {
tasks.register<Copy>("copy-dependencies") {
val dir = layout.buildDirectory.dir("libs")
from(configurations.runtimeClasspath).into(dir)
val jna = libs.jna.asProvider().get()
val pty4j = libs.pty4j.get()
val flatlaf = libs.flatlaf.get()
val jSerialComm = libs.jSerialComm.get()
val restart4j = libs.restart4j.get()
val sqlite = libs.sqlite.get()
// 对 JNA 和 PTY4J 的本地库提取
// 提取出来是为了单独签名,不然无法通过公证
if (os.isMacOsX && macOSSign) {
doLast {
val jna = libs.jna.asProvider().get()
val archName = if (arch.isArm) "aarch64" else "x86_64"
val dylib = dir.get().dir("dylib").asFile
val pty4j = libs.pty4j.get()
val jSerialComm = libs.jSerialComm.get()
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
val targetDir = File(dylib, jna.name)
@@ -177,7 +218,6 @@ tasks.register<Copy>("copy-dependencies") {
// 删除所有二进制类库
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
} else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) {
val archName = if (arch.isArm) "aarch64" else "x86_64"
val targetDir = FileUtils.getFile(dylib, jSerialComm.name, "OSX", archName)
FileUtils.forceMkdir(targetDir)
// @formatter:off
@@ -191,6 +231,40 @@ tasks.register<Copy>("copy-dependencies") {
exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") }
exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") }
exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") }
} else if ("${restart4j.name}-${restart4j.version}" == file.nameWithoutExtension) {
val targetDir = FileUtils.getFile(dylib, restart4j.name)
FileUtils.forceMkdir(targetDir)
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "darwin/${archName}/*", "-d", targetDir.absolutePath) }
// @formatter:on
// 删除所有二进制类库
exec { commandLine("zip", "-d", file.absolutePath, "win32/*") }
exec { commandLine("zip", "-d", file.absolutePath, "darwin/*") }
exec { commandLine("zip", "-d", file.absolutePath, "linux/*") }
// 设置可执行权限
for (e in FileUtils.listFiles(
targetDir,
FileFilterUtils.trueFileFilter(),
FileFilterUtils.falseFileFilter()
)) {
e.setExecutable(true)
}
} else if ("${sqlite.name}-${sqlite.version}" == file.nameWithoutExtension) {
val targetDir = FileUtils.getFile(dylib, sqlite.name)
FileUtils.forceMkdir(targetDir)
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "org/sqlite/native/Mac/${archName}/*", "-d", targetDir.absolutePath) }
// @formatter:on
// 删除所有二进制类库
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/*") }
} else if ("${flatlaf.name}-${flatlaf.version}" == file.nameWithoutExtension) {
val targetDir = FileUtils.getFile(dylib, flatlaf.name)
FileUtils.forceMkdir(targetDir)
val isArm = arch.isArm
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "com/formdev/flatlaf/natives/*macos*${if (isArm) "arm" else "x86"}*", "-d", targetDir.absolutePath) }
// @formatter:on
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*") }
}
}
@@ -203,6 +277,115 @@ tasks.register<Copy>("copy-dependencies") {
}
}
}
} else if (os.isLinux || os.isWindows) { // 缩减安装包
doLast {
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/darwin-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/sunos-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/openbsd-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/freebsd-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/dragonflybsd-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
if (os.isWindows) {
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/linux-*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-x86*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-aarch64/*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-x86/*") }
}
} else if (os.isLinux) {
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-*") }
}
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
exec { commandLine("zip", "-d", file.absolutePath, "resources/*darwin*") }
exec { commandLine("zip", "-d", file.absolutePath, "resources/*freebsd*") }
if (os.isWindows) {
exec { commandLine("zip", "-d", file.absolutePath, "resources/*linux*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86/*") }
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86-64*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86/*") }
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/aarch64/*") }
}
} else if (os.isLinux) {
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win*") }
}
} else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) {
exec { commandLine("zip", "-d", file.absolutePath, "Android/*") }
exec { commandLine("zip", "-d", file.absolutePath, "FreeBSD/*") }
exec { commandLine("zip", "-d", file.absolutePath, "OpenBSD/*") }
exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") }
exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") }
if (os.isWindows) {
exec { commandLine("zip", "-d", file.absolutePath, "Linux/*") }
} else if (os.isLinux) {
exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") }
}
} else if ("${restart4j.name}-${restart4j.version}" == file.nameWithoutExtension) {
exec { commandLine("zip", "-d", file.absolutePath, "darwin/*") }
if (os.isWindows) {
exec { commandLine("zip", "-d", file.absolutePath, "linux/*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "win32/x86_64/*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "win32/aarch64/*") }
}
} else if (os.isLinux) {
exec { commandLine("zip", "-d", file.absolutePath, "win32/*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "linux/x86_64/*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "linux/aarch64/*") }
}
}
} else if ("${sqlite.name}-${sqlite.version}" == file.nameWithoutExtension) {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux-*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/FreeBSD/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Mac/*") }
if (os.isWindows) {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/armv7/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/x86/*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/x86_64/*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/aarch64/*") }
}
} else if (os.isLinux) {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/arm*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/ppc64/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/riscv64/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/x86/*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/x86_64/*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/aarch64/*") }
}
}
} else if ("${flatlaf.name}-${flatlaf.version}" == file.nameWithoutExtension) {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*macos*") }
if (os.isWindows) {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*linux*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*x86*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*x86.dll") }
}
} else if (os.isLinux) {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*windows*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*x86*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*arm*") }
}
}
}
}
}
}
}
@@ -213,6 +396,7 @@ tasks.register<Exec>("jlink") {
"java.logging",
"java.management",
"java.rmi",
"java.sql",
"java.security.jgss",
"jdk.crypto.ec",
"jdk.unsupported",
@@ -238,33 +422,38 @@ tasks.register<Exec>("jpackage") {
val buildDir = layout.buildDirectory.get()
val options = mutableListOf(
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
"-Xmx2g",
"-XX:+UseZGC",
"-XX:+ZUncommit",
"-XX:+ZGenerational",
"-XX:ZUncommitDelay=60",
"-Xmx2048m",
"-XX:+HeapDumpOnOutOfMemoryError",
"-Dlogger.console.level=off",
"-Dkotlinx.coroutines.debug=off",
"-Dapp-version=${project.version}",
"-Drelease-date=${DateFormatUtils.format(Date(), "yyyy-MM-dd")}",
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
)
options.add("-Dsun.java2d.metal=true")
if (os.isMacOsX) {
// NSWindow
options.add("-Dapple.awt.application.appearance=system")
options.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
options.add("--add-opens java.desktop/sun.font=ALL-UNNAMED")
options.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
options.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
}
if (os.isLinux) {
options.add("-Dsun.java2d.opengl=true")
if (isDeb) {
options.add("-Djpackage.app-layout=deb")
}
}
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage")
arguments.addAll(listOf("--runtime-image", "${buildDir}/jlink"))
arguments.addAll(listOf("--name", project.name.uppercaseFirstChar()))
arguments.addAll(listOf("--app-version", "${project.version}"))
arguments.addAll(listOf("--app-version", appVersion))
arguments.addAll(listOf("--main-jar", tasks.jar.get().archiveFileName.get()))
arguments.addAll(listOf("--main-class", application.mainClass.get()))
arguments.addAll(listOf("--input", "$buildDir/libs"))
@@ -273,6 +462,7 @@ tasks.register<Exec>("jpackage") {
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE)))
arguments.addAll(listOf("--vendor", "TermoraDev"))
arguments.addAll(listOf("--copyright", "TermoraDev"))
arguments.addAll(listOf("--app-content", "$buildDir/plugins"))
if (os.isWindows) {
arguments.addAll(
@@ -312,7 +502,11 @@ tasks.register<Exec>("jpackage") {
} else if (os.isWindows) {
arguments.add("msi")
} else if (os.isLinux) {
arguments.add("app-image")
arguments.add(if (isDeb) "deb" else "app-image")
if (isDeb) {
arguments.add("--linux-deb-maintainer")
arguments.add("support@termora.app")
}
} else {
throw UnsupportedOperationException()
}
@@ -329,103 +523,244 @@ tasks.register<Exec>("jpackage") {
tasks.register("dist") {
doLast {
val vendor = Jvm.current().vendor ?: StringUtils.EMPTY
@Suppress("UnstableApiUsage")
if (!JvmVendorSpec.JETBRAINS.matches(vendor)) {
throw GradleException("JVM: $vendor is not supported")
}
val distributionDir = layout.buildDirectory.dir("distributions").get()
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
val macOSFinalFilePath = distributionDir.file("${finalFilenameWithoutExtension}.dmg").asFile.absolutePath
// 清空目录
exec { commandLine(gradlew, "clean") }
// 构建自带的插件
exec { commandLine(gradlew, ":plugins:migration:build") }
// 打包并复制依赖
exec {
commandLine(gradlew, "jar", "copy-dependencies")
environment("ENABLE_BUILD" to true)
commandLine(gradlew, ":jar", ":copy-dependencies")
}
// 检查依赖的开源协议
exec { commandLine(gradlew, "check-license") }
exec { commandLine(gradlew, ":check-license") }
// jlink
exec { commandLine(gradlew, "jlink") }
exec { commandLine(gradlew, ":jlink") }
// 打包
exec { commandLine(gradlew, "jpackage") }
exec { commandLine(gradlew, ":jpackage", "-Dtype=${System.getProperty("type")}") }
// pack
if (os.isWindows) { // zip and msi
// zip
exec {
commandLine(
"tar", "-vacf",
distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile.absolutePath,
project.name.uppercaseFirstChar()
)
workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
// 根据不同的系统构建不同的二进制包
pack()
}
}
tasks.register("check-license") {
doLast {
val iterator = File(projectDir, "THIRDPARTY").readLines().iterator()
val thirdPartyNames = mutableSetOf<String>()
while (iterator.hasNext()) {
val name = iterator.next()
if (name.isBlank()) {
continue
}
// msi
exec {
commandLine(
"cmd", "/c", "move",
"${project.name.uppercaseFirstChar()}-${project.version}.msi",
"${finalFilenameWithoutExtension}.msi"
)
workingDir = distributionDir.asFile
}
} else if (os.isLinux) { // tar.gz
exec {
commandLine(
"tar", "-czvf",
distributionDir.file("${finalFilenameWithoutExtension}.tar.gz").asFile.absolutePath,
project.name.uppercaseFirstChar()
)
workingDir = distributionDir.asFile
}
} else if (os.isMacOsX) { // rename
exec {
commandLine(
"mv",
distributionDir.file("${project.name.uppercaseFirstChar()}-${project.version}.dmg").asFile.absolutePath,
macOSFinalFilePath,
)
}
} else {
throw GradleException("${os.name} is not supported")
// ignore license name
iterator.next()
// ignore license url
iterator.next()
thirdPartyNames.add(name)
}
// AppImage
if (os.isLinux) {
// Download AppImageKit
val appimagetool = FileUtils.getFile(projectDir, ".gradle", "appimagetool")
if (!appimagetool.exists()) {
exec {
commandLine(
"wget",
"-O", appimagetool.absolutePath,
"https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-${if (arch.isArm) "aarch64" else "x86_64"}.AppImage"
)
workingDir = distributionDir.asFile
}
// AppImageKit chmod
exec { commandLine("chmod", "+x", appimagetool.absolutePath) }
for (dependency in configurations.runtimeClasspath.get().allDependencies) {
if (!thirdPartyNames.contains(dependency.name)) {
throw GradleException("${dependency.name} No license found")
}
}
}
}
/**
* 构建包
*/
fun pack() {
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
val distributionDir = layout.buildDirectory.dir("distributions").get()
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
val projectName = project.name.uppercaseFirstChar()
if (os.isWindows) {
packOnWindows(distributionDir, finalFilenameWithoutExtension, projectName)
} else if (os.isLinux) {
packOnLinux(distributionDir, finalFilenameWithoutExtension, projectName)
} else if (os.isMacOsX) {
packOnMac(distributionDir, finalFilenameWithoutExtension, projectName)
} else {
throw GradleException("${os.name} is not supported")
}
}
/**
* 创建 zip、msi
*/
fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
val dir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
val cfg = FileUtils.getFile(dir, projectName, "app", "${projectName}.cfg")
val configText = cfg.readText()
// zip
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=zip").toString())
exec {
commandLine(
"tar", "-vacf",
distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile.absolutePath,
projectName
)
workingDir = dir
}
// exe
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=exe").toString())
exec {
commandLine(
"iscc",
"/DMyAppId=${projectName}",
"/DMyAppName=${projectName}",
"/DMyAppVersion=${appVersion}",
"/DMyOutputDir=${distributionDir.asFile.absolutePath}",
"/DMySetupIconFile=${FileUtils.getFile(projectDir, "src", "main", "resources", "icons", "termora.ico")}",
"/DMySourceDir=${layout.buildDirectory.dir("jpackage/images/win-msi.image/${projectName}").get().asFile}",
"/F${finalFilenameWithoutExtension}",
FileUtils.getFile(projectDir, "src", "main", "resources", "termora.iss")
)
}
// msi
exec {
commandLine(
"cmd", "/c", "move",
"${projectName}-${appVersion}.msi",
"${finalFilenameWithoutExtension}.msi"
)
workingDir = distributionDir.asFile
}
}
/**
* 对于 macOS 先对 jpackage 构建的 dmg 重命名 -> 签名 -> 公证,另外还会创建一个 zip 包
*/
fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
val dmgFile = distributionDir.file("${finalFilenameWithoutExtension}.dmg").asFile
val zipFile = distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile
// rename
// @formatter:off
exec { commandLine("mv", distributionDir.file("${projectName}-${appVersion}.dmg").asFile.absolutePath, dmgFile.absolutePath,) }
// @formatter:on
// sign dmg
if (macOSSign) signMacOSLocalFile(dmgFile)
// 找到 .app
val imageFile = layout.buildDirectory.dir("jpackage/images/").get().asFile
val appFile = imageFile.listFiles()?.firstOrNull()?.listFiles()?.firstOrNull()
?: throw FileNotFoundException("${projectName}.app")
// zip
// @formatter:off
exec { commandLine("ditto", "-c", "-k", "--sequesterRsrc", "--keepParent", appFile.absolutePath, zipFile.absolutePath) }
// @formatter:on
// sign zip
if (macOSSign) signMacOSLocalFile(zipFile)
// 公证
if (macOSNotary) {
val pool = Executors.newCachedThreadPool()
val jobs = mutableListOf<Future<*>>()
// zip
pool.submit {
// 对 zip 公证
notaryMacOSLocalFile(zipFile)
// 对 .app 盖章
stapleMacOSLocalFile(appFile)
// 删除旧的 zip ,旧的 zip 仅仅是为了公证
FileUtils.deleteQuietly(zipFile)
// 再对盖完章的 app 打成 zip 包
// @formatter:off
exec { commandLine("ditto", "-c", "-k", "--sequesterRsrc", "--keepParent", appFile.absolutePath, zipFile.absolutePath) }
// @formatter:on
// 再对 zip 签名
signMacOSLocalFile(zipFile)
}.apply { jobs.add(this) }
// dmg
pool.submit {
// 公证
notaryMacOSLocalFile(dmgFile)
// 盖章
stapleMacOSLocalFile(dmgFile)
}.apply { jobs.add(this) }
// join ...
jobs.forEach { it.get() }
// shutdown
pool.shutdown()
}
}
/**
* 创建 tar.gz 和 AppImage
*/
fun packOnLinux(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
if (isDeb) {
val arch = if (arch.isArm) "arm" else "amd"
distributionDir.file("${project.name}_${appVersion}_${arch}64.deb").asFile
.renameTo(distributionDir.file("${finalFilenameWithoutExtension}.deb").asFile)
return
}
val cfg = FileUtils.getFile(distributionDir.asFile, projectName, "lib", "app", "${projectName}.cfg")
val configText = cfg.readText()
// tar.gz
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=tar.gz").toString())
exec {
commandLine(
"tar", "-czvf",
distributionDir.file("${finalFilenameWithoutExtension}.tar.gz").asFile.absolutePath,
projectName
)
workingDir = distributionDir.asFile
}
// Desktop file
val termoraName = project.name.uppercaseFirstChar()
val desktopFile = distributionDir.file(termoraName + File.separator + termoraName + ".desktop").asFile
desktopFile.writeText(
"""[Desktop Entry]
// AppImage
// Download AppImageKit
val appimagetool = FileUtils.getFile(projectDir, ".gradle", "appimagetool")
if (!appimagetool.exists()) {
exec {
commandLine(
"wget",
"-O", appimagetool.absolutePath,
"https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-${if (arch.isArm) "aarch64" else "x86_64"}.AppImage"
)
workingDir = distributionDir.asFile
}
// AppImageKit chmod
exec { commandLine("chmod", "+x", appimagetool.absolutePath) }
}
// Desktop file
val termoraName = project.name.uppercaseFirstChar()
val desktopFile = distributionDir.file(termoraName + File.separator + termoraName + ".desktop").asFile
desktopFile.writeText(
"""[Desktop Entry]
Type=Application
Name=${termoraName}
Comment=Terminal emulator and SSH client
@@ -433,85 +768,24 @@ Icon=/lib/${termoraName}
Categories=Development;
Terminal=false
""".trimIndent()
)
)
// AppRun file
val appRun = File(desktopFile.parentFile, "AppRun")
val sb = StringBuilder()
sb.append("#!/bin/sh").appendLine()
sb.append("SELF=$(readlink -f \"$0\")").appendLine()
sb.append("HERE=\${SELF%/*}").appendLine()
sb.append("exec \"\${HERE}/bin/${termoraName}\" \"$@\"")
appRun.writeText(sb.toString())
appRun.setExecutable(true)
// AppRun file
val appRun = File(desktopFile.parentFile, "AppRun")
val sb = StringBuilder()
sb.append("#!/bin/sh").appendLine()
sb.append("SELF=$(readlink -f \"$0\")").appendLine()
sb.append("HERE=\${SELF%/*}").appendLine()
sb.append("export LinuxAppImage=true").appendLine()
sb.append("exec \"\${HERE}/bin/${termoraName}\" \"$@\"")
appRun.writeText(sb.toString())
appRun.setExecutable(true)
exec {
commandLine(appimagetool.absolutePath, termoraName, "${finalFilenameWithoutExtension}.AppImage")
workingDir = distributionDir.asFile
}
}
// sign dmg
if (os.isMacOsX && macOSSign) {
// sign
signMacOSLocalFile(File(macOSFinalFilePath))
// notary
if (macOSNotary) {
exec {
commandLine(
"/usr/bin/xcrun", "notarytool",
"submit", macOSFinalFilePath,
"--keychain-profile", macOSNotaryKeychainProfile,
"--wait",
)
}
// 绑定公证信息
exec {
commandLine(
"/usr/bin/xcrun",
"stapler", "staple", macOSFinalFilePath,
)
}
}
}
}
}
tasks.register("check-license") {
doLast {
val thirdParty = mutableMapOf<String, String>()
val iterator = File(projectDir, "THIRDPARTY").readLines().iterator()
val thirdPartyNames = mutableSetOf<String>()
while (iterator.hasNext()) {
val nameWithVersion = iterator.next()
if (nameWithVersion.isBlank()) {
continue
}
// ignore license name
iterator.next()
val license = iterator.next()
thirdParty[nameWithVersion.replace(StringUtils.SPACE, "-")] = license
thirdPartyNames.add(nameWithVersion.split(StringUtils.SPACE).first())
}
for (file in configurations.runtimeClasspath.get()) {
val name = file.nameWithoutExtension
if (!thirdParty.containsKey(name)) {
if (logger.isWarnEnabled) {
logger.warn("$name does not exist in third-party")
}
if (!thirdPartyNames.contains(name)) {
throw GradleException("$name No license found")
}
}
}
// AppImage
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=AppImage").toString())
exec {
commandLine(appimagetool.absolutePath, termoraName, "${finalFilenameWithoutExtension}.AppImage")
workingDir = distributionDir.asFile
}
}
@@ -534,15 +808,51 @@ fun signMacOSLocalFile(file: File) {
}
}
/**
* macOS 对本地文件进行公证
*/
fun notaryMacOSLocalFile(file: File) {
if (os.isMacOsX && macOSNotary) {
if (file.exists()) {
exec {
commandLine(
"/usr/bin/xcrun", "notarytool",
"submit", file,
"--keychain-profile", macOSNotaryKeychainProfile,
"--wait",
)
}
}
}
}
/**
* 盖章
*/
fun stapleMacOSLocalFile(file: File) {
if (os.isMacOsX && macOSNotary) {
if (file.exists()) {
exec {
commandLine(
"/usr/bin/xcrun",
"stapler", "staple", file,
)
}
}
}
}
kotlin {
jvmToolchain {
languageVersion = JavaLanguageVersion.of(21)
@Suppress("UnstableApiUsage")
vendor = JvmVendorSpec.JETBRAINS
}
}
java {
withSourcesJar()
}
idea {
module {
isDownloadJavadoc = true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

BIN
docs/host-zh_CN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
docs/host.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
docs/plugins-zh_CN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
docs/plugins.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/tags-zh_CN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
docs/tags.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
docs/transfer-edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
docs/transfer-zh_CN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

BIN
docs/transfer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -1,47 +1,53 @@
[versions]
kotlin = "2.1.0"
slf4j = "2.0.16"
pty4j = "0.13.2"
kotlin = "2.2.0"
slf4j = "2.0.17"
pty4j = "0.13.6"
tinylog = "2.7.0"
kotlinx-coroutines = "1.10.1"
flatlaf = "3.5.4"
trove4j = "1.0.20200330"
kotlinx-serialization-json = "1.7.3"
commons-codec = "1.17.1"
kotlinx-coroutines = "1.10.2"
flatlaf = "3.6"
kotlinx-serialization-json = "1.9.0"
commons-codec = "1.18.0"
commons-lang3 = "3.17.0"
commons-csv = "1.14.0"
commons-net = "3.11.1"
commons-text = "1.12.0"
commons-text = "1.13.1"
commons-compress = "1.27.1"
koin-bom = "4.0.0"
commons-vfs2 = "2.10.0"
swingx = "1.6.5-1"
jgoodies-forms = "1.9.0"
jfa = "1.2.0"
oshi = "6.6.5"
oshi = "6.8.1"
versioncompare = "1.4.1"
jna = "5.16.0"
jna = "5.17.0"
jSystemThemeDetector = "3.9.1"
commons-io = "2.18.0"
commons-io = "2.19.0"
jbr-api = "17.1.10.1"
leveldb = "0.12"
guava = "33.3.1-jre"
credential-secure-storage = "1.0.3"
hutool = "5.8.34"
jsch = "0.2.21"
hutool = "5.8.37"
jsch = "0.2.26"
okhttp = "4.12.0"
bcprov = "1.79"
sshj = "0.39.0"
sshd-core = "2.14.0"
jgit = "7.1.0.202411261347-r"
commonmark = "0.24.0"
sshd-core = "2.15.0"
jgit = "7.2.0.202503040940-r"
commonmark = "0.25.0"
jnafilechooser = "1.1.2"
xodus = "2.0.1"
bip39 = "1.0.8"
bip39 = "1.0.9"
colorpicker = "2.0.1"
rhino = "1.7.15"
rhino = "1.8.0"
delight-rhino-sandbox = "0.0.17"
testcontainers = "1.20.4"
testcontainers = "1.21.3"
mixpanel = "1.5.3"
jSerialComm="2.11.0"
jSerialComm = "2.11.2"
ini4j = "0.5.5-2"
restart4j = "0.0.1"
eddsa = "0.3.0"
exposed = "1.0.0-beta-3"
h2 = "2.3.232"
sqlite = "3.50.2.0"
jug = "5.1.0"
semver4j = "6.0.0"
jsvg = "1.4.0"
dom4j = "2.2.0"
[libraries]
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
@@ -53,16 +59,18 @@ tinylog-impl = { group = "org.tinylog", name = "tinylog-impl", version.ref = "ti
commons-codec = { group = "commons-codec", name = "commons-codec", version.ref = "commons-codec" }
commons-net = { group = "commons-net", name = "commons-net", version.ref = "commons-net" }
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commons-lang3" }
commons-csv = { group = "org.apache.commons", name = "commons-csv", version.ref = "commons-csv" }
commons-text = { group = "org.apache.commons", name = "commons-text", version.ref = "commons-text" }
commons-compress = { group = "org.apache.commons", name = "commons-compress", version.ref = "commons-compress" }
commons-vfs2 = { group = "org.apache.commons", name = "commons-vfs2", version.ref = "commons-vfs2" }
pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" }
ini4j = { module = "org.jetbrains.intellij.deps:ini4j", version.ref = "ini4j" }
flatlaf = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
flatlaf-extras = { group = "com.formdev", name = "flatlaf-extras", version.ref = "flatlaf" }
trove4j = { group = "org.jetbrains.intellij.deps", name = "trove4j", version.ref = "trove4j" }
koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
flatlafextras = { group = "com.formdev", name = "flatlaf-extras", version.ref = "flatlaf" }
flatlafswingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "testcontainers" }
testcontainers = { module = "org.testcontainers:testcontainers" }
koin-core = { module = "io.insert-koin:koin-core" }
testcontainers-junit-jupiter = { module = "org.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" }
@@ -72,33 +80,39 @@ versioncompare = { module = "io.github.g00fy2:versioncompare", version.ref = "ve
jfa = { module = "de.jangassen:jfa", version.ref = "jfa" }
oshi-core = { module = "com.github.oshi:oshi-core", version.ref = "oshi" }
commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
restart4j = { module = "com.github.hstyi:restart4j", version.ref = "restart4j" }
jbr-api = { module = "com.jetbrains:jbr-api", version.ref = "jbr-api" }
flatlaf-swingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
leveldb = { module = "org.iq80.leveldb:leveldb", version.ref = "leveldb" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
hutool = { module = "cn.hutool:hutool-all", version.ref = "hutool" }
credential-secure-storage = { module = "com.microsoft:credential-secure-storage", version.ref = "credential-secure-storage" }
jsch = { module = "com.github.mwiede:jsch", version.ref = "jsch" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bcprov" }
sshj = { module = "com.hierynomus:sshj", version.ref = "sshj" }
sshd-core = { module = "org.apache.sshd:sshd-core", version.ref = "sshd-core" }
jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" }
commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
jgit-sshd = { module = "org.eclipse.jgit:org.eclipse.jgit.ssh.apache", version.ref = "jgit" }
jgit-agent = { module = "org.eclipse.jgit:org.eclipse.jgit.ssh.apache.agent", version.ref = "jgit" }
xodus-openAPI = { module = "org.jetbrains.xodus:xodus-openAPI", version.ref = "xodus" }
xodus-entity-store = { module = "org.jetbrains.xodus:xodus-entity-store", version.ref = "xodus" }
xodus-environment = { module = "org.jetbrains.xodus:xodus-environment", version.ref = "xodus" }
xodus-crypto = { module = "org.jetbrains.xodus:xodus-crypto", version.ref = "xodus" }
xodus-vfs = { module = "org.jetbrains.xodus:xodus-vfs", version.ref = "xodus" }
jnafilechooser = { module = "com.github.steos.jnafilechooser:jnafilechooser-api", version.ref = "jnafilechooser" }
bip39 = { module = "cash.z.ecc.android:kotlin-bip39-jvm", version.ref = "bip39" }
bip39 = { module = "cash.z.ecc.android:kotlin-bip39", version.ref = "bip39" }
rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "jSerialComm" }
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" }
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" }
jsvg = { module = "com.github.weisj:jsvg", version.ref = "jsvg" }
dom4j = { module = "org.dom4j:dom4j", version.ref = "dom4j" }
semver4j = { module = "org.semver4j:semver4j", version.ref = "semver4j" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.10.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

10
plugins/LICENSE Normal file
View File

@@ -0,0 +1,10 @@
Copyright (c) 2025-present hstyi
The files in this catalogue are for public access only. Specific descriptions are given below:
- You may view and study the contents of these files;
- You may NOT use them for any commercial purpose;
- You may NOT modify, copy, distribute, republish, or use them to create derivative works;
- Written permission must be obtained from the author for any use beyond personal viewing.
All rights reserved.

79
plugins/THIRDPARTY Normal file
View File

@@ -0,0 +1,79 @@
minio
Apache License 2.0
https://github.com/minio/minio-java/blob/master/LICENSE
aliyun-sdk-oss
Apache License 2.0
https://www.apache.org/licenses/LICENSE-2.0.html
jaxb-api
BSD 3-Clause "New" or "Revised" License
https://github.com/jakartaee/jaxb-api/blob/master/LICENSE.md
activation
COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.1
https://github.com/javaee/activation/blob/master/LICENSE.txt
jaxb-runtime
BSD 3-Clause "New" or "Revised" License
https://github.com/eclipse-ee4j/jaxb-ri/blob/master/LICENSE.md
esdk-obs-java-bundle
HUAWEI LICENSE
https://github.com/huaweicloud/huaweicloud-sdk-java-obs/blob/master/LICENSE
xodus-compress
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
xodus-environment
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
xodus-openAPI
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
xodus-utils
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
xodus-vfs
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
kotlin-bip39
MIT License
https://github.com/Electric-Coin-Company/kotlin-bip39/blob/main/LICENSE
commons-compress
Apache License 2.0
https://github.com/apache/commons-compress/blob/master/LICENSE.txt
cos_api
MIT License
https://github.com/tencentyun/cos-java-sdk-v5/blob/master/LICENSE
AutoComplete
BSD-3-Clause license
https://github.com/bobbylight/AutoComplete/blob/master/LICENSE.md
RSTALanguageSupport
BSD-3-Clause license
https://github.com/bobbylight/RSTALanguageSupport/blob/master/README.md
RSyntaxTextArea
BSD-3-Clause license
https://github.com/bobbylight/RSyntaxTextArea/blob/master/LICENSE.md
MaxMind GeoIP2 API
Apache License, Version 2.0
https://www.apache.org/licenses/LICENSE-2.0.html
GeoLite2 (https://www.maxmind.com)
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
https://creativecommons.org/licenses/by-sa/4.0/
smbj
Apache License, Version 2.0
https://github.com/hierynomus/smbj/blob/master/LICENSE_HEADER

View File

@@ -0,0 +1,16 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.4"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -0,0 +1,21 @@
package app.termora.plugins.bg
import app.termora.EnableManager
import app.termora.database.DatabaseManager
object Appearance {
private val enableManager get() = EnableManager.getInstance()
private val appearance get() = DatabaseManager.getInstance().appearance
var backgroundImage: String
get() = enableManager.getFlag("Plugins.bg.backgroundImage", appearance.backgroundImage)
set(value) {
enableManager.setFlag("Plugins.bg.backgroundImage", value)
}
var interval: Int
get() = enableManager.getFlag("Plugins.bg.interval", 360)
set(value) {
enableManager.setFlag("Plugins.bg.interval", value)
}
}

View File

@@ -0,0 +1,26 @@
package app.termora.plugins.bg
import app.termora.GlassPaneExtension
import app.termora.WindowScope
import com.formdev.flatlaf.FlatLaf
import java.awt.AlphaComposite
import java.awt.Graphics2D
import javax.swing.JComponent
class BGGlassPaneExtension private constructor() : GlassPaneExtension {
companion object {
val instance = BGGlassPaneExtension()
}
override fun paint(scope: WindowScope, c: JComponent, g2d: Graphics2D) {
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
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)
}
}

View File

@@ -0,0 +1,26 @@
package app.termora.plugins.bg
import app.termora.AbstractI18n
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.*
object BGI18n : AbstractI18n() {
private val log = LoggerFactory.getLogger(BGI18n::class.java)
private val myBundle by lazy {
val bundle = ResourceBundle.getBundle("i18n/messages", Locale.getDefault(), BGI18n::class.java.classLoader)
if (log.isInfoEnabled) {
log.info("I18n: {}", bundle.baseBundleName ?: "null")
}
return@lazy bundle
}
override fun getBundle(): ResourceBundle {
return myBundle
}
override fun getLogger(): Logger {
return log
}
}

View File

@@ -0,0 +1,36 @@
package app.termora.plugins.bg
import app.termora.ApplicationRunnerExtension
import app.termora.GlassPaneAwareExtension
import app.termora.GlassPaneExtension
import app.termora.SettingsOptionExtension
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.Plugin
class BGPlugin : Plugin {
private val support = ExtensionSupport()
init {
support.addExtension(GlassPaneExtension::class.java) { BGGlassPaneExtension.instance }
support.addExtension(SettingsOptionExtension::class.java) { BackgroundSettingsOptionExtension.instance }
support.addExtension(ApplicationRunnerExtension::class.java) { BackgroundManager.getInstance() }
support.addExtension(GlassPaneAwareExtension::class.java) { BackgroundManager.getInstance() }
}
override fun getAuthor(): String {
return "TermoraDev"
}
override fun getName(): String {
return "Customize Background"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,167 @@
package app.termora.plugins.bg
import app.termora.*
import app.termora.database.DatabaseManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import okhttp3.Request
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import java.awt.Window
import java.awt.image.BufferedImage
import java.io.File
import java.lang.ref.WeakReference
import javax.imageio.ImageIO
import javax.swing.JComponent
import javax.swing.JPopupMenu
import javax.swing.SwingUtilities
import kotlin.math.max
import kotlin.time.Duration.Companion.seconds
internal class BackgroundManager private constructor() : Disposable, GlassPaneAwareExtension,
ApplicationRunnerExtension {
companion object {
private val log = LoggerFactory.getLogger(BackgroundManager::class.java)
fun getInstance(): BackgroundManager {
return ApplicationScope.Companion.forApplicationScope()
.getOrCreate(BackgroundManager::class) { BackgroundManager() }
}
}
private var bufferedImage: BufferedImage? = null
private var imageFilepath = StringUtils.EMPTY
private val glassPanes = mutableListOf<WeakReference<JComponent>>()
fun setBackgroundImage(url: String) {
clearBackgroundImage()
Appearance.backgroundImage = url
refreshBackgroundImage()
}
fun getBackgroundImage(): BufferedImage? {
val bg = doGetBackgroundImage()
if (bg == null) {
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
return null
} else {
JPopupMenu.setDefaultLightWeightPopupEnabled(true)
}
} else {
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
JPopupMenu.setDefaultLightWeightPopupEnabled(false)
}
}
return bg
}
private fun doGetBackgroundImage(): BufferedImage? {
synchronized(this) {
return bufferedImage
}
}
fun clearBackgroundImage() {
synchronized(this) {
bufferedImage = null
imageFilepath = StringUtils.EMPTY
Appearance.backgroundImage = StringUtils.EMPTY
}
refreshGlassPanes()
}
private fun refreshBackgroundImage() {
val backgroundImage = Appearance.backgroundImage
if (backgroundImage.isBlank()) {
return
}
var file: File? = null
// 从网络下载
if (backgroundImage.startsWith("http://") || backgroundImage.startsWith("https://")) {
file = Application.httpClient.newCall(
Request.Builder().get()
.url(backgroundImage).build()
).execute().use { response ->
val tempFile = File(Application.getTemporaryDir(), randomUUID())
if (response.isSuccessful.not()) {
if (log.isErrorEnabled) {
log.error("Request {} failed with code {}", backgroundImage, response.code)
}
return
}
val body = response.body
if (body != null) {
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
}
IOUtils.closeQuietly(body)
return@use tempFile
}
}
val backgroundImageFile = File(backgroundImage)
if (backgroundImageFile.isDirectory) {
val files = FileUtils.listFiles(backgroundImageFile, arrayOf("png", "jpg", "jpeg"), false)
if (files.isNotEmpty()) {
for (i in 0 until files.size) {
file = files.randomOrNull()
if (file == null) break
if (file.absolutePath == imageFilepath) continue
}
} else {
synchronized(this) {
imageFilepath = StringUtils.EMPTY
bufferedImage = null
refreshGlassPanes()
}
}
} else if (backgroundImageFile.isFile) {
file = backgroundImageFile
}
if (file == null || imageFilepath == file.absolutePath) {
return
}
bufferedImage = file.inputStream().use { ImageIO.read(it) }
imageFilepath = file.absolutePath
refreshGlassPanes()
}
private fun refreshGlassPanes() {
SwingUtilities.invokeLater {
glassPanes.removeIf {
val glassPane = it.get()
glassPane?.repaint()
glassPane == null
}
}
}
override fun dispose() {
}
override fun setGlassPane(window: Window, glassPane: JComponent) {
glassPanes.add(WeakReference(glassPane))
}
override fun ready() {
swingCoroutineScope.launch(Dispatchers.IO) {
while (isActive) {
runCatching { refreshBackgroundImage() }.onFailure {
if (log.isErrorEnabled) {
log.error("Refresh failed", it)
}
}
delay(max(Appearance.interval, 30).seconds)
}
}
}
}

View File

@@ -0,0 +1,149 @@
package app.termora.plugins.bg
import app.termora.*
import app.termora.OptionsPane.Companion.FORM_MARGIN
import com.formdev.flatlaf.extras.components.FlatButton
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.exception.ExceptionUtils
import org.slf4j.LoggerFactory
import java.awt.BorderLayout
import java.io.File
import java.nio.file.StandardCopyOption
import javax.swing.*
import javax.swing.event.DocumentEvent
class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
companion object {
private val log = LoggerFactory.getLogger(BackgroundOption::class.java)
}
private val owner get() = SwingUtilities.getWindowAncestor(this)
val backgroundImageTextField = OutlineTextField()
val intervalSpinner = NumberSpinner(360, minimum = 30, maximum = 86400)
private val backgroundButton = JButton(Icons.folder)
private val backgroundClearButton = FlatButton()
init {
initView()
initEvents()
}
private fun initView() {
backgroundImageTextField.isEditable = false
backgroundImageTextField.trailingComponent = backgroundButton
backgroundImageTextField.text = Appearance.backgroundImage
backgroundImageTextField.document.addDocumentListener(object : DocumentAdaptor() {
override fun changedUpdate(e: DocumentEvent) {
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
}
})
backgroundClearButton.isFocusable = false
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
backgroundClearButton.icon = Icons.delete
backgroundClearButton.buttonType = FlatButton.ButtonType.toolBarButton
intervalSpinner.value = Appearance.interval
add(getFormPanel(), BorderLayout.CENTER)
}
private fun initEvents() {
backgroundButton.addActionListener {
val chooser = FileChooser()
chooser.osxAllowedFileTypes = listOf("png", "jpg", "jpeg")
chooser.allowsMultiSelection = false
chooser.win32Filters.add(Pair("Image files", listOf("png", "jpg", "jpeg")))
chooser.fileSelectionMode = JFileChooser.FILES_AND_DIRECTORIES
chooser.showOpenDialog(owner).thenAccept {
if (it.isNotEmpty()) {
onSelectedBackgroundImage(it.first())
}
}
}
backgroundClearButton.addActionListener {
BackgroundManager.getInstance().clearBackgroundImage()
backgroundImageTextField.text = StringUtils.EMPTY
}
intervalSpinner.addChangeListener {
val value = intervalSpinner.value
if (value is Int) {
Appearance.interval = value
}
}
}
private fun onSelectedBackgroundImage(file: File) {
try {
if (file.isFile) {
val destFile = FileUtils.getFile(Application.getBaseDataDir(), "background", file.name)
FileUtils.forceMkdirParent(destFile)
FileUtils.deleteQuietly(destFile)
FileUtils.copyFile(file, destFile, StandardCopyOption.REPLACE_EXISTING)
BackgroundManager.getInstance().setBackgroundImage(destFile.absolutePath)
} else if (file.isDirectory) {
BackgroundManager.getInstance().setBackgroundImage(file.absolutePath)
}
backgroundImageTextField.text = file.absolutePath
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
SwingUtilities.invokeLater {
OptionPane.showMessageDialog(
owner,
ExceptionUtils.getRootCauseMessage(e),
messageType = JOptionPane.ERROR_MESSAGE
)
}
}
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.imageGray
}
override fun getTitle(): String {
return BGI18n.getString("termora.plugins.bg.background-image")
}
override fun getJComponent(): JComponent {
return this
}
private fun getFormPanel(): JPanel {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default",
"pref, $FORM_MARGIN, pref"
)
var rows = 1
val step = 2
val builder = FormBuilder.create().layout(layout)
val bgClearBox = Box.createHorizontalBox()
bgClearBox.add(backgroundClearButton)
builder.add("${BGI18n.getString("termora.plugins.bg.background-image")}:").xy(1, rows)
.add(backgroundImageTextField).xy(3, rows)
.add(bgClearBox).xy(5, rows)
.apply { rows += step }
builder.add("${BGI18n.getString("termora.plugins.bg.interval")}:").xy(1, rows)
.add(intervalSpinner).xy(3, rows)
.apply { rows += step }
return builder.build()
}
}

View File

@@ -0,0 +1,14 @@
package app.termora.plugins.bg
import app.termora.OptionsPane
import app.termora.SettingsOptionExtension
class BackgroundSettingsOptionExtension private constructor(): SettingsOptionExtension {
companion object {
val instance by lazy { BackgroundSettingsOptionExtension() }
}
override fun createSettingsOption(): OptionsPane.Option {
return BackgroundOption()
}
}

View File

@@ -0,0 +1,23 @@
<termora-plugin>
<id>bg</id>
<name>Customize Background</name>
<version>${projectVersion}</version>
<entry>app.termora.plugins.bg.BGPlugin</entry>
<termora-version since=">=${rootProjectVersion}" until=""/>
<descriptions>
<description>Customize application background</description>
<description language="zh_CN">自定义应用程序背景</description>
<description language="zh_TW">自訂應用程式背景</description>
</descriptions>
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
</termora-plugin>

View File

@@ -0,0 +1,6 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#3574F0"/>
<path d="M2.5 9.33566L4.1822 7.66899C4.56052 7.29415 5.16625 7.28159 5.55979 7.64043L11.9861 13.5" stroke="#3574F0"/>
<circle cx="10" cy="6" r="1.5" stroke="#3574F0"/>
</svg>

After

Width:  |  Height:  |  Size: 472 B

View File

@@ -0,0 +1,6 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#548AF7"/>
<path d="M2.5 9.33566L4.1822 7.66899C4.56052 7.29415 5.16625 7.28159 5.55979 7.64043L11.9861 13.5" stroke="#548AF7"/>
<circle cx="10" cy="6" r="1.5" stroke="#548AF7"/>
</svg>

After

Width:  |  Height:  |  Size: 472 B

View File

@@ -0,0 +1,2 @@
termora.plugins.bg.interval=Interval
termora.plugins.bg.background-image=Background Image

View File

@@ -0,0 +1,2 @@
termora.plugins.bg.background-image=背景图
termora.plugins.bg.interval=切换间隔

View File

@@ -0,0 +1,2 @@
termora.plugins.bg.background-image=背景圖
termora.plugins.bg.interval=切換間隔

89
plugins/common.gradle.kts Normal file
View File

@@ -0,0 +1,89 @@
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
tasks.withType<Jar> {
manifest {
attributes(
"Implementation-Title" to project.name,
"Implementation-Version" to project.version,
)
}
from("${rootProject.projectDir}/plugins/LICENSE") {
into("META-INF")
}
from("${rootProject.projectDir}/plugins/THIRDPARTY") {
into("META-INF")
}
// archiveBaseName.set("${project.name}-${rootProject.version}")
destinationDirectory.set(file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}"))
}
tasks.named<Copy>("processResources") {
filesMatching("META-INF/plugin.xml") {
expand(
"projectName" to project.name,
"projectVersion" to project.version,
"rootProjectVersion" to rootProject.version,
)
}
}
tasks.register<Copy>("copy-dependencies") {
from(configurations.getByName("runtimeClasspath").filterNot {
it.name.startsWith("kotlin-stdlib") || it.name.startsWith("annotations")
})
into("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}")
}
tasks.named("build") {
dependsOn("copy-dependencies")
}
tasks.register("run-plugin") {
dependsOn("build")
doLast {
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
val runtimeCompileOnly by configurations.creating { extendsFrom(configurations.getByName("compileOnly")) }
val mainClass = "app.termora.MainKt"
val executable = System.getProperty("java.home") + "/bin/java"
val classpath = (configurations.getByName("compileClasspath") + configurations.getByName("runtimeClasspath")
+ runtimeCompileOnly).joinToString(if (os.isWindows) ";" else ":")
val commands = mutableListOf<String>(executable)
commands.add("-Dapp-version=${rootProject.version}")
commands.add("--add-exports java.base/sun.nio.ch=ALL-UNNAMED")
if (os.isMacOsX) {
// NSWindow
commands.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
commands.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
commands.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
commands.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
commands.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
commands.add("-Dapple.awt.application.appearance=system")
}
commands.addAll(listOf("-cp", classpath, mainClass))
exec {
commandLine = commands
environment(
"TERMORA_PLUGIN_DIRECTORY" to file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/"),
"TERMORA_BASE_DATA_DIR" to "${layout.buildDirectory.get().asFile.absolutePath}/data",
)
}
}
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
tasks.named("clean") {
doLast {
file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}").deleteRecursively()
}
}

View File

@@ -0,0 +1,16 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.3"
dependencies {
testImplementation(kotlin("test"))
implementation("com.qcloud:cos_api:5.6.247")
compileOnly(project(":"))
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -0,0 +1,69 @@
package app.termora.plugins.cos
import app.termora.AuthenticationType
import app.termora.Proxy
import app.termora.ProxyType
import com.qcloud.cos.COSClient
import com.qcloud.cos.ClientConfig
import com.qcloud.cos.auth.BasicCOSCredentials
import com.qcloud.cos.model.Bucket
import com.qcloud.cos.region.Region
import java.io.Closeable
import java.util.concurrent.atomic.AtomicBoolean
class COSClientHandler(
private val cred: BasicCOSCredentials,
private val proxy: Proxy,
val buckets: List<Bucket>
) : Closeable {
companion object {
fun createCOSClient(cred: BasicCOSCredentials, region: String, proxy: Proxy): COSClient {
val clientConfig = ClientConfig()
if (region.isNotBlank()) {
clientConfig.region = Region(region)
}
clientConfig.isPrintShutdownStackTrace = false
if (proxy.type == ProxyType.HTTP) {
clientConfig.httpProxyIp = proxy.host
clientConfig.httpProxyPort = proxy.port
if (proxy.authenticationType == AuthenticationType.Password) {
clientConfig.proxyPassword = proxy.password
clientConfig.proxyUsername = proxy.username
}
}
return COSClient(cred, clientConfig)
}
}
/**
* key: Region
* value: Client
*/
private val clients = mutableMapOf<String, COSClient>()
private val closed = AtomicBoolean(false)
fun getClientForBucket(bucket: String): COSClient {
if (closed.get()) throw IllegalStateException("Client already closed")
synchronized(this) {
val bucket = buckets.first { it.name == bucket }
if (clients.containsKey(bucket.location)) {
return clients.getValue(bucket.location)
}
clients[bucket.location] = createCOSClient(cred, bucket.location, proxy)
return clients.getValue(bucket.location)
}
}
override fun close() {
if (closed.compareAndSet(false, true)) {
synchronized(this) {
clients.forEach { it.value.shutdown() }
clients.clear()
}
}
}
}

View File

@@ -0,0 +1,16 @@
package app.termora.plugins.cos
import app.termora.transfer.s3.S3FileSystem
import org.apache.commons.io.IOUtils
/**
* key: region
*/
class COSFileSystem(private val clientHandler: COSClientHandler) :
S3FileSystem(COSFileSystemProvider(clientHandler)) {
override fun close() {
IOUtils.closeQuietly(clientHandler)
super.close()
}
}

View File

@@ -0,0 +1,142 @@
package app.termora.plugins.cos
import app.termora.transfer.s3.S3FileAttributes
import app.termora.transfer.s3.S3FileSystemProvider
import app.termora.transfer.s3.S3Path
import com.qcloud.cos.model.ListObjectsRequest
import com.qcloud.cos.model.ObjectMetadata
import com.qcloud.cos.model.PutObjectRequest
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import java.io.InputStream
import java.io.OutputStream
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.nio.file.AccessMode
import java.nio.file.NoSuchFileException
import java.util.concurrent.atomic.AtomicReference
import kotlin.io.path.absolutePathString
class COSFileSystemProvider(private val clientHandler: COSClientHandler) : S3FileSystemProvider() {
override fun getScheme(): String? {
return "cos"
}
override fun getOutputStream(path: S3Path): OutputStream {
return createStreamer(path)
}
override fun getInputStream(path: S3Path): InputStream {
val client = clientHandler.getClientForBucket(path.bucketName)
return client.getObject(path.bucketName, path.objectName).objectContent
}
private fun createStreamer(path: S3Path): OutputStream {
val pis = PipedInputStream()
val pos = PipedOutputStream(pis)
val exception = AtomicReference<Throwable>()
val thread = Thread.ofVirtual().start {
try {
val client = clientHandler.getClientForBucket(path.bucketName)
client.putObject(PutObjectRequest(path.bucketName, path.objectName, pis, ObjectMetadata()))
} catch (e: Exception) {
exception.set(e)
} finally {
IOUtils.closeQuietly(pis)
}
}
return object : OutputStream() {
override fun write(b: Int) {
val exception = exception.get()
if (exception != null) throw exception
pos.write(b)
}
override fun close() {
pos.close()
if (thread.isAlive) thread.join()
}
}
}
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
val paths = mutableListOf<S3Path>()
// root
if (path.isRoot) {
for (bucket in clientHandler.buckets) {
val p = path.resolve(bucket.name)
p.attributes = S3FileAttributes(
directory = true,
lastModifiedTime = bucket.creationDate.toInstant().toEpochMilli()
)
paths.add(p)
}
return paths
}
var nextMarker = StringUtils.EMPTY
val maxKeys = 100
val bucketName = path.bucketName
while (true) {
val request = ListObjectsRequest()
.withBucketName(bucketName)
.withMaxKeys(maxKeys)
.withDelimiter(path.fileSystem.separator)
if (path.objectName.isNotBlank()) request.withPrefix(path.objectName + path.fileSystem.separator)
if (nextMarker.isNotBlank()) request.withMarker(nextMarker)
val objectListing = clientHandler.getClientForBucket(bucketName).listObjects(request)
for (e in objectListing.commonPrefixes) {
val p = path.bucket.resolve(e)
p.attributes = p.attributes.copy(directory = true)
delete(p)
paths.add(p)
}
for (e in objectListing.objectSummaries) {
val p = path.bucket.resolve(e.key)
p.attributes = p.attributes.copy(
regularFile = true, size = e.size,
lastModifiedTime = e.lastModified.time
)
paths.add(p)
}
if (objectListing.isTruncated.not()) {
break
}
nextMarker = objectListing.nextMarker
}
paths.addAll(directories[path.absolutePathString()] ?: emptyList())
return paths
}
override fun delete(path: S3Path, isDirectory: Boolean) {
if (isDirectory.not())
clientHandler.getClientForBucket(path.bucketName).deleteObject(path.bucketName, path.objectName)
}
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
try {
val client = clientHandler.getClientForBucket(path.bucketName)
if (client.doesObjectExist(path.bucketName, path.objectName).not()) {
throw NoSuchFileException(path.objectName)
}
} catch (e: Exception) {
if (e is NoSuchFileException) throw e
throw NoSuchFileException(e.message)
}
}
}

View File

@@ -0,0 +1,313 @@
package app.termora.plugins.cos
import app.termora.*
import app.termora.plugin.internal.BasicProxyOption
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.ui.FlatTextBorder
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import org.apache.commons.lang3.StringUtils
import java.awt.BorderLayout
import java.awt.KeyboardFocusManager
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.*
class COSHostOptionsPane : OptionsPane() {
private val generalOption = GeneralOption()
private val proxyOption = BasicProxyOption(listOf(ProxyType.HTTP))
private val sftpOption = SFTPOption()
init {
addOption(generalOption)
addOption(proxyOption)
addOption(sftpOption)
}
fun getHost(): Host {
val name = generalOption.nameTextField.text
val protocol = COSProtocolProvider.PROTOCOL
val port = 0
var authentication = Authentication.Companion.No
var proxy = Proxy.Companion.No
val authenticationType = AuthenticationType.Password
authentication = authentication.copy(
type = authenticationType,
password = String(generalOption.passwordTextField.password)
)
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
proxy = proxy.copy(
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
host = proxyOption.proxyHostTextField.text,
username = proxyOption.proxyUsernameTextField.text,
password = String(proxyOption.proxyPasswordTextField.password),
port = proxyOption.proxyPortTextField.value as Int,
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
)
}
val options = Options.Default.copy(
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
extras = mutableMapOf(
"cos.delimiter" to generalOption.delimiterTextField.text,
)
)
return Host(
name = name,
protocol = protocol,
port = port,
username = generalOption.usernameTextField.text,
authentication = authentication,
proxy = proxy,
sort = System.currentTimeMillis(),
remark = generalOption.remarkTextArea.text,
options = options,
)
}
fun setHost(host: Host) {
generalOption.nameTextField.text = host.name
generalOption.usernameTextField.text = host.username
generalOption.remarkTextArea.text = host.remark
generalOption.passwordTextField.text = host.authentication.password
generalOption.delimiterTextField.text = host.options.extras["cos.delimiter"] ?: StringUtils.EMPTY
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
proxyOption.proxyHostTextField.text = host.proxy.host
proxyOption.proxyPasswordTextField.text = host.proxy.password
proxyOption.proxyUsernameTextField.text = host.proxy.username
proxyOption.proxyPortTextField.value = host.proxy.port
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
}
fun validateFields(): Boolean {
val host = getHost()
// general
if (validateField(generalOption.nameTextField)) {
return false
}
if (validateField(generalOption.usernameTextField)) {
return false
}
if (host.authentication.type == AuthenticationType.Password) {
if (validateField(generalOption.passwordTextField)) {
return false
}
}
// proxy
if (host.proxy.type != ProxyType.No) {
if (validateField(proxyOption.proxyHostTextField)
) {
return false
}
if (host.proxy.authenticationType != AuthenticationType.No) {
if (validateField(proxyOption.proxyUsernameTextField)
|| validateField(proxyOption.proxyPasswordTextField)
) {
return false
}
}
}
return true
}
/**
* 返回 true 表示有错误
*/
private fun validateField(textField: JTextField): Boolean {
if (textField.isEnabled && textField.text.isBlank()) {
setOutlineError(textField)
return true
}
return false
}
private fun setOutlineError(c: JComponent) {
selectOptionJComponent(c)
c.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
c.requestFocusInWindow()
}
private inner class GeneralOption : JPanel(BorderLayout()), Option {
val nameTextField = OutlineTextField(128)
val usernameTextField = OutlineTextField(128)
val passwordTextField = OutlinePasswordField(255)
val remarkTextArea = FixedLengthTextArea(512)
// val regionComboBox = OutlineComboBox<String>()
val delimiterTextField = OutlineTextField(128)
init {
initView()
initEvents()
}
private fun initView() {
/*regionComboBox.addItem("ap-beijing-1")
regionComboBox.addItem("ap-beijing")
regionComboBox.addItem("ap-nanjing")
regionComboBox.addItem("ap-shanghai")
regionComboBox.addItem("ap-guangzhou")
regionComboBox.addItem("ap-chengdu")
regionComboBox.addItem("ap-chongqing")
regionComboBox.addItem("ap-shenzhen-fsi")
regionComboBox.addItem("ap-shanghai-fsi")
regionComboBox.addItem("ap-beijing-fsi")
regionComboBox.addItem("ap-hongkong")
regionComboBox.addItem("ap-singapore")
regionComboBox.addItem("ap-jakarta")
regionComboBox.addItem("ap-seoul")
regionComboBox.addItem("ap-bangkok")
regionComboBox.addItem("ap-tokyo")
regionComboBox.addItem("na-siliconvalley")
regionComboBox.addItem("na-ashburn")
regionComboBox.addItem("sa-saopaulo")
regionComboBox.addItem("eu-frankfurt")
regionComboBox.isEditable = true*/
delimiterTextField.text = "/"
delimiterTextField.isEditable = false
add(getCenterComponent(), BorderLayout.CENTER)
}
private fun initEvents() {
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
removeComponentListener(this)
}
})
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.settings
}
override fun getTitle(): String {
return I18n.getString("termora.new-host.general")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
"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"
)
remarkTextArea.setFocusTraversalKeys(
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
)
remarkTextArea.setFocusTraversalKeys(
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
)
remarkTextArea.rows = 8
remarkTextArea.lineWrap = true
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
var rows = 1
val step = 2
val panel = FormBuilder.create().layout(layout)
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
.add("SecretId:").xy(1, rows)
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
.add("SecretKey:").xy(1, rows)
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
.add("Delimiter:").xy(1, rows)
.add(delimiterTextField).xyw(3, rows, 5).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
.xyw(3, rows, 5).apply { rows += step }
.build()
return panel
}
}
private inner class SFTPOption : JPanel(BorderLayout()), Option {
val defaultDirectoryField = OutlineTextField(255)
init {
initView()
initEvents()
}
private fun initView() {
add(getCenterComponent(), BorderLayout.CENTER)
}
private fun initEvents() {
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.folder
}
override fun getTitle(): String {
return I18n.getString("termora.transport.sftp")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
var rows = 1
val step = 2
val panel = FormBuilder.create().layout(layout)
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
.build()
return panel
}
}
}

View File

@@ -0,0 +1,33 @@
package app.termora.plugins.cos
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.PaidPlugin
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProviderExtension
class COSPlugin : PaidPlugin {
private val support = ExtensionSupport()
init {
support.addExtension(ProtocolProviderExtension::class.java) { COSProtocolProviderExtension.instance }
support.addExtension(ProtocolHostPanelExtension::class.java) { COSProtocolHostPanelExtension.instance }
}
override fun getAuthor(): String {
return "TermoraDev"
}
override fun getName(): String {
return "Tencent COS"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,36 @@
package app.termora.plugins.cos
import app.termora.Disposer
import app.termora.Host
import app.termora.protocol.ProtocolHostPanel
import java.awt.BorderLayout
class COSProtocolHostPanel : ProtocolHostPanel() {
private val pane = COSHostOptionsPane()
init {
initView()
initEvents()
}
private fun initView() {
add(pane, BorderLayout.CENTER)
Disposer.register(this, pane)
}
private fun initEvents() {}
override fun getHost(): Host {
return pane.getHost()
}
override fun setHost(host: Host) {
pane.setHost(host)
}
override fun validateFields(): Boolean {
return pane.validateFields()
}
}

View File

@@ -0,0 +1,20 @@
package app.termora.plugins.cos
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
class COSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
companion object {
val instance by lazy { COSProtocolHostPanelExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return COSProtocolProvider.instance
}
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return COSProtocolHostPanel()
}
}

View File

@@ -0,0 +1,52 @@
package app.termora.plugins.cos
import app.termora.DynamicIcon
import app.termora.Icons
import app.termora.protocol.PathHandler
import app.termora.protocol.PathHandlerRequest
import app.termora.protocol.TransferProtocolProvider
import com.qcloud.cos.ClientConfig
import com.qcloud.cos.auth.BasicCOSCredentials
import com.qcloud.cos.model.Bucket
import org.apache.commons.lang3.StringUtils
class COSProtocolProvider private constructor() : TransferProtocolProvider {
companion object {
val instance by lazy { COSProtocolProvider() }
const val PROTOCOL = "COS"
}
override fun getProtocol(): String {
return PROTOCOL
}
override fun getIcon(width: Int, height: Int): DynamicIcon {
return Icons.tencent
}
override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
val host = requester.host
val secretId = host.username
val secretKey = host.authentication.password
val cred = BasicCOSCredentials(secretId, secretKey)
val clientConfig = ClientConfig()
clientConfig.isPrintShutdownStackTrace = false
val cosClient = COSClientHandler.createCOSClient(cred, StringUtils.EMPTY, host.proxy)
val buckets: List<Bucket>
try {
buckets = cosClient.listBuckets()
} finally {
cosClient.shutdown()
}
val defaultPath = host.options.sftpDefaultDirectory
val fs = COSFileSystem(COSClientHandler(cred, host.proxy, buckets))
return PathHandler(fs, fs.getPath(defaultPath))
}
}

View File

@@ -0,0 +1,14 @@
package app.termora.plugins.cos
import app.termora.protocol.ProtocolProvider
import app.termora.protocol.ProtocolProviderExtension
class COSProtocolProviderExtension private constructor() : ProtocolProviderExtension {
companion object {
val instance by lazy { COSProtocolProviderExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return COSProtocolProvider.instance
}
}

View File

@@ -0,0 +1,25 @@
<termora-plugin>
<id>cos</id>
<name>Tencent COS</name>
<paid/>
<version>${projectVersion}</version>
<termora-version since=">=${rootProjectVersion}" until=""/>
<entry>app.termora.plugins.cos.COSPlugin</entry>
<descriptions>
<description>Connecting to Tencent COS</description>
<description language="zh_CN">支持连接到腾讯云对象存储</description>
<description language="zh_TW">支援連接到騰訊雲物件存儲</description>
</descriptions>
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
</termora-plugin>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -0,0 +1,19 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.6"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
implementation("com.fifesoft:rsyntaxtextarea:3.6.0")
implementation("com.fifesoft:languagesupport:3.4.0")
implementation("com.fifesoft:autocomplete:3.3.2")
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -0,0 +1,94 @@
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,334 @@
package app.termora.plugins.editor
import app.termora.DocumentAdaptor
import app.termora.DynamicColor
import app.termora.EnableManager
import app.termora.Icons
import app.termora.database.DatabaseManager
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.components.FlatTextField
import com.formdev.flatlaf.extras.components.FlatToolBar
import kotlinx.serialization.json.Json
import org.apache.commons.io.FilenameUtils
import org.dom4j.io.OutputFormat
import org.dom4j.io.SAXReader
import org.dom4j.io.XMLWriter
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea
import org.fife.ui.rsyntaxtextarea.SyntaxConstants
import org.fife.ui.rsyntaxtextarea.Theme
import org.fife.ui.rtextarea.RTextScrollPane
import org.fife.ui.rtextarea.SearchContext
import org.fife.ui.rtextarea.SearchEngine
import org.slf4j.LoggerFactory
import java.awt.BorderLayout
import java.awt.Insets
import java.awt.event.ActionEvent
import java.awt.event.KeyEvent
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import java.io.File
import java.io.StringReader
import java.io.StringWriter
import javax.swing.*
import javax.swing.SwingConstants.VERTICAL
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()) {
companion object {
private val log = LoggerFactory.getLogger(EditorPanel::class.java)
}
private var text = file.readText(Charsets.UTF_8)
private val layeredPane = LayeredPane()
private val textArea = RSyntaxTextArea()
private val scrollPane = RTextScrollPane(textArea)
private val findPanel = FlatToolBar().apply { isFloatable = false }
private val toolbar = FlatToolBar().apply { isFloatable = false }
private val searchTextField = FlatTextField()
private val closeFindPanelBtn = JButton(Icons.close)
private val nextBtn = JButton(Icons.down)
private val prevBtn = JButton(Icons.up)
private val context = SearchContext()
private val softWrapBtn = JToggleButton(Icons.softWrap)
private val scrollUpBtn = JButton(Icons.scrollUp)
private val scrollEndBtn = JButton(Icons.scrollDown)
private val prettyBtn = JButton(Icons.reformatCode)
private val enableManager get() = EnableManager.getInstance()
private val prettyJson = Json {
prettyPrint = true
}
init {
initView()
initEvents()
}
private fun initView() {
textArea.font = textArea.font.deriveFont(DatabaseManager.getInstance().terminal.fontSize.toFloat())
textArea.text = text
textArea.antiAliasingEnabled = true
softWrapBtn.isSelected = enableManager.getFlag("Plugins.editor.softWrap", false)
val theme = if (FlatLaf.isLafDark())
Theme.load(javaClass.getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/dark.xml"))
else
Theme.load(javaClass.getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/idea.xml"))
theme.apply(textArea)
val extension = FilenameUtils.getExtension(file.name)?.lowercase()
textArea.syntaxEditingStyle = when (extension) {
"java" -> SyntaxConstants.SYNTAX_STYLE_JAVA
"kt" -> SyntaxConstants.SYNTAX_STYLE_KOTLIN
"properties" -> SyntaxConstants.SYNTAX_STYLE_PROPERTIES_FILE
"cpp", "c++" -> SyntaxConstants.SYNTAX_STYLE_CPLUSPLUS
"c" -> SyntaxConstants.SYNTAX_STYLE_C
"cs" -> SyntaxConstants.SYNTAX_STYLE_CSHARP
"css" -> SyntaxConstants.SYNTAX_STYLE_CSS
"html", "htm", "htmlx" -> SyntaxConstants.SYNTAX_STYLE_HTML
"js" -> SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT
"ts" -> SyntaxConstants.SYNTAX_STYLE_TYPESCRIPT
"xml", "svg" -> SyntaxConstants.SYNTAX_STYLE_XML
"yaml", "yml" -> SyntaxConstants.SYNTAX_STYLE_YAML
"sh", "shell" -> SyntaxConstants.SYNTAX_STYLE_UNIX_SHELL
"sql" -> SyntaxConstants.SYNTAX_STYLE_SQL
"bat" -> SyntaxConstants.SYNTAX_STYLE_WINDOWS_BATCH
"py" -> SyntaxConstants.SYNTAX_STYLE_PYTHON
"php" -> SyntaxConstants.SYNTAX_STYLE_PHP
"lua" -> SyntaxConstants.SYNTAX_STYLE_LUA
"less" -> SyntaxConstants.SYNTAX_STYLE_LESS
"jsp" -> SyntaxConstants.SYNTAX_STYLE_JSP
"json" -> SyntaxConstants.SYNTAX_STYLE_JSON
"ini" -> SyntaxConstants.SYNTAX_STYLE_INI
"hosts" -> SyntaxConstants.SYNTAX_STYLE_HOSTS
"go" -> SyntaxConstants.SYNTAX_STYLE_GO
"dtd" -> SyntaxConstants.SYNTAX_STYLE_DTD
"dart" -> SyntaxConstants.SYNTAX_STYLE_DART
"csv" -> SyntaxConstants.SYNTAX_STYLE_CSV
"md" -> SyntaxConstants.SYNTAX_STYLE_MARKDOWN
else -> SyntaxConstants.SYNTAX_STYLE_NONE
}
// 只有 JSON 才可以格式化
prettyBtn.isVisible = textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_JSON ||
textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_XML
textArea.discardAllEdits()
scrollPane.border = BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor)
findPanel.isVisible = false
findPanel.isOpaque = true
findPanel.background = DynamicColor("window")
searchTextField.background = findPanel.background
searchTextField.padding = Insets(0, 4, 0, 0)
searchTextField.border = BorderFactory.createEmptyBorder()
findPanel.add(searchTextField)
findPanel.add(prevBtn)
findPanel.add(nextBtn)
findPanel.add(closeFindPanelBtn)
findPanel.border = BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 1, 1, 0, DynamicColor.BorderColor),
BorderFactory.createEmptyBorder(2, 2, 2, 2)
)
toolbar.orientation = VERTICAL
toolbar.add(scrollUpBtn)
toolbar.add(prettyBtn)
toolbar.add(softWrapBtn)
toolbar.add(scrollEndBtn)
val viewPanel = JPanel(BorderLayout())
viewPanel.add(scrollPane, BorderLayout.CENTER)
viewPanel.add(toolbar, BorderLayout.EAST)
viewPanel.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
layeredPane.add(findPanel, JLayeredPane.MODAL_LAYER as Any)
layeredPane.add(viewPanel, JLayeredPane.DEFAULT_LAYER as Any)
add(layeredPane, BorderLayout.CENTER)
}
private fun initEvents() {
window.addWindowListener(object : WindowAdapter() {
override fun windowOpened(e: WindowEvent?) {
scrollPane.verticalScrollBar.value = 0
window.removeWindowListener(this)
}
})
softWrapBtn.addActionListener {
enableManager.getFlag("Plugins.editor.softWrap", softWrapBtn.isSelected)
textArea.lineWrap = softWrapBtn.isSelected
}
scrollUpBtn.addActionListener { scrollPane.verticalScrollBar.value = 0 }
scrollEndBtn.addActionListener { scrollPane.verticalScrollBar.value = scrollPane.verticalScrollBar.maximum }
textArea.inputMap.put(
KeyStroke.getKeyStroke(KeyEvent.VK_S, toolkit.menuShortcutKeyMaskEx),
"Save"
)
textArea.inputMap.put(
KeyStroke.getKeyStroke(KeyEvent.VK_F, toolkit.menuShortcutKeyMaskEx),
"Find"
)
textArea.inputMap.put(
KeyStroke.getKeyStroke(KeyEvent.VK_F, toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK),
"Format"
)
searchTextField.inputMap.put(
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
"Esc"
)
searchTextField.actionMap.put("Esc", object : AbstractAction("Esc") {
override fun actionPerformed(e: ActionEvent) {
textArea.clearMarkAllHighlights()
textArea.requestFocusInWindow()
findPanel.isVisible = false
}
})
closeFindPanelBtn.addActionListener { searchTextField.actionMap.get("Esc").actionPerformed(it) }
textArea.actionMap.put("Save", object : AbstractAction("Save") {
override fun actionPerformed(e: ActionEvent) {
file.writeText(textArea.text, Charsets.UTF_8)
text = textArea.text
window.title = file.name
}
})
textArea.actionMap.put("Format", object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
format()
}
})
textArea.actionMap.put("Find", object : AbstractAction("Find") {
override fun actionPerformed(e: ActionEvent) {
findPanel.isVisible = true
searchTextField.selectAll()
searchTextField.requestFocusInWindow()
}
})
textArea.document.addDocumentListener(object : DocumentAdaptor() {
override fun changedUpdate(e: DocumentEvent) {
window.title = if (textArea.text.hashCode() != text.hashCode()) {
"${file.name} *"
} else {
file.name
}
}
})
searchTextField.document.addDocumentListener(object : DocumentAdaptor() {
override fun changedUpdate(e: DocumentEvent) {
search()
}
})
searchTextField.addActionListener { nextBtn.doClick(0) }
prettyBtn.addActionListener(textArea.actionMap.get("Format"))
prevBtn.addActionListener { search(false) }
nextBtn.addActionListener { search(true) }
}
private fun format() {
val vertical = scrollPane.verticalScrollBar.value
val horizontal = scrollPane.horizontalScrollBar.value
val caretPosition = textArea.caretPosition
val c = if (textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_JSON) {
runCatching {
val json = prettyJson.parseToJsonElement(textArea.text)
textArea.text = prettyJson.encodeToString(json)
}.onFailure {
if (log.isErrorEnabled) {
log.error(it.message, it)
}
}
} else if (textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_XML) {
runCatching {
val document = SAXReader().read(StringReader(textArea.text))
val sw = StringWriter()
val writer = XMLWriter(sw, OutputFormat.createPrettyPrint())
writer.write(document)
textArea.text = sw.toString()
}.onFailure {
if (log.isErrorEnabled) {
log.error(it.message, it)
}
}
} else {
null
} ?: return
c.onSuccess {
SwingUtilities.invokeLater {
scrollPane.verticalScrollBar.value = min(
vertical,
scrollPane.verticalScrollBar.maximum
)
scrollPane.horizontalScrollBar.value = min(
horizontal,
scrollPane.horizontalScrollBar.maximum
)
if (caretPosition >= 0 && caretPosition < textArea.document.length) {
textArea.caretPosition = caretPosition
}
}
}
}
private fun search(searchForward: Boolean = true) {
textArea.clearMarkAllHighlights()
val text: String = searchTextField.getText()
if (text.isEmpty()) return
context.searchFor = text
context.searchForward = searchForward
context.wholeWord = false
val result = SearchEngine.find(textArea, context)
prevBtn.isEnabled = result.markedCount > 0
nextBtn.isEnabled = result.markedCount > 0
}
fun changes() = text != textArea.text
private inner class LayeredPane : JLayeredPane() {
override fun doLayout() {
synchronized(treeLock) {
for (c in components) {
if (c == findPanel) {
val height = max(findPanel.preferredSize.height, findPanel.height)
val x = width / 2
c.setBounds(x, 1, width - x, height)
} else {
c.setBounds(0, 0, width, height)
}
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
package app.termora.plugins.editor
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.Plugin
import app.termora.transfer.TransportEditFileExtension
class EditorPlugin : Plugin {
private val support = ExtensionSupport()
init {
support.addExtension(TransportEditFileExtension::class.java) { MyTransportEditFileExtension.instance }
}
override fun getAuthor(): String {
return "TermoraDev"
}
override fun getName(): String {
return "SFTP File Editor"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,20 @@
package app.termora.plugins.editor
import app.termora.Disposable
import app.termora.Disposer
import app.termora.transfer.TransportEditFileExtension
import java.awt.Window
import java.nio.file.Path
import javax.swing.SwingUtilities
class MyTransportEditFileExtension private constructor() : TransportEditFileExtension {
companion object {
val instance = MyTransportEditFileExtension()
}
override fun edit(owner: Window, path: Path): Disposable {
val disposable = Disposer.newDisposable()
SwingUtilities.invokeLater { EditorDialog(path, owner, disposable).isVisible = true }
return disposable
}
}

View File

@@ -0,0 +1,22 @@
<termora-plugin>
<id>editor</id>
<name>SFTP File Editor</name>
<version>${projectVersion}</version>
<termora-version since=">=${rootProjectVersion}" until=""/>
<entry>app.termora.plugins.editor.EditorPlugin</entry>
<descriptions>
<description>Edit SFTP files using the built-in editor</description>
<description language="zh_CN">使用内置编辑器编辑 SFTP 文件</description>
<description language="zh_TW">使用內建編輯器編輯 SFTP 文件</description>
</descriptions>
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
</termora-plugin>

View File

@@ -0,0 +1,6 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V7.47774C14.2142 6.80872 13.0333 6.84543 12.2909 7.58786L7 12.8787V14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" />
<path d="M8.09379 5H13C13.5523 5 14 5.44772 14 6V7.02381C14.3594 7.07711 14.7072 7.22842 15 7.47774V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H7V13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823L8.09379 5Z" fill="#6C707E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4122 8.29497C14.0217 7.90444 13.3885 7.90444 12.998 8.29497L11.6466 9.64633L8 13.2929V16H10.7071L15.7051 11.0021C16.0956 10.6116 16.0956 9.97839 15.7051 9.58786L14.4122 8.29497ZM14 11.2929L14.998 10.295L13.7051 9.00208L12.7071 10L14 11.2929ZM12 10.7072L13.2929 12L10.2929 15H9V13.7072L12 10.7072Z" fill="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,6 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V7.47774C14.2142 6.80872 13.0333 6.84543 12.2909 7.58786L7 12.8787V14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" />
<path d="M8.09379 5H13C13.5523 5 14 5.44772 14 6V7.02381C14.3594 7.07711 14.7072 7.22842 15 7.47774V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H7V13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823L8.09379 5Z" fill="#CED0D6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4122 8.29497C14.0217 7.90444 13.3885 7.90444 12.998 8.29497L11.6466 9.64633L8 13.2929V16H10.7071L15.7051 11.0021C16.0956 10.6116 16.0956 9.97839 15.7051 9.58786L14.4122 8.29497ZM14 11.2929L14.998 10.295L13.7051 9.00208L12.7071 10L14 11.2929ZM12 10.7072L13.2929 12L10.2929 15H9V13.7072L12 10.7072Z" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,107 @@
package app.termora.plugins.editor;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import org.fife.ui.rtextarea.*;
import org.fife.ui.rsyntaxtextarea.*;
/**
* A simple example showing how to do search and replace in a RSyntaxTextArea.
* The toolbar isn't very user-friendly, but this is just to show you how to use
* the API.<p>
*
* This example uses RSyntaxTextArea 2.5.6.
*/
public class FindAndReplaceDemo extends JFrame implements ActionListener {
private static final long serialVersionUID = 1L;
private RSyntaxTextArea textArea;
private JTextField searchField;
private JCheckBox regexCB;
private JCheckBox matchCaseCB;
public FindAndReplaceDemo() {
JPanel cp = new JPanel(new BorderLayout());
textArea = new RSyntaxTextArea(20, 60);
textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
textArea.setCodeFoldingEnabled(true);
RTextScrollPane sp = new RTextScrollPane(textArea);
cp.add(sp);
// Create a toolbar with searching options.
JToolBar toolBar = new JToolBar();
searchField = new JTextField(30);
toolBar.add(searchField);
final JButton nextButton = new JButton("Find Next");
nextButton.setActionCommand("FindNext");
nextButton.addActionListener(this);
toolBar.add(nextButton);
searchField.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
nextButton.doClick(0);
}
});
JButton prevButton = new JButton("Find Previous");
prevButton.setActionCommand("FindPrev");
prevButton.addActionListener(this);
toolBar.add(prevButton);
regexCB = new JCheckBox("Regex");
toolBar.add(regexCB);
matchCaseCB = new JCheckBox("Match Case");
toolBar.add(matchCaseCB);
cp.add(toolBar, BorderLayout.NORTH);
setContentPane(cp);
setTitle("Find and Replace Demo");
setDefaultCloseOperation(EXIT_ON_CLOSE);
pack();
setLocationRelativeTo(null);
}
public void actionPerformed(ActionEvent e) {
// "FindNext" => search forward, "FindPrev" => search backward
String command = e.getActionCommand();
boolean forward = "FindNext".equals(command);
// Create an object defining our search parameters.
SearchContext context = new SearchContext();
String text = searchField.getText();
if (text.length() == 0) {
return;
}
context.setSearchFor(text);
context.setMatchCase(matchCaseCB.isSelected());
context.setRegularExpression(regexCB.isSelected());
context.setSearchForward(forward);
context.setWholeWord(false);
boolean found = SearchEngine.find(textArea, context).wasFound();
if (!found) {
JOptionPane.showMessageDialog(this, "Text not found");
}
}
public static void main(String[] args) {
// Start all Swing applications on the EDT.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {
String laf = UIManager.getSystemLookAndFeelClassName();
UIManager.setLookAndFeel(laf);
} catch (Exception e) { /* never happens */ }
FindAndReplaceDemo demo = new FindAndReplaceDemo();
demo.setVisible(true);
demo.textArea.requestFocusInWindow();
}
});
}
}

View File

@@ -0,0 +1,15 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.1"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -0,0 +1,41 @@
package app.termora.plugins.ftp
import org.apache.commons.vfs2.Capability
import org.apache.commons.vfs2.FileName
import org.apache.commons.vfs2.FileSystem
import org.apache.commons.vfs2.FileSystemOptions
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
class FTPFileProvider private constructor() : AbstractOriginatingFileProvider() {
companion object {
val instance by lazy { FTPFileProvider() }
val capabilities = listOf(
Capability.CREATE,
Capability.DELETE,
Capability.RENAME,
Capability.GET_TYPE,
Capability.LIST_CHILDREN,
Capability.READ_CONTENT,
Capability.URI,
Capability.WRITE_CONTENT,
Capability.GET_LAST_MODIFIED,
Capability.SET_LAST_MODIFIED_FILE,
Capability.RANDOM_ACCESS_READ,
Capability.APPEND_CONTENT
)
}
override fun getCapabilities(): Collection<Capability> {
return FTPFileProvider.capabilities
}
override fun doCreateFileSystem(
rootFileName: FileName,
fileSystemOptions: FileSystemOptions
): FileSystem? {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,35 @@
package app.termora.plugins.ftp
import app.termora.DynamicIcon
import app.termora.I18n
import app.termora.Icons
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.PaidPlugin
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProviderExtension
class FTPPlugin : PaidPlugin {
private val support = ExtensionSupport()
init {
support.addExtension(ProtocolProviderExtension::class.java) { FTPProtocolProviderExtension.Companion.instance }
support.addExtension(ProtocolHostPanelExtension::class.java) { FTPProtocolHostPanelExtension.Companion.instance }
}
override fun getAuthor(): String {
return "TermoraDev"
}
override fun getName(): String {
return "FTP"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,22 @@
package app.termora.plugins.ftp
import app.termora.Host
import app.termora.protocol.ProtocolHostPanel
import org.apache.commons.lang3.StringUtils
class FTPProtocolHostPanel : ProtocolHostPanel() {
override fun getHost(): Host {
return Host(
name = StringUtils.EMPTY,
protocol = FTPProtocolProvider.PROTOCOL
)
}
override fun setHost(host: Host) {
}
override fun validateFields(): Boolean {
return true
}
}

View File

@@ -0,0 +1,19 @@
package app.termora.plugins.ftp
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
class FTPProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
companion object {
val instance by lazy { FTPProtocolHostPanelExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return FTPProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
return FTPProtocolHostPanel()
}
}

View File

@@ -0,0 +1,33 @@
package app.termora.plugins.ftp
import app.termora.DynamicIcon
import app.termora.Icons
import app.termora.protocol.FileObjectHandler
import app.termora.protocol.FileObjectRequest
import app.termora.protocol.TransferProtocolProvider
import org.apache.commons.vfs2.provider.FileProvider
class FTPProtocolProvider private constructor() : TransferProtocolProvider {
companion object {
val instance by lazy { FTPProtocolProvider() }
const val PROTOCOL = "FTP"
}
override fun getProtocol(): String {
return PROTOCOL
}
override fun getIcon(width: Int, height: Int): DynamicIcon {
return Icons.ftp
}
override fun getFileProvider(): FileProvider {
return FTPFileProvider.instance
}
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,14 @@
package app.termora.plugins.ftp
import app.termora.protocol.ProtocolProvider
import app.termora.protocol.ProtocolProviderExtension
class FTPProtocolProviderExtension private constructor() : ProtocolProviderExtension {
companion object {
val instance by lazy { FTPProtocolProviderExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return FTPProtocolProvider.Companion.instance
}
}

View File

@@ -0,0 +1,24 @@
<termora-plugin>
<id>ftp</id>
<name>FTP</name>
<paid/>
<version>${projectVersion}</version>
<termora-version since=">=${rootProjectVersion}" until=""/>
<entry>app.termora.plugins.ftp.FTPPlugin</entry>
<descriptions>
<description>Connecting to FTP</description>
<description language="zh_CN">支持连接到到 FTP</description>
<description language="zh_TW">支援連接到 FTP</description>
</descriptions>
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
</termora-plugin>

View File

@@ -0,0 +1 @@
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#6C707E"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#6C707E"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#CED0D6"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#CED0D6"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

View File

@@ -0,0 +1,82 @@
package app.termora.plugins.geo
import app.termora.ApplicationScope
import app.termora.Disposable
import app.termora.geo.GeoLibrary
import com.maxmind.db.CHMCache
import com.maxmind.geoip2.DatabaseReader
import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
import java.net.InetAddress
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.jvm.optionals.getOrNull
internal class Geo private constructor() : Disposable {
companion object {
private val log = LoggerFactory.getLogger(Geo::class.java)
fun getInstance(): Geo {
return ApplicationScope.forApplicationScope()
.getOrCreate(Geo::class) { Geo() }
}
}
private val initialized = AtomicBoolean(false)
private var reader: DatabaseReader? = null
private fun initialize() {
if (isInitialized()) return
if (initialized.compareAndSet(false, true)) {
try {
val input = GeoLibrary.getInputStream()
if (input == null) {
throw IllegalStateException("GeoLite2-Country.mmdb not be found")
}
val locale = Locale.getDefault().toString().replace("_", "-")
try {
reader = DatabaseReader.Builder(input)
.locales(listOf(locale, "en"))
.withCache(CHMCache()).build()
} catch (e: Exception) {
throw e
}
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error("Failed to initialize geo database", e)
}
initialized.set(false)
}
}
}
fun country(ip: String): Country? {
try {
initialize()
val reader = reader ?: return null
val response = reader.tryCountry(InetAddress.getByName(ip)).getOrNull() ?: return null
val isoCode = response.country.isoCode
var name = response.country.name
// 控制名称不要太长如果太长则使用缩写。例如United States
if (name != null && name.length > 6) name = isoCode
return Country(isoCode, name ?: isoCode)
} catch (e: Exception) {
if (log.isDebugEnabled) {
log.error("Failed to initialize geo database", e)
}
return null
}
}
fun isInitialized(): Boolean = initialized.get()
override fun dispose() {
IOUtils.closeQuietly(reader)
}
data class Country(val isoCode: String, val name: String)
}

View File

@@ -0,0 +1,43 @@
package app.termora.plugins.geo
import app.termora.EnableManager
import app.termora.SwingUtils
import app.termora.TermoraFrameManager
import app.termora.tree.HostTreeShowMoreEnableExtension
import app.termora.tree.NewHostTree
import javax.swing.JCheckBoxMenuItem
import javax.swing.JTree
import javax.swing.SwingUtilities
internal class GeoHostTreeShowMoreEnableExtension private constructor() : HostTreeShowMoreEnableExtension {
companion object {
private const val KEY = "Plugins.Geo.ShowMore.Enable"
val instance = GeoHostTreeShowMoreEnableExtension()
}
private val enableManager get() = EnableManager.getInstance()
override fun createJCheckBoxMenuItem(tree: JTree): JCheckBoxMenuItem {
val item = JCheckBoxMenuItem("Geo")
item.isSelected = item.isEnabled && enableManager.getFlag(KEY, true)
item.addActionListener {
enableManager.setFlag(KEY, item.isSelected)
updateComponentTreeUI()
}
return item
}
fun updateComponentTreeUI() {
// reload all tree
for (frame in TermoraFrameManager.getInstance().getWindows()) {
for (tree in SwingUtils.getDescendantsOfClass(NewHostTree::class.java, frame)) {
SwingUtilities.updateComponentTreeUI(tree)
}
}
}
fun isShowMore(): Boolean {
return enableManager.getFlag(KEY, true)
}
}

View File

@@ -0,0 +1,26 @@
package app.termora.plugins.geo
import app.termora.AbstractI18n
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.*
object GeoI18n : AbstractI18n() {
private val log = LoggerFactory.getLogger(GeoI18n::class.java)
private val myBundle by lazy {
val bundle = ResourceBundle.getBundle("i18n/messages", Locale.getDefault(), GeoI18n::class.java.classLoader)
if (log.isInfoEnabled) {
log.info("I18n: {}", bundle.baseBundleName ?: "null")
}
return@lazy bundle
}
override fun getBundle(): ResourceBundle {
return myBundle
}
override fun getLogger(): Logger {
return log
}
}

View File

@@ -0,0 +1,33 @@
package app.termora.plugins.geo
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.Plugin
import app.termora.tree.HostTreeShowMoreEnableExtension
import app.termora.tree.SimpleTreeCellRendererExtension
class GeoPlugin : Plugin {
private val support = ExtensionSupport()
init {
support.addExtension(SimpleTreeCellRendererExtension::class.java) { GeoSimpleTreeCellRendererExtension.instance }
support.addExtension(HostTreeShowMoreEnableExtension::class.java) { GeoHostTreeShowMoreEnableExtension.instance }
}
override fun getAuthor(): String {
return "TermoraDev"
}
override fun getName(): String {
return "Geo"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,59 @@
package app.termora.plugins.geo
import app.termora.ColorHash
import app.termora.tree.HostTreeNode
import app.termora.tree.MarkerSimpleTreeCellAnnotation
import app.termora.tree.SimpleTreeCellAnnotation
import app.termora.tree.SimpleTreeCellRendererExtension
import com.formdev.flatlaf.util.SystemInfo
import java.awt.Color
import javax.swing.JTree
class GeoSimpleTreeCellRendererExtension private constructor() : SimpleTreeCellRendererExtension {
companion object {
val instance = GeoSimpleTreeCellRendererExtension()
}
private val geo get() = Geo.getInstance()
override fun createAnnotations(
tree: JTree,
value: Any?,
sel: Boolean,
expanded: Boolean,
leaf: Boolean,
row: Int,
hasFocus: Boolean
): List<SimpleTreeCellAnnotation> {
val node = value as? HostTreeNode ?: return emptyList()
if (node.isFolder) return emptyList()
val protocol = node.data.protocol
if ((protocol == "SSH" || protocol == "RDP").not()) return emptyList()
if (GeoHostTreeShowMoreEnableExtension.instance.isShowMore().not()) return emptyList()
val country = geo.country(node.data.host) ?: return emptyList()
val text = if (SystemInfo.isMacOS) "${countryCodeToFlagEmoji(country.isoCode)}${country.name}" else country.name
return listOf(
MarkerSimpleTreeCellAnnotation(
text,
foreground = Color.white,
background = ColorHash.hash(country.isoCode),
)
)
}
private fun countryCodeToFlagEmoji(code: String): String {
if (code.length < 2) return ""
val upper = code.take(2).uppercase()
val first = Character.codePointAt(upper, 0) - 'A'.code + 0x1F1E6
val second = Character.codePointAt(upper, 1) - 'A'.code + 0x1F1E6
return String(Character.toChars(first)) + String(Character.toChars(second))
}
override fun ordered(): Long {
return 1
}
}

View File

@@ -0,0 +1,23 @@
<termora-plugin>
<id>geo</id>
<name>Geo</name>
<version>${projectVersion}</version>
<entry>app.termora.plugins.geo.GeoPlugin</entry>
<termora-version since=">=${rootProjectVersion}" until=""/>
<descriptions>
<description>Display the geographical location of the host</description>
<description language="zh_CN">显示主机的地理位置</description>
<description language="zh_TW">顯示主機的地理位置</description>
</descriptions>
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
</termora-plugin>

View File

@@ -0,0 +1,5 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="6.5" stroke="#6C707E"/>
<path d="M10.5 8C10.5 9.38071 9.38071 10.5 8 10.5C6.61929 10.5 5.5 9.38071 5.5 8C5.5 6.61929 6.61929 5.5 8 5.5C9.38071 5.5 10.5 6.61929 10.5 8Z" stroke="#3574F0"/>
</svg>

After

Width:  |  Height:  |  Size: 453 B

View File

@@ -0,0 +1,5 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="6.5" stroke="#CED0D6"/>
<path d="M10.5 8C10.5 9.38071 9.38071 10.5 8 10.5C6.61929 10.5 5.5 9.38071 5.5 8C5.5 6.61929 6.61929 5.5 8 5.5C9.38071 5.5 10.5 6.61929 10.5 8Z" stroke="#548AF7"/>
</svg>

After

Width:  |  Height:  |  Size: 453 B

View File

@@ -0,0 +1,2 @@
termora.plugins.geo.first-message=The first time you use the <b>Geo</b> plugin, it will download the <b>GeoLite2.mmdb</b> database. <br/>Once the download is complete, it will display the host region information.
termora.plugins.geo.coming-soon=Geo loading

View File

@@ -0,0 +1,2 @@
termora.plugins.geo.first-message=首次使用 <b>Geo</b> 插件会下载 <b>GeoLite2.mmdb</b> 数据库,下载完成后会显示主机地域信息
termora.plugins.geo.coming-soon=Geo 加载中

View File

@@ -0,0 +1,2 @@
termora.plugins.geo.first-message=首次使用 <b>Geo</b> 外掛程式會下載 <b>GeoLite2.mmdb</b> 資料庫,下載完成後會顯示主機地域訊息
termora.plugins.geo.coming-soon=Geo 加载中

View File

@@ -0,0 +1,22 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.2"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
implementation(libs.xodus.vfs)
implementation(libs.xodus.openAPI)
implementation(libs.xodus.environment)
implementation(libs.bip39)
implementation(libs.commons.compress)
}
ext.set("Termora-Plugin-Entry", "app.termora.plugins.migration.MigrationPlugin")
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -1,18 +1,17 @@
package app.termora
package app.termora.plugins.migration
import app.termora.*
import app.termora.Application.ohMyJson
import app.termora.highlight.KeywordHighlight
import app.termora.keymap.Keymap
import app.termora.keymgr.OhKeyPair
import app.termora.macro.Macro
import app.termora.sync.SyncType
import app.termora.snippet.Snippet
import app.termora.terminal.CursorStyle
import jetbrains.exodus.bindings.StringBinding
import jetbrains.exodus.env.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
@@ -26,9 +25,11 @@ class Database private constructor(private val env: Environment) : Disposable {
companion object {
private const val KEYMAP_STORE = "Keymap"
private const val HOST_STORE = "Host"
private const val SNIPPET_STORE = "Snippet"
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
private const val MACRO_STORE = "Macro"
private const val KEY_PAIR_STORE = "KeyPair"
private const val DELETED_DATA_STORE = "DeletedData"
private val log = LoggerFactory.getLogger(Database::class.java)
@@ -45,7 +46,7 @@ class Database private constructor(private val env: Environment) : Disposable {
fun getDatabase(): Database {
return ApplicationScope.forApplicationScope()
.getOrCreate(Database::class) { open(Application.getDatabaseFile()) }
.getOrCreate(Database::class) { open(MigrationApplicationRunnerExtension.instance.getDatabaseFile()) }
}
}
@@ -105,17 +106,6 @@ class Database private constructor(private val env: Environment) : Disposable {
}
}
fun removeAllHost() {
env.executeInTransaction { tx ->
val store = env.openStore(HOST_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
store.openCursor(tx).use {
while (it.next) {
it.deleteCurrent()
}
}
}
}
fun removeAllKeyPair() {
env.executeInTransaction { tx ->
val store = env.openStore(KEY_PAIR_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
@@ -156,11 +146,67 @@ class Database private constructor(private val env: Environment) : Disposable {
env.executeInTransaction {
delete(it, HOST_STORE, id)
if (log.isDebugEnabled) {
log.debug("Removed Host: $id")
log.debug("Removed host: $id")
}
}
}
fun addDeletedData(deletedData: DeletedData) {
val text = ohMyJson.encodeToString(deletedData)
env.executeInTransaction {
put(it, DELETED_DATA_STORE, deletedData.id, text)
if (log.isDebugEnabled) {
log.debug("Added DeletedData: ${deletedData.id} , $text")
}
}
}
fun getDeletedData(): Collection<DeletedData> {
return env.computeInTransaction { tx ->
openCursor<DeletedData?>(tx, DELETED_DATA_STORE) { _, value ->
try {
ohMyJson.decodeFromString(value)
} catch (e: Exception) {
null
}
}.values.filterNotNull()
}
}
fun addSnippet(snippet: Snippet) {
var text = ohMyJson.encodeToString(snippet)
if (doorman.isWorking()) {
text = doorman.encrypt(text)
}
env.executeInTransaction {
put(it, SNIPPET_STORE, snippet.id, text)
if (log.isDebugEnabled) {
log.debug("Added Snippet: ${snippet.id} , ${snippet.name}")
}
}
}
fun removeSnippet(id: String) {
env.executeInTransaction {
delete(it, SNIPPET_STORE, id)
if (log.isDebugEnabled) {
log.debug("Removed snippet: $id")
}
}
}
fun getSnippets(): Collection<Snippet> {
val isWorking = doorman.isWorking()
return env.computeInTransaction { tx ->
openCursor<Snippet>(tx, SNIPPET_STORE) { _, value ->
if (isWorking)
ohMyJson.decodeFromString(doorman.decrypt(value))
else
ohMyJson.decodeFromString(value)
}.values
}
}
fun getKeywordHighlights(): Collection<KeywordHighlight> {
return env.computeInTransaction { tx ->
openCursor<KeywordHighlight>(tx, KEYWORD_HIGHLIGHT_STORE) { _, value ->
@@ -297,12 +343,11 @@ class Database private constructor(private val env: Environment) : Disposable {
}
abstract inner class Property(private val name: String) {
abstract inner class Property(val name: String) {
private val properties = Collections.synchronizedMap(mutableMapOf<String, String>())
init {
@Suppress("OPT_IN_USAGE")
GlobalScope.launch(Dispatchers.IO) { properties.putAll(getProperties()) }
swingCoroutineScope.launch(Dispatchers.IO) { properties.putAll(getProperties()) }
}
protected open fun getString(key: String): String? {
@@ -374,6 +419,13 @@ class Database private constructor(private val env: Environment) : Disposable {
}
}
protected inner class DoublePropertyDelegate(defaultValue: Double) :
PropertyDelegate<Double>(defaultValue) {
override fun convertValue(value: String): Double {
return value.toDoubleOrNull() ?: initializer.invoke()
}
}
protected inner class LongPropertyDelegate(defaultValue: Long) :
PropertyDelegate<Long>(defaultValue) {
@@ -400,10 +452,10 @@ class Database private constructor(private val env: Environment) : Disposable {
protected inner class CursorStylePropertyDelegate(defaultValue: CursorStyle) :
PropertyDelegate<CursorStyle>(defaultValue) {
override fun convertValue(value: String): CursorStyle {
try {
return CursorStyle.valueOf(value)
} catch (e: Exception) {
return initializer.invoke()
return try {
CursorStyle.valueOf(value)
} catch (_: Exception) {
initializer.invoke()
}
}
}
@@ -458,6 +510,16 @@ class Database private constructor(private val env: Environment) : Disposable {
*/
var beep by BooleanPropertyDelegate(true)
/**
* 超链接
*/
var hyperlink by BooleanPropertyDelegate(true)
/**
* 光标闪烁
*/
var cursorBlink by BooleanPropertyDelegate(false)
/**
* 选中复制
*/
@@ -568,6 +630,21 @@ class Database private constructor(private val env: Environment) : Disposable {
var darkTheme by StringPropertyDelegate("Dark")
var lightTheme by StringPropertyDelegate("Light")
/**
* 允许后台运行也就是托盘
*/
var backgroundRunning by BooleanPropertyDelegate(false)
/**
* 标签关闭前确认
*/
var confirmTabClose by BooleanPropertyDelegate(false)
/**
* 背景图片的地址
*/
var backgroundImage by StringPropertyDelegate(StringUtils.EMPTY)
/**
* 语言
*/
@@ -575,6 +652,11 @@ class Database private constructor(private val env: Environment) : Disposable {
I18n.containsLanguage(Locale.getDefault()) ?: Locale.US.toString()
}
/**
* 透明度
*/
var opacity by DoublePropertyDelegate(1.0)
}
/**
@@ -588,6 +670,28 @@ class Database private constructor(private val env: Environment) : Disposable {
*/
var editCommand by StringPropertyDelegate(StringUtils.EMPTY)
/**
* sftp command
*/
var sftpCommand by StringPropertyDelegate(StringUtils.EMPTY)
/**
* defaultDirectory
*/
var defaultDirectory by StringPropertyDelegate(StringUtils.EMPTY)
/**
* 是否固定在标签栏
*/
var pinTab by BooleanPropertyDelegate(false)
/**
* 是否保留原始文件时间
*/
var preserveModificationTime by BooleanPropertyDelegate(false)
}
/**
@@ -604,6 +708,7 @@ class Database private constructor(private val env: Environment) : Disposable {
*/
var rangeHosts by BooleanPropertyDelegate(true)
var rangeKeyPairs by BooleanPropertyDelegate(true)
var rangeSnippets by BooleanPropertyDelegate(true)
var rangeKeywordHighlights by BooleanPropertyDelegate(true)
var rangeMacros by BooleanPropertyDelegate(true)
var rangeKeymap by BooleanPropertyDelegate(true)
@@ -627,6 +732,11 @@ class Database private constructor(private val env: Environment) : Disposable {
* 最后同步时间
*/
var lastSyncTime by LongPropertyDelegate(0L)
/**
* 同步策略为空就是默认手动
*/
var policy by StringPropertyDelegate(StringUtils.EMPTY)
}
override fun dispose() {

View File

@@ -1,5 +1,6 @@
package app.termora
package app.termora.plugins.migration
import app.termora.*
import app.termora.AES.decodeBase64
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
@@ -86,7 +87,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
.layout(
FormLayout(
"$formMargin, default:grow, 4dlu, pref, $formMargin",
"${if (SystemInfo.isWindows) "20dlu" else "0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
"${"0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
)
)
.add(icon).xyw(2, rows, 4).apply { rows += step }
@@ -114,7 +115,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
I18n.getString("termora.doorman.delete-data"),
messageType = JOptionPane.WARNING_MESSAGE
)
Application.browse(Application.getDatabaseFile().toURI())
Application.browse(MigrationApplicationRunnerExtension.instance.getDatabaseFile().toURI())
}
}
}).apply { isFocusable = false }).xyw(2, rows, 4, "center, fill")
@@ -136,6 +137,9 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
val key = AES.ECB.decrypt(entropy, keyBackup.decodeBase64())
Doorman.getInstance().work(key)
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
OptionPane.showMessageDialog(
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
messageType = JOptionPane.ERROR_MESSAGE
@@ -219,7 +223,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
)
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
.layout(layout).debug(true)
.layout(layout).debug(false)
val iterator = textFields.iterator()
for (i in 1..5 step 2) {
for (j in 1..7 step 2) {

View File

@@ -0,0 +1,198 @@
package app.termora.plugins.migration
import app.termora.*
import app.termora.account.AccountManager
import app.termora.account.AccountOwner
import app.termora.database.DatabaseManager
import app.termora.database.OwnerType
import app.termora.highlight.KeywordHighlightManager
import app.termora.keymap.KeymapManager
import app.termora.keymgr.KeyManager
import app.termora.macro.MacroManager
import app.termora.snippet.SnippetManager
import org.apache.commons.io.FileUtils
import org.slf4j.LoggerFactory
import java.io.File
import java.util.concurrent.CountDownLatch
import javax.swing.JOptionPane
import javax.swing.SwingUtilities
import kotlin.system.exitProcess
class MigrationApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
companion object {
private val log = LoggerFactory.getLogger(MigrationApplicationRunnerExtension::class.java)
val instance by lazy { MigrationApplicationRunnerExtension() }
}
override fun ready() {
val file = getDatabaseFile()
if (file.exists().not()) return
// 如果数据库文件存在,那么需要迁移文件
val countDownLatch = CountDownLatch(1)
SwingUtilities.invokeAndWait {
try {
// 打开数据
openDatabase()
// 尝试解锁
openDoor()
// 询问是否迁移
if (askMigrate()) {
// 迁移
migrate()
// 移动到旧的目录
moveOldDirectory()
// 重启
restart()
}
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
} finally {
countDownLatch.countDown()
}
}
countDownLatch.await()
}
private fun openDoor() {
if (Doorman.getInstance().isWorking()) {
if (DoormanDialog(null).open().not()) {
Disposer.dispose(TermoraFrameManager.getInstance())
}
}
}
private fun openDatabase() {
try {
// 初始化数据库
Database.getDatabase()
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
JOptionPane.showMessageDialog(
null, "Unable to open database",
I18n.getString("termora.title"), JOptionPane.ERROR_MESSAGE
)
exitProcess(1)
}
}
private fun migrate() {
val database = Database.getDatabase()
val accountManager = AccountManager.getInstance()
val databaseManager = DatabaseManager.getInstance()
val ownerId = accountManager.getAccountId()
val hostManager = HostManager.getInstance()
val snippetManager = SnippetManager.getInstance()
val macroManager = MacroManager.getInstance()
val keymapManager = KeymapManager.getInstance()
val keyManager = KeyManager.getInstance()
val highlightManager = KeywordHighlightManager.getInstance()
val accountOwner = AccountOwner(
id = accountManager.getAccountId(),
name = accountManager.getEmail(),
type = OwnerType.User
)
for (host in database.getHosts()) {
if (host.deleted) continue
hostManager.addHost(host.copy(ownerId = accountManager.getAccountId(), ownerType = OwnerType.User.name))
}
for (snippet in database.getSnippets()) {
if (snippet.deleted) continue
snippetManager.addSnippet(snippet)
}
for (macro in database.getMacros()) {
macroManager.addMacro(macro)
}
for (keymap in database.getKeymaps()) {
keymapManager.addKeymap(keymap)
}
for (keypair in database.getKeyPairs()) {
keyManager.addOhKeyPair(keypair, accountOwner)
}
for (e in database.getKeywordHighlights()) {
highlightManager.addKeywordHighlight(e, accountOwner)
}
val list = listOf(
database.sync,
database.properties,
database.terminal,
database.sftp,
database.appearance,
)
for (e in list) {
for (k in e.getProperties()) {
databaseManager.setSetting(e.name + "." + k.key, k.value)
}
}
for (e in database.safetyProperties.getProperties()) {
databaseManager.setSetting(database.properties.name + "." + e.key, e.value)
}
}
private fun askMigrate(): Boolean {
if (MigrationDialog(null).open()) {
return true
}
// 移动到旧的目录
moveOldDirectory()
// 重启
restart()
return false
}
private fun moveOldDirectory() {
// 关闭数据库
Disposer.dispose(Database.getDatabase())
val file = getDatabaseFile()
FileUtils.moveDirectory(
file,
FileUtils.getFile(file.parentFile, file.name + "-old-" + System.currentTimeMillis())
)
}
private fun restart() {
// 重启
TermoraRestarter.getInstance().scheduleRestart(null, ask = false)
// 退出程序
Disposer.dispose(TermoraFrameManager.getInstance())
}
fun getDatabaseFile(): File {
return FileUtils.getFile(Application.getBaseDataDir(), "storage")
}
}

View File

@@ -0,0 +1,112 @@
package app.termora.plugins.migration
import app.termora.*
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.FlatSVGIcon
import com.formdev.flatlaf.util.SystemInfo
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.JXEditorPane
import java.awt.Dimension
import java.awt.Window
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.imageio.ImageIO
import javax.swing.*
import javax.swing.event.HyperlinkEvent
class MigrationDialog(owner: Window?) : DialogWrapper(owner) {
private var isOpened = false
init {
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
isModal = true
isResizable = false
controlsVisible = false
escapeDispose = false
if (SystemInfo.isWindows || SystemInfo.isLinux) {
title = StringUtils.EMPTY
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
}
if (SystemInfo.isWindows || SystemInfo.isLinux) {
val sizes = listOf(16, 20, 24, 28, 32, 48, 64)
val loader = TermoraFrame::class.java.classLoader
val images = sizes.mapNotNull { e ->
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
}
iconImages = images
}
setLocationRelativeTo(null)
init()
}
override fun createCenterPanel(): JComponent {
var rows = 2
val step = 2
val formMargin = "7dlu"
val icon = JLabel()
icon.horizontalAlignment = SwingConstants.CENTER
icon.icon = FlatSVGIcon(Icons.newUI.name, 80, 80)
val editorPane = JXEditorPane()
editorPane.contentType = "text/html"
editorPane.text = MigrationI18n.getString("termora.plugins.migration.message")
editorPane.isEditable = false
editorPane.addHyperlinkListener {
if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) {
Application.browse(it.url.toURI())
}
}
editorPane.background = DynamicColor("window")
val scrollPane = JScrollPane(editorPane)
scrollPane.border = BorderFactory.createEmptyBorder()
scrollPane.preferredSize = Dimension(Int.MAX_VALUE, 225)
addWindowListener(object : WindowAdapter() {
override fun windowOpened(e: WindowEvent) {
removeWindowListener(this)
SwingUtilities.invokeLater { scrollPane.verticalScrollBar.value = 0 }
}
})
return FormBuilder.create().debug(false)
.layout(
FormLayout(
"$formMargin, default:grow, 4dlu, pref, $formMargin",
"${"0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
)
)
.add(icon).xyw(2, rows, 4).apply { rows += step }
.add(scrollPane).xyw(2, rows, 4).apply { rows += step }
.build()
}
fun open(): Boolean {
isModal = true
isVisible = true
return isOpened
}
override fun doOKAction() {
isOpened = true
super.doOKAction()
}
override fun doCancelAction() {
isOpened = false
super.doCancelAction()
}
override fun createOkAction(): AbstractAction {
return OkAction(MigrationI18n.getString("termora.plugins.migration.migrate"))
}
}

View File

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

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