Compare commits

...

74 Commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -81,6 +81,10 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle- ${{ runner.os }}-${{ runner.arch }}-gradle-
- name: Install create-dmg
shell: bash
run: brew install create-dmg
- name: Compile - name: Compile
shell: bash shell: bash
run: ./gradlew :check-license && ./gradlew classes -x test run: ./gradlew :check-license && ./gradlew classes -x test
@@ -93,13 +97,6 @@ jobs:
shell: bash shell: bash
run: ./gradlew :jpackage && ./gradlew :dist run: ./gradlew :jpackage && ./gradlew :dist
- name: Upload zip artifact
uses: actions/upload-artifact@v4
with:
name: termora-osx-zip-${{ runner.arch }}
path: |
build/distributions/*.zip
- name: Upload dmg artifact - name: Upload dmg artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.5 3V5.5H10.5V3M4.5 13V9.5H11.5V13M2.5 13.5V2.5H11.5L13.5 4.5V13.5H2.5Z" stroke="#6C707E" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 357 B

View File

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.5 3V5.5H10.5V3M4.5 13V9.5H11.5V13M2.5 13.5V2.5H11.5L13.5 4.5V13.5H2.5Z" stroke="#CED0D6" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 357 B

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ plugins {
project.version = "0.0.4" project.version = "0.0.5"
dependencies { dependencies {

View File

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

View File

@@ -2,13 +2,6 @@ termora.plugins.sync.disabled-sync=You are already logged in and cannot use this
termora.settings.sync=Sync termora.settings.sync=Sync
termora.settings.sync.done=Synchronized data successfully termora.settings.sync.done=Synchronized data successfully
termora.settings.sync.export=${termora.keymgr.export}
termora.settings.sync.import=${termora.keymgr.import}
termora.settings.sync.import.file-too-large=The file is too large
termora.settings.sync.import.successful=Import data successfully
termora.settings.sync.export-done=The export was successful
termora.settings.sync.export-encrypt=Enter password to encrypt file (optional)
termora.settings.sync.export-done-open-folder=The export was successful. Do you want to open the folder?
termora.settings.sync.range=Range termora.settings.sync.range=Range
termora.settings.sync.range.keys=My keys termora.settings.sync.range.keys=My keys
termora.settings.sync.range.keyword-highlights=${termora.highlight} termora.settings.sync.range.keyword-highlights=${termora.highlight}

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import org.apache.commons.lang3.SystemUtils
import org.apache.commons.lang3.math.NumberUtils import org.apache.commons.lang3.math.NumberUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.tinylog.configuration.Configuration import org.tinylog.configuration.Configuration
import java.awt.Toolkit
import java.io.File import java.io.File
import kotlin.system.exitProcess import kotlin.system.exitProcess
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
@@ -35,10 +36,20 @@ class ApplicationInitializr {
// 检查是否单例 // 检查是否单例
checkSingleton() checkSingleton()
if (SystemUtils.IS_OS_MAC_OSX) { if (SystemInfo.isMacOS) {
System.setProperty("apple.awt.application.name", Application.getName()) System.setProperty("apple.awt.application.name", Application.getName())
} }
if (SystemInfo.isLinux) {
// https://stackoverflow.com/questions/10593075
runCatching {
val toolkit = Toolkit.getDefaultToolkit()
val awtAppClassNameField = toolkit.javaClass.getDeclaredField("awtAppClassName")
awtAppClassNameField.setAccessible(true)
awtAppClassNameField.set(toolkit, Application.getName())
}
}
// 启动 // 启动
val runtime = measureTimeMillis { ApplicationRunner().run() } val runtime = measureTimeMillis { ApplicationRunner().run() }
val log = LoggerFactory.getLogger(javaClass) val log = LoggerFactory.getLogger(javaClass)

View File

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

View File

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

View File

@@ -533,7 +533,7 @@ class AuraLaf : FlatPropertiesLaf("Aura", Properties().apply {
TerminalColor.Bright.WHITE -> 0xffffff TerminalColor.Bright.WHITE -> 0xffffff
TerminalColor.Basic.SELECTION_BACKGROUND, TerminalColor.Basic.SELECTION_BACKGROUND,
TerminalColor.Cursor.BACKGROUND -> 0xedecee TerminalColor.Cursor.BACKGROUND -> 0xacacac
else -> Int.MAX_VALUE else -> Int.MAX_VALUE
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -337,13 +337,7 @@ class TerminalTabbed(
val c = tab.getJComponent() val c = tab.getJComponent()
val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString() val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
tabbedPane.insertTab( tabbedPane.insertTab(title, tab.getIcon(), c, StringUtils.EMPTY, index)
title,
tab.getIcon(),
c,
StringUtils.EMPTY,
index
)
// 设置标题 // 设置标题
c.putClientProperty(titleProperty, title) c.putClientProperty(titleProperty, title)
@@ -367,6 +361,10 @@ class TerminalTabbed(
} }
} }
override fun indexOfTerminalTab(tab: TerminalTab):Int {
return tabbedPane.indexOfComponent(tab.getJComponent())
}
private inner class SwitchFindEverywhereResult( private inner class SwitchFindEverywhereResult(
private val title: String, private val title: String,
private val icon: Icon?, private val icon: Icon?,

View File

@@ -8,4 +8,5 @@ interface TerminalTabbedManager {
fun setSelectedTerminalTab(tab: TerminalTab) fun setSelectedTerminalTab(tab: TerminalTab)
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true) fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true)
fun refreshTerminalTabs() fun refreshTerminalTabs()
fun indexOfTerminalTab(tab: TerminalTab): Int
} }

View File

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

View File

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

View File

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

View File

@@ -224,7 +224,7 @@ class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProv
override fun getTitle(): String { override fun getTitle(): String {
return I18n.getString("termora.title") return StringUtils.EMPTY
} }
override fun getIcon(): Icon { override fun getIcon(): Icon {

View File

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

View File

@@ -126,7 +126,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
while (true) { while (true) {
val request = Request.Builder() val request = Request.Builder()
.get() .get()
.url("${accountManager.getServer()}/v1/data/changes?since=${since}&after=${after}&limit=${limit}") .url("${accountManager.getServer()}/v1/data/changes?since=${nextSince}&after=${after}&limit=${limit}")
.build() .build()
val text = AccountHttp.execute(request = request) val text = AccountHttp.execute(request = request)
val response = ohMyJson.decodeFromString<DataChangesResponse>(text) val response = ohMyJson.decodeFromString<DataChangesResponse>(text)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,8 @@ package app.termora.terminal.panel
import app.termora.terminal.ClickableHighlighter import app.termora.terminal.ClickableHighlighter
import app.termora.terminal.Terminal import app.termora.terminal.Terminal
import com.formdev.flatlaf.util.SystemInfo
import java.awt.Cursor
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
@@ -11,19 +13,42 @@ import javax.swing.SwingUtilities
*/ */
class TerminalPanelMouseHyperlinkAdapter( class TerminalPanelMouseHyperlinkAdapter(
private val terminalPanel: TerminalPanel, private val terminalPanel: TerminalPanel,
private val terminalDisplay: TerminalDisplay,
private val terminal: Terminal, private val terminal: Terminal,
) : MouseAdapter() { ) : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { override fun mouseClicked(e: MouseEvent) {
if (SwingUtilities.isLeftMouseButton(e)) { if (SwingUtilities.isLeftMouseButton(e).not()) {
val position = terminalPanel.pointToPosition(e.point) return
for (highlighter in terminal.getMarkupModel().getHighlighters(position)) { }
if (highlighter is ClickableHighlighter) {
highlighter.onClicked(position) if (SystemInfo.isMacOS) {
} if (e.isMetaDown.not())
return
} else if (e.isControlDown.not()) {
return
}
val position = terminalPanel.pointToPosition(e.point)
for (highlighter in terminal.getMarkupModel().getHighlighters(position)) {
if (highlighter is ClickableHighlighter) {
highlighter.onClicked(position)
} }
} }
} }
override fun mouseMoved(e: MouseEvent) {
val position = terminalPanel.pointToPosition(e.point)
var cursor = Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)
for (highlighter in terminal.getMarkupModel().getHighlighters(position)) {
if (highlighter is ClickableHighlighter) {
cursor = if (SystemInfo.isMacOS) Cursor.getDefaultCursor()
else Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
break
}
}
terminalDisplay.cursor = cursor
}
} }

