Compare commits

..

176 Commits

Author SHA1 Message Date
hstyi
bd9b73ad6a release: 2.0.0-beta.13 2025-08-17 11:46:43 +08:00
hstyi
dbea769994 fix: sftp command may cause bad key types 2025-08-17 08:54:42 +08:00
hstyi
9cd83c4025 feat: host tree supports copy and paste 2025-08-15 16:35:43 +08:00
hstyi
d4cc080e7b chore: transfer text 2025-08-15 16:35:32 +08:00
hstyi
a324bc3d96 fix: keyword highlighting causes crash 2025-08-15 13:12:55 +08:00
dependabot[bot]
36929e9ea3 chore(deps): bump kotlin from 2.2.0 to 2.2.10
Bumps `kotlin` from 2.2.0 to 2.2.10.

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-05 21:43:25 +08:00
dependabot[bot]
00e695b7d5 chore(deps): bump org.commonmark:commonmark from 0.25.0 to 0.25.1
Bumps [org.commonmark:commonmark](https://github.com/commonmark/commonmark-java) from 0.25.0 to 0.25.1.
- [Release notes](https://github.com/commonmark/commonmark-java/releases)
- [Changelog](https://github.com/commonmark/commonmark-java/blob/main/CHANGELOG.md)
- [Commits](https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.25.0...commonmark-parent-0.25.1)

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-01 11:09:44 +08:00
hstyi
d249e5da5a fix: XTerm Set Scrolling Region 2025-07-31 16:57:33 +08:00
hstyi
7243e933e6 fix: aura selection text color 2025-07-31 16:22:32 +08:00
hstyi
f92e43ee41 release: 2.0.0-beta.10 2025-07-31 10:33:37 +08:00
dependabot[bot]
f6243e33da chore(deps): bump com.github.hstyi:geolite2
Bumps com.github.hstyi:geolite2 from v1.0-202507070058 to v1.0-202507280101.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-31 10:28:32 +08:00
hstyi
72f334d572 chore: import CSV with password support 2025-07-31 10:28:17 +08:00
dependabot[bot]
68cbb10dec chore(deps): bump org.apache.commons:commons-csv from 1.14.0 to 1.14.1
Bumps [org.apache.commons:commons-csv](https://github.com/apache/commons-csv) from 1.14.0 to 1.14.1.
- [Changelog](https://github.com/apache/commons-csv/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-csv/compare/rel/commons-csv-1.14.0...rel/commons-csv-1.14.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-31 10:26:16 +08:00
dependabot[bot]
45f5c4ee91 chore(deps): bump com.qcloud:cos_api from 5.6.247 to 5.6.249
Bumps [com.qcloud:cos_api](https://github.com/tencentyun/cos-java-sdk-v5) from 5.6.247 to 5.6.249.
- [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.249
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-30 12:29:12 +08:00
dependabot[bot]
48c511613e chore(deps): bump org.apache.commons:commons-compress
Bumps [org.apache.commons:commons-compress](https://github.com/apache/commons-compress) from 1.27.1 to 1.28.0.
- [Changelog](https://github.com/apache/commons-compress/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-compress/compare/rel/commons-compress-1.27.1...rel/commons-compress-1.28.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-30 12:29:01 +08:00
hstyi
c94063d459 fix: SSH timeout is always 30 seconds 2025-07-29 17:57:14 +08:00
hstyi
c26aafd831 chore: heartbeat interval 60 seconds 2025-07-29 17:31:00 +08:00
hstyi
a5638329e7 feat: support transfer double-click behavior 2025-07-29 09:23:31 +08:00
hstyi
8323f8eb5d fix: missing placeholder 2025-07-28 19:46:26 +08:00
hstyi
35199ed094 chore: RDP encrypt password on Windows 2025-07-28 10:02:02 +08:00
dependabot[bot]
b5d53cf416 chore(deps): bump org.xerial:sqlite-jdbc from 3.50.2.0 to 3.50.3.0
Bumps [org.xerial:sqlite-jdbc](https://github.com/xerial/sqlite-jdbc) from 3.50.2.0 to 3.50.3.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.2.0...3.50.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-27 07:10:19 +08:00
hstyi
39e26a6e3d fix: keyword background color 2025-07-26 07:38:18 +08:00
dependabot[bot]
15cb06af0f chore(deps): bump commons-codec:commons-codec from 1.18.0 to 1.19.0
Bumps [commons-codec:commons-codec](https://github.com/apache/commons-codec) from 1.18.0 to 1.19.0.
- [Changelog](https://github.com/apache/commons-codec/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-codec/compare/rel/commons-codec-1.18.0...rel/commons-codec-1.19.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-25 20:06:59 +08:00
dependabot[bot]
1e0bbb5a00 chore(deps): bump org.apache.commons:commons-text from 1.13.1 to 1.14.0
Bumps [org.apache.commons:commons-text](https://github.com/apache/commons-text) from 1.13.1 to 1.14.0.
- [Changelog](https://github.com/apache/commons-text/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-text/compare/rel/commons-text-1.13.1...rel/commons-text-1.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-25 20:06:48 +08:00
hstyi
fb6fdbc14c chore: sync plugin 0.0.4 2025-07-23 09:45:30 +08:00
dependabot[bot]
96df53ce40 chore(deps): bump commons-io:commons-io from 2.19.0 to 2.20.0
Bumps [commons-io:commons-io](https://github.com/apache/commons-io) from 2.19.0 to 2.20.0.
- [Changelog](https://github.com/apache/commons-io/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-io/compare/rel/commons-io-2.19.0...rel/commons-io-2.20.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 10:47:18 +08:00
hstyi
42f86dc3a3 chore: linux appimagetool 2025-07-21 10:47:07 +08:00
hstyi
32b11c6063 fix: macOS not opening local terminal 2025-07-21 10:47:07 +08:00
hstyi
b2f43ba439 chore: improve upgrade 2025-07-21 09:58:37 +08:00
hstyi
7d9cfc79cf release: 2.0.0-beta.9 2025-07-20 13:55:38 +08:00
hstyi
5624a44104 chore: vnc clipboard charset 2025-07-20 13:46:59 +08:00
hstyi
2aaa6371ab feat: support VNC protocol 2025-07-20 13:46:59 +08:00
hstyi
fdc1b0c07d chore: compatible with multiple keyboard layouts 2025-07-20 10:14:51 +08:00
hstyi
8ec7e416ea fix: key shortcut not working 2025-07-19 21:27:57 +08:00
hstyi
ae7730fb35 feat: keyword highlight set 2025-07-18 21:47:57 +08:00
hstyi
f99e3e9147 chore: title uses the selected tab 2025-07-18 17:09:56 +08:00
hstyi
5fcda04544 fix: reconnection causing cloned sessions to close unexpectedly 2025-07-18 13:29:25 +08:00
hstyi
aca0fbb1e3 chore: disable keyword highlighting when AlternateScreenBuffer 2025-07-18 10:20:12 +08:00
hstyi
4f8ffbf225 fix: osx notary 2025-07-17 11:28:17 +08:00
hstyi
8b08edf38f chore: osx.yml 2025-07-17 11:22:09 +08:00
hstyi
e3d43111e8 fix: memory leaks 2025-07-17 11:13:31 +08:00
hstyi
6bb9a33a04 feat: terminal preview 2025-07-17 10:22:14 +08:00
hstyi
ca64880a01 chore: windows inno setup 2025-07-17 10:10:06 +08:00
hstyi
c8d70e2a5a chore: Windows MSIX 2025-07-16 14:34:17 +08:00
hstyi
cee7eb8928 fix: tooltip not showing 2025-07-15 13:51:41 +08:00
dependabot[bot]
abc5f203d0 chore(deps): bump org.jetbrains.pty4j:pty4j from 0.13.6 to 0.13.10
Bumps [org.jetbrains.pty4j:pty4j](https://github.com/JetBrains/pty4j) from 0.13.6 to 0.13.10.
- [Commits](https://github.com/JetBrains/pty4j/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-15 13:51:05 +08:00
hstyi
c3fc8f8e12 chore: improve extension 2025-07-15 10:31:46 +08:00
hstyi
afd4f8baa9 fix: alt modifier echo problem 2025-07-15 10:12:59 +08:00
hstyi
b5ca8b988c chore: support plugin incompatibility 2025-07-15 09:34:38 +08:00
hstyi
89b22ba984 feat: support modifying log format 2025-07-14 16:45:50 +08:00
hstyi
f329ef60df release: 2.0.0-beta.8 2025-07-14 14:20:44 +08:00
hstyi
8acfdb8bca feat: transfer supports copy and paste 2025-07-14 13:50:12 +08:00
hstyi
a7aec52f2a fix: password text field status 2025-07-14 11:56:59 +08:00
hstyi
7f1317a9a7 chore: improve terminal options 2025-07-14 11:11:20 +08:00
hstyi
a8a1fea91b feat: support focus mode 2025-07-14 11:02:40 +08:00
hstyi
675ad4608a chore: improve keymap refresh 2025-07-11 14:57:24 +08:00
hstyi
72ba3757e2 chore: discussion group 2025-07-11 11:24:29 +08:00
hstyi
c58e84d2ae chore: windows action 2025-07-11 10:05:38 +08:00
hstyi
18a7a5059b feat: keyword highlight support import and export 2025-07-11 09:20:24 +08:00
hstyi
f0102b6f13 fix: windows tray icon size 2025-07-10 16:13:50 +08:00
hstyi
0cf8eb3c17 chore: README 2025-07-10 12:14:33 +08:00
hstyi
c08a9f2b18 release: 2.0.0-beta.7 2025-07-10 11:50:43 +08:00
hstyi
728f1f2802 fix: xterm ScrollRegion 2025-07-10 11:30:18 +08:00
hstyi
7310211fba feat: telnet support login scripts 2025-07-10 11:30:05 +08:00
dependabot[bot]
1f3267de0a chore(deps): bump org.apache.commons:commons-lang3 from 3.17.0 to 3.18.0
Bumps org.apache.commons:commons-lang3 from 3.17.0 to 3.18.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-10 11:25:28 +08:00
hstyi
8ddad59c70 chore: use docker jbr 2025-07-10 09:30:12 +08:00
hstyi
9ff6d0afa1 feat: telnet character mode 2025-07-09 19:57:56 +08:00
hstyi
2341b09f81 fix: osx.yml 2025-07-09 19:57:22 +08:00
hstyi
5830aa937a feat: improve GitHub actions 2025-07-09 18:15:36 +08:00
hstyi
56a9361e86 release: 2.0.0-beta.7 2025-07-09 12:22:34 +08:00
hstyi
5868aa4d2f fix: unable to update automatically 2025-07-09 12:18:19 +08:00
dependabot[bot]
45135b7299 chore(deps): bump exposed from 1.0.0-beta-3 to 1.0.0-beta-4
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-09 12:14:23 +08:00
hstyi
a0020fede1 release: 2.0.0-beta.6 2025-07-09 10:27:28 +08:00
hstyi
6f1eaab456 chore: ru_RU i18n 2025-07-09 09:52:02 +08:00
ForumLiker
6173eae772 chore: messages_ru_RU.properties 2025-07-09 09:23:09 +08:00
hstyi
0bb366b1f7 chore: quick connect typo 2025-07-08 21:30:45 +08:00
hstyi
9a4d6f7f4d chore: temporary host disabled editing 2025-07-08 21:22:34 +08:00
hstyi
a4ae11e301 feat: quick connect 2025-07-08 18:21:19 +08:00
dependabot[bot]
5af0acb619 chore(deps): bump cn.hutool:hutool-all from 5.8.37 to 5.8.39
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 16:16:42 +08:00
hstyi
042434b8f8 chore: find everywhere 2025-07-08 16:10:14 +08:00
hstyi
eddc7ef0c6 refactor: frame toolbar 2025-07-08 16:05:31 +08:00
dependabot[bot]
c96ca2d424 chore(deps): bump com.github.mwiede:jsch from 0.2.26 to 2.27.2
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 16:05:01 +08:00
hstyi
45be9008fd refactor: key shortcuts 2025-07-08 14:47:37 +08:00
hstyi
057da4e297 chore: okhttp 5.1.0 2025-07-08 14:01:11 +08:00
hstyi
e4e41667ff chore: osx actions 2025-07-08 12:24:25 +08:00
dependabot[bot]
95ca0a4af7 chore(deps): bump com.github.hstyi:geolite2 from v1.0-202506280327 to v1.0-202507040118
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hstyi <hstyi@foxmail.com>
2025-07-08 12:09:42 +08:00
hstyi
702dee7983 feat: serial plugin 2025-07-08 12:00:02 +08:00
hstyi
165d544448 feat: ftp plugin 2025-07-08 11:38:40 +08:00
hstyi
ecf61bedc4 chore: remove button 2025-07-07 15:50:48 +08:00
hstyi
66a81a5da3 feat: transfer support compress and extract 2025-07-07 15:48:04 +08:00
hstyi
574c816ebb fix: system disk cannot be entered 2025-07-07 11:06:53 +08:00
hstyi
e7cafb74e4 chore: clone session order 2025-07-07 10:34:01 +08:00
hstyi
5050aa37f5 release: 2.0.0-beta.5 2025-07-07 09:47:35 +08:00
hstyi
53d3d96a06 chore: dynamically modify icons 2025-07-07 09:35:55 +08:00
hstyi
d40b8a4c9c fix: windows drive list failure 2025-07-06 16:48:50 +08:00
hstyi
728671509c feat: transfer support disconnection and reconnection 2025-07-06 16:16:43 +08:00
hstyi
b7178a30fb chore: telnet supports backspace key setting 2025-07-06 11:01:44 +08:00
hstyi
939d6a1fd7 chore: improve flatlaf 2025-07-05 16:02:56 +08:00
hstyi
2986a9cc46 fix: binary compatibility 2025-07-05 14:35:42 +08:00
hstyi
f36afaf5d3 chore: telnet default port 23 2025-07-05 14:07:10 +08:00
hstyi
8cec835583 feat: support telnet 2025-07-05 14:07:10 +08:00
hstyi
a32838dad6 feat: support clone session 2025-07-05 12:07:33 +08:00
hstyi
d54671757e fix: tab drag and drop 2025-07-05 10:17:57 +08:00
hstyi
d1dba56bcd chore: improve sidebar 2025-07-04 16:36:37 +08:00
hstyi
919c06779d chore: improve rm -rf 2025-07-04 15:32:51 +08:00
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
518 changed files with 27017 additions and 3608 deletions

View File

@@ -1,52 +0,0 @@
name: Linux aarch64
on: [ push, pull_request ]
jobs:
build:
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
with:
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.7-linux-aarch64-b1034.51.tar.gz
# appimagetool
- run: sudo apt install libfuse2
# install jdk
- name: Installing Java
uses: actions/setup-java@v4
with:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.7'
architecture: aarch64
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
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
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-linux-aarch64
path: |
build/distributions/*.tar.gz
build/distributions/*.AppImage

View File

@@ -1,52 +0,0 @@
name: Linux x86-64
on: [ push, pull_request ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
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.7-linux-x64-b1034.51.tar.gz
# appimagetool
- run: sudo apt install libfuse2
# install jdk
- name: Installing Java
uses: actions/setup-java@v4
with:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.7'
architecture: x64
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
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
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-linux-x86-64
path: |
build/distributions/*.tar.gz
build/distributions/*.AppImage

81
.github/workflows/linux.yml vendored Normal file
View File

@@ -0,0 +1,81 @@
name: Linux
on: [ push, pull_request ]
env:
JBR_MAJOR: 21.0.7
JBR_PATCH: b1038.58
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ ubuntu-24.04-arm, ubuntu-latest ]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ runner.arch }}-gradlexyz-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradlexyz-
- name: Set dynamic DOCKER_NAME
run: |
echo "DOCKER_NAME=hstyi/jbr:${{ env.JBR_MAJOR }}${{ env.JBR_PATCH }}" >> $GITHUB_ENV
- name: Create docker-run.sh helper script
shell: bash
run: |
cat <<'EOF' > docker-run.sh
#!/bin/bash
docker run --rm -v $HOME/.gradle:/root/.gradle -v "$(pwd)":/app -w /app "$@"
EOF
chmod +x docker-run.sh
- name: Compile
shell: bash
run: ./docker-run.sh $DOCKER_NAME bash -c './gradlew :check-license && ./gradlew classes -x test'
- name: JLink
shell: bash
run: ./docker-run.sh $DOCKER_NAME bash -c './gradlew :jar :copy-dependencies :plugins:migration:build :jlink'
- name: Package Deb
shell: bash
run: ./docker-run.sh -e TERMORA_TYPE=deb $DOCKER_NAME bash -c './gradlew :jpackage && ./gradlew :dist'
- name: Package AppImage
shell: bash
run: ./docker-run.sh --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor:unconfined $DOCKER_NAME bash -c 'rm -rf build/jpackage && ./gradlew :jpackage && ./gradlew :dist'
- name: Make ~/.gradle world-writable
shell: bash
run: sudo chmod -R 777 ~/.gradle
- name: Upload targz artifact
uses: actions/upload-artifact@v4
with:
name: termora-linux-targz-${{ runner.arch }}
path: |
build/distributions/*.tar.gz
- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
with:
name: termora-linux-AppImage-${{ runner.arch }}
path: |
build/distributions/*.AppImage
- name: Upload deb artifact
uses: actions/upload-artifact@v4
with:
name: termora-linux-deb-${{ runner.arch }}
path: |
build/distributions/*.deb

View File

@@ -1,89 +0,0 @@
name: macOS aarch64
on: [ push, pull_request ]
jobs:
build:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install the Apple certificate
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
# create variables
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# import certificate from secrets
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
# create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
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.7-osx-aarch64-b1034.51.tar.gz
# install jdk
- name: Installing Java
uses: actions/setup-java@v4
with:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.7'
architecture: aarch64
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
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' && 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
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-osx-aarch64
path: |
build/distributions/*.zip
build/distributions/*.dmg

View File

@@ -1,17 +1,29 @@
name: macOS x86-64
name: macOS
on: [ push, pull_request ]
env:
TERMORA_MAC_SIGN: "${{ 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 }}
JBR_MAJOR: 21.0.7
JBR_PATCH: b1038.58
jobs:
build:
runs-on: macos-13
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ macos-15, macos-13 ]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 1
- name: Install the Apple certificate
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
if: ${{ fromJSON(env.TERMORA_MAC_SIGN) && env.BUILD_CERTIFICATE_BASE64 != '' }}
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
@@ -34,7 +46,7 @@ jobs:
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'"
if: ${{ fromJSON(env.TERMORA_MAC_NOTARY) && env.APPLE_ID != '' }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
@@ -43,8 +55,14 @@ jobs:
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.7-osx-x64-b1034.51.tar.gz
- name: Download Java
run: |
if [[ "$(uname -m)" == "arm64" ]]; then
ARCH="aarch64"
else
ARCH="x64"
fi
wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-${{ env.JBR_MAJOR }}-osx-$ARCH-${{ env.JBR_PATCH }}.tar.gz
# install jdk
- name: Installing Java
@@ -53,8 +71,6 @@ jobs:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.7'
architecture: x64
- uses: actions/cache@v4
with:
@@ -65,27 +81,25 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
- name: Install create-dmg
shell: bash
run: brew install create-dmg
# test build
- run: |
./gradlew classes -x test --no-daemon
./gradlew clean --no-daemon
- name: Compile
shell: bash
run: ./gradlew :check-license && ./gradlew classes -x test
# dist
- name: Dist
env:
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
- name: JLink
shell: bash
run: ./gradlew :jar :copy-dependencies :plugins:migration:build :jlink
- name: Upload artifact
- name: Package
shell: bash
run: ./gradlew :jpackage && ./gradlew :dist
- name: Upload dmg artifact
uses: actions/upload-artifact@v4
with:
name: termora-osx-x86-64
name: termora-osx-dmg-${{ runner.arch }}
path: |
build/distributions/*.zip
build/distributions/*.dmg

View File

@@ -1,53 +0,0 @@
name: Windows x86-64
on: [ push, pull_request ]
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
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
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:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
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
.\gradlew.bat --stop
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-windows-x86-64
path: |
build/distributions/*.zip
build/distributions/*.exe

116
.github/workflows/windows.yml vendored Normal file
View File

@@ -0,0 +1,116 @@
name: Windows
on: [ push, pull_request ]
env:
JBR_MAJOR: 21.0.7
JBR_PATCH: b1038.58
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ windows-11-arm, windows-2022 ]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Setup MSbuild
uses: microsoft/setup-msbuild@v2
- name: Set architecture
id: set-arch
run: |
if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
echo "ARCH=aarch64" >> $env:GITHUB_ENV
} else {
echo "ARCH=x64" >> $env:GITHUB_ENV
}
- name: Find MakeAppx
shell: pwsh
run: |
$installedRootsKey = "HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots"
$kitsRoot = (Get-ItemProperty $installedRootsKey).KitsRoot10
$versions = Get-ChildItem -Path $installedRootsKey | Select-Object -ExpandProperty PSChildName
$maxVersion = $versions | ForEach-Object { [version]$_ } | Sort-Object -Descending | Select-Object -First 1
$arch = if ($env:ARCH -eq "aarch64") { "arm64" } else { "x64" }
$makeAppXPath = Join-Path -Path $kitsRoot -ChildPath "bin\$maxVersion\$arch\makeappx.exe"
Write-Output "MakeAppx.exe path: $makeAppXPath"
if (Test-Path $makeAppXPath) {
"MAKEAPPX_PATH=$makeAppXPath" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
} else {
Write-Output "MakeAppx.exe not found!"
exit 1
}
- 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
run: |
$zipPath = "${{ runner.temp }}\java_package.zip"
$extractDir = "${{ runner.temp }}\jbr"
$url = "https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-${{ env.JBR_MAJOR }}-windows-${{ env.ARCH }}-${{ env.JBR_PATCH }}.zip"
curl -s --output $zipPath -L $url
unzip -q $zipPath -d $extractDir
$jbrDir = Get-ChildItem $extractDir | Select-Object -First 1
echo "JAVA_HOME=$($jbrDir.FullName)" >> $env:GITHUB_ENV
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
- name: Compile
run: .\gradlew :check-license && .\gradlew classes -x test
- name: JLink
run: .\gradlew :jar :copy-dependencies :plugins:migration:build :jlink
- name: Package
run: .\gradlew :jpackage && .\gradlew :dist
- name: MSIX
env:
TERMORA_TYPE: appx
run: |
.\gradlew --stop
.\gradlew :dist
- name: Stop Gradle
run: .\gradlew.bat --stop
- name: Upload zip artifact
uses: actions/upload-artifact@v4
with:
name: termora-windows-zip-${{ runner.arch }}
path: |
build/distributions/*.zip
- name: Upload exe artifact
uses: actions/upload-artifact@v4
with:
name: termora-windows-exe-${{ runner.arch }}
path: |
build/distributions/*.exe
- name: Upload msix artifact
uses: actions/upload-artifact@v4
with:
name: termora-windows-msix-${{ runner.arch }}
path: |
build/distributions/*.msix

126
README.md
View File

@@ -1,53 +1,101 @@
<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
- Support for X11 and SSH-Agent
- 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
- Support [plugins](https://www.termora.app/plugins)
- ...
## 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`
- <img src="https://apps.microsoft.com/assets/icons/logo-16x16.png" alt="microsoft logo"/> <b>Microsoft Store</b>: <a href="https://apps.microsoft.com/store/detail/9NRZBHG43SB9?cid=DevShareMCLPCS">Visit Termora in the Microsoft Store</a>
## 🛠️ Development
We recommend using the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK for development.
- Run locally: `./gradlew :run`
## 📄 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,48 +1,99 @@
# 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 端口转发和跳板机
- 支持 X11 和 SSH-Agent
- 终端日志记录
- 支持配置同步到 [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
- 支持宏(录制脚本并回放)
- 支持关键词高亮
- 支持密钥管理器
- 支持将命令发送到多个会话
- 支持 [Find Everywhere](./docs/findeverywhere-zh_CN.png?raw=1) 快速跳转
- 支持数据加密
- 支持[插件](https://www.termora.app/plugins)
- ...
## 下载
## ✨ 功能特性
- [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`
- <img src="https://apps.microsoft.com/assets/icons/logo-16x16.png" alt="microsoft logo"/> <b>Microsoft Store</b>: <a href="https://apps.microsoft.com/store/detail/9NRZBHG43SB9?cid=DevShareMCLPCS">Termora</a>
## 🛠️ 开发指南
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK 运行环境。
- 本地运行:`./gradlew :run`
## 📄 授权协议
Termora 采用双重许可方式,您可以选择:
- **AGPL-3.0**:自由使用、修改、分发(遵循 [AGPL 条款](https://opensource.org/license/agpl-v3)
- **专有许可**:如需闭源或商业用途,请联系作者获取授权

View File

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

View File

@@ -1,3 +1,4 @@
import org.apache.tools.ant.filters.ReplaceTokens
import org.gradle.internal.jvm.Jvm
import org.gradle.kotlin.dsl.support.uppercaseFirstChar
import org.gradle.nativeplatform.platform.internal.ArchitectureInternal
@@ -28,7 +29,10 @@ 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"
val makeAppx = if (os.isWindows) StringUtils.defaultString(System.getenv("MAKEAPPX_PATH")) else StringUtils.EMPTY
val isDeb = os.isLinux && System.getenv("TERMORA_TYPE") == "deb"
val isAppx = os.isWindows && makeAppx.isNotBlank() && System.getenv("TERMORA_TYPE") == "appx"
val isBeta = project.version.toString().contains("beta", ignoreCase = true)
// macOS 签名信息
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
@@ -62,9 +66,6 @@ dependencies {
testImplementation(libs.h2)
testImplementation(libs.exposed.migration)
// implementation(platform(libs.koin.bom))
// implementation(libs.koin.core)
api(kotlin("reflect"))
api(libs.slf4j.api)
api(libs.pty4j)
@@ -105,7 +106,6 @@ dependencies {
api(libs.colorpicker)
api(libs.mixpanel)
api(libs.jSerialComm)
api(libs.ini4j)
api(libs.restart4j)
api(libs.exposed.core)
@@ -137,10 +137,6 @@ application {
args.add("-DTERMORA_PLUGIN_DIRECTORY=${layout.buildDirectory.get().asFile.absolutePath}${File.separator}plugins")
if (os.isLinux) {
args.add("-Dsun.java2d.opengl=true")
}
applicationDefaultJvmArgs = args
mainClass = "app.termora.MainKt"
}
@@ -177,102 +173,149 @@ publishing {
}
}
tasks.processResources {
val betaVersion = project.version.toString().substringAfterLast('.')
filesMatching("**/AppxManifest.xml") {
filter<ReplaceTokens>(
"tokens" to mapOf(
"version" to appVersion,
"betaVersion" to if (isBeta) betaVersion else "0",
"architecture" to if (arch.isArm64) "arm64" else "x64",
"projectDir" to project.projectDir.absolutePath,
)
)
}
}
tasks.test {
useJUnitPlatform()
}
@Suppress("CascadeIf")
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()
val archName = if (arch.isArm) "aarch64" else "x86_64"
val dylib = dir.get().dir("dylib").asFile
// 对 JNA 和 PTY4J 的本地库提取
// 提取出来是为了单独签名,不然无法通过公证
if (os.isMacOsX && macOSSign) {
doLast {
val archName = if (arch.isArm) "aarch64" else "x86_64"
val dylib = dir.get().dir("dylib").asFile
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
val targetDir = File(dylib, jna.name)
FileUtils.forceMkdir(targetDir)
doLast {
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
val targetDir = File(dylib, jna.name)
FileUtils.forceMkdir(targetDir)
if (os.isWindows) {
// @formatter:off
exec { commandLine("unzip","-j","-o", file.absolutePath, "com/sun/jna/win32-${arch.name}/*", "-d", targetDir.absolutePath) }
// @formatter:on
} else if (os.isLinux) {
// @formatter:off
exec { commandLine("unzip","-j","-o", file.absolutePath, "com/sun/jna/linux-${arch.name}/*", "-d", targetDir.absolutePath) }
// @formatter:on
} else if (os.isMacOsX) {
// @formatter:off
exec { commandLine("unzip","-j","-o", file.absolutePath, "com/sun/jna/darwin-${arch.name}/*", "-d", targetDir.absolutePath) }
// @formatter:on
// 删除所有二进制类库
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/darwin-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-*") }
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/linux-*") }
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-*") }
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
val targetDir = FileUtils.getFile(dylib, pty4j.name, "darwin")
FileUtils.forceMkdir(targetDir)
}
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/linux-*") }
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-*") }
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
val osName = if (os.isWindows) "win32" else if (os.isMacOsX) "darwin" else "linux"
val myArchName = if (arch.isArm) "aarch64" else "x86-64"
val targetDir = if (os.isMacOsX) FileUtils.getFile(dylib, pty4j.name, osName)
else FileUtils.getFile(dylib, pty4j.name, osName, myArchName)
FileUtils.forceMkdir(targetDir)
if (os.isWindows) {
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/*win/${myArchName}/*", "-d", targetDir.absolutePath) }
// @formatter:on
} else if (os.isLinux) {
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/*linux/${myArchName}/*", "-d", targetDir.absolutePath) }
// @formatter:on
} else if (os.isMacOsX) {
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/com/pty4j/native/darwin*", "-d", targetDir.absolutePath) }
// @formatter:on
// 删除所有二进制类库
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
} else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) {
val targetDir = FileUtils.getFile(dylib, jSerialComm.name, "OSX", archName)
FileUtils.forceMkdir(targetDir)
}
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
} else if ("${restart4j.name}-${restart4j.version}" == file.nameWithoutExtension) {
val targetDir = FileUtils.getFile(dylib, restart4j.name)
FileUtils.forceMkdir(targetDir)
if (os.isWindows) {
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "OSX/${archName}/*", "-d", targetDir.absolutePath) }
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "win32/${archName}/*", "-d", targetDir.absolutePath) }
// @formatter:on
// 删除所有二进制类库
exec { commandLine("zip", "-d", file.absolutePath, "Android/*") }
exec { commandLine("zip", "-d", file.absolutePath, "FreeBSD/*") }
exec { commandLine("zip", "-d", file.absolutePath, "Linux/*") }
exec { commandLine("zip", "-d", file.absolutePath, "OpenBSD/*") }
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)
} else if (os.isLinux) {
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "linux/${archName}/*", "-d", targetDir.absolutePath) }
// @formatter:on
} else if (os.isMacOsX) {
// @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)
}
// 设置可执行权限
for (e in FileUtils.listFiles(
targetDir,
FileFilterUtils.trueFileFilter(),
FileFilterUtils.falseFileFilter()
)) e.setExecutable(true)
exec { commandLine("zip", "-d", file.absolutePath, "win32/*") }
exec { commandLine("zip", "-d", file.absolutePath, "darwin/*") }
exec { commandLine("zip", "-d", file.absolutePath, "linux/*") }
} else if ("${sqlite.name}-${sqlite.version}" == file.nameWithoutExtension) {
val targetDir = FileUtils.getFile(dylib, sqlite.name)
FileUtils.forceMkdir(targetDir)
if (os.isWindows) {
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "org/sqlite/native/Windows/${archName}/*", "-d", targetDir.absolutePath) }
// @formatter:on
} else if (os.isLinux) {
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "org/sqlite/native/Linux/${archName}/*", "-d", targetDir.absolutePath) }
// @formatter:on
} else if (os.isMacOsX) {
// @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
}
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
if (os.isWindows) {
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "com/formdev/flatlaf/natives/*windows*${if (isArm) "arm64" else "x86_64"}*", "-d", targetDir.absolutePath) }
// @formatter:on
} else if (os.isLinux) {
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "com/formdev/flatlaf/natives/*linux*${if (isArm) "arm64" else "x86_64"}*", "-d", targetDir.absolutePath) }
// @formatter:on
} else if (os.isMacOsX) {
// @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/*") }
}
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*") }
}
}
// 对二进制签名
// 对二进制签名
if (os.isMacOsX) {
Files.walk(dylib.toPath()).use { paths ->
for (path in paths) {
if (Files.isRegularFile(path)) {
@@ -281,116 +324,8 @@ 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*") }
}
}
}
}
}
}
}
tasks.register<Exec>("jlink") {
@@ -441,6 +376,7 @@ tasks.register<Exec>("jpackage") {
// 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")
@@ -448,7 +384,7 @@ tasks.register<Exec>("jpackage") {
}
if (os.isLinux) {
options.add("-Dsun.java2d.opengl=true")
options.add("--add-opens=java.desktop/sun.awt.X11=ALL-UNNAMED")
if (isDeb) {
options.add("-Djpackage.app-layout=deb")
}
@@ -488,10 +424,6 @@ tasks.register<Exec>("jpackage") {
}
if (os.isWindows) {
arguments.add("--win-dir-chooser")
arguments.add("--win-shortcut")
arguments.add("--win-shortcut-prompt")
arguments.addAll(listOf("--win-upgrade-uuid", "E1D93CAD-5BF8-442E-93BA-6E90DE601E4C"))
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.ico"))
}
@@ -504,7 +436,7 @@ tasks.register<Exec>("jpackage") {
if (os.isMacOsX) {
arguments.add("dmg")
} else if (os.isWindows) {
arguments.add("msi")
arguments.add("app-image")
} else if (os.isLinux) {
arguments.add(if (isDeb) "deb" else "app-image")
if (isDeb) {
@@ -515,7 +447,7 @@ tasks.register<Exec>("jpackage") {
throw UnsupportedOperationException()
}
if (os.isMacOsX && macOSSign) {
if (macOSSign) {
arguments.add("--mac-sign")
arguments.add("--mac-signing-key-user-name")
arguments.add(macOSSignUsername)
@@ -527,31 +459,20 @@ tasks.register<Exec>("jpackage") {
tasks.register("dist") {
doLast {
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()
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
// 清空目录
exec { commandLine(gradlew, "clean") }
// 构建自带的插件
exec { commandLine(gradlew, ":plugins:migration:build") }
// 打包并复制依赖
exec {
commandLine(gradlew, ":jar", ":copy-dependencies")
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")
}
// 检查依赖的开源协议
exec { commandLine(gradlew, ":check-license") }
// jlink
exec { commandLine(gradlew, ":jlink") }
// 打包
exec { commandLine(gradlew, ":jpackage", "-Dtype=${System.getProperty("type")}") }
// 根据不同的系统构建不同的二进制包
pack()
}
}
@@ -582,35 +503,36 @@ tasks.register("check-license") {
}
}
/**
* 构建包
*/
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 dir = layout.buildDirectory.dir("distributions").get().asFile
val cfg = FileUtils.getFile(dir, projectName, "app", "${projectName}.cfg")
val configText = cfg.readText()
// appx
if (isAppx) {
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=appx").toString())
val appxManifest = FileUtils.getFile(dir, projectName, "AppxManifest.xml")
layout.buildDirectory.file("resources/main/AppxManifest.xml").get().asFile
.renameTo(appxManifest)
val icons = setOf("termora.png", "termora_44x44.png", "termora_150x150.png")
for (file in projectDir.resolve("src/main/resources/icons/").listFiles()) {
if (icons.contains(file.name)) {
val p = appxManifest.parentFile.resolve("icons/${file.name}")
FileUtils.forceMkdirParent(p)
file.copyTo(p, true)
}
}
exec {
commandLine(makeAppx, "pack", "/d", projectName, "/p", "${finalFilenameWithoutExtension}.msix")
workingDir = dir
}
return
}
// zip
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=zip").toString())
exec {
@@ -632,21 +554,22 @@ fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: Str
"/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}",
"/DMyWizardSmallImageFile=${
FileUtils.getFile(
projectDir,
"src",
"main",
"resources",
"icons",
"termora_128x128.bmp"
)
}",
"/DMySourceDir=${FileUtils.getFile(dir, projectName).absolutePath}",
"/F${finalFilenameWithoutExtension}",
FileUtils.getFile(projectDir, "src", "main", "resources", "termora.iss")
)
}
// msi
exec {
commandLine(
"cmd", "/c", "move",
"${projectName}-${appVersion}.msi",
"${finalFilenameWithoutExtension}.msi"
)
workingDir = distributionDir.asFile
}
}
/**
@@ -662,7 +585,7 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
// @formatter:on
// sign dmg
if (macOSSign) signMacOSLocalFile(dmgFile)
signMacOSLocalFile(dmgFile)
// 找到 .app
val imageFile = layout.buildDirectory.dir("jpackage/images/").get().asFile
@@ -675,7 +598,7 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
// @formatter:on
// sign zip
if (macOSSign) signMacOSLocalFile(zipFile)
signMacOSLocalFile(zipFile)
// 公证
if (macOSNotary) {
@@ -750,7 +673,7 @@ fun packOnLinux(distributionDir: Directory, finalFilenameWithoutExtension: Strin
commandLine(
"wget",
"-O", appimagetool.absolutePath,
"https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-${if (arch.isArm) "aarch64" else "x86_64"}.AppImage"
"https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${if (arch.isArm) "aarch64" else "x86_64"}.AppImage"
)
workingDir = distributionDir.asFile
}
@@ -759,17 +682,24 @@ fun packOnLinux(distributionDir: Directory, finalFilenameWithoutExtension: Strin
exec { commandLine("chmod", "+x", appimagetool.absolutePath) }
}
// Desktop file
val termoraName = project.name.uppercaseFirstChar()
// copy icon
FileUtils.copyFile(
File("${projectDir.absolutePath}/src/main/resources/icons/termora_256x256.png"),
distributionDir.file(termoraName + File.separator + termoraName + ".png").asFile
)
val desktopFile = distributionDir.file(termoraName + File.separator + termoraName + ".desktop").asFile
desktopFile.writeText(
"""[Desktop Entry]
Type=Application
Name=${termoraName}
Comment=Terminal emulator and SSH client
Icon=/lib/${termoraName}
Icon=${termoraName}
Categories=Development;
StartupWMClass=${termoraName}
Terminal=false
""".trimIndent()
)

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,17 +1,17 @@
[versions]
kotlin = "2.2.0"
kotlin = "2.2.10"
slf4j = "2.0.17"
pty4j = "0.13.6"
pty4j = "0.13.10"
tinylog = "2.7.0"
kotlinx-coroutines = "1.10.2"
flatlaf = "3.6"
kotlinx-serialization-json = "1.8.1"
commons-codec = "1.18.0"
commons-lang3 = "3.17.0"
commons-csv = "1.14.0"
commons-net = "3.11.1"
commons-text = "1.13.1"
commons-compress = "1.27.1"
flatlaf = "3.6.1"
kotlinx-serialization-json = "1.9.0"
commons-codec = "1.19.0"
commons-lang3 = "3.18.0"
commons-csv = "1.14.1"
commons-net = "3.12.0"
commons-text = "1.14.0"
commons-compress = "1.28.0"
commons-vfs2 = "2.10.0"
swingx = "1.6.5-1"
jgoodies-forms = "1.9.0"
@@ -20,34 +20,34 @@ oshi = "6.8.1"
versioncompare = "1.4.1"
jna = "5.17.0"
jSystemThemeDetector = "3.9.1"
commons-io = "2.19.0"
commons-io = "2.20.0"
jbr-api = "17.1.10.1"
hutool = "5.8.37"
jsch = "0.2.26"
okhttp = "4.12.0"
hutool = "5.8.39"
jsch = "2.27.2"
okhttp = "5.1.0"
sshj = "0.39.0"
sshd-core = "2.15.0"
jgit = "7.2.0.202503040940-r"
commonmark = "0.25.0"
commonmark = "0.25.1"
jnafilechooser = "1.1.2"
xodus = "2.0.1"
bip39 = "1.0.9"
colorpicker = "2.0.1"
rhino = "1.8.0"
delight-rhino-sandbox = "0.0.17"
testcontainers = "1.21.2"
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-2"
exposed = "1.0.0-beta-5"
h2 = "2.3.232"
sqlite = "3.50.1.0"
sqlite = "3.50.3.0"
jug = "5.1.0"
semver4j = "5.8.0"
jsvg = "1.4.0"
dom4j = "2.1.4"
semver4j = "6.0.0"
jsvg = "2.0.0"
dom4j = "2.2.0"
[libraries]
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }

View File

@@ -5,6 +5,7 @@ The files in this catalogue are for public access only. Specific descriptions ar
- 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.
- Written permission must be obtained from the author for any use beyond personal viewing;
- If you submit a Pull Request that modifies, supplements, or adds to the files in this directory or its subdirectories, unless otherwise agreed in writing, you agree that the copyright of your contribution is owned by hstyi and may be used and managed under the current license terms.
All rights reserved.

View File

@@ -72,4 +72,8 @@ 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/
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

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
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
@@ -96,9 +95,7 @@ internal class BackgroundManager private constructor() : Disposable, GlassPaneAw
return
}
val body = response.body
if (body != null) {
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
}
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
IOUtils.closeQuietly(body)
return@use tempFile
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,13 +2,13 @@ plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.2"
project.version = "0.0.4"
dependencies {
testImplementation(kotlin("test"))
implementation("com.qcloud:cos_api:5.6.245")
implementation("com.qcloud:cos_api:5.6.251")
compileOnly(project(":"))
}

View File

@@ -1,5 +1,6 @@
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
@@ -13,7 +14,7 @@ class COSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
return COSProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return COSProtocolHostPanel()
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,7 +35,7 @@ import javax.swing.event.DocumentEvent
import kotlin.math.max
import kotlin.math.min
class EditorPanel(private val window: JDialog, private val file: File) : JPanel(BorderLayout()) {
class EditorPanel(private val window: JFrame, private val file: File) : JPanel(BorderLayout()) {
companion object {
private val log = LoggerFactory.getLogger(EditorPanel::class.java)

View File

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

View File

@@ -0,0 +1,2 @@
termora.plugins.editor.not-save=The file has not been saved. Are you sure you want to exit?

View File

@@ -0,0 +1 @@
termora.plugins.editor.not-save=Файл не сохранён. Вы уверены, что хотите выйти?

View File

@@ -0,0 +1 @@
termora.plugins.editor.not-save=文件尚未保存,你确定要退出吗?

View File

@@ -0,0 +1 @@
termora.plugins.editor.not-save=檔案尚未儲存,你確定要退出嗎?

View File

@@ -2,13 +2,13 @@ plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.1"
project.version = "0.0.2"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
implementation("org.apache.commons:commons-pool2:2.12.1")
testImplementation(project(":"))
}

View File

@@ -1,41 +0,0 @@
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,23 @@
package app.termora.plugins.ftp
import app.termora.transfer.s3.S3FileSystem
import app.termora.transfer.s3.S3Path
import org.apache.commons.io.IOUtils
import org.apache.commons.net.ftp.FTPClient
import org.apache.commons.pool2.impl.GenericObjectPool
class FTPFileSystem(private val pool: GenericObjectPool<FTPClient>) : S3FileSystem(FTPSystemProvider(pool)) {
override fun create(root: String?, names: List<String>): S3Path {
val path = FTPPath(this, root, names)
if (names.isEmpty()) {
path.attributes = path.attributes.copy(directory = true)
}
return path
}
override fun close() {
IOUtils.closeQuietly(pool)
super.close()
}
}

View File

@@ -0,0 +1,393 @@
package app.termora.plugins.ftp
import app.termora.*
import app.termora.keymgr.KeyManager
import app.termora.plugin.internal.BasicProxyOption
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatComboBox
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.Component
import java.awt.KeyboardFocusManager
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.awt.event.ItemEvent
import java.nio.charset.Charset
import javax.swing.*
class FTPHostOptionsPane : OptionsPane() {
private val generalOption = GeneralOption()
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
private val sftpOption = SFTPOption()
init {
addOption(generalOption)
addOption(proxyOption)
addOption(sftpOption)
}
fun getHost(): Host {
val name = generalOption.nameTextField.text
val protocol = FTPProtocolProvider.PROTOCOL
val port = generalOption.portTextField.value as Int
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,
encoding = sftpOption.charsetComboBox.selectedItem as String,
extras = mutableMapOf("passive" to (sftpOption.passiveComboBox.selectedItem as PassiveMode).name)
)
return Host(
name = name,
protocol = protocol,
port = port,
host = generalOption.hostTextField.text,
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.hostTextField.text = host.host
generalOption.portTextField.value = host.port
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
val passive = host.options.extras["passive"] ?: PassiveMode.Local.name
sftpOption.charsetComboBox.selectedItem = host.options.encoding
sftpOption.passiveComboBox.selectedItem = runCatching { PassiveMode.valueOf(passive) }
.getOrNull() ?: PassiveMode.Local
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
}
fun validateFields(): Boolean {
val host = getHost()
// general
if (validateField(generalOption.nameTextField)) {
return false
}
if (validateField(generalOption.hostTextField)) {
return false
}
if (StringUtils.isNotBlank(generalOption.usernameTextField.text) || generalOption.passwordTextField.password.isNotEmpty()) {
if (validateField(generalOption.usernameTextField)) {
return false
}
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 && (if (textField is JPasswordField) textField.password.isEmpty() else 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()
}
inner class GeneralOption : JPanel(BorderLayout()), Option {
val portTextField = PortSpinner(21)
val nameTextField = OutlineTextField(128)
val usernameTextField = OutlineTextField(128)
val hostTextField = OutlineTextField(255)
val passwordTextField = OutlinePasswordField(255)
val publicKeyComboBox = OutlineComboBox<String>()
val remarkTextArea = FixedLengthTextArea(512)
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
init {
initView()
initEvents()
}
private fun initView() {
add(getCenterComponent(), BorderLayout.CENTER)
publicKeyComboBox.isEditable = false
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component {
var text = StringUtils.EMPTY
if (value is String) {
text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text
}
return super.getListCellRendererComponent(
list,
text,
index,
isSelected,
cellHasFocus
)
}
}
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component {
var text = value?.toString() ?: ""
when (value) {
AuthenticationType.Password -> {
text = "Password"
}
AuthenticationType.PublicKey -> {
text = "Public Key"
}
AuthenticationType.KeyboardInteractive -> {
text = "Keyboard Interactive"
}
}
return super.getListCellRendererComponent(
list,
text,
index,
isSelected,
cellHasFocus
)
}
}
authenticationTypeComboBox.addItem(AuthenticationType.No)
authenticationTypeComboBox.addItem(AuthenticationType.Password)
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
}
private fun initEvents() {
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
removeComponentListener(this)
}
})
authenticationTypeComboBox.addItemListener {
if (it.stateChange == ItemEvent.SELECTED) {
passwordTextField.isEnabled = authenticationTypeComboBox.selectedItem == AuthenticationType.Password
}
}
}
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("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
.add(hostTextField).xy(3, rows)
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
.add(portTextField).xy(7, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
.add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
.add(passwordTextField).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)
val charsetComboBox = JComboBox<String>()
val passiveComboBox = JComboBox<PassiveMode>()
init {
initView()
initEvents()
}
private fun initView() {
for (e in Charset.availableCharsets()) {
charsetComboBox.addItem(e.key)
}
charsetComboBox.selectedItem = "UTF-8"
passiveComboBox.addItem(PassiveMode.Local)
passiveComboBox.addItem(PassiveMode.Remote)
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.new-host.terminal.encoding")}:").xy(1, rows)
.add(charsetComboBox).xy(3, rows).apply { rows += step }
.add("${FTPI18n.getString("termora.plugins.ftp.passive")}:").xy(1, rows)
.add(passiveComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
.build()
return panel
}
}
enum class PassiveMode {
Local,
Remote,
}
}

View File

@@ -0,0 +1,24 @@
package app.termora.plugins.ftp
import app.termora.I18n
import app.termora.NamedI18n
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.*
object FTPI18n : NamedI18n("i18n/messages") {
private val log = LoggerFactory.getLogger(FTPI18n::class.java)
override fun getLogger(): Logger {
return log
}
override fun getString(key: String): String {
return try {
substitutor.replace(getBundle().getString(key))
} catch (_: MissingResourceException) {
I18n.getString(key)
}
}
}

View File

@@ -0,0 +1,20 @@
package app.termora.plugins.ftp
import app.termora.transfer.s3.S3Path
class FTPPath(fileSystem: FTPFileSystem, root: String?, names: List<String>) : S3Path(fileSystem, root, names) {
override val isBucket: Boolean
get() = false
override val bucketName: String
get() = throw UnsupportedOperationException()
override val objectName: String
get() = throw UnsupportedOperationException()
override fun getCustomType(): String? {
return null
}
}

View File

@@ -1,8 +1,5 @@
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
@@ -27,6 +24,7 @@ class FTPPlugin : PaidPlugin {
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}

View File

@@ -1,22 +1,36 @@
package app.termora.plugins.ftp
import app.termora.Disposer
import app.termora.Host
import app.termora.protocol.ProtocolHostPanel
import org.apache.commons.lang3.StringUtils
import java.awt.BorderLayout
class FTPProtocolHostPanel : ProtocolHostPanel() {
private val pane = FTPHostOptionsPane()
init {
initView()
initEvents()
}
private fun initView() {
add(pane, BorderLayout.CENTER)
Disposer.register(this, pane)
}
private fun initEvents() {}
override fun getHost(): Host {
return Host(
name = StringUtils.EMPTY,
protocol = FTPProtocolProvider.PROTOCOL
)
return pane.getHost()
}
override fun setHost(host: Host) {
pane.setHost(host)
}
override fun validateFields(): Boolean {
return true
return pane.validateFields()
}
}

View File

@@ -1,19 +1,20 @@
package app.termora.plugins.ftp
import app.termora.account.AccountOwner
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() }
val instance = FTPProtocolHostPanelExtension()
}
override fun getProtocolProvider(): ProtocolProvider {
return FTPProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return FTPProtocolHostPanel()
}
}

View File

@@ -1,16 +1,33 @@
package app.termora.plugins.ftp
import app.termora.AuthenticationType
import app.termora.DynamicIcon
import app.termora.Icons
import app.termora.protocol.FileObjectHandler
import app.termora.protocol.FileObjectRequest
import app.termora.ProxyType
import app.termora.protocol.PathHandler
import app.termora.protocol.PathHandlerRequest
import app.termora.protocol.TransferProtocolProvider
import org.apache.commons.vfs2.provider.FileProvider
import org.apache.commons.lang3.StringUtils
import org.apache.commons.net.ftp.FTPClient
import org.apache.commons.pool2.BasePooledObjectFactory
import org.apache.commons.pool2.PooledObject
import org.apache.commons.pool2.impl.DefaultPooledObject
import org.apache.commons.pool2.impl.GenericObjectPool
import org.apache.commons.pool2.impl.GenericObjectPoolConfig
import org.slf4j.LoggerFactory
import java.net.InetSocketAddress
import java.net.Proxy
import java.nio.charset.Charset
import java.time.Duration
class FTPProtocolProvider private constructor() : TransferProtocolProvider {
companion object {
val instance by lazy { FTPProtocolProvider() }
private val log = LoggerFactory.getLogger(FTPProtocolProvider::class.java)
val instance = FTPProtocolProvider()
const val PROTOCOL = "FTP"
}
@@ -22,12 +39,82 @@ class FTPProtocolProvider private constructor() : TransferProtocolProvider {
return Icons.ftp
}
override fun getFileProvider(): FileProvider {
return FTPFileProvider.instance
override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
val host = requester.host
val config = GenericObjectPoolConfig<FTPClient>().apply {
maxTotal = 12
// 与 transfer 最大传输量匹配
maxIdle = 6
minIdle = 1
testOnBorrow = false
testWhileIdle = true
// 检测空闲对象线程每次运行时检测的空闲对象的数量
timeBetweenEvictionRuns = Duration.ofSeconds(30)
// 连接空闲的最小时间,达到此值后空闲链接将会被移除,且保留 minIdle 个空闲连接数
softMinEvictableIdleDuration = Duration.ofSeconds(30)
// 连接的最小空闲时间,达到此值后该空闲连接可能会被移除(还需看是否已达最大空闲连接数)
minEvictableIdleDuration = Duration.ofMinutes(3)
}
val ftpClientPool = GenericObjectPool(object : BasePooledObjectFactory<FTPClient>() {
override fun create(): FTPClient {
val client = FTPClient()
client.charset = Charset.forName(host.options.encoding)
client.controlEncoding = client.charset.name()
client.connect(host.host, host.port)
if (client.isConnected.not()) {
throw IllegalStateException("FTP client is not connected")
}
if (host.proxy.type == ProxyType.HTTP) {
client.proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress(host.proxy.host, host.proxy.port))
} else if (host.proxy.type == ProxyType.SOCKS5) {
client.proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(host.proxy.host, host.proxy.port))
}
val password = if (host.authentication.type == AuthenticationType.Password)
host.authentication.password else StringUtils.EMPTY
if (client.login(host.username, password).not()) {
throw IllegalStateException("Incorrect account or password")
}
if (host.options.extras["passive"] == FTPHostOptionsPane.PassiveMode.Remote.name) {
client.enterRemotePassiveMode()
} else {
client.enterLocalPassiveMode()
}
client.listHiddenFiles = true
return client
}
override fun wrap(obj: FTPClient): PooledObject<FTPClient> {
return DefaultPooledObject(obj)
}
override fun validateObject(p: PooledObject<FTPClient>): Boolean {
val ftp = p.`object`
return ftp.isConnected.not() && ftp.sendNoOp()
}
override fun destroyObject(p: PooledObject<FTPClient>) {
try {
p.`object`.disconnect()
} catch (e: Exception) {
if (log.isWarnEnabled) {
log.warn(e.message, e)
}
}
}
}, config)
val defaultPath = host.options.sftpDefaultDirectory
val fs = FTPFileSystem(ftpClientPool)
return PathHandler(fs, fs.getPath(defaultPath))
}
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
TODO("Not yet implemented")
}
}

View File

@@ -5,10 +5,10 @@ import app.termora.protocol.ProtocolProviderExtension
class FTPProtocolProviderExtension private constructor() : ProtocolProviderExtension {
companion object {
val instance by lazy { FTPProtocolProviderExtension() }
val instance = FTPProtocolProviderExtension()
}
override fun getProtocolProvider(): ProtocolProvider {
return FTPProtocolProvider.Companion.instance
return FTPProtocolProvider.instance
}
}

View File

@@ -0,0 +1,158 @@
package app.termora.plugins.ftp
import app.termora.transfer.s3.S3FileSystemProvider
import app.termora.transfer.s3.S3Path
import org.apache.commons.io.IOUtils
import org.apache.commons.net.ftp.FTPClient
import org.apache.commons.net.ftp.FTPFile
import org.apache.commons.pool2.impl.GenericObjectPool
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.AccessMode
import java.nio.file.CopyOption
import java.nio.file.NoSuchFileException
import java.nio.file.Path
import java.nio.file.attribute.FileAttribute
import java.nio.file.attribute.PosixFilePermission
import kotlin.io.path.absolutePathString
import kotlin.io.path.exists
class FTPSystemProvider(private val pool: GenericObjectPool<FTPClient>) : S3FileSystemProvider() {
override fun getScheme(): String? {
return "ftp"
}
override fun getOutputStream(path: S3Path): OutputStream {
return createStreamer(path)
}
override fun getInputStream(path: S3Path): InputStream {
val ftp = pool.borrowObject()
val fs = ftp.retrieveFileStream(path.absolutePathString())
return object : InputStream() {
override fun read(): Int {
return fs.read()
}
override fun close() {
IOUtils.closeQuietly(fs)
ftp.completePendingCommand()
pool.returnObject(ftp)
}
}
}
private fun createStreamer(path: S3Path): OutputStream {
val ftp = pool.borrowObject()
val os = ftp.storeFileStream(path.absolutePathString())
return object : OutputStream() {
override fun write(b: Int) {
os.write(b)
}
override fun close() {
IOUtils.closeQuietly(os)
ftp.completePendingCommand()
pool.returnObject(ftp)
}
}
}
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
val paths = mutableListOf<S3Path>()
if (path.exists().not()) {
throw NoSuchFileException(path.absolutePathString())
}
withFtpClient {
val files = it.listFiles(path.absolutePathString())
for (file in files) {
val p = path.resolve(file.name)
p.attributes = p.attributes.copy(
directory = file.isDirectory,
regularFile = file.isFile,
size = file.size,
lastModifiedTime = file.timestamp.timeInMillis,
)
p.attributes.permissions = ftpPermissionsToPosix(file)
paths.add(p)
}
}
return paths
}
private fun ftpPermissionsToPosix(file: FTPFile): Set<PosixFilePermission> {
val perms = mutableSetOf<PosixFilePermission>()
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION))
perms.add(PosixFilePermission.OWNER_READ)
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION))
perms.add(PosixFilePermission.OWNER_WRITE)
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION))
perms.add(PosixFilePermission.OWNER_EXECUTE)
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.READ_PERMISSION))
perms.add(PosixFilePermission.GROUP_READ)
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.WRITE_PERMISSION))
perms.add(PosixFilePermission.GROUP_WRITE)
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.EXECUTE_PERMISSION))
perms.add(PosixFilePermission.GROUP_EXECUTE)
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.READ_PERMISSION))
perms.add(PosixFilePermission.OTHERS_READ)
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.WRITE_PERMISSION))
perms.add(PosixFilePermission.OTHERS_WRITE)
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.EXECUTE_PERMISSION))
perms.add(PosixFilePermission.OTHERS_EXECUTE)
return perms
}
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) {
withFtpClient { it.mkd(dir.absolutePathString()) }
}
override fun move(source: Path?, target: Path?, vararg options: CopyOption?) {
if (source != null && target != null) {
withFtpClient {
it.rename(source.absolutePathString(), target.absolutePathString())
}
}
}
override fun delete(path: S3Path, isDirectory: Boolean) {
withFtpClient {
if (isDirectory) {
it.rmd(path.absolutePathString())
} else {
it.deleteFile(path.absolutePathString())
}
}
}
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
withFtpClient {
if (it.cwd(path.absolutePathString()) == 250) {
return
}
if (it.listFiles(path.absolutePathString()).isNotEmpty()) {
return
}
}
throw NoSuchFileException(path.absolutePathString())
}
private inline fun <T> withFtpClient(block: (FTPClient) -> T): T {
val client = pool.borrowObject()
return try {
block(client)
} finally {
pool.returnObject(client)
}
}
}

View File

@@ -14,7 +14,7 @@
<descriptions>
<description>Connecting to FTP</description>
<description language="zh_CN">支持连接到 FTP</description>
<description language="zh_CN">支持连接到 FTP</description>
<description language="zh_TW">支援連接到 FTP</description>
</descriptions>

View File

@@ -1 +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>
<svg t="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#6C707E"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-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.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#6C707E"></path></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1 +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>
<svg t="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#CED0D6"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-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.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#CED0D6"></path></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
termora.plugins.ftp.passive=Passive Mode

View File

@@ -0,0 +1 @@
termora.plugins.ftp.passive=被动模式

View File

@@ -0,0 +1 @@
termora.plugins.ftp.passive=被動模式

View File

@@ -2,14 +2,14 @@ plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.5"
project.version = "0.0.8"
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")
implementation("com.github.hstyi:geolite2:v1.0-202508110059")
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -5,6 +5,7 @@ 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
@@ -33,7 +34,7 @@ class GeoSimpleTreeCellRendererExtension private constructor() : SimpleTreeCellR
if (GeoHostTreeShowMoreEnableExtension.instance.isShowMore().not()) return emptyList()
val country = geo.country(node.data.host) ?: return emptyList()
val text = "${countryCodeToFlagEmoji(country.isoCode)}${country.name}"
val text = if (SystemInfo.isMacOS) "${countryCodeToFlagEmoji(country.isoCode)}${country.name}" else country.name
return listOf(
MarkerSimpleTreeCellAnnotation(
text,

View File

@@ -3,7 +3,7 @@ plugins {
}
project.version = "0.0.2"
project.version = "0.0.4"
dependencies {

View File

@@ -46,7 +46,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
controlsVisible = false
if (SystemInfo.isWindows || SystemInfo.isLinux) {
title = I18n.getString("termora.doorman.safe")
title = MigrationI18n.getString("termora.doorman.safe")
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
}
@@ -65,8 +65,8 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
}
override fun createCenterPanel(): JComponent {
label.text = I18n.getString("termora.doorman.safe")
tip.text = I18n.getString("termora.doorman.unlock-data")
label.text = MigrationI18n.getString("termora.doorman.safe")
tip.text = MigrationI18n.getString("termora.doorman.unlock-data")
icon.icon = FlatSVGIcon(Icons.role.name, 80, 80)
safeBtn.icon = Icons.unlocked
@@ -95,24 +95,24 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
.add(passwordTextField).xy(2, rows)
.add(safeBtn).xy(4, rows).apply { rows += step }
.add(tip).xyw(2, rows, 4, "center, fill").apply { rows += step }
.add(JXHyperlink(object : AnAction(I18n.getString("termora.doorman.forget-password")) {
.add(JXHyperlink(object : AnAction(MigrationI18n.getString("termora.doorman.forget-password")) {
override fun actionPerformed(evt: AnActionEvent) {
val option = OptionPane.showConfirmDialog(
this@DoormanDialog, I18n.getString("termora.doorman.forget-password-message"),
this@DoormanDialog, MigrationI18n.getString("termora.doorman.forget-password-message"),
options = arrayOf(
I18n.getString("termora.doorman.have-a-mnemonic"),
I18n.getString("termora.doorman.dont-have-a-mnemonic"),
MigrationI18n.getString("termora.doorman.have-a-mnemonic"),
MigrationI18n.getString("termora.doorman.dont-have-a-mnemonic"),
),
optionType = JOptionPane.YES_NO_OPTION,
messageType = JOptionPane.INFORMATION_MESSAGE,
initialValue = I18n.getString("termora.doorman.have-a-mnemonic")
initialValue = MigrationI18n.getString("termora.doorman.have-a-mnemonic")
)
if (option == JOptionPane.YES_OPTION) {
showMnemonicsDialog()
} else if (option == JOptionPane.NO_OPTION) {
OptionPane.showMessageDialog(
this@DoormanDialog,
I18n.getString("termora.doorman.delete-data"),
MigrationI18n.getString("termora.doorman.delete-data"),
messageType = JOptionPane.WARNING_MESSAGE
)
Application.browse(MigrationApplicationRunnerExtension.instance.getDatabaseFile().toURI())
@@ -141,7 +141,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
log.error(e.message, e)
}
OptionPane.showMessageDialog(
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
this, MigrationI18n.getString("termora.doorman.mnemonic-data-corrupted"),
messageType = JOptionPane.ERROR_MESSAGE
)
passwordTextField.outline = "error"
@@ -166,7 +166,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
} catch (e: Exception) {
if (e is PasswordWrongException) {
OptionPane.showMessageDialog(
this, I18n.getString("termora.doorman.password-wrong"),
this, MigrationI18n.getString("termora.doorman.password-wrong"),
messageType = JOptionPane.ERROR_MESSAGE
)
}
@@ -197,7 +197,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
isModal = true
isResizable = true
controlsVisible = false
title = I18n.getString("termora.doorman.mnemonic.title")
title = MigrationI18n.getString("termora.doorman.mnemonic.title")
init()
pack()
size = Dimension(max(size.width, UIManager.getInt("Dialog.width") - 250), size.height)
@@ -251,7 +251,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
} catch (e: Exception) {
OptionPane.showMessageDialog(
this,
I18n.getString("termora.doorman.mnemonic.incorrect"),
MigrationI18n.getString("termora.doorman.mnemonic.incorrect"),
messageType = JOptionPane.ERROR_MESSAGE
)
return

View File

@@ -187,8 +187,6 @@ class MigrationApplicationRunnerExtension private constructor() : ApplicationRun
// 重启
TermoraRestarter.getInstance().scheduleRestart(null, ask = false)
// 退出程序
Disposer.dispose(TermoraFrameManager.getInstance())
}

View File

@@ -7,3 +7,18 @@ termora.plugins.migration.message=<html> \
<h3 align="center">📎 For more information, please see: <a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
</html>
termora.plugins.migration.migrate=Migrate
# Doorman
termora.doorman.safe=Data is encrypted
termora.doorman.unlock-data=Enter password to unlock data
termora.doorman.password-wrong=Wrong password
termora.doorman.forget-password=Forgot password?
termora.doorman.delete-data=Delete the data catalog and restart, This will lose all data
termora.doorman.forget-password-message=Unlock data with a mnemonic. Without it, data cannot be accessed
termora.doorman.have-a-mnemonic=I have a mnemonic
termora.doorman.dont-have-a-mnemonic=I don't have a mnemonic
termora.doorman.mnemonic-data-corrupted=Unable to decrypt data with the mnemonic, the data maybe corrupted
termora.doorman.mnemonic.title=Enter 12 mnemonic words
termora.doorman.mnemonic.incorrect=Incorrect mnemonic

View File

@@ -0,0 +1,18 @@
# Doorman
termora.doorman.safe=Данные защифрованы
termora.doorman.unlock-data=Введите пароль для разблокировки данных
termora.doorman.verify-password=Введите пароль для проверки
termora.doorman.password-wrong=Неверный пароль
termora.doorman.password-correct=Пароль верный
termora.doorman.unsafe=Данные не зашифрованы
termora.doorman.lock-data=Спрашивать пароль при запуске
termora.doorman.forget-password=Забыли пароль?
termora.doorman.delete-data=Удалить данные и перезапустить, это приведет к потере всех данных
termora.doorman.forget-password-message=Разблокировать данные с помощью мнемоники. Без него доступ к данным невозможен.
termora.doorman.have-a-mnemonic=У меня есть мнемоники
termora.doorman.dont-have-a-mnemonic=У меня нет мнемоники
termora.doorman.mnemonic-data-corrupted=Невозможно расшифровать данные с помощью мнемоники, возможно, данные повреждены.
termora.doorman.mnemonic.title=Введите 12 слов мнемоники
termora.doorman.mnemonic.incorrect=Неверные мнемоники

View File

@@ -7,3 +7,17 @@ termora.plugins.migration.message=<html> \
<h3 align="center">📎 更多信息请查看:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
</html>
termora.plugins.migration.migrate=迁移
# Doorman
termora.doorman.safe=数据已加密
termora.doorman.unlock-data=输入密码解锁数据
termora.doorman.password-wrong=密码错误
termora.doorman.forget-password=忘记密码?
termora.doorman.delete-data=删除数据目录后重新启动程序,这样会丢失所有数据
termora.doorman.forget-password-message=通过助记词解锁数据,没有助记词则无法解锁
termora.doorman.have-a-mnemonic=我有助记词
termora.doorman.dont-have-a-mnemonic=我没有助记词
termora.doorman.mnemonic-data-corrupted=无法从助记词解密数据,数据可能已经损坏
termora.doorman.mnemonic.title=输入 12 个助记词
termora.doorman.mnemonic.incorrect=助记词错误

View File

@@ -7,3 +7,18 @@ termora.plugins.migration.message=<html> \
<h3 align="center">📎 更多資訊請參見:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
</html>
termora.plugins.migration.migrate=遷移
# Doorman
termora.doorman.safe=資料已加密
termora.doorman.unlock-data=輸入密碼解鎖資料
termora.doorman.password-wrong=密碼錯誤
termora.doorman.forget-password=忘記密碼?
termora.doorman.delete-data=刪除資料目錄後重新啟動程序,這樣會遺失所有數據
termora.doorman.forget-password-message=透過助記詞解鎖數據,沒有助記詞則無法解鎖
termora.doorman.have-a-mnemonic=我有助記詞
termora.doorman.dont-have-a-mnemonic=我沒有助記詞
termora.doorman.mnemonic-data-corrupted=無法從助記詞解密數據,資料可能已損壞
termora.doorman.mnemonic.title=輸入 12 個助記詞
termora.doorman.mnemonic.incorrect=助記詞錯誤

View File

@@ -3,12 +3,12 @@ plugins {
}
project.version = "0.0.1"
project.version = "0.0.2"
dependencies {
testImplementation(kotlin("test"))
implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.4")
implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.7")
compileOnly(project(":"))
}

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.obs
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class OBSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
return OBSProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return OBSProtocolHostPanel()
}
}

View File

@@ -2,11 +2,11 @@ plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.2"
project.version = "0.0.3"
dependencies {
testImplementation(kotlin("test"))
implementation("com.aliyun.oss:aliyun-sdk-oss:3.18.2")
implementation("com.aliyun.oss:aliyun-sdk-oss:3.18.3")
implementation("javax.xml.bind:jaxb-api:2.3.1")
implementation("javax.activation:activation:1.1.1")
implementation("org.glassfish.jaxb:jaxb-runtime:2.3.3")

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.oss
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class OSSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
return OSSProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return OSSProtocolHostPanel()
}
}

View File

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

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.s3
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class S3ProtocolHostPanelExtension private constructor() : ProtocolHostPanelExte
return S3ProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return S3ProtocolHostPanel()
}
}

View File

@@ -0,0 +1,17 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.5"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
implementation("com.fazecast:jSerialComm:2.11.2")
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -1,7 +1,11 @@
package app.termora.plugin.internal.serial
package app.termora.plugins.serial
import app.termora.*
import app.termora.account.AccountOwner
import app.termora.highlight.KeywordHighlight
import app.termora.plugin.internal.AltKeyModifier
import app.termora.plugin.internal.BasicGeneralOption
import app.termora.plugin.internal.BasicTerminalOption
import com.fazecast.jSerialComm.SerialPort
import com.formdev.flatlaf.FlatClientProperties
import com.jgoodies.forms.builder.FormBuilder
@@ -15,12 +19,16 @@ import java.awt.BorderLayout
import java.awt.Component
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.nio.charset.Charset
import javax.swing.*
class SerialHostOptionsPane : OptionsPane() {
class SerialHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() {
private val generalOption = BasicGeneralOption()
private val terminalOption = TerminalOption()
private val terminalOption = BasicTerminalOption().apply {
showCharsetComboBox = true
showStartupCommandTextField = true
accountOwner = this@SerialHostOptionsPane.accountOwner
init()
}
private val serialCommOption = SerialCommOption()
init {
@@ -48,6 +56,12 @@ class SerialHostOptionsPane : OptionsPane() {
encoding = terminalOption.charsetComboBox.selectedItem as String,
startupCommand = terminalOption.startupCommandTextField.text,
serialComm = serialComm,
extras = mutableMapOf(
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
?: AltKeyModifier.EightBit.name),
"keywordHighlightSetId" to ((terminalOption.highlightSetComboBox.selectedItem as? KeywordHighlight)?.id
?: "-1"),
)
)
return Host(
@@ -76,6 +90,20 @@ class SerialHostOptionsPane : OptionsPane() {
serialCommOption.stopBitsComboBox.selectedItem = serialComm.stopBits
serialCommOption.flowControlComboBox.selectedItem = serialComm.flowControl
val altModifier = host.options.extras["altModifier"] ?: AltKeyModifier.EightBit.name
terminalOption.altModifierComboBox.selectedItem = runCatching { AltKeyModifier.valueOf(altModifier) }
.getOrNull() ?: AltKeyModifier.EightBit
val keywordHighlightSetId = host.options.extras["keywordHighlightSetId"]
for (i in 0 until terminalOption.highlightSetComboBox.itemCount) {
val item = terminalOption.highlightSetComboBox.getItemAt(i)
if (item.id == keywordHighlightSetId) {
terminalOption.highlightSetComboBox.selectedItem = item
break
}
}
}
fun validateFields(): Boolean {
@@ -128,67 +156,6 @@ class SerialHostOptionsPane : OptionsPane() {
}
protected inner class TerminalOption : JPanel(BorderLayout()), Option {
val charsetComboBox = JComboBox<String>()
val startupCommandTextField = OutlineTextField()
init {
initView()
initEvents()
}
private fun initView() {
add(getCenterComponent(), BorderLayout.CENTER)
for (e in Charset.availableCharsets()) {
charsetComboBox.addItem(e.key)
}
charsetComboBox.selectedItem = "UTF-8"
}
private fun initEvents() {
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.terminal
}
override fun getTitle(): String {
return I18n.getString("termora.new-host.terminal")
}
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.new-host.terminal.encoding")}:").xy(1, rows)
.add(charsetComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows)
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
.apply { rows += step }
.build()
return panel
}
}
protected inner class SerialCommOption : JPanel(BorderLayout()), Option {
val serialPortComboBox = OutlineComboBox<String>()
val baudRateComboBox = OutlineComboBox<Int>()

View File

@@ -0,0 +1,32 @@
package app.termora.plugins.serial
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.Plugin
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProviderExtension
internal class SerialPlugin : Plugin {
private val support = ExtensionSupport()
override fun getAuthor(): String {
return "TermoraDev"
}
init {
support.addExtension(ProtocolProviderExtension::class.java) { SerialProtocolProviderExtension.instance }
support.addExtension(ProtocolHostPanelExtension::class.java) { SerialProtocolHostPanelExtension.instance }
}
override fun getName(): String {
return "Serial Comm"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -1,4 +1,4 @@
package app.termora
package app.termora.plugins.serial
import app.termora.terminal.PtyConnector
import com.fazecast.jSerialComm.SerialPort

View File

@@ -0,0 +1,37 @@
package app.termora.plugins.serial
import app.termora.Disposer
import app.termora.Host
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import java.awt.BorderLayout
class SerialProtocolHostPanel(accountOwner: AccountOwner) : ProtocolHostPanel() {
private val pane = SerialHostOptionsPane(accountOwner)
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

@@ -1,5 +1,6 @@
package app.termora.plugin.internal.serial
package app.termora.plugins.serial
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -14,8 +15,11 @@ internal class SerialProtocolHostPanelExtension private constructor() : Protocol
return SerialProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
return SerialProtocolHostPanel()
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return SerialProtocolHostPanel(accountOwner)
}
override fun ordered(): Long {
return 5
}
}

View File

@@ -1,4 +1,4 @@
package app.termora.plugin.internal.serial
package app.termora.plugins.serial
import app.termora.*
import app.termora.actions.DataProvider

View File

@@ -1,4 +1,4 @@
package app.termora.plugin.internal.serial
package app.termora.plugins.serial
import app.termora.protocol.ProtocolProvider
import app.termora.protocol.ProtocolProviderExtension

View File

@@ -1,4 +1,4 @@
package app.termora.plugin.internal.serial
package app.termora.plugins.serial
import app.termora.*
import app.termora.terminal.PtyConnector
@@ -8,6 +8,8 @@ import javax.swing.Icon
class SerialTerminalTab(windowScope: WindowScope, host: Host) :
PtyHostTerminalTab(windowScope, host) {
override suspend fun openPtyConnector(): PtyConnector {
val serialPort = Serials.openPort(host)
return SerialPortPtyConnector(
@@ -16,6 +18,10 @@ class SerialTerminalTab(windowScope: WindowScope, host: Host) :
)
}
override fun createReconnectTerminalTab(): TerminalTab {
return SerialTerminalTab(windowScope, host)
}
override fun getIcon(): Icon {
return Icons.plugin
}

View File

@@ -1,5 +1,8 @@
package app.termora
package app.termora.plugins.serial
import app.termora.Host
import app.termora.SerialCommFlowControl
import app.termora.SerialCommParity
import com.fazecast.jSerialComm.SerialPort
object Serials {

View File

@@ -0,0 +1,22 @@
<termora-plugin>
<id>serial</id>
<name>Serial Comm</name>
<version>${projectVersion}</version>
<termora-version since=">=${rootProjectVersion}" until=""/>
<entry>app.termora.plugins.serial.SerialPlugin</entry>
<descriptions>
<description>Supports access to serial ports</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 @@
<svg t="1747210120200" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1169" width="16" height="16"><path d="M806.11718723 531.44140652l-78.2578125 78.50390571L410.22265625 291.21874973l82.08984402-78.53906223a231.46874973 231.46874973 0 0 1 162.49218723-68.66015625 206.54296902 206.54296902 0 0 1 131.02734402 44.296875L856.98828125 117.125a35.15625027 35.15625027 0 0 1 49.67578125-0.03515652h0.03515652a35.15625027 35.15625027 0 0 1 0 49.74609429l-74.35546875 74.35546875a225.21093777 225.21093777 0 0 1-26.22656304 290.25z m-191.63671875 27.07031223a24.57421875 24.57421875 0 0 1-1.51171821 33.08203098l-57.72656277 57.76171929 63.98437473 63.73828071-83.42578125 83.46093777a230.62499973 230.62499973 0 0 1-161.71874946 68.66015625 204.92578125 204.92578125 0 0 1-136.75781304-49.21874973l-95.83593723 95.80078071a24.18750027 24.18750027 0 0 1-34.24218723-0.07031223 24.890625 24.890625 0 0 1-0.35156277-34.76953098l95.94140598-100.12500027a225.98437473 225.98437473 0 0 1 21.09375-299.28515598L305.98437473 394.0859375l68.66015625 68.66015625L427.13281277 411.10156277a24.39843777 24.39843777 0 0 1 34.34765598 34.59375L409.94140652 497.234375l117.9140625 117.63281277 56.53125-57.5859375a20.28515652 20.28515652 0 0 1 30.09374946 1.23046848z m-112.32421875 199.19531223l44.05078179-44.05078098-240.22265679-240.46875-44.296875 44.57812473a169.91015625 169.91015625 0 0 0-4.67578071 240.18750027l4.640625 4.92187473a151.875 151.875 0 0 0 117.9140625 48.97265652 175.35937473 175.35937473 0 0 0 122.58984321-54.70312473v0.56249946zM933.875 512c0 232.98046875-188.89453125 421.875-421.875 421.875-7.87499973 0-15.71484375-0.2109375-23.48437473-0.6328125l73.12499946-73.16015598a351.77343777 351.77343777 0 0 0 298.44140679-298.40625027l73.12499946-73.16015598c0.45703152 7.76953098 0.66796902 15.609375 0.66796902 23.48437473zM535.48437473 90.7578125L462.35937527 163.953125a351.77343777 351.77343777 0 0 0-298.44140679 298.40625027l-73.12499946 73.16015598A428.625 428.625 0 0 1 90.125 512C90.125 279.01953125 279.01953125 90.125 512 90.125c7.87499973 0 15.71484375 0.2109375 23.48437473 0.6328125z" fill="#6C707E" p-id="1170"></path></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,5 @@
<svg t="1747210120200" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1169"
width="16" height="16">
<path d="M806.11718723 531.44140652l-78.2578125 78.50390571L410.22265625 291.21874973l82.08984402-78.53906223a231.46874973 231.46874973 0 0 1 162.49218723-68.66015625 206.54296902 206.54296902 0 0 1 131.02734402 44.296875L856.98828125 117.125a35.15625027 35.15625027 0 0 1 49.67578125-0.03515652h0.03515652a35.15625027 35.15625027 0 0 1 0 49.74609429l-74.35546875 74.35546875a225.21093777 225.21093777 0 0 1-26.22656304 290.25z m-191.63671875 27.07031223a24.57421875 24.57421875 0 0 1-1.51171821 33.08203098l-57.72656277 57.76171929 63.98437473 63.73828071-83.42578125 83.46093777a230.62499973 230.62499973 0 0 1-161.71874946 68.66015625 204.92578125 204.92578125 0 0 1-136.75781304-49.21874973l-95.83593723 95.80078071a24.18750027 24.18750027 0 0 1-34.24218723-0.07031223 24.890625 24.890625 0 0 1-0.35156277-34.76953098l95.94140598-100.12500027a225.98437473 225.98437473 0 0 1 21.09375-299.28515598L305.98437473 394.0859375l68.66015625 68.66015625L427.13281277 411.10156277a24.39843777 24.39843777 0 0 1 34.34765598 34.59375L409.94140652 497.234375l117.9140625 117.63281277 56.53125-57.5859375a20.28515652 20.28515652 0 0 1 30.09374946 1.23046848z m-112.32421875 199.19531223l44.05078179-44.05078098-240.22265679-240.46875-44.296875 44.57812473a169.91015625 169.91015625 0 0 0-4.67578071 240.18750027l4.640625 4.92187473a151.875 151.875 0 0 0 117.9140625 48.97265652 175.35937473 175.35937473 0 0 0 122.58984321-54.70312473v0.56249946zM933.875 512c0 232.98046875-188.89453125 421.875-421.875 421.875-7.87499973 0-15.71484375-0.2109375-23.48437473-0.6328125l73.12499946-73.16015598a351.77343777 351.77343777 0 0 0 298.44140679-298.40625027l73.12499946-73.16015598c0.45703152 7.76953098 0.66796902 15.609375 0.66796902 23.48437473zM535.48437473 90.7578125L462.35937527 163.953125a351.77343777 351.77343777 0 0 0-298.44140679 298.40625027l-73.12499946 73.16015598A428.625 428.625 0 0 1 90.125 512C90.125 279.01953125 279.01953125 90.125 512 90.125c7.87499973 0 15.71484375 0.2109375 23.48437473 0.6328125z"
fill="#CED0D6" p-id="1170"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

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

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