View File

@@ -3,6 +3,7 @@ package app.termora.terminal.panel
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
import app.termora.actions.TerminalCopyAction import app.termora.actions.TerminalCopyAction
import app.termora.actions.TerminalPasteAction import app.termora.actions.TerminalPasteAction
import app.termora.database.DatabaseManager
import app.termora.terminal.* import app.termora.terminal.*
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.action.ActionManager
@@ -27,6 +28,7 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
private val isSelectCopy get() = terminalModel.getData(TerminalPanel.SelectCopy, false) private val isSelectCopy get() = terminalModel.getData(TerminalPanel.SelectCopy, false)
private val selectionModel get() = terminal.getSelectionModel() private val selectionModel get() = terminal.getSelectionModel()
private val wordBreakIterator = BreakIterator.getWordInstance() private val wordBreakIterator = BreakIterator.getWordInstance()
private val rightClickMode get() = DatabaseManager.getInstance().terminal.rightClick
companion object { companion object {
private val log = LoggerFactory.getLogger(TerminalPanelMouseSelectionAdapter::class.java) private val log = LoggerFactory.getLogger(TerminalPanelMouseSelectionAdapter::class.java)
@@ -50,7 +52,7 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
if (SwingUtilities.isRightMouseButton(e)) { if (SwingUtilities.isRightMouseButton(e)) {
// 如果有选中并且开启了选中复制,那么右键直接是粘贴 // 如果有选中并且开启了选中复制,那么右键直接是粘贴
if (selectionModel.hasSelection() && !isSelectCopy) { if (selectionModel.hasSelection() && isSelectCopy.not()) {
triggerCopyAction( triggerCopyAction(
KeyEvent( KeyEvent(
e.component, e.component,
@@ -61,6 +63,20 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
'C' 'C'
) )
) )
if (rightClickMode == "CopyAndPaste") {
triggerPasteAction(
KeyEvent(
e.component,
KeyEvent.KEY_PRESSED,
e.`when`,
e.modifiersEx,
KeyEvent.VK_V,
'V'
)
)
}
} else { } else {
// paste // paste
triggerPasteAction( triggerPasteAction(

View File

@@ -1,9 +1,7 @@
package app.termora.terminal.panel.vw package app.termora.terminal.panel.vw
import app.termora.* import app.termora.*
import app.termora.actions.AnAction import app.termora.actions.*
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
import app.termora.plugin.internal.badge.Badge import app.termora.plugin.internal.badge.Badge
import app.termora.plugin.internal.ssh.SSHTerminalTab import app.termora.plugin.internal.ssh.SSHTerminalTab
import app.termora.plugin.internal.ssh.SSHTerminalTab.Companion.SSHSession import app.termora.plugin.internal.ssh.SSHTerminalTab.Companion.SSHSession
@@ -32,6 +30,8 @@ import java.nio.file.Path
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors import java.util.concurrent.Executors
import javax.swing.* import javax.swing.*
import javax.swing.event.PopupMenuEvent
import javax.swing.event.PopupMenuListener
import kotlin.io.path.absolutePathString import kotlin.io.path.absolutePathString
import kotlin.math.max import kotlin.math.max
import kotlin.reflect.cast import kotlin.reflect.cast
@@ -39,7 +39,7 @@ import kotlin.time.Duration.Companion.milliseconds
internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) : internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
SSHVisualWindow(tab, "Transfer", visualWindowManager) { SSHVisualWindow(tab, "Transfer", visualWindowManager), DataProvider {
companion object { companion object {
private val log = LoggerFactory.getLogger(TransferVisualWindow::class.java) private val log = LoggerFactory.getLogger(TransferVisualWindow::class.java)
@@ -60,12 +60,24 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
private val connectFailedPanel = ConnectFailedPanel() private val connectFailedPanel = ConnectFailedPanel()
private val transferManager = TransferTableModel(coroutineScope) private val transferManager = TransferTableModel(coroutineScope)
private val disposable = Disposer.newDisposable() private val disposable = Disposer.newDisposable()
private val focusedWindow get() = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
private val owner get() = SwingUtilities.getWindowAncestor(this) private val owner get() = SwingUtilities.getWindowAncestor(this)
private val questionBtn = JButton(Icons.questionMark) private val questionBtn = JButton(Icons.questionMark)
private val downloadBtn = JButton(Icons.download) private val downloadBtn = JButton(Icons.download)
private val badgePresentation = Badge.getInstance(tab.windowScope) private val badgePresentation = Badge.getInstance(tab.windowScope)
.addBadge(downloadBtn).apply { visible = false } .addBadge(downloadBtn).apply { visible = false }
private val support = DataProviderSupport()
private var isShowPopupMenu = false
override var isStickHover: Boolean
get() = super.isStickHover
set(value) {
if (isShowPopupMenu || owner != focusedWindow) {
super.isStickHover = true
} else {
super.isStickHover = value
}
}
init { init {
initViews() initViews()
@@ -82,6 +94,8 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
add(panel, BorderLayout.CENTER) add(panel, BorderLayout.CENTER)
support.addData(TransportViewer.MyTransferManager, transferManager)
} }
private fun initEvents() { private fun initEvents() {
@@ -135,6 +149,8 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
} }
}) })
questionBtn.toolTipText = I18n.getString("termora.visual-window.transport.question")
// 立即连接 // 立即连接
connect() connect()
} }
@@ -151,7 +167,7 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
val support = DefaultTransportSupport(fileSystem, fileSystem.defaultDir) val support = DefaultTransportSupport(fileSystem, fileSystem.defaultDir)
withContext(Dispatchers.Swing) { withContext(Dispatchers.Swing) {
val internalTransferManager = MyInternalTransferManager() val internalTransferManager = MyInternalTransferManager()
val transportPanel = TransportPanel( val transportPanel = object : TransportPanel(
internalTransferManager, tab.host, internalTransferManager, tab.host,
object : TransportSupportLoader { object : TransportSupportLoader {
override suspend fun getTransportSupport(): TransportSupport { override suspend fun getTransportSupport(): TransportSupport {
@@ -165,7 +181,27 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
override fun isLoaded(): Boolean { override fun isLoaded(): Boolean {
return true return true
} }
}) }) {
override fun customizeContextmenu(
rows: Array<Int>,
e: MouseEvent,
popupMenu: TransportPopupMenu
) {
popupMenu.addPopupMenuListener(object : PopupMenuListener {
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent?) {
isShowPopupMenu = true
}
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent?) {
isShowPopupMenu = false
}
override fun popupMenuCanceled(e: PopupMenuEvent?) {
isShowPopupMenu = false
}
})
}
}
internalTransferManager.setTransferPanel(transportPanel) internalTransferManager.setTransferPanel(transportPanel)
Disposer.register(transportPanel, object : Disposable { Disposer.register(transportPanel, object : Disposable {
override fun dispose() { override fun dispose() {
@@ -240,6 +276,14 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
super.dispose() super.dispose()
} }
override fun reassemble() {
super.reassemble()
}
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return support.getData(dataKey)
}
override fun toolbarButtons(): List<Pair<JButton, Position>> { override fun toolbarButtons(): List<Pair<JButton, Position>> {
return listOf(downloadBtn to Position.Left, questionBtn to Position.Right) return listOf(downloadBtn to Position.Left, questionBtn to Position.Right)
} }

View File

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

View File

@@ -1,9 +1,6 @@
package app.termora.tlog package app.termora.tlog
import app.termora.Host import app.termora.*
import app.termora.Icons
import app.termora.PtyHostTerminalTab
import app.termora.WindowScope
import app.termora.terminal.PtyConnector import app.termora.terminal.PtyConnector
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -71,6 +68,10 @@ class LogViewerTerminalTab(
return false return false
} }
override fun createReconnectTerminalTab(): TerminalTab {
throw UnsupportedOperationException()
}
override fun canClone(): Boolean { override fun canClone(): Boolean {
return false return false
} }

View File

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

View File

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

View File

@@ -63,7 +63,7 @@ import kotlin.io.path.*
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
internal class TransportPanel( internal open class TransportPanel(
private val internalTransferManager: InternalTransferManager, private val internalTransferManager: InternalTransferManager,
val host: Host, val host: Host,
val loader: TransportSupportLoader, val loader: TransportSupportLoader,
@@ -106,6 +106,7 @@ internal class TransportPanel(
private val loadingPanel = LoadingPanel() private val loadingPanel = LoadingPanel()
private val model = TransportTableModel() private val model = TransportTableModel()
private val table = JTable(model) private val table = JTable(model)
private val tableScrollPane = JScrollPane(table)
private val sorter = TableRowSorter(table.model) private val sorter = TableRowSorter(table.model)
private var hasParent = false private var hasParent = false
private val panel get() = this private val panel get() = this
@@ -130,10 +131,10 @@ internal class TransportPanel(
* 工作目录 * 工作目录
*/ */
override var workdir: Path? = null override var workdir: Path? = null
private set protected set
override var loading = false override var loading = false
private set(value) { protected set(value) {
val oldValue = field val oldValue = field
field = value field = value
if (oldValue != value) { if (oldValue != value) {
@@ -164,6 +165,14 @@ internal class TransportPanel(
toolbar.add(eyeBtn) toolbar.add(eyeBtn)
toolbar.add(refreshBtn) toolbar.add(refreshBtn)
prevBtn.toolTipText = I18n.getString("termora.transport.toolbar.prev")
homeBtn.toolTipText = I18n.getString("termora.transport.toolbar.home")
nextBtn.toolTipText = I18n.getString("termora.transport.toolbar.next")
parentBtn.toolTipText = I18n.getString("termora.transport.toolbar.parent")
eyeBtn.toolTipText = I18n.getString("termora.transport.toolbar.show-hide")
refreshBtn.toolTipText = I18n.getString("termora.transport.toolbar.refresh")
sorter.maxSortKeys = 1 sorter.maxSortKeys = 1
table.setRowSorter(sorter) table.setRowSorter(sorter)
table.setAutoCreateRowSorter(false) table.setAutoCreateRowSorter(false)
@@ -211,7 +220,7 @@ internal class TransportPanel(
table.setDefaultRenderer(Any::class.java, MyDefaultTableCellRenderer()) table.setDefaultRenderer(Any::class.java, MyDefaultTableCellRenderer())
val scrollPane = JScrollPane(table) val scrollPane = tableScrollPane
scrollPane.apply { border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor) } scrollPane.apply { border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor) }
layeredPane.add(scrollPane, JLayeredPane.DEFAULT_LAYER as Any) layeredPane.add(scrollPane, JLayeredPane.DEFAULT_LAYER as Any)
@@ -241,7 +250,11 @@ internal class TransportPanel(
Disposer.register(this, editTransferListener) Disposer.register(this, editTransferListener)
refreshBtn.addActionListener { reload(requestFocus = true) } refreshBtn.addActionListener {
val filename = getSelectFilename()
if (filename != null) registerSelectRow(filename)
reload(requestFocus = true)
}
prevBtn.addActionListener { navigator.back() } prevBtn.addActionListener { navigator.back() }
nextBtn.addActionListener { navigator.forward() } nextBtn.addActionListener { navigator.forward() }
@@ -303,11 +316,16 @@ internal class TransportPanel(
if (target.fileSystem != loader.getSyncTransportSupport().getFileSystem()) return if (target.fileSystem != loader.getSyncTransportSupport().getFileSystem()) return
} }
if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) { if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) {
if (loading) { val c = {
registerNextReloadCallback { reload(requestFocus = false) } val filename = getSelectFilename()
} else { if (filename != null) registerSelectRow(filename)
reload(requestFocus = false) reload(requestFocus = false)
} }
if (loading) {
registerNextReloadCallback { c.invoke() }
} else {
c.invoke()
}
} }
} }
}).let { Disposer.register(this, it) } }).let { Disposer.register(this, it) }
@@ -401,7 +419,7 @@ internal class TransportPanel(
} }
}) })
addPropertyChangeListener("workdir") { evt -> reload() } addPropertyChangeListener("workdir") { _ -> reload() }
reload() reload()
} }
@@ -497,6 +515,29 @@ internal class TransportPanel(
} }
}) })
table.actionMap.put("Delete", object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
val rows = table.selectedRows.map { sorter.convertRowIndexToModel(it) }.toTypedArray()
val files = rows.map { model.getPath(it) to model.getAttributes(it) }
// 排除父目录
val validFiles = files.filter { !it.second.isParent }
if (validFiles.isNotEmpty()) {
// 显示删除确认对话框
if (OptionPane.showConfirmDialog(
owner,
I18n.getString("termora.keymgr.delete-warning"),
messageType = JOptionPane.WARNING_MESSAGE
) == JOptionPane.YES_OPTION
) {
// 直接执行删除操作
val future =
internalTransferManager.addTransfer(validFiles, InternalTransferManager.TransferMode.Delete)
mountFuture(future)
}
}
}
})
// 快速导航 // 快速导航
table.addKeyListener(object : KeyAdapter() { table.addKeyListener(object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) { override fun keyPressed(e: KeyEvent) {
@@ -520,12 +561,25 @@ internal class TransportPanel(
} }
}) })
// 重写全选行为,排除".."父目录
table.actionMap.put("selectAll", object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
table.clearSelection()
val startRow = if (hasParent) 1 else 0 // 跳过".."行
if (startRow < table.rowCount) {
table.setRowSelectionInterval(startRow, table.rowCount - 1)
}
}
})
val inputMap = table.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) val inputMap = table.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
if (SystemInfo.isMacOS.not()) { if (SystemInfo.isMacOS.not()) {
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), "Reload") inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), "Reload")
} }
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "EnterSelectionFolder") inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "EnterSelectionFolder")
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, toolkit.menuShortcutKeyMaskEx), "Reload") inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, toolkit.menuShortcutKeyMaskEx), "Reload")
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "Delete")
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), "Delete")
} }
private fun initTransferHandler() { private fun initTransferHandler() {
@@ -658,6 +712,9 @@ internal class TransportPanel(
} }
fun registerSelectRow(name: String) { fun registerSelectRow(name: String) {
val verticalValue = tableScrollPane.verticalScrollBar.value
val horizontalValue = tableScrollPane.horizontalScrollBar.value
registerNextReloadCallback { registerNextReloadCallback {
for (i in 0 until model.rowCount) { for (i in 0 until model.rowCount) {
if (model.getAttributes(i).name == name) { if (model.getAttributes(i).name == name) {
@@ -665,12 +722,22 @@ internal class TransportPanel(
table.clearSelection() table.clearSelection()
table.setRowSelectionInterval(c, c) table.setRowSelectionInterval(c, c)
table.scrollRectToVisible(table.getCellRect(c, TransportTableModel.COLUMN_NAME, true)) table.scrollRectToVisible(table.getCellRect(c, TransportTableModel.COLUMN_NAME, true))
tableScrollPane.verticalScrollBar.value = verticalValue
tableScrollPane.horizontalScrollBar.value = horizontalValue
break break
} }
} }
} }
} }
fun getSelectFilename(): String? {
val row = table.selectedRow
if (row < 0) return null
val c = sorter.convertRowIndexToModel(row)
if (c < 0) return null
return model.getAttributes(c).name
}
private fun registerNextReloadCallback(block: () -> Unit) { private fun registerNextReloadCallback(block: () -> Unit) {
nextReloadCallbacks.computeIfAbsent(mod.get()) { mutableListOf() } nextReloadCallbacks.computeIfAbsent(mod.get()) { mutableListOf() }
.add(block) .add(block)
@@ -858,13 +925,18 @@ internal class TransportPanel(
} }
} }
private fun showContextmenu(rows: Array<Int>, e: MouseEvent) { protected open fun showContextmenu(rows: Array<Int>, e: MouseEvent) {
val files = rows.map { model.getPath(it) to model.getAttributes(it) } val files = rows.map { model.getPath(it) to model.getAttributes(it) }
val popupMenu = TransportPopupMenu(owner, model, internalTransferManager, loader, files) val popupMenu = TransportPopupMenu(owner, model, internalTransferManager, loader, files)
popupMenu.addActionListener(PopupMenuActionListener(files)) popupMenu.addActionListener(PopupMenuActionListener(files))
customizeContextmenu(rows, e, popupMenu)
popupMenu.show(table, e.x, e.y) popupMenu.show(table, e.x, e.y)
} }
protected open fun customizeContextmenu(rows: Array<Int>, e: MouseEvent, popupMenu: TransportPopupMenu) {
}
override fun <T : Any> getData(dataKey: DataKey<T>): T? { override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return support.getData(dataKey) return support.getData(dataKey)
} }
@@ -1083,6 +1155,8 @@ internal class TransportPanel(
} else if (actionCommand == TransportPopupMenu.ActionCommand.Delete) { } else if (actionCommand == TransportPopupMenu.ActionCommand.Delete) {
transfer(InternalTransferManager.TransferMode.Delete) transfer(InternalTransferManager.TransferMode.Delete)
} else if (actionCommand == TransportPopupMenu.ActionCommand.Refresh) { } else if (actionCommand == TransportPopupMenu.ActionCommand.Refresh) {
val filename = getSelectFilename()
if (filename != null) registerSelectRow(filename)
reload(requestFocus = true) reload(requestFocus = true)
} else if (actionCommand == TransportPopupMenu.ActionCommand.Edit) { } else if (actionCommand == TransportPopupMenu.ActionCommand.Edit) {
edit() edit()
@@ -1139,7 +1213,9 @@ internal class TransportPanel(
private fun edit() { private fun edit() {
for (path in files.map { it.first }) { for (path in files.map { it.first }) {
val target = Application.createSubTemporaryDir().resolve(path.name) var name = path.name
if (SystemInfo.isWindows) name = name.replace(":", "-")
val target = Application.createSubTemporaryDir().resolve(name)
val transferId = internalTransferManager.addHighTransfer(path, target) val transferId = internalTransferManager.addHighTransfer(path, target)
editTransferListener.addListenTransfer(transferId) editTransferListener.addListenTransfer(transferId)
} }

View File

@@ -1,12 +1,11 @@
package app.termora.transfer package app.termora.transfer
import app.termora.Application import app.termora.*
import app.termora.ApplicationScope
import app.termora.I18n
import app.termora.OptionPane
import app.termora.plugin.ExtensionManager import app.termora.plugin.ExtensionManager
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.util.SystemInfo
import kotlinx.coroutines.launch
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.apache.sshd.sftp.client.fs.SftpFileSystem import org.apache.sshd.sftp.client.fs.SftpFileSystem
@@ -43,7 +42,14 @@ internal class TransportPopupMenu(
private val copyPathMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.copy-path")) private val copyPathMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.copy-path"))
private val copyMenu = JMenuItem(I18n.getString("termora.copy")) private val copyMenu = JMenuItem(I18n.getString("termora.copy"))
private val pasteMenu = JMenuItem(I18n.getString("termora.paste")) private val pasteMenu = JMenuItem(I18n.getString("termora.paste"))
private val openInFinderMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.open-in-folder")) private val openInFinderMenu = JMenuItem(
I18n.getString(
"termora.transport.table.contextmenu.open-in-folder",
if (SystemInfo.isMacOS) I18n.getString("termora.finder")
else if (SystemInfo.isWindows) I18n.getString("termora.explorer")
else I18n.getString("termora.folder")
)
)
private val renameMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.rename")) private val renameMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.rename"))
private val deleteMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.delete")) private val deleteMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.delete"))
@@ -149,7 +155,12 @@ internal class TransportPopupMenu(
} }
private fun initEvents() { private fun initEvents() {
transferMenu.addActionListener { fireActionPerformed(it, ActionCommand.Transfer) } transferMenu.addActionListener {
swingCoroutineScope.launch {
fireActionPerformed(it, ActionCommand.Transfer)
}
}
deleteMenu.addActionListener { deleteMenu.addActionListener {
if (OptionPane.showConfirmDialog( if (OptionPane.showConfirmDialog(
owner, owner,

View File

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

View File

@@ -3,6 +3,8 @@ package app.termora.tree
import app.termora.* import app.termora.*
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.account.AccountManager import app.termora.account.AccountManager
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.actions.OpenHostAction import app.termora.actions.OpenHostAction
import app.termora.database.DatabaseChangedExtension import app.termora.database.DatabaseChangedExtension
import app.termora.database.DatabaseManager import app.termora.database.DatabaseManager
@@ -32,6 +34,10 @@ import org.jdesktop.swingx.action.ActionManager
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.NodeList import org.w3c.dom.NodeList
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
import java.awt.datatransfer.Transferable
import java.awt.datatransfer.UnsupportedFlavorException
import java.awt.event.* import java.awt.event.*
import java.io.* import java.io.*
import java.util.* import java.util.*
@@ -140,6 +146,61 @@ class NewHostTree : SimpleTree(), Disposable {
} }
}) })
// 开启 ToolTip 功能
ToolTipManager.sharedInstance().registerComponent(this)
// 设置鼠标移动提示
addMouseMotionListener(object : java.awt.event.MouseMotionAdapter() {
override fun mouseMoved(e: MouseEvent) {
val path: TreePath? = getPathForLocation(e.x, e.y)
if (path != null) {
val node: HostTreeNode = path.lastPathComponent as HostTreeNode
if (node.host.remark.isNotEmpty()){
toolTipText = node.host.remark
}else{
toolTipText = null
}
} else {
toolTipText = null
}
}
})
actionMap.put("copy", object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
toolkit.systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
val nodes = getSelectionSimpleTreeNodes(false).toMutableList()
nodes.removeIf { e -> e.getParents().any { nodes.contains(it) } }
if (nodes.isEmpty() || nodes.any { it is TeamTreeNode }) return
if (nodes.any { it.id == "0" || it.id.isBlank() }) return
toolkit.systemClipboard.setContents(NodesTransferable(nodes), null)
}
})
actionMap.put("paste", object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
val lastNode = getLastSelectedPathNode() ?: return
val folder = if (lastNode.isFolder) lastNode.parent ?: simpleTreeModel.root
else lastNode.parent ?: return
if (toolkit.systemClipboard.isDataFlavorAvailable(NodesTransferable.FLAVOR).not()) return
val nodes = (toolkit.systemClipboard.getData(NodesTransferable.FLAVOR) as? List<*>)
?.filterIsInstance<HostTreeNode>() ?: return
for (node in nodes) {
val newNode = copyNode(node, folder.id)
// 复制的是文件夹,就在最后面
if (newNode.isFolder) {
simpleTreeModel.insertNodeInto(newNode, folder, folder.folderCount)
} else if (lastNode.isFolder) { // 用户选的节点是文件夹那就在最后一个child下面
simpleTreeModel.insertNodeInto(newNode, folder, folder.childCount)
} else { // 用户选的是主机并且复制的是主机
simpleTreeModel.insertNodeInto(newNode, folder, folder.getIndex(lastNode) + 1)
}
}
}
})
} }
fun restoreExpansions() { fun restoreExpansions() {
@@ -152,6 +213,9 @@ class NewHostTree : SimpleTree(), Disposable {
} }
override fun dispose() { override fun dispose() {
// 销毁
ToolTipManager.sharedInstance().unregisterComponent(this)
val name = super.getName() val name = super.getName()
if (name.isNullOrBlank().not()) { if (name.isNullOrBlank().not()) {
properties.putString("${name}.state", TreeUtils.saveExpansionState(this)) properties.putString("${name}.state", TreeUtils.saveExpansionState(this))
@@ -198,10 +262,12 @@ class NewHostTree : SimpleTree(), Disposable {
val sshMenu = importMenu.add(".ssh/config") val sshMenu = importMenu.add(".ssh/config")
val mobaXtermMenu = importMenu.add("MobaXterm") val mobaXtermMenu = importMenu.add("MobaXterm")
// 为了避免误导,如果是 SSH 右键时显示 SFTP
val sftpText = if (SSHProtocolProvider.PROTOCOL.equals(lastHost.protocol, true))
"SFTP" else I18n.getString("termora.transport.sftp")
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect")) val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect"))
val openWith = popupMenu.add(JMenu(I18n.getString("termora.welcome.contextmenu.connect-with"))) as JMenu val openWith = popupMenu.add(JMenu(I18n.getString("termora.welcome.contextmenu.connect-with"))) as JMenu
val openWithSFTP = openWith.add(I18n.getString("termora.transport.sftp")) val openWithSFTP = openWith.add(sftpText)
val openWithSFTPCommand = openWith.add(I18n.getString("termora.tabbed.contextmenu.sftp-command")) val openWithSFTPCommand = openWith.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
val openInNewWindow = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open-in-new-window")) val openInNewWindow = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open-in-new-window"))
popupMenu.addSeparator() popupMenu.addSeparator()
@@ -389,6 +455,20 @@ class NewHostTree : SimpleTree(), Disposable {
}) })
val mnemonics = mapOf(
refresh to KeyEvent.VK_R,
newMenu to KeyEvent.VK_W,
newFolder to KeyEvent.VK_F,
rename to KeyEvent.VK_M,
remove to KeyEvent.VK_D,
property to KeyEvent.VK_I,
)
for ((item, mnemonic) in mnemonics) {
item.text = "${item.text}(${KeyEvent.getKeyText(mnemonic)})"
item.setMnemonic(mnemonic)
}
popupMenu.show(this, evt.x, evt.y) popupMenu.show(this, evt.x, evt.y)
} }
@@ -1075,5 +1155,23 @@ class NewHostTree : SimpleTree(), Disposable {
electerm, electerm,
} }
private class NodesTransferable(val nodes: List<HostTreeNode>) : Transferable {
companion object {
val FLAVOR = DataFlavor("termora/host-tree", "Termora host tree transfers")
}
} override fun getTransferDataFlavors(): Array<out DataFlavor> {
return arrayOf(FLAVOR)
}
override fun isDataFlavorSupported(flavor: DataFlavor?): Boolean {
return flavor == FLAVOR
}
override fun getTransferData(flavor: DataFlavor?): Any {
return if (flavor == FLAVOR) nodes else throw UnsupportedFlavorException(flavor)
}
}
}

View File

@@ -2,6 +2,7 @@ package app.termora.tree
import javax.swing.Icon import javax.swing.Icon
import javax.swing.tree.DefaultMutableTreeNode import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.TreeNode
abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) { abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@@ -35,4 +36,15 @@ abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) {
return children return children
} }
open fun getParents(): List<SimpleTreeNode<T>> {
val parents = mutableListOf<SimpleTreeNode<T>>()
var p = parent as TreeNode?
while (p != null) {
if (p is SimpleTreeNode<T>) {
parents.add(p)
}
p = p.parent
}
return parents
}
} }

View File

@@ -6,7 +6,7 @@
<Identity Name="TermoraDev.Termora" <Identity Name="TermoraDev.Termora"
Publisher="CN=C804E131-4368-4BF7-9E7F-95C681AD0AAC" Publisher="CN=C804E131-4368-4BF7-9E7F-95C681AD0AAC"
Version="@version@.0" Version="@version@.@betaVersion@"
ProcessorArchitecture="@architecture@"/> ProcessorArchitecture="@architecture@"/>
<Properties> <Properties>

View File

@@ -45,6 +45,7 @@ termora.settings.appearance.i-want-to-translate=I want to translate
termora.settings.appearance.follow-system=Sync with OS termora.settings.appearance.follow-system=Sync with OS
termora.settings.appearance.opacity=Opacity termora.settings.appearance.opacity=Opacity
termora.settings.appearance.background-running=Backgrounding termora.settings.appearance.background-running=Backgrounding
termora.settings.appearance.tab-order=Tab Order
termora.settings.appearance.confirm-tab-close=Confirm tab close termora.settings.appearance.confirm-tab-close=Confirm tab close
termora.settings.terminal=Terminal termora.settings.terminal=Terminal
@@ -56,6 +57,9 @@ termora.settings.terminal.debug=Debug mode
termora.settings.terminal.beep=Beep termora.settings.terminal.beep=Beep
termora.settings.terminal.hyperlink=Hyperlink termora.settings.terminal.hyperlink=Hyperlink
termora.settings.terminal.select-copy=Select copy termora.settings.terminal.select-copy=Select copy
termora.settings.terminal.right-click=Right click
termora.settings.terminal.right-click.copy-and-paste=Copy and Paste
termora.settings.terminal.right-click.copy=${termora.copy}
termora.settings.terminal.cursor-style=Cursor type termora.settings.terminal.cursor-style=Cursor type
termora.settings.terminal.cursor-blink=Cursor blink termora.settings.terminal.cursor-blink=Cursor blink
termora.settings.terminal.local-shell=Local shell termora.settings.terminal.local-shell=Local shell
@@ -235,6 +239,8 @@ termora.keymgr.table.name=Name
termora.keymgr.table.type=Type termora.keymgr.table.type=Type
termora.keymgr.table.length=Length termora.keymgr.table.length=Length
termora.keymgr.table.remark=Description termora.keymgr.table.remark=Description
termora.keymgr.export-done=The export was successful
termora.keymgr.export-done-open-folder=The export was successful. Do you want to open the folder?
termora.keymgr.ssh-copy-id.number=Number of hosts [{0}] Number of public keys [{1}] termora.keymgr.ssh-copy-id.number=Number of hosts [{0}] Number of public keys [{1}]
termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied} termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied}
@@ -244,6 +250,7 @@ termora.keymgr.ssh-copy-id.end=End of public key copying
# Tabbed # Tabbed
termora.tabbed.contextmenu.rename=Rename termora.tabbed.contextmenu.rename=Rename
termora.tabbed.contextmenu.select-host=Select Host
termora.tabbed.contextmenu.sftp-command=SFTP Command termora.tabbed.contextmenu.sftp-command=SFTP Command
termora.tabbed.contextmenu.sftp-not-install=SFTP programme not found, please install and try again termora.tabbed.contextmenu.sftp-not-install=SFTP programme not found, please install and try again
termora.tabbed.contextmenu.clone=Clone termora.tabbed.contextmenu.clone=Clone
@@ -304,6 +311,14 @@ termora.tools.multiple=Send command to the current window sessions
termora.transport.local=Local termora.transport.local=Local
termora.transport.file-already-exists=The file {0} already exists termora.transport.file-already-exists=The file {0} already exists
termora.transport.toolbar.prev=Backward
termora.transport.toolbar.home=Home Folder
termora.transport.toolbar.next=Forward
termora.transport.toolbar.parent=Parent Folder
termora.transport.toolbar.show-hide=Show/Hide Folders
termora.transport.toolbar.refresh=Refresh Folder
termora.transport.bookmarks=Bookmarks Manager termora.transport.bookmarks=Bookmarks Manager
termora.transport.bookmarks.up=Up termora.transport.bookmarks.up=Up
termora.transport.bookmarks.down=Down termora.transport.bookmarks.down=Down
@@ -320,7 +335,7 @@ termora.transport.table.owner=Owner
termora.transport.table.contextmenu.transfer=Transfer termora.transport.table.contextmenu.transfer=Transfer
termora.transport.table.contextmenu.edit=${termora.keymgr.edit} termora.transport.table.contextmenu.edit=${termora.keymgr.edit}
termora.transport.table.contextmenu.copy-path=Copy Path termora.transport.table.contextmenu.copy-path=Copy Path
termora.transport.table.contextmenu.open-in-folder=Open in ${termora.finder} termora.transport.table.contextmenu.open-in-folder=Open in {0}
termora.transport.table.contextmenu.rename=${termora.welcome.contextmenu.rename} termora.transport.table.contextmenu.rename=${termora.welcome.contextmenu.rename}
termora.transport.table.contextmenu.delete=${termora.remove} termora.transport.table.contextmenu.delete=${termora.remove}
termora.transport.table.contextmenu.rm-warning=Using the rm -rf command to delete a file is very dangerous termora.transport.table.contextmenu.rm-warning=Using the rm -rf command to delete a file is very dangerous
@@ -426,6 +441,8 @@ termora.visual-window.system-information.mem=Mem
termora.visual-window.system-information.swap=Swap termora.visual-window.system-information.swap=Swap
termora.visual-window.system-information.filesystem=Filesystem termora.visual-window.system-information.filesystem=Filesystem
termora.visual-window.system-information.used-total=Used / Total termora.visual-window.system-information.used-total=Used / Total
termora.visual-window.toggle-window=Toggle window
termora.visual-window.transport.question=More Features
termora.visual-window.nvidia-smi=NVIDIA SMI termora.visual-window.nvidia-smi=NVIDIA SMI

View File

@@ -40,6 +40,7 @@ termora.settings.appearance.follow-system=Как в системе
termora.settings.appearance.opacity=Прозрачность termora.settings.appearance.opacity=Прозрачность
termora.settings.appearance.background-image=Фоновое изображение termora.settings.appearance.background-image=Фоновое изображение
termora.settings.appearance.background-running=Фон termora.settings.appearance.background-running=Фон
termora.settings.appearance.tab-order=Порядок вкладок
termora.settings.appearance.confirm-tab-close=Подтверждение закрытия вкладки termora.settings.appearance.confirm-tab-close=Подтверждение закрытия вкладки
termora.setting.security=Безопасность termora.setting.security=Безопасность
@@ -59,6 +60,8 @@ termora.settings.terminal.debug=Режим отладки
termora.settings.terminal.beep=Сигнал termora.settings.terminal.beep=Сигнал
termora.settings.terminal.hyperlink=Ссылки termora.settings.terminal.hyperlink=Ссылки
termora.settings.terminal.select-copy=Копировать выделенное termora.settings.terminal.select-copy=Копировать выделенное
termora.settings.terminal.right-click=правой кнопкой мыши
termora.settings.terminal.right-click.copy-and-paste=Копировать и вставить
termora.settings.terminal.cursor-style=Вид курсора termora.settings.terminal.cursor-style=Вид курсора
termora.settings.terminal.cursor-blink=Мигать курсором termora.settings.terminal.cursor-blink=Мигать курсором
termora.settings.terminal.local-shell=Локальный терминал termora.settings.terminal.local-shell=Локальный терминал
@@ -66,27 +69,6 @@ termora.settings.terminal.floating-toolbar=Плавающая панель
termora.settings.terminal.auto-close-tab=Автозакрытие вкладки termora.settings.terminal.auto-close-tab=Автозакрытие вкладки
termora.settings.terminal.auto-close-tab-description=Автоматически закрывать вкладку при обычном отключении терминала termora.settings.terminal.auto-close-tab-description=Автоматически закрывать вкладку при обычном отключении терминала
termora.settings.sync=Синхронизация
termora.settings.sync.done=Синхронизация успешна
termora.settings.sync.export=${termora.keymgr.export}
termora.settings.sync.import=${termora.keymgr.import}
termora.settings.sync.import.file-too-large=Файл слишком большой
termora.settings.sync.import.successful=Импортировано успешно
termora.settings.sync.export-done=Экспортировано успешно
termora.settings.sync.export-encrypt=Введите пароль для расшифровки файла (выборочно)
termora.settings.sync.export-done-open-folder=Экспорт прошел успешно. Открыть папку?
termora.settings.sync.range=Диапазон
termora.settings.sync.range.keys=Мои ключи
termora.settings.sync.range.keyword-highlights=${termora.highlight}
termora.settings.sync.last-sync-time=Последняя синхронизация
termora.settings.sync.gist=Gist
termora.settings.sync.token=Токен
termora.settings.sync.type=Сервис
termora.settings.sync.webdav.help=WebDAV адрес, https://yourhost/webdav/termora.json
termora.settings.sync.policy=Тип синхронизации
termora.settings.sync.policy.manual=Вручную
termora.settings.sync.policy.on-change=При изменениях
termora.settings.about=О программе termora.settings.about=О программе
termora.settings.about.author=Автор termora.settings.about.author=Автор
termora.settings.about.source=Ссылка termora.settings.about.source=Ссылка
@@ -201,6 +183,8 @@ termora.keymgr.table.name=Название
termora.keymgr.table.type=Тип termora.keymgr.table.type=Тип
termora.keymgr.table.length=Длина termora.keymgr.table.length=Длина
termora.keymgr.table.remark=Описание termora.keymgr.table.remark=Описание
termora.keymgr.export-done=Экспорт выполнен успешно
termora.keymgr.export-done-open-folder=Экспорт выполнен успешно. Открыть папку?
termora.keymgr.ssh-copy-id.number=Кол-во хостов [{0}] Кол-во публичных ключей [{1}] termora.keymgr.ssh-copy-id.number=Кол-во хостов [{0}] Кол-во публичных ключей [{1}]
termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied} termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied}
@@ -209,6 +193,7 @@ termora.keymgr.ssh-copy-id.end=Копирования открытого клю
# Tabbed # Tabbed
termora.tabbed.contextmenu.rename=Переименовать termora.tabbed.contextmenu.rename=Переименовать
termora.tabbed.contextmenu.select-host=Выбрать хост
termora.tabbed.contextmenu.sftp-command=SFTP Команда termora.tabbed.contextmenu.sftp-command=SFTP Команда
termora.tabbed.contextmenu.sftp-not-install=Программа SFTP не найдена, пожалуйста, установите и повторите попытку. termora.tabbed.contextmenu.sftp-not-install=Программа SFTP не найдена, пожалуйста, установите и повторите попытку.
termora.tabbed.contextmenu.clone=Дублировать termora.tabbed.contextmenu.clone=Дублировать
@@ -266,6 +251,14 @@ termora.transport.bookmarks=Менеджер закладок
termora.transport.bookmarks.up=Вверх termora.transport.bookmarks.up=Вверх
termora.transport.bookmarks.down=Вниз termora.transport.bookmarks.down=Вниз
termora.transport.toolbar.prev=Назад
termora.transport.toolbar.home=Домашняя папка
termora.transport.toolbar.next=Вперёд
termora.transport.toolbar.parent=Родительская папка
termora.transport.toolbar.show-hide=Показать/Скрыть папки
termora.transport.toolbar.refresh=Обновить
termora.transport.table.filename=Имя файла termora.transport.table.filename=Имя файла
termora.transport.table.type=Тип termora.transport.table.type=Тип
termora.transport.table.type.symbolic-link=Символьная Ссылка termora.transport.table.type.symbolic-link=Символьная Ссылка
@@ -373,6 +366,8 @@ termora.visual-window.system-information.mem=Память
termora.visual-window.system-information.swap=Подкачка termora.visual-window.system-information.swap=Подкачка
termora.visual-window.system-information.filesystem=Файловая система termora.visual-window.system-information.filesystem=Файловая система
termora.visual-window.system-information.used-total=Использовано / Всего termora.visual-window.system-information.used-total=Использовано / Всего
termora.visual-window.toggle-window=Переключить окно
termora.visual-window.transport.question=Больше возможностей
termora.visual-window.nvidia-smi=NVIDIA SMI termora.visual-window.nvidia-smi=NVIDIA SMI

View File

@@ -49,6 +49,7 @@ termora.settings.appearance.i-want-to-translate=我想要翻译
termora.settings.appearance.follow-system=跟随系统 termora.settings.appearance.follow-system=跟随系统
termora.settings.appearance.opacity=透明度 termora.settings.appearance.opacity=透明度
termora.settings.appearance.background-running=后台运行 termora.settings.appearance.background-running=后台运行
termora.settings.appearance.tab-order=标签序号
termora.settings.appearance.confirm-tab-close=标签关闭前确认 termora.settings.appearance.confirm-tab-close=标签关闭前确认
# Find everywhere # Find everywhere
@@ -70,6 +71,8 @@ termora.settings.terminal.debug=调试模式
termora.settings.terminal.beep=蜂鸣声 termora.settings.terminal.beep=蜂鸣声
termora.settings.terminal.hyperlink=超链接 termora.settings.terminal.hyperlink=超链接
termora.settings.terminal.select-copy=选中复制 termora.settings.terminal.select-copy=选中复制
termora.settings.terminal.right-click=右键点击
termora.settings.terminal.right-click.copy-and-paste=复制 & 粘贴
termora.settings.terminal.cursor-style=光标样式 termora.settings.terminal.cursor-style=光标样式
termora.settings.terminal.cursor-blink=光标闪烁 termora.settings.terminal.cursor-blink=光标闪烁
termora.settings.terminal.local-shell=本地终端 termora.settings.terminal.local-shell=本地终端
@@ -229,6 +232,8 @@ termora.keymgr.table.name=名称
termora.keymgr.table.type=类型 termora.keymgr.table.type=类型
termora.keymgr.table.length=长度 termora.keymgr.table.length=长度
termora.keymgr.table.remark=备注 termora.keymgr.table.remark=备注
termora.keymgr.export-done=导出成功
termora.keymgr.export-done-open-folder=导出成功,是否需要打开所在文件夹?
termora.keymgr.ssh-copy-id.number=主机数量 [{0}] 公钥数量 [{1}] termora.keymgr.ssh-copy-id.number=主机数量 [{0}] 公钥数量 [{1}]
termora.keymgr.ssh-copy-id.failed=复制失败 termora.keymgr.ssh-copy-id.failed=复制失败
@@ -241,6 +246,7 @@ termora.tools.multiple=将命令发送到当前窗口会话
# Tabbed # Tabbed
termora.tabbed.contextmenu.rename=重命名 termora.tabbed.contextmenu.rename=重命名
termora.tabbed.contextmenu.select-host=选中主机
termora.tabbed.contextmenu.sftp-command=SFTP 终端 termora.tabbed.contextmenu.sftp-command=SFTP 终端
termora.tabbed.contextmenu.sftp-not-install=没有找到 SFTP 程序,请安装后重试 termora.tabbed.contextmenu.sftp-not-install=没有找到 SFTP 程序,请安装后重试
termora.tabbed.contextmenu.clone=克隆 termora.tabbed.contextmenu.clone=克隆
@@ -306,6 +312,15 @@ termora.transport.bookmarks=书签管理
termora.transport.bookmarks.up=上移 termora.transport.bookmarks.up=上移
termora.transport.bookmarks.down=下移 termora.transport.bookmarks.down=下移
termora.transport.toolbar.prev=返回
termora.transport.toolbar.home=默认目录
termora.transport.toolbar.next=前进
termora.transport.toolbar.parent=父目录
termora.transport.toolbar.show-hide=显示/隐藏目录
termora.transport.toolbar.refresh=刷新
termora.transport.table.filename=文件名 termora.transport.table.filename=文件名
termora.transport.table.type=类型 termora.transport.table.type=类型
termora.transport.table.size=大小 termora.transport.table.size=大小
@@ -317,7 +332,7 @@ termora.transport.table.owner=所有者
# contextmenu # contextmenu
termora.transport.table.contextmenu.transfer=传输 termora.transport.table.contextmenu.transfer=传输
termora.transport.table.contextmenu.copy-path=复制路径 termora.transport.table.contextmenu.copy-path=复制路径
termora.transport.table.contextmenu.open-in-folder=${termora.finder}中打开 termora.transport.table.contextmenu.open-in-folder={0}中打开
termora.transport.table.contextmenu.change-permissions=更改权限... termora.transport.table.contextmenu.change-permissions=更改权限...
termora.transport.table.contextmenu.refresh=刷新 termora.transport.table.contextmenu.refresh=刷新
termora.transport.table.contextmenu.compress=压缩 termora.transport.table.contextmenu.compress=压缩
@@ -423,6 +438,8 @@ termora.visual-window.system-information.mem=内存
termora.visual-window.system-information.swap=交换 termora.visual-window.system-information.swap=交换
termora.visual-window.system-information.filesystem=文件系统 termora.visual-window.system-information.filesystem=文件系统
termora.visual-window.system-information.used-total=使用 / 大小 termora.visual-window.system-information.used-total=使用 / 大小
termora.visual-window.toggle-window=切换窗口
termora.visual-window.transport.question=更多功能
termora.floating-toolbar.close-in-current-tab=在当前标签页关闭 termora.floating-toolbar.close-in-current-tab=在当前标签页关闭

View File

@@ -48,6 +48,7 @@ termora.settings.appearance.i-want-to-translate=我想要翻譯
termora.settings.appearance.follow-system=跟隨系統 termora.settings.appearance.follow-system=跟隨系統
termora.settings.appearance.opacity=透明度 termora.settings.appearance.opacity=透明度
termora.settings.appearance.background-running=後台運行 termora.settings.appearance.background-running=後台運行
termora.settings.appearance.tab-order=標籤序號
termora.settings.appearance.confirm-tab-close=關閉分頁確認 termora.settings.appearance.confirm-tab-close=關閉分頁確認
termora.settings.keymap=鍵盤 termora.settings.keymap=鍵盤
@@ -79,8 +80,10 @@ termora.settings.terminal.size=大小
termora.settings.terminal.max-rows=最大行數 termora.settings.terminal.max-rows=最大行數
termora.settings.terminal.debug=偵錯模式 termora.settings.terminal.debug=偵錯模式
termora.settings.terminal.beep=超連結 termora.settings.terminal.beep=超連結
termora.settings.terminal.hyperlink=Hyperlink termora.settings.terminal.hyperlink=超連結
termora.settings.terminal.select-copy=選取複製 termora.settings.terminal.select-copy=選取複製
termora.settings.terminal.right-click=右鍵點擊
termora.settings.terminal.right-click.copy-and-paste=複製 & 貼上
termora.settings.terminal.cursor-style=遊標風格 termora.settings.terminal.cursor-style=遊標風格
termora.settings.terminal.cursor-blink=遊標閃爍 termora.settings.terminal.cursor-blink=遊標閃爍
termora.settings.terminal.local-shell=本地端 termora.settings.terminal.local-shell=本地端
@@ -225,6 +228,8 @@ termora.keymgr.table.name=名稱
termora.keymgr.table.type=型別 termora.keymgr.table.type=型別
termora.keymgr.table.length=長度 termora.keymgr.table.length=長度
termora.keymgr.table.remark=備註 termora.keymgr.table.remark=備註
termora.keymgr.export-done=匯出成功
termora.keymgr.export-done-open-folder=匯出成功,是否需要打開所在資料夾?
termora.keymgr.ssh-copy-id.number=主機數量 [{0}] 公鑰數量 [{1}] termora.keymgr.ssh-copy-id.number=主機數量 [{0}] 公鑰數量 [{1}]
termora.keymgr.ssh-copy-id.failed=複製失敗 termora.keymgr.ssh-copy-id.failed=複製失敗
@@ -236,6 +241,7 @@ termora.tools.multiple=將命令傳送到目前視窗會話
# Tabbed # Tabbed
termora.tabbed.contextmenu.rename=重新命名 termora.tabbed.contextmenu.rename=重新命名
termora.tabbed.contextmenu.select-host=選取主機
termora.tabbed.contextmenu.sftp-command=SFTP 終端 termora.tabbed.contextmenu.sftp-command=SFTP 終端
termora.tabbed.contextmenu.sftp-not-install=沒有找到 SFTP 程序,請安裝後重試 termora.tabbed.contextmenu.sftp-not-install=沒有找到 SFTP 程序,請安裝後重試
termora.tabbed.contextmenu.clone=克隆 termora.tabbed.contextmenu.clone=克隆
@@ -301,6 +307,13 @@ termora.transport.bookmarks=書籤管理
termora.transport.bookmarks.up=上移 termora.transport.bookmarks.up=上移
termora.transport.bookmarks.down=下移 termora.transport.bookmarks.down=下移
termora.transport.toolbar.prev=返回
termora.transport.toolbar.home=預設目錄
termora.transport.toolbar.next=前進
termora.transport.toolbar.parent=父目錄
termora.transport.toolbar.show-hide=顯示/隱藏目錄
termora.transport.toolbar.refresh=重新整理
termora.transport.table.filename=檔名 termora.transport.table.filename=檔名
termora.transport.table.type=類型 termora.transport.table.type=類型
termora.transport.table.size=大小 termora.transport.table.size=大小
@@ -312,7 +325,7 @@ termora.transport.table.owner=所有者
# contextmenu # contextmenu
termora.transport.table.contextmenu.transfer=傳輸 termora.transport.table.contextmenu.transfer=傳輸
termora.transport.table.contextmenu.copy-path=複製路徑 termora.transport.table.contextmenu.copy-path=複製路徑
termora.transport.table.contextmenu.open-in-folder=${termora.finder}中打開 termora.transport.table.contextmenu.open-in-folder={0}中打開
termora.transport.table.contextmenu.change-permissions=更改權限... termora.transport.table.contextmenu.change-permissions=更改權限...
termora.transport.table.contextmenu.refresh=刷新 termora.transport.table.contextmenu.refresh=刷新
termora.transport.table.contextmenu.compress=壓縮 termora.transport.table.contextmenu.compress=壓縮
@@ -410,6 +423,8 @@ termora.visual-window.system-information.mem=內存
termora.visual-window.system-information.swap=交換 termora.visual-window.system-information.swap=交換
termora.visual-window.system-information.filesystem=檔案系統 termora.visual-window.system-information.filesystem=檔案系統
termora.visual-window.system-information.used-total=使用 / 大小 termora.visual-window.system-information.used-total=使用 / 大小
termora.visual-window.toggle-window=切換視窗
termora.visual-window.transport.question=更多功能
termora.floating-toolbar.close-in-current-tab=在目前標籤頁關閉 termora.floating-toolbar.close-in-current-tab=在目前標籤頁關閉

View File

@@ -1,4 +1,4 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. --> <!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.33214 2.63203L13.3321 7.07539C13.4389 7.17028 13.5 7.3063 13.5 7.44914V13C13.5 13.2761 13.2761 13.5 13 13.5H10C9.72386 13.5 9.5 13.2761 9.5 13V11C9.5 10.1716 8.82843 9.5 8 9.5C7.17157 9.5 6.5 10.1716 6.5 11V13C6.5 13.2761 6.27614 13.5 6 13.5H3C2.72386 13.5 2.5 13.2761 2.5 13V7.44914C2.5 7.3063 2.56109 7.17028 2.66786 7.07539L7.66786 2.63203C7.85729 2.46369 8.14271 2.46369 8.33214 2.63203Z" fill="#EBECF0" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/> <path d="M8.33214 2.63203L13.3321 7.07539C13.4389 7.17028 13.5 7.3063 13.5 7.44914V13C13.5 13.2761 13.2761 13.5 13 13.5H10C9.72386 13.5 9.5 13.2761 9.5 13V11C9.5 10.1716 8.82843 9.5 8 9.5C7.17157 9.5 6.5 10.1716 6.5 11V13C6.5 13.2761 6.27614 13.5 6 13.5H3C2.72386 13.5 2.5 13.2761 2.5 13V7.44914C2.5 7.3063 2.56109 7.17028 2.66786 7.07539L7.66786 2.63203C7.85729 2.46369 8.14271 2.46369 8.33214 2.63203Z" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 716 B

After

Width:  |  Height:  |  Size: 701 B

View File

@@ -1,4 +1,4 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. --> <!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.33214 2.63203L13.3321 7.07539C13.4389 7.17028 13.5 7.3063 13.5 7.44914V13C13.5 13.2761 13.2761 13.5 13 13.5H10C9.72386 13.5 9.5 13.2761 9.5 13V11C9.5 10.1716 8.82843 9.5 8 9.5C7.17157 9.5 6.5 10.1716 6.5 11V13C6.5 13.2761 6.27614 13.5 6 13.5H3C2.72386 13.5 2.5 13.2761 2.5 13V7.44914C2.5 7.3063 2.56109 7.17028 2.66786 7.07539L7.66786 2.63203C7.85729 2.46369 8.14271 2.46369 8.33214 2.63203Z" fill="#43454A" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/> <path d="M8.33214 2.63203L13.3321 7.07539C13.4389 7.17028 13.5 7.3063 13.5 7.44914V13C13.5 13.2761 13.2761 13.5 13 13.5H10C9.72386 13.5 9.5 13.2761 9.5 13V11C9.5 10.1716 8.82843 9.5 8 9.5C7.17157 9.5 6.5 10.1716 6.5 11V13C6.5 13.2761 6.27614 13.5 6 13.5H3C2.72386 13.5 2.5 13.2761 2.5 13V7.44914C2.5 7.3063 2.56109 7.17028 2.66786 7.07539L7.66786 2.63203C7.85729 2.46369 8.14271 2.46369 8.33214 2.63203Z" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 716 B

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

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

View File

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