Compare commits

...

86 Commits
1.0.12 ... main

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

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

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

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

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

View File

@@ -11,7 +11,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
# download jdk # download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-aarch64-b895.91.tar.gz - run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-aarch64-b1034.51.tar.gz
# appimagetool # appimagetool
- run: sudo apt install libfuse2 - run: sudo apt install libfuse2
@@ -22,7 +22,7 @@ jobs:
with: with:
distribution: 'jdkfile' distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.6' java-version: '21.0.7'
architecture: aarch64 architecture: aarch64
- uses: actions/cache@v4 - uses: actions/cache@v4

View File

@@ -11,7 +11,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
# download jdk # download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-x64-b895.91.tar.gz - run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-x64-b1034.51.tar.gz
# appimagetool # appimagetool
- run: sudo apt install libfuse2 - run: sudo apt install libfuse2
@@ -22,7 +22,7 @@ jobs:
with: with:
distribution: 'jdkfile' distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.6' java-version: '21.0.7'
architecture: x64 architecture: x64
- uses: actions/cache@v4 - uses: actions/cache@v4

View File

@@ -44,7 +44,7 @@ jobs:
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD" xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
# download jdk # download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b895.91.tar.gz - run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-osx-aarch64-b1034.51.tar.gz
# install jdk # install jdk
- name: Installing Java - name: Installing Java
@@ -52,7 +52,7 @@ jobs:
with: with:
distribution: 'jdkfile' distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.6' java-version: '21.0.7'
architecture: aarch64 architecture: aarch64
- uses: actions/cache@v4 - uses: actions/cache@v4

View File

@@ -44,7 +44,7 @@ jobs:
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD" xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
# download jdk # download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b895.91.tar.gz - run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-osx-x64-b1034.51.tar.gz
# install jdk # install jdk
- name: Installing Java - name: Installing Java
@@ -52,7 +52,7 @@ jobs:
with: with:
distribution: 'jdkfile' distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.6' java-version: '21.0.7'
architecture: x64 architecture: x64

View File

@@ -21,9 +21,9 @@ jobs:
- name: Installing Java - name: Installing Java
run: | run: |
curl -s --output ${{ runner.temp }}\java_package.zip -L https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-windows-x64-b895.91.zip curl -s --output ${{ runner.temp }}\java_package.zip -L https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-windows-x64-b1034.51.zip
unzip -q ${{ runner.temp }}\java_package.zip -d ${{ runner.temp }}\jbr unzip -q ${{ runner.temp }}\java_package.zip -d ${{ runner.temp }}\jbr
echo "JAVA_HOME=${{ runner.temp }}\jbr\jbrsdk-21.0.6-windows-x64-b895.91" >> $env:GITHUB_ENV echo "JAVA_HOME=${{ runner.temp }}\jbr\jbrsdk-21.0.7-windows-x64-b1034.51" >> $env:GITHUB_ENV
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:

View File

@@ -10,5 +10,5 @@ jobs:
if: github.repository == 'TermoraDev/termora' if: github.repository == 'TermoraDev/termora'
with: with:
identifier: TermoraDev.Termora identifier: TermoraDev.Termora
installers-regex: 'x86-64\.exe$' # Only x86-64.exe files installers-regex: '\.exe$'
token: ${{ secrets.WINGET_TOKEN }} token: ${{ secrets.WINGET_TOKEN }}

View File

@@ -14,13 +14,14 @@ plugins {
java java
idea idea
application application
`maven-publish`
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlinx.serialization) alias(libs.plugins.kotlinx.serialization)
} }
group = "app.termora" group = "app.termora"
version = "1.0.12" version = "1.0.17"
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem() val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture() val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
@@ -56,67 +57,67 @@ dependencies {
// implementation(platform(libs.koin.bom)) // implementation(platform(libs.koin.bom))
// implementation(libs.koin.core) // implementation(libs.koin.core)
implementation(libs.slf4j.api) api(libs.slf4j.api)
implementation(libs.pty4j) api(libs.pty4j)
implementation(libs.slf4j.tinylog) api(libs.slf4j.tinylog)
implementation(libs.tinylog.impl) api(libs.tinylog.impl)
implementation(libs.commons.codec) api(libs.commons.codec)
implementation(libs.commons.io) api(libs.commons.io)
implementation(libs.commons.lang3) api(libs.commons.lang3)
implementation(libs.commons.csv) api(libs.commons.csv)
implementation(libs.commons.net) api(libs.commons.net)
implementation(libs.commons.text) api(libs.commons.text)
implementation(libs.commons.compress) api(libs.commons.compress)
implementation(libs.commons.vfs2) { exclude(group = "*", module = "*") } api(libs.commons.vfs2) { exclude(group = "*", module = "*") }
implementation(libs.kotlinx.coroutines.swing) api(libs.kotlinx.coroutines.swing)
implementation(libs.kotlinx.coroutines.core) api(libs.kotlinx.coroutines.core)
implementation(libs.flatlaf) { api(libs.flatlaf) {
artifact { artifact {
if (useNoNativesFlatLaf) { if (useNoNativesFlatLaf) {
classifier = "no-natives" classifier = "no-natives"
} }
} }
} }
implementation(libs.flatlaf.extras) { api(libs.flatlaf.extras) {
if (useNoNativesFlatLaf) { if (useNoNativesFlatLaf) {
exclude(group = "com.formdev", module = "flatlaf") exclude(group = "com.formdev", module = "flatlaf")
} }
} }
implementation(libs.flatlaf.swingx) { api(libs.flatlaf.swingx) {
if (useNoNativesFlatLaf) { if (useNoNativesFlatLaf) {
exclude(group = "com.formdev", module = "flatlaf") exclude(group = "com.formdev", module = "flatlaf")
} }
} }
implementation(libs.kotlinx.serialization.json) api(libs.kotlinx.serialization.json)
implementation(libs.swingx) api(libs.swingx)
implementation(libs.jgoodies.forms) api(libs.jgoodies.forms)
implementation(libs.jna) api(libs.jna)
implementation(libs.jna.platform) api(libs.jna.platform)
implementation(libs.versioncompare) api(libs.versioncompare)
implementation(libs.oshi.core) api(libs.oshi.core)
implementation(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") } api(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
implementation(libs.jfa) { exclude(group = "*", module = "*") } api(libs.jfa) { exclude(group = "*", module = "*") }
implementation(libs.jbr.api) api(libs.jbr.api)
implementation(libs.okhttp) api(libs.okhttp)
implementation(libs.okhttp.logging) api(libs.okhttp.logging)
implementation(libs.sshd.core) api(libs.sshd.core)
implementation(libs.commonmark) api(libs.commonmark)
implementation(libs.jgit) api(libs.jgit)
implementation(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") } api(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
implementation(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") } api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
implementation(libs.eddsa) api(libs.eddsa)
implementation(libs.jnafilechooser) api(libs.jnafilechooser)
implementation(libs.xodus.vfs) api(libs.xodus.vfs)
implementation(libs.xodus.openAPI) api(libs.xodus.openAPI)
implementation(libs.xodus.environment) api(libs.xodus.environment)
implementation(libs.bip39) api(libs.bip39)
implementation(libs.colorpicker) api(libs.colorpicker)
implementation(libs.mixpanel) api(libs.mixpanel)
implementation(libs.jSerialComm) api(libs.jSerialComm)
implementation(libs.ini4j) api(libs.ini4j)
implementation(libs.restart4j) api(libs.restart4j)
} }
application { application {
@@ -133,20 +134,48 @@ application {
args.add("--add-opens java.desktop/java.awt=ALL-UNNAMED") args.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
args.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED") args.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
args.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED") args.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
args.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
args.add("-Dsun.java2d.metal=true") args.add("-Dsun.java2d.metal=true")
args.add("-Dapple.awt.application.appearance=system") args.add("-Dapple.awt.application.appearance=system")
} }
args.add("-Dapp-version=${project.version}") args.add("-Dapp-version=${project.version}")
if (os.isLinux) {
args.add("-Dsun.java2d.opengl=true")
}
applicationDefaultJvmArgs = args applicationDefaultJvmArgs = args
mainClass = "app.termora.MainKt" mainClass = "app.termora.MainKt"
} }
publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
pom {
name = project.name
description = "Termora is a terminal emulator and SSH client for Windows, macOS and Linux"
url = "https://github.com/TermoraDev/termora"
licenses {
license {
name = "AGPL-3.0"
url = "https://opensource.org/license/agpl-v3"
}
}
developers {
developer {
name = "hstyi"
url = "https://github.com/hstyi"
}
}
scm {
url = "https://github.com/TermoraDev/termora"
}
}
}
}
}
tasks.test { tasks.test {
useJUnitPlatform() useJUnitPlatform()
} }
@@ -356,10 +385,7 @@ tasks.register<Exec>("jpackage") {
options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED") options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
options.add("-Dapple.awt.application.appearance=system") options.add("-Dapple.awt.application.appearance=system")
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED") options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
} options.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
if (os.isLinux) {
options.add("-Dsun.java2d.opengl=true")
} }
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage") val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage")

View File

@@ -1,29 +1,29 @@
[versions] [versions]
kotlin = "2.1.20" kotlin = "2.1.21"
slf4j = "2.0.17" slf4j = "2.0.17"
pty4j = "0.13.2" pty4j = "0.13.6"
tinylog = "2.7.0" tinylog = "2.7.0"
kotlinx-coroutines = "1.10.1" kotlinx-coroutines = "1.10.2"
flatlaf = "3.5.4" flatlaf = "3.6"
kotlinx-serialization-json = "1.8.1" kotlinx-serialization-json = "1.8.1"
commons-codec = "1.18.0" commons-codec = "1.18.0"
commons-lang3 = "3.17.0" commons-lang3 = "3.17.0"
commons-csv = "1.14.0" commons-csv = "1.14.0"
commons-net = "3.11.1" commons-net = "3.11.1"
commons-text = "1.13.0" commons-text = "1.13.1"
commons-compress = "1.27.1" commons-compress = "1.27.1"
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.6.5" oshi = "6.8.1"
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.18.0" commons-io = "2.19.0"
jbr-api = "17.1.10.1" jbr-api = "17.1.10.1"
hutool = "5.8.37" hutool = "5.8.37"
jsch = "0.2.21" jsch = "0.2.26"
okhttp = "4.12.0" okhttp = "4.12.0"
sshj = "0.39.0" sshj = "0.39.0"
sshd-core = "2.15.0" sshd-core = "2.15.0"
@@ -35,7 +35,7 @@ bip39 = "1.0.9"
colorpicker = "2.0.1" colorpicker = "2.0.1"
rhino = "1.8.0" rhino = "1.8.0"
delight-rhino-sandbox = "0.0.17" delight-rhino-sandbox = "0.0.17"
testcontainers = "1.20.6" testcontainers = "1.21.1"
mixpanel = "1.5.3" mixpanel = "1.5.3"
jSerialComm = "2.11.0" jSerialComm = "2.11.0"
ini4j = "0.5.5-2" ini4j = "0.5.5-2"

View File

@@ -1,5 +1,5 @@
plugins { plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
} }
rootProject.name = "termora" rootProject.name = "termora"

View File

@@ -8,6 +8,7 @@ import java.security.GeneralSecurityException;
import java.security.KeyPair; import java.security.KeyPair;
import java.util.*; import java.util.*;
@Deprecated
public class CombinedKeyIdentityProvider implements KeyIdentityProvider { public class CombinedKeyIdentityProvider implements KeyIdentityProvider {
private final List<KeyIdentityProvider> providers = new ArrayList<>(); private final List<KeyIdentityProvider> providers = new ArrayList<>();

View File

@@ -14,6 +14,7 @@ import static com.formdev.flatlaf.util.UIScale.scale;
/** /**
* 如果要升级 FlatLaf 需要检查是否兼容 * 如果要升级 FlatLaf 需要检查是否兼容
*/ */
@Deprecated
public class MyFlatTabbedPaneUI extends FlatTabbedPaneUI { public class MyFlatTabbedPaneUI extends FlatTabbedPaneUI {
@Override @Override
protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) { protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {

View File

@@ -28,8 +28,13 @@ import java.awt.MenuItem
import java.awt.PopupMenu import java.awt.PopupMenu
import java.awt.SystemTray import java.awt.SystemTray
import java.awt.TrayIcon import java.awt.TrayIcon
import java.awt.desktop.AppReopenedEvent
import java.awt.desktop.AppReopenedListener
import java.awt.desktop.SystemEventListener
import java.awt.event.ActionEvent import java.awt.event.ActionEvent
import java.awt.event.WindowEvent
import java.util.* import java.util.*
import java.util.concurrent.CountDownLatch
import javax.imageio.ImageIO import javax.imageio.ImageIO
import javax.swing.* import javax.swing.*
import kotlin.system.exitProcess import kotlin.system.exitProcess
@@ -64,6 +69,9 @@ class ApplicationRunner {
fileSystemManager.filesCache = WeakRefFilesCache() fileSystemManager.filesCache = WeakRefFilesCache()
fileSystemManager.init() fileSystemManager.init()
VFS.setManager(fileSystemManager) VFS.setManager(fileSystemManager)
// async init
BackgroundManager.getInstance().getBackgroundImage()
} }
// 设置 LAF // 设置 LAF
@@ -78,9 +86,6 @@ class ApplicationRunner {
// 启动主窗口 // 启动主窗口
val startMainFrame = measureTimeMillis { startMainFrame() } val startMainFrame = measureTimeMillis { startMainFrame() }
// 设置托盘
val setupSystemTray = measureTimeMillis { SwingUtilities.invokeLater { setupSystemTray() } }
if (log.isDebugEnabled) { if (log.isDebugEnabled) {
log.debug("printSystemInfo: {}ms", printSystemInfo) log.debug("printSystemInfo: {}ms", printSystemInfo)
log.debug("openDatabase: {}ms", openDatabase) log.debug("openDatabase: {}ms", openDatabase)
@@ -89,7 +94,6 @@ class ApplicationRunner {
log.debug("setupLaf: {}ms", setupLaf) log.debug("setupLaf: {}ms", setupLaf)
log.debug("openDoor: {}ms", openDoor) log.debug("openDoor: {}ms", openDoor)
log.debug("startMainFrame: {}ms", startMainFrame) log.debug("startMainFrame: {}ms", startMainFrame)
log.debug("setupSystemTray: {}ms", setupSystemTray)
} }
}.let { }.let {
if (log.isDebugEnabled) { if (log.isDebugEnabled) {
@@ -119,8 +123,24 @@ class ApplicationRunner {
TermoraFrameManager.getInstance().createWindow().isVisible = true TermoraFrameManager.getInstance().createWindow().isVisible = true
if (SystemUtils.IS_OS_MAC_OSX) { if (SystemInfo.isMacOS) {
SwingUtilities.invokeLater { FlatDesktop.setQuitHandler { quitHandler() } } SwingUtilities.invokeLater {
try {
// 设置 Dock
setupMacOSDock()
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
// Command + Q
FlatDesktop.setQuitHandler { quitHandler() }
}
} else if (SystemInfo.isWindows) {
// 设置托盘
SwingUtilities.invokeLater { setupSystemTray() }
} }
} }
@@ -156,9 +176,13 @@ class ApplicationRunner {
} }
private fun quitHandler() { private fun quitHandler() {
for (frame in TermoraFrameManager.getInstance().getWindows()) { val windows = TermoraFrameManager.getInstance().getWindows()
frame.dispose()
for (frame in windows) {
frame.dispatchEvent(WindowEvent(frame, WindowEvent.WINDOW_CLOSED))
} }
Disposer.dispose(TermoraFrameManager.getInstance())
} }
private fun loadSettings() { private fun loadSettings() {
@@ -240,7 +264,35 @@ class ApplicationRunner {
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc")) UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
}
private fun setupMacOSDock() {
val countDownLatch = CountDownLatch(1)
val cls = Class.forName("com.apple.eawt.Application")
val app = cls.getMethod("getApplication").invoke(null)
val addAppEventListener = cls.getMethod("addAppEventListener", SystemEventListener::class.java)
addAppEventListener.invoke(app, object : AppReopenedListener {
override fun appReopened(e: AppReopenedEvent) {
val manager = TermoraFrameManager.getInstance()
if (manager.getWindows().isEmpty()) {
manager.createWindow().isVisible = true
}
}
})
// 当应用程序销毁时,驻守线程也可以退出了
Disposer.register(ApplicationScope.forApplicationScope(), object : Disposable {
override fun dispose() {
countDownLatch.countDown()
}
})
// 驻守线程,不然当所有窗口都关闭时,程序会自动退出
// wait application exit
Thread.ofPlatform().daemon(false)
.priority(Thread.MIN_PRIORITY)
.start { countDownLatch.await() }
} }
private fun printSystemInfo() { private fun printSystemInfo() {
@@ -303,7 +355,27 @@ class ApplicationRunner {
.event(getAnalyticsUserID(), "launch", properties) .event(getAnalyticsUserID(), "launch", properties)
val delivery = ClientDelivery() val delivery = ClientDelivery()
delivery.addMessage(message) delivery.addMessage(message)
MixpanelAPI().deliver(delivery, true) val endpoints = listOf(
"https://api-eu.mixpanel.com",
"https://api-in.mixpanel.com",
"https://api.mixpanel.com",
"http://api.mixpanel.com",
)
for (endpoint in endpoints) {
try {
MixpanelAPI(
"$endpoint/track",
"$endpoint/engage",
"$endpoint/groups"
).deliver(delivery, true)
break
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
continue
}
}
} catch (e: Exception) { } catch (e: Exception) {
if (log.isErrorEnabled) { if (log.isErrorEnabled) {
log.error(e.message, e) log.error(e.message, e)

View File

@@ -0,0 +1,88 @@
package app.termora
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO
import javax.swing.JPopupMenu
import javax.swing.SwingUtilities
class BackgroundManager private constructor() {
companion object {
private val log = LoggerFactory.getLogger(BackgroundManager::class.java)
fun getInstance(): BackgroundManager {
return ApplicationScope.forApplicationScope().getOrCreate(BackgroundManager::class) { BackgroundManager() }
}
}
private val appearance get() = Database.getDatabase().appearance
private var bufferedImage: BufferedImage? = null
private var imageFilepath = StringUtils.EMPTY
fun setBackgroundImage(file: File) {
synchronized(this) {
try {
bufferedImage = file.inputStream().use { ImageIO.read(it) }
imageFilepath = file.absolutePath
appearance.backgroundImage = file.absolutePath
SwingUtilities.invokeLater {
for (window in TermoraFrameManager.getInstance().getWindows()) {
SwingUtilities.updateComponentTreeUI(window)
}
}
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
}
}
fun getBackgroundImage(): BufferedImage? {
val bg = doGetBackgroundImage()
if (bg == null) {
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
return null
} else {
JPopupMenu.setDefaultLightWeightPopupEnabled(true)
}
} else {
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
JPopupMenu.setDefaultLightWeightPopupEnabled(false)
}
}
return bg
}
private fun doGetBackgroundImage(): BufferedImage? {
synchronized(this) {
if (bufferedImage == null || imageFilepath.isEmpty()) {
if (appearance.backgroundImage.isBlank()) {
return null
}
val file = File(appearance.backgroundImage)
if (file.exists()) {
setBackgroundImage(file)
}
}
return bufferedImage
}
}
fun clearBackgroundImage() {
synchronized(this) {
bufferedImage = null
imageFilepath = StringUtils.EMPTY
appearance.backgroundImage = StringUtils.EMPTY
SwingUtilities.invokeLater {
for (window in TermoraFrameManager.getInstance().getWindows()) {
SwingUtilities.updateComponentTreeUI(window)
}
}
}
}
}

View File

@@ -1,10 +1,9 @@
package app.termora package app.termora
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.actions.MultipleAction
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.serialization.encodeToString
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.action.ActionManager
import java.awt.Component import java.awt.Component
@@ -20,6 +19,7 @@ import kotlin.math.min
class CustomizeToolBarDialog( class CustomizeToolBarDialog(
owner: Window, owner: Window,
private val windowScope: WindowScope,
private val toolbar: TermoraToolBar private val toolbar: TermoraToolBar
) : DialogWrapper(owner) { ) : DialogWrapper(owner) {
@@ -147,9 +147,7 @@ class CustomizeToolBarDialog(
leftList.model.removeAllElements() leftList.model.removeAllElements()
rightList.model.removeAllElements() rightList.model.removeAllElements()
for (action in toolbar.getAllActions()) { for (action in toolbar.getAllActions()) {
actionManager.getAction(action.id)?.let { getActionHolder(action.id)?.let { rightList.model.addElement(it) }
rightList.model.addElement(ActionHolder(action.id, it))
}
} }
} }
@@ -259,14 +257,11 @@ class CustomizeToolBarDialog(
override fun windowOpened(e: WindowEvent) { override fun windowOpened(e: WindowEvent) {
removeWindowListener(this) removeWindowListener(this)
for (action in toolbar.getActions()) { for (action in toolbar.getActions()) {
if (action.visible) { if (action.visible) {
actionManager.getAction(action.id) getActionHolder(action.id)?.let { rightList.model.addElement(it) }
?.let { rightList.model.addElement(ActionHolder(action.id, it)) }
} else { } else {
actionManager.getAction(action.id) getActionHolder(action.id)?.let { leftList.model.addElement(it) }
?.let { leftList.model.addElement(ActionHolder(action.id, it)) }
} }
} }
@@ -274,6 +269,17 @@ class CustomizeToolBarDialog(
}) })
} }
private fun getActionHolder(actionId: String): ActionHolder? {
var action = actionManager.getAction(actionId)
if (action == null) {
if (actionId == MultipleAction.MULTIPLE) {
action = MultipleAction.getInstance(windowScope)
}
}
if (action == null) return null
return ActionHolder(actionId, action)
}
private fun resetMoveButtons() { private fun resetMoveButtons() {
val indices = rightList.selectedIndices val indices = rightList.selectedIndices
if (indices.isEmpty()) { if (indices.isEmpty()) {

View File

@@ -523,6 +523,11 @@ class Database private constructor(private val env: Environment) : Disposable {
*/ */
var beep by BooleanPropertyDelegate(true) var beep by BooleanPropertyDelegate(true)
/**
* 超链接
*/
var hyperlink by BooleanPropertyDelegate(true)
/** /**
* 光标闪烁 * 光标闪烁
*/ */
@@ -643,6 +648,16 @@ class Database private constructor(private val env: Environment) : Disposable {
*/ */
var backgroundRunning by BooleanPropertyDelegate(false) var backgroundRunning by BooleanPropertyDelegate(false)
/**
* 标签关闭前确认
*/
var confirmTabClose by BooleanPropertyDelegate(false)
/**
* 背景图片的地址
*/
var backgroundImage by StringPropertyDelegate(StringUtils.EMPTY)
/** /**
* 语言 * 语言
*/ */

View File

@@ -25,6 +25,7 @@ enum class Protocol {
SSH, SSH,
Local, Local,
Serial, Serial,
RDP,
/** /**
* 交互式的 SFTP此协议只在系统内部交互不应该暴露给用户也不应该持久化 * 交互式的 SFTP此协议只在系统内部交互不应该暴露给用户也不应该持久化

View File

@@ -12,6 +12,7 @@ import org.apache.sshd.client.session.ClientSession
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Dimension import java.awt.Dimension
import java.awt.Window import java.awt.Window
import java.util.*
import javax.swing.* import javax.swing.*
class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) { class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
@@ -24,6 +25,7 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
isModal = true isModal = true
title = I18n.getString("termora.new-host.title") title = I18n.getString("termora.new-host.title")
setLocationRelativeTo(null) setLocationRelativeTo(null)
pane.setSelectedIndex(0)
init() init()
} }
@@ -54,7 +56,8 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
isEnabled = false isEnabled = false
swingCoroutineScope.launch(Dispatchers.IO) { swingCoroutineScope.launch(Dispatchers.IO) {
testConnection(pane.getHost()) // 因为测试连接的时候从数据库读取会导致失效所以这里生成随机ID
testConnection(pane.getHost().copy(id = UUID.randomUUID().toSimpleString()))
withContext(Dispatchers.Swing) { withContext(Dispatchers.Swing) {
putValue(NAME, I18n.getString("termora.new-host.test-connection")) putValue(NAME, I18n.getString("termora.new-host.test-connection"))
isEnabled = true isEnabled = true

View File

@@ -13,11 +13,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.apache.commons.lang3.RegExUtils
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
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.agent.connector.WinPipeConnector import org.eclipse.jgit.internal.transport.sshd.agent.connector.WinPipeConnector
import java.awt.* import java.awt.*
import java.awt.datatransfer.DataFlavor
import java.awt.event.* import java.awt.event.*
import java.nio.charset.Charset import java.nio.charset.Charset
import javax.swing.* import javax.swing.*
@@ -221,7 +223,24 @@ open class HostOptionsPane : OptionsPane() {
val nameTextField = OutlineTextField(128) val nameTextField = OutlineTextField(128)
val protocolTypeComboBox = FlatComboBox<Protocol>() val protocolTypeComboBox = FlatComboBox<Protocol>()
val usernameTextField = OutlineTextField(128) val usernameTextField = OutlineTextField(128)
val hostTextField = OutlineTextField(255) val hostTextField = object : OutlineTextField(255) {
override fun paste() {
if (!toolkit.systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
return
}
var text = toolkit.systemClipboard.getData(DataFlavor.stringFlavor)?.toString() ?: return
if (text.isBlank()) {
return
}
// 移除所有不可见字符
text = RegExUtils.replaceAll(text, "[\\p{C}\\s]", StringUtils.EMPTY)
// text
replaceSelection(text)
}
}
private val passwordPanel = JPanel(BorderLayout()) private val passwordPanel = JPanel(BorderLayout())
private val chooseKeyBtn = JButton(Icons.greyKey) private val chooseKeyBtn = JButton(Icons.greyKey)
val passwordTextField = OutlinePasswordField(255) val passwordTextField = OutlinePasswordField(255)
@@ -320,6 +339,7 @@ open class HostOptionsPane : OptionsPane() {
protocolTypeComboBox.addItem(Protocol.SSH) protocolTypeComboBox.addItem(Protocol.SSH)
protocolTypeComboBox.addItem(Protocol.Local) protocolTypeComboBox.addItem(Protocol.Local)
protocolTypeComboBox.addItem(Protocol.Serial) protocolTypeComboBox.addItem(Protocol.Serial)
protocolTypeComboBox.addItem(Protocol.RDP)
authenticationTypeComboBox.addItem(AuthenticationType.No) authenticationTypeComboBox.addItem(AuthenticationType.No)
authenticationTypeComboBox.addItem(AuthenticationType.Password) authenticationTypeComboBox.addItem(AuthenticationType.Password)

View File

@@ -49,6 +49,7 @@ class HostTreeNode(host: Host) : SimpleTreeNode<Host>(host) {
return when (host.protocol) { return when (host.protocol) {
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon() Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
Protocol.Serial -> if (selected && hasFocus) Icons.plugin.dark else Icons.plugin Protocol.Serial -> if (selected && hasFocus) Icons.plugin.dark else Icons.plugin
Protocol.RDP -> if (selected && hasFocus) Icons.microsoftWindows.dark else Icons.microsoftWindows
else -> if (selected && hasFocus) Icons.terminal.dark else Icons.terminal else -> if (selected && hasFocus) Icons.terminal.dark else Icons.terminal
} }
} }

View File

@@ -10,6 +10,7 @@ object Icons {
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") } val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") } val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
val close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") } val close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") }
val settingSync by lazy { DynamicIcon("icons/settingSync.svg", "icons/settingSync_dark.svg") }
val openInNewWindow by lazy { DynamicIcon("icons/openInNewWindow.svg", "icons/openInNewWindow_dark.svg") } val openInNewWindow by lazy { DynamicIcon("icons/openInNewWindow.svg", "icons/openInNewWindow_dark.svg") }
val openInToolWindow by lazy { DynamicIcon("icons/openInToolWindow.svg", "icons/openInToolWindow_dark.svg") } val openInToolWindow by lazy { DynamicIcon("icons/openInToolWindow.svg", "icons/openInToolWindow_dark.svg") }
val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") } val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
@@ -63,6 +64,7 @@ object Icons {
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") } val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") } val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") }
val microsoft by lazy { DynamicIcon("icons/microsoft.svg", "icons/microsoft_dark.svg") } val microsoft by lazy { DynamicIcon("icons/microsoft.svg", "icons/microsoft_dark.svg") }
val microsoftWindows by lazy { DynamicIcon("icons/microsoftWindows.svg", "icons/microsoftWindows_dark.svg") }
val tencent by lazy { DynamicIcon("icons/tencent.svg") } val tencent by lazy { DynamicIcon("icons/tencent.svg") }
val google by lazy { DynamicIcon("icons/google-small.svg") } val google by lazy { DynamicIcon("icons/google-small.svg") }
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") } val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }

View File

@@ -1,12 +1,22 @@
package app.termora package app.termora
import app.termora.terminal.PtyConnector import app.termora.terminal.PtyConnector
import app.termora.terminal.PtyConnectorDelegate
import app.termora.terminal.PtyProcessConnector
import org.apache.commons.io.Charsets import org.apache.commons.io.Charsets
import org.slf4j.LoggerFactory
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import javax.swing.JOptionPane
import javax.swing.SwingUtilities
import kotlin.jvm.optionals.getOrNull
class LocalTerminalTab(windowScope: WindowScope, host: Host) : class LocalTerminalTab(windowScope: WindowScope, host: Host) :
PtyHostTerminalTab(windowScope, host) { PtyHostTerminalTab(windowScope, host) {
companion object {
private val log = LoggerFactory.getLogger(LocalTerminalTab::class.java)
}
override suspend fun openPtyConnector(): PtyConnector { override suspend fun openPtyConnector(): PtyConnector {
val winSize = terminalPanel.winSize() val winSize = terminalPanel.winSize()
val ptyConnector = PtyConnectorFactory.getInstance().createPtyConnector( val ptyConnector = PtyConnectorFactory.getInstance().createPtyConnector(
@@ -18,4 +28,42 @@ class LocalTerminalTab(windowScope: WindowScope, host: Host) :
return ptyConnector return ptyConnector
} }
override fun willBeClose(): Boolean {
val ptyProcessConnector = getPtyProcessConnector() ?: return true
val process = ptyProcessConnector.process
var consoleProcessCount = 0
try {
val processHandle = ProcessHandle.of(process.pid()).getOrNull()
if (processHandle != null) {
consoleProcessCount = processHandle.children().count().toInt()
}
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
// 没有正在运行的进程
if (consoleProcessCount < 1) return true
val owner = SwingUtilities.getWindowAncestor(terminalPanel) ?: return true
return OptionPane.showConfirmDialog(
owner,
I18n.getString("termora.tabbed.local-tab.close-prompt"),
messageType = JOptionPane.INFORMATION_MESSAGE,
optionType = JOptionPane.OK_CANCEL_OPTION
) == JOptionPane.OK_OPTION
}
private fun getPtyProcessConnector(): PtyProcessConnector? {
var p = getPtyConnector() as PtyConnector?
while (p != null) {
if (p is PtyProcessConnector) return p
if (p is PtyConnectorDelegate) p = p.ptyConnector
}
return null
}
} }

View File

@@ -1,11 +0,0 @@
package app.termora
import com.formdev.flatlaf.ui.FlatRootPaneUI
import com.formdev.flatlaf.ui.FlatTitlePane
class MyFlatRootPaneUI : FlatRootPaneUI() {
fun getTitlePane(): FlatTitlePane? {
return super.titlePane
}
}

View File

@@ -9,7 +9,6 @@ import java.awt.event.*
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.util.* import java.util.*
import javax.swing.* import javax.swing.*
import javax.swing.plaf.TabbedPaneUI
import kotlin.math.abs import kotlin.math.abs
class MyTabbedPane : FlatTabbedPane() { class MyTabbedPane : FlatTabbedPane() {
@@ -21,18 +20,12 @@ 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 myUI = MyFlatTabbedPaneUI()
init { init {
isFocusable = false isFocusable = false
super.setUI(myUI)
initEvents() initEvents()
} }
override fun setUI(ui: TabbedPaneUI?) {
super.setUI(myUI)
}
override fun updateUI() { override fun updateUI() {
styleMap = mapOf( styleMap = mapOf(
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground"), "focusColor" to UIManager.getColor("TabbedPane.selectedBackground"),

View File

@@ -37,6 +37,7 @@ import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.XPathConstants import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory import javax.xml.xpath.XPathFactory
@Suppress("CascadeIf")
class NewHostTree : SimpleTree() { class NewHostTree : SimpleTree() {
companion object { companion object {
@@ -97,7 +98,7 @@ class NewHostTree : SimpleTree() {
// 是否显示更多信息 // 是否显示更多信息
if (isShowMoreInfo) { if (isShowMoreInfo) {
val color = if (sel) { val color = if (sel) {
if (tree.hasFocus()) { if (tree.hasFocus() || isPopupMenu) {
UIManager.getColor("textHighlightText") UIManager.getColor("textHighlightText")
} else { } else {
this.foreground this.foreground
@@ -110,15 +111,15 @@ class NewHostTree : SimpleTree() {
"""<font color=rgb(${color.red},${color.green},${color.blue})>${it}</font>""" """<font color=rgb(${color.red},${color.green},${color.blue})>${it}</font>"""
} }
if (host.protocol == Protocol.SSH) { // @formatter:off
text = if (host.protocol == Protocol.SSH || host.protocol == Protocol.RDP) {
"<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;${fontTag.apply("${host.username}@${host.host}")}</html>" text = "<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;${fontTag.apply("${host.username}@${host.host}")}</html>"
} else if (host.protocol == Protocol.Serial) { } else if (host.protocol == Protocol.Serial) {
text = text = "<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;${fontTag.apply(host.options.serialComm.port)}</html>"
"<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;${fontTag.apply(host.options.serialComm.port)}</html>"
} else if (host.protocol == Protocol.Folder) { } else if (host.protocol == Protocol.Folder) {
text = "<html>${host.name}${fontTag.apply(" (${node.childCount})")}</html>" text = "<html>${host.name}${fontTag.apply(" (${node.getAllChildren().size})")}</html>"
} }
// @formatter:on
} }
val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus) val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus)
@@ -137,6 +138,9 @@ class NewHostTree : SimpleTree() {
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) { if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return
if (lastNode.host.protocol != Protocol.Folder) { if (lastNode.host.protocol != Protocol.Folder) {
val path = tree.getClosestPathForLocation(e.x, e.y) ?: return
val bounds = tree.getRowBounds(tree.getRowForPath(path)) ?: return
if ((e.y >= bounds.y && e.y < (bounds.y + bounds.height)).not()) return
openHostAction?.actionPerformed(OpenHostActionEvent(e.source, lastNode.host, e)) openHostAction?.actionPerformed(OpenHostActionEvent(e.source, lastNode.host, e))
} }
} }
@@ -852,7 +856,8 @@ class NewHostTree : SimpleTree() {
val port = map["Port"]?.toIntOrNull() ?: 22 val port = map["Port"]?.toIntOrNull() ?: 22
val username = map["Username"] ?: StringUtils.EMPTY val username = map["Username"] ?: StringUtils.EMPTY
val protocol = map["Protocol"] ?: "SSH" val protocol = map["Protocol"] ?: "SSH"
if (!StringUtils.equalsIgnoreCase(protocol, "SSH")) continue // 仅支持 SSH、RDP 协议
if (StringUtils.equalsAnyIgnoreCase(protocol, "SSH", "RDP").not()) continue
if (StringUtils.isAllBlank(hostname, label)) continue if (StringUtils.isAllBlank(hostname, label)) continue
var p: HostTreeNode? = null var p: HostTreeNode? = null
@@ -887,7 +892,7 @@ class NewHostTree : SimpleTree() {
host = hostname, host = hostname,
port = port, port = port,
username = username, username = username,
protocol = Protocol.SSH, protocol = runCatching { Protocol.valueOf(protocol) }.getOrNull() ?: Protocol.SSH,
parentId = p?.host?.id ?: StringUtils.EMPTY, parentId = p?.host?.id ?: StringUtils.EMPTY,
) )
) )

View File

@@ -40,7 +40,7 @@ class NewHostTreeDialog(
init() init()
setLocationRelativeTo(null) setLocationRelativeTo(owner)
} }

View File

@@ -17,6 +17,7 @@ open class OptionsPane : JPanel(BorderLayout()) {
} }
private val cardLayout = CardLayout() private val cardLayout = CardLayout()
private val contentPanel = JPanel(cardLayout) private val contentPanel = JPanel(cardLayout)
private val loadedComponents = mutableMapOf<String, JComponent>()
init { init {
initView() initView()
@@ -103,16 +104,15 @@ open class OptionsPane : JPanel(BorderLayout()) {
throw UnsupportedOperationException("Title already exists") throw UnsupportedOperationException("Title already exists")
} }
} }
contentPanel.add(option.getJComponent(), option.getTitle())
tabListModel.addElement(option) tabListModel.addElement(option)
if (tabList.selectedIndex < 0) {
tabList.selectedIndex = 0
}
} }
fun removeOption(option: Option) { fun removeOption(option: Option) {
contentPanel.remove(option.getJComponent()) val title = option.getTitle()
loadedComponents[title]?.let {
contentPanel.remove(it)
loadedComponents.remove(title)
}
tabListModel.removeElement(option) tabListModel.removeElement(option)
} }
@@ -123,7 +123,17 @@ open class OptionsPane : JPanel(BorderLayout()) {
private fun initEvents() { private fun initEvents() {
tabList.addListSelectionListener { tabList.addListSelectionListener {
if (tabList.selectedIndex >= 0) { if (tabList.selectedIndex >= 0) {
cardLayout.show(contentPanel, tabListModel.get(tabList.selectedIndex).getTitle()) val option = tabListModel.get(tabList.selectedIndex)
val title = option.getTitle()
if (!loadedComponents.containsKey(title)) {
val component = option.getJComponent()
loadedComponents[title] = component
contentPanel.add(component, title)
SwingUtilities.updateComponentTreeUI(component)
}
cardLayout.show(contentPanel, title)
} }
} }
} }

View File

@@ -52,6 +52,7 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
init { init {
terminalPanel.dropFiles = false terminalPanel.dropFiles = false
terminalPanel.dataProviderSupport.addData(DataProviders.TerminalTab, this)
} }
override fun getJComponent(): JComponent { override fun getJComponent(): JComponent {
@@ -222,6 +223,10 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
} }
} }
override fun beforeClose() {
// 保存窗口状态
terminalPanel.storeVisualWindows(host.id)
}
private inner class MySessionListener : SessionListener, Disposable { private inner class MySessionListener : SessionListener, Disposable {
override fun sessionEvent(session: Session, event: Event) { override fun sessionEvent(session: Session, event: Event) {

View File

@@ -151,6 +151,7 @@ class ApplicationScope private constructor() : Scope() {
} }
fun windowScopes(): List<WindowScope> { fun windowScopes(): List<WindowScope> {
if (scopes.isEmpty()) return emptyList()
return scopes.values.toList() return scopes.values.toList()
} }

View File

@@ -3,8 +3,6 @@ package app.termora
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Dimension import java.awt.Dimension
import java.awt.Window import java.awt.Window
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.swing.BorderFactory import javax.swing.BorderFactory
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.JPanel import javax.swing.JPanel
@@ -20,8 +18,10 @@ class SettingsDialog(owner: Window) : DialogWrapper(owner) {
title = I18n.getString("termora.setting") title = I18n.getString("termora.setting")
setLocationRelativeTo(null) setLocationRelativeTo(null)
init() val index = properties.getString("Settings-SelectedOption")?.toIntOrNull() ?: 0
optionsPane.setSelectedIndex(index)
init()
initEvents() initEvents()
} }
@@ -31,14 +31,6 @@ class SettingsDialog(owner: Window) : DialogWrapper(owner) {
properties.putString("Settings-SelectedOption", optionsPane.getSelectedIndex().toString()) properties.putString("Settings-SelectedOption", optionsPane.getSelectedIndex().toString())
} }
}) })
addWindowListener(object : WindowAdapter() {
override fun windowActivated(e: WindowEvent) {
removeWindowListener(this)
val index = properties.getString("Settings-SelectedOption")?.toIntOrNull() ?: return
optionsPane.setSelectedIndex(index)
}
})
} }
override fun createCenterPanel(): JComponent { override fun createCenterPanel(): JComponent {

View File

@@ -33,12 +33,18 @@ import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout 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.platform.win32.Shell32
import com.sun.jna.platform.win32.ShlObj
import com.sun.jna.platform.win32.WinDef
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import org.apache.commons.codec.binary.Base64 import org.apache.commons.codec.binary.Base64
import org.apache.commons.io.FileUtils
import org.apache.commons.io.FilenameUtils
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.commons.lang3.SystemUtils import org.apache.commons.lang3.SystemUtils
@@ -57,6 +63,7 @@ import java.awt.event.ItemListener
import java.io.File import java.io.File
import java.net.URI import java.net.URI
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.nio.file.StandardCopyOption
import java.util.* import java.util.*
import java.util.function.Consumer import java.util.function.Consumer
import javax.swing.* import javax.swing.*
@@ -129,11 +136,15 @@ class SettingsOptionsPane : OptionsPane() {
val themeComboBox = FlatComboBox<String>() val themeComboBox = FlatComboBox<String>()
val languageComboBox = FlatComboBox<String>() val languageComboBox = FlatComboBox<String>()
val backgroundComBoBox = YesOrNoComboBox() val backgroundComBoBox = YesOrNoComboBox()
val confirmTabCloseComBoBox = YesOrNoComboBox()
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)
val backgroundImageTextField = OutlineTextField()
private val appearance get() = database.appearance private val appearance get() = database.appearance
private val backgroundButton = JButton(Icons.folder)
private val backgroundClearButton = FlatButton()
init { init {
initView() initView()
@@ -142,7 +153,21 @@ class SettingsOptionsPane : OptionsPane() {
private fun initView() { private fun initView() {
backgroundComBoBox.isEnabled = SystemInfo.isWindows backgroundComBoBox.isEnabled = SystemInfo.isWindows || SystemInfo.isMacOS
backgroundImageTextField.isEditable = false
backgroundImageTextField.trailingComponent = backgroundButton
backgroundImageTextField.text = FilenameUtils.getName(appearance.backgroundImage)
backgroundImageTextField.document.addDocumentListener(object : DocumentAdaptor() {
override fun changedUpdate(e: DocumentEvent) {
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
}
})
backgroundClearButton.isFocusable = false
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
backgroundClearButton.icon = Icons.delete
backgroundClearButton.buttonType = FlatButton.ButtonType.toolBarButton
opacitySpinner.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows opacitySpinner.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows
opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) { opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) {
@@ -160,6 +185,7 @@ class SettingsOptionsPane : OptionsPane() {
followSystemCheckBox.isSelected = appearance.followSystem followSystemCheckBox.isSelected = appearance.followSystem
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
backgroundComBoBox.selectedItem = appearance.backgroundRunning backgroundComBoBox.selectedItem = appearance.backgroundRunning
confirmTabCloseComBoBox.selectedItem = appearance.confirmTabClose
themeComboBox.isEnabled = !followSystemCheckBox.isSelected themeComboBox.isEnabled = !followSystemCheckBox.isSelected
themeManager.themes.keys.forEach { themeComboBox.addItem(it) } themeManager.themes.keys.forEach { themeComboBox.addItem(it) }
@@ -210,6 +236,13 @@ class SettingsOptionsPane : OptionsPane() {
} }
} }
confirmTabCloseComBoBox.addItemListener {
if (it.stateChange == ItemEvent.SELECTED) {
appearance.confirmTabClose = confirmTabCloseComBoBox.selectedItem as Boolean
}
}
followSystemCheckBox.addActionListener { followSystemCheckBox.addActionListener {
appearance.followSystem = followSystemCheckBox.isSelected appearance.followSystem = followSystemCheckBox.isSelected
themeComboBox.isEnabled = !followSystemCheckBox.isSelected themeComboBox.isEnabled = !followSystemCheckBox.isSelected
@@ -239,6 +272,46 @@ class SettingsOptionsPane : OptionsPane() {
} }
preferredThemeBtn.addActionListener { showPreferredThemeContextmenu() } preferredThemeBtn.addActionListener { showPreferredThemeContextmenu() }
backgroundButton.addActionListener {
val chooser = FileChooser()
chooser.osxAllowedFileTypes = listOf("png", "jpg", "jpeg")
chooser.allowsMultiSelection = false
chooser.win32Filters.add(Pair("Image files", listOf("png", "jpg", "jpeg")))
chooser.fileSelectionMode = JFileChooser.FILES_ONLY
chooser.showOpenDialog(owner).thenAccept {
if (it.isNotEmpty()) {
onSelectedBackgroundImage(it.first())
}
}
}
backgroundClearButton.addActionListener {
BackgroundManager.getInstance().clearBackgroundImage()
backgroundImageTextField.text = StringUtils.EMPTY
}
}
private fun onSelectedBackgroundImage(file: File) {
try {
val destFile = FileUtils.getFile(Application.getBaseDataDir(), "background", file.name)
FileUtils.forceMkdirParent(destFile)
FileUtils.deleteQuietly(destFile)
FileUtils.copyFile(file, destFile, StandardCopyOption.REPLACE_EXISTING)
backgroundImageTextField.text = destFile.name
BackgroundManager.getInstance().setBackgroundImage(destFile)
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
SwingUtilities.invokeLater {
OptionPane.showMessageDialog(
owner,
ExceptionUtils.getRootCauseMessage(e),
messageType = JOptionPane.ERROR_MESSAGE
)
}
}
} }
override fun getIcon(isSelected: Boolean): Icon { override fun getIcon(isSelected: Boolean): Icon {
@@ -308,7 +381,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun getFormPanel(): JPanel { private fun getFormPanel(): JPanel {
val layout = FormLayout( val layout = FormLayout(
"left:pref, $formMargin, default:grow, $formMargin, default, default:grow", "left:pref, $formMargin, default:grow, $formMargin, default, default:grow",
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref" "pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
) )
val box = FlatToolBar() val box = FlatToolBar()
box.add(followSystemCheckBox) box.add(followSystemCheckBox)
@@ -330,11 +403,24 @@ class SettingsOptionsPane : OptionsPane() {
})).xy(5, rows).apply { rows += step } })).xy(5, rows).apply { rows += step }
val bgClearBox = Box.createHorizontalBox()
bgClearBox.add(backgroundClearButton)
builder.add("${I18n.getString("termora.settings.appearance.background-image")}:").xy(1, rows)
.add(backgroundImageTextField).xy(3, rows)
.add(bgClearBox).xy(5, rows)
.apply { rows += step }
builder.add("${I18n.getString("termora.settings.appearance.opacity")}:").xy(1, rows) builder.add("${I18n.getString("termora.settings.appearance.opacity")}:").xy(1, rows)
.add(opacitySpinner).xy(3, rows).apply { rows += step } .add(opacitySpinner).xy(3, rows).apply { rows += step }
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) .add(backgroundComBoBox).xy(3, rows).apply { rows += step }
val confirmTabCloseBox = Box.createHorizontalBox()
confirmTabCloseBox.add(JLabel("${I18n.getString("termora.settings.appearance.confirm-tab-close")}:"))
confirmTabCloseBox.add(Box.createHorizontalStrut(8))
confirmTabCloseBox.add(confirmTabCloseComBoBox)
builder.add(confirmTabCloseBox).xyw(1, rows, 3).apply { rows += step }
return builder.build() return builder.build()
} }
@@ -355,6 +441,7 @@ class SettingsOptionsPane : OptionsPane() {
private val selectCopyComboBox = YesOrNoComboBox() private val selectCopyComboBox = YesOrNoComboBox()
private val autoCloseTabComboBox = YesOrNoComboBox() private val autoCloseTabComboBox = YesOrNoComboBox()
private val floatingToolbarComboBox = YesOrNoComboBox() private val floatingToolbarComboBox = YesOrNoComboBox()
private val hyperlinkComboBox = YesOrNoComboBox()
init { init {
initView() initView()
@@ -432,6 +519,13 @@ class SettingsOptionsPane : OptionsPane() {
} }
} }
hyperlinkComboBox.addItemListener { e ->
if (e.stateChange == ItemEvent.SELECTED) {
terminalSetting.hyperlink = hyperlinkComboBox.selectedItem as Boolean
TerminalPanelFactory.getInstance().repaintAll()
}
}
cursorBlinkComboBox.addItemListener { e -> cursorBlinkComboBox.addItemListener { e ->
if (e.stateChange == ItemEvent.SELECTED) { if (e.stateChange == ItemEvent.SELECTED) {
terminalSetting.cursorBlink = cursorBlinkComboBox.selectedItem as Boolean terminalSetting.cursorBlink = cursorBlinkComboBox.selectedItem as Boolean
@@ -510,20 +604,33 @@ class SettingsOptionsPane : OptionsPane() {
shellComboBox.selectedItem = terminalSetting.localShell shellComboBox.selectedItem = terminalSetting.localShell
val fonts = linkedSetOf("JetBrains Mono", "Source Code Pro", "Monospaced") fontComboBox.addItem(terminalSetting.font)
FontUtils.getAllFonts().forEach { var fontsLoaded = false
if (!fonts.contains(it.family)) {
fonts.addLast(it.family)
}
}
for (font in fonts) { fontComboBox.addPopupMenuListener(object : PopupMenuListener {
fontComboBox.addItem(font) override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
} if (!fontsLoaded) {
val selectedItem = fontComboBox.selectedItem
fontComboBox.removeAllItems();
fontComboBox.addItem("JetBrains Mono")
fontComboBox.addItem("Source Code Pro")
fontComboBox.addItem("Monospaced")
FontUtils.getAvailableFontFamilyNames().forEach {
fontComboBox.addItem(it)
}
fontComboBox.selectedItem = selectedItem
fontsLoaded = true
}
}
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {}
override fun popupMenuCanceled(e: PopupMenuEvent) {}
})
fontComboBox.selectedItem = terminalSetting.font fontComboBox.selectedItem = terminalSetting.font
debugComboBox.selectedItem = terminalSetting.debug debugComboBox.selectedItem = terminalSetting.debug
beepComboBox.selectedItem = terminalSetting.beep beepComboBox.selectedItem = terminalSetting.beep
hyperlinkComboBox.selectedItem = terminalSetting.hyperlink
cursorBlinkComboBox.selectedItem = terminalSetting.cursorBlink cursorBlinkComboBox.selectedItem = terminalSetting.cursorBlink
cursorStyleComboBox.selectedItem = terminalSetting.cursor cursorStyleComboBox.selectedItem = terminalSetting.cursor
selectCopyComboBox.selectedItem = terminalSetting.selectCopy selectCopyComboBox.selectedItem = terminalSetting.selectCopy
@@ -546,7 +653,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun getCenterComponent(): JComponent { private fun getCenterComponent(): JComponent {
val layout = FormLayout( val layout = FormLayout(
"left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow", "left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow",
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref" "pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
) )
val beepBtn = JButton(Icons.run) val beepBtn = JButton(Icons.run)
@@ -569,6 +676,8 @@ class SettingsOptionsPane : OptionsPane() {
.add("${I18n.getString("termora.settings.terminal.beep")}:").xy(1, rows) .add("${I18n.getString("termora.settings.terminal.beep")}:").xy(1, rows)
.add(beepComboBox).xy(3, rows) .add(beepComboBox).xy(3, rows)
.add(beepBtn).xy(5, rows).apply { rows += step } .add(beepBtn).xy(5, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.terminal.hyperlink")}:").xy(1, rows)
.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.cursor-style")}:").xy(1, rows) .add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
@@ -595,7 +704,7 @@ class SettingsOptionsPane : OptionsPane() {
val gistTextField = OutlineTextField(255) val gistTextField = OutlineTextField(255)
val policyComboBox = JComboBox<SyncPolicy>() val policyComboBox = JComboBox<SyncPolicy>()
val domainTextField = OutlineTextField(255) val domainTextField = OutlineTextField(255)
val syncConfigButton = JButton(I18n.getString("termora.settings.sync"), Icons.download) val syncConfigButton = JButton(I18n.getString("termora.settings.sync"), Icons.settingSync)
val exportConfigButton = JButton(I18n.getString("termora.settings.sync.export"), Icons.export) val exportConfigButton = JButton(I18n.getString("termora.settings.sync.export"), Icons.export)
val importConfigButton = JButton(I18n.getString("termora.settings.sync.import"), Icons.import) val importConfigButton = JButton(I18n.getString("termora.settings.sync.import"), Icons.import)
val lastSyncTimeLabel = JLabel() val lastSyncTimeLabel = JLabel()
@@ -1409,6 +1518,7 @@ class SettingsOptionsPane : OptionsPane() {
private val sftpCommandField = OutlineTextField(255) private val sftpCommandField = OutlineTextField(255)
private val defaultDirectoryField = OutlineTextField(255) private val defaultDirectoryField = OutlineTextField(255)
private val browseDirectoryBtn = JButton(Icons.folder) private val browseDirectoryBtn = JButton(Icons.folder)
private val browseEditCommandBtn = JButton(Icons.folder)
private val pinTabComboBox = YesOrNoComboBox() private val pinTabComboBox = YesOrNoComboBox()
private val preserveModificationTimeComboBox = YesOrNoComboBox() private val preserveModificationTimeComboBox = YesOrNoComboBox()
private val sftp get() = database.sftp private val sftp get() = database.sftp
@@ -1480,6 +1590,41 @@ class SettingsOptionsPane : OptionsPane() {
} }
} }
}) })
browseEditCommandBtn.addActionListener(object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
val chooser = FileChooser()
chooser.allowsMultiSelection = false
chooser.fileSelectionMode = JFileChooser.FILES_ONLY
if (SystemInfo.isMacOS) {
chooser.defaultDirectory = "/Applications"
} else {
if (SystemInfo.isWindows) {
val pszPath = CharArray(WinDef.MAX_PATH)
Shell32.INSTANCE.SHGetFolderPath(
null,
ShlObj.CSIDL_DESKTOPDIRECTORY, null, ShlObj.SHGFP_TYPE_CURRENT,
pszPath
)
chooser.defaultDirectory = Native.toString(pszPath)
} else {
chooser.defaultDirectory = SystemUtils.USER_HOME
}
}
chooser.showOpenDialog(owner).thenAccept { files ->
if (files.isNotEmpty()) {
val file = files.first()
if (SystemInfo.isMacOS) {
editCommandField.text = "open -a ${file.absolutePath} {0}"
} else {
editCommandField.text = "${file.absolutePath} {0}"
}
}
}
}
})
} }
@@ -1496,6 +1641,8 @@ class SettingsOptionsPane : OptionsPane() {
sftpCommandField.placeholderText = "sftp" sftpCommandField.placeholderText = "sftp"
} }
editCommandField.trailingComponent = browseEditCommandBtn
defaultDirectoryField.placeholderText = SystemUtils.USER_HOME defaultDirectoryField.placeholderText = SystemUtils.USER_HOME
defaultDirectoryField.trailingComponent = browseDirectoryBtn defaultDirectoryField.trailingComponent = browseDirectoryBtn

View File

@@ -34,6 +34,7 @@ import org.apache.sshd.common.channel.ChannelFactory
import org.apache.sshd.common.channel.PtyChannelConfiguration import org.apache.sshd.common.channel.PtyChannelConfiguration
import org.apache.sshd.common.channel.PtyChannelConfigurationHolder import org.apache.sshd.common.channel.PtyChannelConfigurationHolder
import org.apache.sshd.common.cipher.CipherNone import org.apache.sshd.common.cipher.CipherNone
import org.apache.sshd.common.compression.BuiltinCompressions
import org.apache.sshd.common.config.keys.KeyRandomArt import org.apache.sshd.common.config.keys.KeyRandomArt
import org.apache.sshd.common.config.keys.KeyUtils import org.apache.sshd.common.config.keys.KeyUtils
import org.apache.sshd.common.future.CloseFuture import org.apache.sshd.common.future.CloseFuture
@@ -47,6 +48,7 @@ import org.apache.sshd.common.kex.BuiltinDHFactories
import org.apache.sshd.common.keyprovider.KeyIdentityProvider import org.apache.sshd.common.keyprovider.KeyIdentityProvider
import org.apache.sshd.common.session.Session import org.apache.sshd.common.session.Session
import org.apache.sshd.common.session.SessionListener import org.apache.sshd.common.session.SessionListener
import org.apache.sshd.common.signature.BuiltinSignatures
import org.apache.sshd.common.util.net.SshdSocketAddress import org.apache.sshd.common.util.net.SshdSocketAddress
import org.apache.sshd.core.CoreModuleProperties import org.apache.sshd.core.CoreModuleProperties
import org.apache.sshd.server.forward.AcceptAllForwardingFilter import org.apache.sshd.server.forward.AcceptAllForwardingFilter
@@ -263,9 +265,14 @@ object SshClients {
} catch (e: Exception) { } catch (e: Exception) {
if (e !is SshException || e.disconnectCode != SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) throw e if (e !is SshException || e.disconnectCode != SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) throw e
val owner = client.properties["owner"] as Window? ?: throw e val owner = client.properties["owner"] as Window? ?: throw e
val authentication = ask(host, owner) ?: throw e val askUserInfo = ask(host, entry, owner) ?: throw e
if (authentication.type == AuthenticationType.No) throw e if (askUserInfo.authentication.type == AuthenticationType.No) throw e
return doOpenSession(host.copy(authentication = authentication), client) return doOpenSession(
host.copy(
authentication = askUserInfo.authentication,
username = askUserInfo.username
), client
)
} }
session.setAttribute(HOST_KEY, host) session.setAttribute(HOST_KEY, host)
@@ -339,6 +346,24 @@ object SshClients {
) )
builder.keyExchangeFactories(keyExchangeFactories) builder.keyExchangeFactories(keyExchangeFactories)
val compressionFactories = ClientBuilder.setUpDefaultCompressionFactories(true).toMutableList()
for (compression in listOf(
BuiltinCompressions.none,
BuiltinCompressions.zlib,
BuiltinCompressions.delayedZlib
)) {
if (compressionFactories.contains(compression)) continue
compressionFactories.add(compression)
}
builder.compressionFactories(compressionFactories)
val signatureFactories = ClientBuilder.setUpDefaultSignatureFactories(true).toMutableList()
for (signature in BuiltinSignatures.entries) {
if (signatureFactories.contains(signature)) continue
signatureFactories.add(signature)
}
builder.signatureFactories(signatureFactories)
if (host.tunnelings.isEmpty() && host.options.jumpHosts.isEmpty()) { if (host.tunnelings.isEmpty() && host.options.jumpHosts.isEmpty()) {
builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE) builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE)
} else { } else {
@@ -394,20 +419,32 @@ object SshClients {
return sshClient return sshClient
} }
private fun ask(host: Host, owner: Window): Authentication? {
val ref = AtomicReference<Authentication>(null) private data class AskUserInfo(val username: String, val authentication: Authentication)
private fun ask(host: Host, entry: HostConfigEntry, owner: Window): AskUserInfo? {
val ref = AtomicReference<AskUserInfo>(null)
SwingUtilities.invokeAndWait { SwingUtilities.invokeAndWait {
val dialog = RequestAuthenticationDialog(owner, host) val dialog = RequestAuthenticationDialog(owner, host)
dialog.setLocationRelativeTo(owner) dialog.setLocationRelativeTo(owner)
val authentication = dialog.getAuthentication().apply { ref.set(this) } val authentication = dialog.getAuthentication()
ref.set(AskUserInfo(dialog.getUsername(), authentication))
// save // save
if (dialog.isRemembered()) { if (dialog.isRemembered()) {
hostManager.addHost( // fix https://github.com/TermoraDev/termora/issues/609
host.copy( val hostId = entry.getProperty("Host", host.id)
authentication = authentication, val h = hostManager.getHost(hostId)
username = dialog.getUsername(), updateDate = System.currentTimeMillis(), if (h != null) {
hostManager.addHost(
h.copy(
authentication = authentication,
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
)
) )
) }
} }
} }
return ref.get() return ref.get()

View File

@@ -4,6 +4,7 @@ import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders import app.termora.actions.DataProviders
import app.termora.actions.MultipleAction import app.termora.actions.MultipleAction
import app.termora.highlight.KeywordHighlightPaintListener import app.termora.highlight.KeywordHighlightPaintListener
import app.termora.terminal.DataKey
import app.termora.terminal.PtyConnector import app.termora.terminal.PtyConnector
import app.termora.terminal.Terminal import app.termora.terminal.Terminal
import app.termora.terminal.panel.TerminalHyperlinkPaintListener import app.termora.terminal.panel.TerminalHyperlinkPaintListener
@@ -40,6 +41,10 @@ class TerminalPanelFactory : Disposable {
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel { fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
val writer = MyTerminalWriter(ptyConnector) val writer = MyTerminalWriter(ptyConnector)
val terminalPanel = TerminalPanel(terminal, writer) val terminalPanel = TerminalPanel(terminal, writer)
// processDeviceStatusReport
terminal.getTerminalModel().setData(DataKey.TerminalWriter, writer)
terminalPanel.addTerminalPaintListener(MultipleTerminalListener()) terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance()) terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance()) terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())

View File

@@ -1,5 +1,6 @@
package app.termora package app.termora
import app.termora.Database.Appearance
import app.termora.actions.DataProvider import app.termora.actions.DataProvider
import java.beans.PropertyChangeListener import java.beans.PropertyChangeListener
import javax.swing.Icon import javax.swing.Icon
@@ -43,8 +44,16 @@ interface TerminalTab : Disposable, DataProvider {
*/ */
fun canClose(): Boolean = true fun canClose(): Boolean = true
/**
* 返回 true 表示可以关闭,只有当 [Appearance.confirmTabClose] 为 false 时才会调用
*/
fun willBeClose(): Boolean = true fun willBeClose(): Boolean = true
/**
* 即将关闭,已经无法挽回
*/
fun beforeClose() {}
/** /**
* 是否可以克隆 * 是否可以克隆
*/ */

View File

@@ -32,6 +32,7 @@ class TerminalTabbed(
private val actionManager = ActionManager.getInstance() private val actionManager = ActionManager.getInstance()
private val dataProviderSupport = DataProviderSupport() private val dataProviderSupport = DataProviderSupport()
private val titleProperty = UUID.randomUUID().toSimpleString() private val titleProperty = UUID.randomUUID().toSimpleString()
private val appearance get() = Database.getDatabase().appearance
private val iconListener = PropertyChangeListener { e -> private val iconListener = PropertyChangeListener { e ->
val source = e.source val source = e.source
if (e.propertyName == "icon" && source is TerminalTab) { if (e.propertyName == "icon" && source is TerminalTab) {
@@ -153,8 +154,29 @@ class TerminalTabbed(
if (tabbedPane.isTabClosable(index)) { if (tabbedPane.isTabClosable(index)) {
val tab = tabs[index] val tab = tabs[index]
// 询问是否可以关闭
if (disposable) { if (disposable) {
if (!tab.willBeClose()) { // 如果开启了关闭确认,那么直接询问用户
if (appearance.confirmTabClose) {
if (OptionPane.showConfirmDialog(
windowScope.window,
I18n.getString("termora.tabbed.tab.close-prompt"),
messageType = JOptionPane.QUESTION_MESSAGE,
optionType = JOptionPane.OK_CANCEL_OPTION
) != JOptionPane.OK_OPTION
) {
return
}
} else if (!tab.willBeClose()) { // 如果没有开启则询问用户
return
}
}
// 通知即将关闭
if (disposable) {
try {
tab.beforeClose()
} catch (_: Exception) {
return return
} }
} }
@@ -400,12 +422,11 @@ class TerminalTabbed(
private fun showContextMenu(event: MouseEvent) { private fun showContextMenu(event: MouseEvent) {
val popupMenu = FlatPopupMenu() val popupMenu = FlatPopupMenu()
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener { popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
val dialog = CustomizeToolBarDialog( val owner = SwingUtilities.getWindowAncestor(this@TerminalTabbed)
SwingUtilities.getWindowAncestor(this@TerminalTabbed), val dialog = CustomizeToolBarDialog(owner, windowScope, termoraToolBar)
termoraToolBar dialog.setLocationRelativeTo(owner)
)
if (dialog.open()) { if (dialog.open()) {
termoraToolBar.rebuild() TermoraToolBar.rebuild()
} }
} }
popupMenu.show(event.component, event.x, event.y) popupMenu.show(event.component, event.x, event.y)

View File

@@ -7,12 +7,13 @@ import app.termora.actions.DataProviders
import app.termora.sftp.SFTPTab import app.termora.sftp.SFTPTab
import app.termora.terminal.DataKey import app.termora.terminal.DataKey
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.ui.FlatRootPaneUI
import com.formdev.flatlaf.ui.FlatTitlePane
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import com.jetbrains.JBR import com.jetbrains.JBR
import org.apache.commons.lang3.ArrayUtils import org.apache.commons.lang3.ArrayUtils
import java.awt.BorderLayout import java.awt.*
import java.awt.Dimension
import java.awt.Insets
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import java.awt.event.MouseListener import java.awt.event.MouseListener
@@ -37,12 +38,11 @@ class TermoraFrame : JFrame(), DataProvider {
private val id = UUID.randomUUID().toString() private val id = UUID.randomUUID().toString()
private val windowScope = ApplicationScope.forWindowScope(this) private val windowScope = ApplicationScope.forWindowScope(this)
private val tabbedPane = MyTabbedPane() private val tabbedPane = MyTabbedPane()
private val toolbar = TermoraToolBar(windowScope, this, tabbedPane) private val toolbar = TermoraToolBar(windowScope, this)
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane) private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane)
private val dataProviderSupport = DataProviderSupport() private val dataProviderSupport = DataProviderSupport()
private val welcomePanel = WelcomePanel(windowScope) private val welcomePanel = WelcomePanel(windowScope)
private val sftp get() = Database.getDatabase().sftp private val sftp get() = Database.getDatabase().sftp
private val myUI = MyFlatRootPaneUI()
private var notifyListeners = emptyArray<NotifyListener>() private var notifyListeners = emptyArray<NotifyListener>()
@@ -63,10 +63,9 @@ class TermoraFrame : JFrame(), DataProvider {
} }
override fun mouseDragged(e: MouseEvent) { override fun mouseDragged(e: MouseEvent) {
val mouseLayer = getMouseLayer() ?: return
getMouseMotionListener()?.mouseDragged( getMouseMotionListener()?.mouseDragged(
MouseEvent( MouseEvent(
mouseLayer, e.component,
e.id, e.id,
e.`when`, e.`when`,
e.modifiersEx, e.modifiersEx,
@@ -87,19 +86,19 @@ class TermoraFrame : JFrame(), DataProvider {
return getHandler() as? MouseMotionListener return getHandler() as? MouseMotionListener
} }
private fun getMouseLayer(): JComponent? {
val titlePane = myUI.getTitlePane() ?: return null
val handlerField = titlePane.javaClass.getDeclaredField("mouseLayer") ?: return null
handlerField.isAccessible = true
return handlerField.get(titlePane) as? JComponent
}
private fun getHandler(): Any? { private fun getHandler(): Any? {
val titlePane = myUI.getTitlePane() ?: return null val titlePane = getTitlePane() ?: return null
val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null
handlerField.isAccessible = true handlerField.isAccessible = true
return handlerField.get(titlePane) return handlerField.get(titlePane)
} }
private fun getTitlePane(): FlatTitlePane? {
val ui = rootPane.ui as? FlatRootPaneUI ?: return null
val titlePaneField = ui.javaClass.getDeclaredField("titlePane")
titlePaneField.isAccessible = true
return titlePaneField.get(ui) as? FlatTitlePane
}
} }
toolbar.getJToolBar().addMouseListener(mouseAdapter) toolbar.getJToolBar().addMouseListener(mouseAdapter)
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter) toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
@@ -173,7 +172,6 @@ class TermoraFrame : JFrame(), DataProvider {
// Windows 10 会有1像素误差 // Windows 10 会有1像素误差
tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0) tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0)
} else if (SystemInfo.isLinux) { } else if (SystemInfo.isLinux) {
rootPane.setUI(myUI)
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0) tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
} }
@@ -213,6 +211,11 @@ class TermoraFrame : JFrame(), DataProvider {
} }
} }
val glassPane = GlassPane()
rootPane.glassPane = glassPane
glassPane.isOpaque = false
glassPane.isVisible = true
Disposer.register(windowScope, terminalTabbed) Disposer.register(windowScope, terminalTabbed)
add(terminalTabbed, BorderLayout.CENTER) add(terminalTabbed, BorderLayout.CENTER)
@@ -254,4 +257,27 @@ class TermoraFrame : JFrame(), DataProvider {
super.addNotify() super.addNotify()
notifyListeners.forEach { it.addNotify() } notifyListeners.forEach { it.addNotify() }
} }
private class GlassPane : JComponent() {
init {
isFocusable = false
}
override fun paintComponent(g: Graphics) {
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
val g2d = g as Graphics2D
g2d.composite = AlphaComposite.getInstance(
AlphaComposite.SRC_OVER,
if (FlatLaf.isLafDark()) 0.2f else 0.1f
)
g2d.drawImage(img, 0, 0, width, height, null)
g2d.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER)
}
override fun contains(x: Int, y: Int): Boolean {
return false
}
}
} }

View File

@@ -15,6 +15,7 @@ import java.awt.Frame
import java.awt.Window import java.awt.Window
import java.awt.event.WindowAdapter import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent import java.awt.event.WindowEvent
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.JFrame import javax.swing.JFrame
import javax.swing.JOptionPane import javax.swing.JOptionPane
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
@@ -24,7 +25,7 @@ import kotlin.math.max
import kotlin.system.exitProcess import kotlin.system.exitProcess
class TermoraFrameManager { class TermoraFrameManager : Disposable {
companion object { companion object {
private val log = LoggerFactory.getLogger(TermoraFrameManager::class.java) private val log = LoggerFactory.getLogger(TermoraFrameManager::class.java)
@@ -37,11 +38,12 @@ class TermoraFrameManager {
private val frames = mutableListOf<TermoraFrame>() private val frames = mutableListOf<TermoraFrame>()
private val properties get() = Database.getDatabase().properties private val properties get() = Database.getDatabase().properties
private val isDisposed = AtomicBoolean(false)
private val isBackgroundRunning get() = Database.getDatabase().appearance.backgroundRunning private val isBackgroundRunning get() = Database.getDatabase().appearance.backgroundRunning
fun createWindow(): TermoraFrame { fun createWindow(): TermoraFrame {
val frame = TermoraFrame().apply { registerCloseCallback(this) } val frame = TermoraFrame().apply { registerCloseCallback(this) }
frame.title = if (SystemInfo.isLinux) null else Application.getName() frame.title = Application.getName()
frame.defaultCloseOperation = DO_NOTHING_ON_CLOSE frame.defaultCloseOperation = DO_NOTHING_ON_CLOSE
val rectangle = getFrameRectangle() ?: FrameRectangle(-1, -1, 1280, 800, 0) val rectangle = getFrameRectangle() ?: FrameRectangle(-1, -1, 1280, 800, 0)
@@ -80,6 +82,7 @@ class TermoraFrameManager {
private fun registerCloseCallback(window: TermoraFrame) { private fun registerCloseCallback(window: TermoraFrame) {
val manager = this
window.addWindowListener(object : WindowAdapter() { window.addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) { override fun windowClosed(e: WindowEvent) {
@@ -95,31 +98,49 @@ class TermoraFrameManager {
Disposer.dispose(windowScope) Disposer.dispose(windowScope)
val windowScopes = ApplicationScope.windowScopes() val windowScopes = ApplicationScope.windowScopes()
if (windowScopes.isNotEmpty()) {
return
}
// 如果已经没有 Window 域了,那么就可以退出程序了 // 如果已经没有 Window 域了,那么就可以退出程序了
if (windowScopes.isEmpty()) { if (SystemInfo.isWindows || SystemInfo.isLinux) {
this@TermoraFrameManager.dispose() Disposer.dispose(manager)
} else if (SystemInfo.isMacOS) {
// 如果 macOS 开启了后台运行,那么尽管所有窗口都没了,也不会退出
if (isBackgroundRunning) {
return
}
Disposer.dispose(manager)
} }
} }
override fun windowClosing(e: WindowEvent) { override fun windowClosing(e: WindowEvent) {
if (ApplicationScope.windowScopes().size == 1) { if (ApplicationScope.windowScopes().size != 1) {
if (SystemInfo.isWindows && isBackgroundRunning) { window.dispose()
// 最小化 return
window.extendedState = window.extendedState or JFrame.ICONIFIED }
// 隐藏
window.isVisible = false // 如果 Windows 开启了后台运行,那么最小化
} else { if (SystemInfo.isWindows && isBackgroundRunning) {
if (OptionPane.showConfirmDialog( // 最小化
window, window.extendedState = window.extendedState or JFrame.ICONIFIED
I18n.getString("termora.quit-confirm", Application.getName()), // 隐藏
optionType = JOptionPane.YES_NO_OPTION, window.isVisible = false
) == JOptionPane.YES_OPTION return
) { }
window.dispose()
} // 如果 macOS 已经开启了后台运行,那么直接销毁,因为会有一个进程驻守
} if (SystemInfo.isMacOS && isBackgroundRunning) {
} else { window.dispose()
return
}
val option = OptionPane.showConfirmDialog(
window,
I18n.getString("termora.quit-confirm", Application.getName()),
optionType = JOptionPane.YES_NO_OPTION,
)
if (option == JOptionPane.YES_OPTION) {
window.dispose() window.dispose()
} }
} }
@@ -142,14 +163,16 @@ class TermoraFrameManager {
} }
} }
private fun dispose() { override fun dispose() {
Disposer.dispose(ApplicationScope.forApplicationScope()) if (isDisposed.compareAndSet(false, true)) {
Disposer.dispose(ApplicationScope.forApplicationScope())
try { try {
Disposer.getTree().assertIsEmpty(true) Disposer.getTree().assertIsEmpty(true)
} catch (e: Exception) { } catch (e: Exception) {
if (log.isErrorEnabled) { if (log.isErrorEnabled) {
log.error(e.message, e) log.error(e.message, e)
}
} }
} }

View File

@@ -5,7 +5,6 @@ import app.termora.actions.*
import app.termora.findeverywhere.FindEverywhereAction import app.termora.findeverywhere.FindEverywhereAction
import app.termora.snippet.SnippetAction import app.termora.snippet.SnippetAction
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
@@ -26,10 +25,21 @@ data class ToolBarAction(
class TermoraToolBar( class TermoraToolBar(
private val windowScope: WindowScope, private val windowScope: WindowScope,
private val frame: TermoraFrame, private val frame: TermoraFrame,
private val tabbedPane: FlatTabbedPane
) { ) {
companion object {
fun rebuild() {
for (frame in TermoraFrameManager.getInstance().getWindows()) {
val toolbars = SwingUtils.getDescendantsOfClass(MyToolBar::class.java, frame)
for (toolbar in toolbars) {
toolbar.rebuild()
}
}
}
}
private val properties by lazy { Database.getDatabase().properties } private val properties by lazy { Database.getDatabase().properties }
private val toolbar by lazy { MyToolBar().apply { rebuild(this) } } private val toolbar by lazy { MyToolBar().apply { rebuild() } }
fun getJToolBar(): JToolBar { fun getJToolBar(): JToolBar {
@@ -87,63 +97,6 @@ class TermoraToolBar(
return storageActions return storageActions
} }
fun rebuild() {
rebuild(this.toolbar)
}
private fun rebuild(toolbar: JToolBar) {
val actionManager = ActionManager.getInstance()
val actionContainerFactory = ActionContainerFactory(actionManager)
toolbar.removeAll()
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
override fun actionPerformed(evt: AnActionEvent) {
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
}
override fun isEnabled(): Boolean {
return actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.isEnabled ?: false
}
}))
toolbar.add(Box.createHorizontalGlue())
if (SystemInfo.isLinux || SystemInfo.isWindows) {
toolbar.add(Box.createHorizontalStrut(16))
}
// update btn
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
updateBtn.isVisible = updateBtn.isEnabled
updateBtn.addChangeListener { updateBtn.isVisible = updateBtn.isEnabled }
toolbar.add(updateBtn)
// 获取显示的Action如果不是 false 那么就是显示出来
for (action in getActions()) {
if (action.visible) {
val ac = actionManager.getAction(action.id)
if (ac == null) {
if (action.id == MultipleAction.MULTIPLE) {
toolbar.add(actionContainerFactory.createButton(MultipleAction.getInstance(windowScope)))
}
} else {
toolbar.add(actionContainerFactory.createButton(ac))
}
}
}
if (toolbar is MyToolBar) {
toolbar.adjust()
}
toolbar.revalidate()
toolbar.repaint()
}
private inner class MyToolBar : JToolBar() { private inner class MyToolBar : JToolBar() {
init { init {
// 监听窗口大小变动,然后修改边距避开控制按钮 // 监听窗口大小变动,然后修改边距避开控制按钮
@@ -179,5 +132,60 @@ class TermoraToolBar(
} }
} }
} }
fun rebuild() {
val toolbar: JToolBar = this
val actionManager = ActionManager.getInstance()
val actionContainerFactory = ActionContainerFactory(actionManager)
toolbar.removeAll()
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
override fun actionPerformed(evt: AnActionEvent) {
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
}
override fun isEnabled(): Boolean {
return actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.isEnabled ?: false
}
}))
toolbar.add(Box.createHorizontalGlue())
if (SystemInfo.isLinux || SystemInfo.isWindows) {
toolbar.add(Box.createHorizontalStrut(16))
}
// update btn
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
updateBtn.isVisible = updateBtn.isEnabled
updateBtn.addChangeListener { updateBtn.isVisible = updateBtn.isEnabled }
toolbar.add(updateBtn)
// 获取显示的Action如果不是 false 那么就是显示出来
for (action in getActions()) {
if (action.visible) {
val ac = actionManager.getAction(action.id)
if (ac == null) {
if (action.id == MultipleAction.MULTIPLE) {
toolbar.add(actionContainerFactory.createButton(MultipleAction.getInstance(windowScope)))
}
} else {
toolbar.add(actionContainerFactory.createButton(ac))
}
}
}
if (toolbar is MyToolBar) {
toolbar.adjust()
}
toolbar.revalidate()
toolbar.repaint()
}
} }
} }

View File

@@ -1,6 +1,19 @@
package app.termora.actions package app.termora.actions
import app.termora.* import app.termora.*
import com.formdev.flatlaf.util.SystemInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
import java.net.URI
import java.util.*
import javax.swing.JOptionPane
import kotlin.time.Duration.Companion.seconds
class OpenHostAction : AnAction() { class OpenHostAction : AnAction() {
companion object { companion object {
@@ -26,10 +39,70 @@ class OpenHostAction : AnAction() {
Protocol.SSH -> SSHTerminalTab(windowScope, evt.host) Protocol.SSH -> SSHTerminalTab(windowScope, evt.host)
Protocol.Serial -> SerialTerminalTab(windowScope, evt.host) Protocol.Serial -> SerialTerminalTab(windowScope, evt.host)
Protocol.SFTPPty -> SFTPPtyTerminalTab(windowScope, evt.host) Protocol.SFTPPty -> SFTPPtyTerminalTab(windowScope, evt.host)
Protocol.RDP -> openRDP(windowScope, evt.host)
else -> LocalTerminalTab(windowScope, evt.host) else -> LocalTerminalTab(windowScope, evt.host)
} }
terminalTabbedManager.addTerminalTab(tab) if (tab is TerminalTab) {
tab.start() terminalTabbedManager.addTerminalTab(tab)
if (tab is PtyHostTerminalTab) {
tab.start()
}
}
}
private fun openRDP(windowScope: WindowScope, host: Host) {
if (SystemInfo.isLinux) {
OptionPane.showMessageDialog(
windowScope.window,
"Linux cannot connect to Windows Remote Server, Supported only for macOS and Windows",
messageType = JOptionPane.WARNING_MESSAGE
)
return
}
if (SystemInfo.isMacOS) {
if (!FileUtils.getFile("/Applications/Windows App.app").exists()) {
val option = OptionPane.showConfirmDialog(
windowScope.window,
"If you want to connect to a Windows Remote Server, You have to install the Windows App",
optionType = JOptionPane.OK_CANCEL_OPTION
)
if (option == JOptionPane.OK_OPTION) {
Application.browse(URI.create("https://apps.apple.com/app/windows-app/id1295203466"))
}
return
}
}
val sb = StringBuilder()
sb.append("full address:s:").append(host.host).append(':').append(host.port).appendLine()
sb.append("username:s:").append(host.username).appendLine()
val file = FileUtils.getFile(Application.getTemporaryDir(), UUID.randomUUID().toSimpleString() + ".rdp")
file.outputStream().use { IOUtils.write(sb.toString(), it, Charsets.UTF_8) }
if (host.authentication.type == AuthenticationType.Password) {
val systemClipboard = windowScope.window.toolkit.systemClipboard
val password = host.authentication.password
systemClipboard.setContents(StringSelection(password), null)
// clear password
swingCoroutineScope.launch(Dispatchers.IO) {
delay(30.seconds)
if (systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
if (systemClipboard.getData(DataFlavor.stringFlavor) == password) {
systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
}
}
}
}
if (SystemInfo.isMacOS) {
ProcessBuilder("open", file.absolutePath).start()
} else if (SystemInfo.isWindows) {
ProcessBuilder("mstsc", file.absolutePath).start()
}
} }
} }

View File

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

View File

@@ -51,6 +51,7 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
val customBtn = JButton("Custom") val customBtn = JButton("Custom")
customBtn.addActionListener { customBtn.addActionListener {
val dialog = MyColorPickerDialog(this) val dialog = MyColorPickerDialog(this)
dialog.setLocationRelativeTo(this)
dialog.colorPicker.color = defaultColor dialog.colorPicker.color = defaultColor
dialog.isVisible = true dialog.isVisible = true
val color = dialog.color val color = dialog.color

View File

@@ -24,6 +24,11 @@ data class KeywordHighlight(
*/ */
val matchCase: Boolean = false, val matchCase: Boolean = false,
/**
* 是否是正则表达式
*/
val regex: Boolean = false,
/** /**
* 0 是取前景色 * 0 是取前景色
*/ */

View File

@@ -20,10 +20,8 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
private val model = KeywordHighlightTableModel() private val model = KeywordHighlightTableModel()
private val table = FlatTable() private val table = FlatTable()
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() } private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
private val colorPalette by lazy { private val terminal by lazy { TerminalFactory.getInstance().createTerminal() }
TerminalFactory.getInstance().createTerminal().getTerminalModel() private val colorPalette by lazy { terminal.getTerminalModel().getColorPalette() }
.getColorPalette()
}
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add")) private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
private val editBtn = JButton(I18n.getString("termora.keymgr.edit")) private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
@@ -130,6 +128,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
addBtn.addActionListener { addBtn.addActionListener {
val dialog = NewKeywordHighlightDialog(this, colorPalette) val dialog = NewKeywordHighlightDialog(this, colorPalette)
dialog.setLocationRelativeTo(this)
dialog.isVisible = true dialog.isVisible = true
val keywordHighlight = dialog.keywordHighlight val keywordHighlight = dialog.keywordHighlight
if (keywordHighlight != null) { if (keywordHighlight != null) {
@@ -143,6 +142,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
if (row > -1) { if (row > -1) {
var keywordHighlight = model.getKeywordHighlight(row) var keywordHighlight = model.getKeywordHighlight(row)
val dialog = NewKeywordHighlightDialog(this, colorPalette) val dialog = NewKeywordHighlightDialog(this, colorPalette)
dialog.setLocationRelativeTo(this)
dialog.keywordTextField.text = keywordHighlight.keyword dialog.keywordTextField.text = keywordHighlight.keyword
dialog.descriptionTextField.text = keywordHighlight.description dialog.descriptionTextField.text = keywordHighlight.description
@@ -176,6 +176,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
dialog.underlineCheckBox.isSelected = keywordHighlight.underline dialog.underlineCheckBox.isSelected = keywordHighlight.underline
dialog.lineThroughCheckBox.isSelected = keywordHighlight.lineThrough dialog.lineThroughCheckBox.isSelected = keywordHighlight.lineThrough
dialog.matchCaseBtn.isSelected = keywordHighlight.matchCase dialog.matchCaseBtn.isSelected = keywordHighlight.matchCase
dialog.regexBtn.isSelected = keywordHighlight.regex
dialog.isVisible = true dialog.isVisible = true
@@ -211,6 +212,12 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
editBtn.isEnabled = table.selectedRowCount > 0 editBtn.isEnabled = table.selectedRowCount > 0
deleteBtn.isEnabled = editBtn.isEnabled deleteBtn.isEnabled = editBtn.isEnabled
} }
Disposer.register(disposable, object : Disposable {
override fun dispose() {
terminal.close()
}
})
} }
override fun createCenterPanel(): JComponent { override fun createCenterPanel(): JComponent {

View File

@@ -5,6 +5,7 @@ import app.termora.terminal.*
import app.termora.terminal.panel.TerminalDisplay import app.termora.terminal.panel.TerminalDisplay
import app.termora.terminal.panel.TerminalPaintListener import app.termora.terminal.panel.TerminalPaintListener
import app.termora.terminal.panel.TerminalPanel import app.termora.terminal.panel.TerminalPanel
import org.slf4j.LoggerFactory
import java.awt.Graphics import java.awt.Graphics
import kotlin.math.min import kotlin.math.min
import kotlin.random.Random import kotlin.random.Random
@@ -18,9 +19,10 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
} }
private val tag = Random.nextInt() private val tag = Random.nextInt()
private val log = LoggerFactory.getLogger(KeywordHighlightPaintListener::class.java)
} }
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() } private val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
override fun before( override fun before(
offset: Int, offset: Int,
@@ -36,7 +38,8 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
} }
val document = terminal.getDocument() val document = terminal.getDocument()
val kinds = SubstrFinder(object : Iterator<TerminalLine> { val kinds = mutableListOf<FindKind>()
val iterator = object : Iterator<TerminalLine> {
private var index = offset + 1 private var index = offset + 1
private val maxCount = min(index + count, document.getLineCount()) private val maxCount = min(index + count, document.getLineCount())
override fun hasNext(): Boolean { override fun hasNext(): Boolean {
@@ -46,8 +49,24 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
override fun next(): TerminalLine { override fun next(): TerminalLine {
return document.getLine(index++) return document.getLine(index++)
} }
}
}, CharArraySubstr(highlight.keyword.toCharArray())).find(!highlight.matchCase) if (highlight.regex) {
try {
val regex = if (highlight.matchCase)
highlight.keyword.toRegex()
else highlight.keyword.toRegex(RegexOption.IGNORE_CASE)
RegexFinder(regex, iterator).find()
.apply { kinds.addAll(this) }
} catch (e: Exception) {
if (log.isDebugEnabled) {
log.error(e.message, e)
}
}
} else {
SubstrFinder(iterator, CharArraySubstr(highlight.keyword.toCharArray())).find(!highlight.matchCase)
.apply { kinds.addAll(this) }
}
for (kind in kinds) { for (kind in kinds) {
terminal.getMarkupModel().addHighlighter( terminal.getMarkupModel().addHighlighter(
@@ -77,6 +96,74 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
terminal.getMarkupModel().removeAllHighlighters(tag) terminal.getMarkupModel().removeAllHighlighters(tag)
} }
private class RegexFinder(
private val regex: Regex,
private val iterator: Iterator<TerminalLine>
) {
private data class Coords(val row: Int, val col: Int)
private data class MatchResultWithCoords(
val match: String,
val coords: List<Coords>
)
fun find(): List<FindKind> {
val lines = mutableListOf<TerminalLine>()
val kinds = mutableListOf<FindKind>()
for ((index, line) in iterator.withIndex()) {
lines.add(line)
if (line.wrapped) continue
val data = mutableListOf<MutableList<Char>>()
for (e in lines) {
data.add(mutableListOf())
for (c in e.chars()) {
if (c.first.isNull) break
data.last().add(c.first)
}
}
lines.clear()
val resultWithCoords = findMatchesWithCoords(data)
if (resultWithCoords.isEmpty()) continue
val offset = index - data.size + 1
for (e in resultWithCoords) {
val coords = e.coords
if (coords.isEmpty()) continue
kinds.add(
FindKind(
startPosition = Position(coords.first().row + offset + 1, coords.first().col + 1),
endPosition = Position(coords.last().row + offset + 1, coords.last().col + 1)
)
)
}
}
return kinds
}
private fun findMatchesWithCoords(data: List<List<Char>>): List<MatchResultWithCoords> {
val flatChars = StringBuilder()
val indexMap = mutableListOf<Coords>()
// 拉平成字符串,并记录每个字符的位置
for ((rowIndex, row) in data.withIndex()) {
for ((colIndex, char) in row.withIndex()) {
flatChars.append(char)
indexMap.add(Coords(rowIndex, colIndex))
}
}
return regex.findAll(flatChars.toString())
.map { MatchResultWithCoords(it.value, indexMap.subList(it.range.first, it.range.last + 1)) }
.toList()
}
}
private class KeywordHighlightHighlighter( private class KeywordHighlightHighlighter(
range: HighlighterRange, terminal: Terminal, range: HighlighterRange, terminal: Terminal,
@@ -94,3 +181,5 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
} }
} }
} }

View File

@@ -1,10 +1,6 @@
package app.termora.highlight package app.termora.highlight
import app.termora.DialogWrapper import app.termora.*
import app.termora.DynamicColor
import app.termora.I18n
import app.termora.Icons
import app.termora.Database
import app.termora.terminal.ColorPalette import app.termora.terminal.ColorPalette
import app.termora.terminal.TerminalColor import app.termora.terminal.TerminalColor
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
@@ -46,6 +42,7 @@ class NewKeywordHighlightDialog(
I18n.getString("termora.highlight.background-color") I18n.getString("termora.highlight.background-color")
) )
val matchCaseBtn = JToggleButton(Icons.matchCase) val matchCaseBtn = JToggleButton(Icons.matchCase)
val regexBtn = JToggleButton(Icons.regex)
private val textColorRevert = JButton(Icons.revert) private val textColorRevert = JButton(Icons.revert)
@@ -85,6 +82,7 @@ class NewKeywordHighlightDialog(
val box = FlatToolBar() val box = FlatToolBar()
box.add(matchCaseBtn) box.add(matchCaseBtn)
box.add(regexBtn)
keywordTextField.trailingComponent = box keywordTextField.trailingComponent = box
repaintKeywordHighlightView() repaintKeywordHighlightView()
@@ -187,6 +185,7 @@ class NewKeywordHighlightDialog(
} }
private fun createColorPanel(color: Color, title: String): ColorPanel { private fun createColorPanel(color: Color, title: String): ColorPanel {
val owner = this
val arc = UIManager.getInt("Component.arc") val arc = UIManager.getInt("Component.arc")
val lineBorder = FlatLineBorder(Insets(1, 1, 1, 1), DynamicColor.BorderColor, 1f, arc) val lineBorder = FlatLineBorder(Insets(1, 1, 1, 1), DynamicColor.BorderColor, 1f, arc)
val colorPanel = ColorPanel(color) val colorPanel = ColorPanel(color)
@@ -195,7 +194,8 @@ class NewKeywordHighlightDialog(
colorPanel.addMouseListener(object : MouseAdapter() { colorPanel.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { override fun mouseClicked(e: MouseEvent) {
if (SwingUtilities.isLeftMouseButton(e)) { if (SwingUtilities.isLeftMouseButton(e)) {
val dialog = ChooseColorTemplateDialog(this@NewKeywordHighlightDialog, title) val dialog = ChooseColorTemplateDialog(owner, title)
dialog.setLocationRelativeTo(owner)
dialog.defaultColor = colorPanel.color dialog.defaultColor = colorPanel.color
dialog.isVisible = true dialog.isVisible = true
colorPanel.color = dialog.color ?: return colorPanel.color = dialog.color ?: return
@@ -218,6 +218,7 @@ class NewKeywordHighlightDialog(
keyword = keywordTextField.text, keyword = keywordTextField.text,
description = descriptionTextField.text, description = descriptionTextField.text,
matchCase = matchCaseBtn.isSelected, matchCase = matchCaseBtn.isSelected,
regex = regexBtn.isSelected,
textColor = if (textColor.colorIndex != -1) textColor.colorIndex else textColor.color.toRGB(), textColor = if (textColor.colorIndex != -1) textColor.colorIndex else textColor.color.toRGB(),
backgroundColor = if (backgroundColor.colorIndex != -1) backgroundColor.colorIndex else backgroundColor.color.toRGB(), backgroundColor = if (backgroundColor.colorIndex != -1) backgroundColor.colorIndex else backgroundColor.color.toRGB(),
bold = boldCheckBox.isSelected, bold = boldCheckBox.isSelected,

View File

@@ -1,5 +1,6 @@
package app.termora.keymap package app.termora.keymap
import com.formdev.flatlaf.util.SystemInfo
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import javax.swing.KeyStroke import javax.swing.KeyStroke
@@ -23,7 +24,14 @@ class KeyShortcut(val keyStroke: KeyStroke) : Shortcut() {
text = text.replace("MINUS", "-") text = text.replace("MINUS", "-")
} }
return text.toCharArray().joinToString(" + ") text = text.toCharArray().joinToString(" + ")
if (SystemInfo.isWindows || SystemInfo.isLinux) {
text = text.replace("", "Shift")
text = text.replace("", "Ctrl")
text = text.replace("", "Alt")
}
return text
} }
} }

View File

@@ -1,7 +1,6 @@
package app.termora.keymap package app.termora.keymap
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import javax.swing.KeyStroke import javax.swing.KeyStroke
@@ -12,6 +11,10 @@ open class Keymap(
*/ */
private val parent: Keymap?, private val parent: Keymap?,
val isReadonly: Boolean = false, val isReadonly: Boolean = false,
/**
* 修改时间
*/
var updateDate: Long = 0L,
) { ) {
companion object { companion object {
@@ -23,7 +26,8 @@ open class Keymap(
val shortcuts = mutableListOf<Keymap>() val shortcuts = mutableListOf<Keymap>()
val name = json["name"]?.jsonPrimitive?.content ?: return null val name = json["name"]?.jsonPrimitive?.content ?: return null
val readonly = json["readonly"]?.jsonPrimitive?.booleanOrNull ?: return null val readonly = json["readonly"]?.jsonPrimitive?.booleanOrNull ?: return null
val keymap = Keymap(name, null, readonly) val updateDate = json["updateDate"]?.jsonPrimitive?.longOrNull ?: 0
val keymap = Keymap(name, null, readonly, updateDate)
for (shortcut in (json["shortcuts"]?.jsonArray ?: emptyList()).map { it.jsonObject }) { for (shortcut in (json["shortcuts"]?.jsonArray ?: emptyList()).map { it.jsonObject }) {
val keyStroke = shortcut["keyStroke"]?.jsonPrimitive?.contentOrNull ?: continue val keyStroke = shortcut["keyStroke"]?.jsonPrimitive?.contentOrNull ?: continue
@@ -40,6 +44,9 @@ open class Keymap(
} }
} }
// 最后设置修改时间
keymap.updateDate = updateDate
shortcuts.add(keymap) shortcuts.add(keymap)
return keymap return keymap
} }
@@ -51,6 +58,7 @@ open class Keymap(
val actionIds = shortcuts.getOrPut(shortcut) { mutableListOf() } val actionIds = shortcuts.getOrPut(shortcut) { mutableListOf() }
actionIds.removeIf { it == actionId } actionIds.removeIf { it == actionId }
actionIds.add(actionId) actionIds.add(actionId)
updateDate = System.currentTimeMillis()
} }
open fun removeAllActionShortcuts(actionId: Any) { open fun removeAllActionShortcuts(actionId: Any) {
@@ -62,6 +70,7 @@ open class Keymap(
iterator.remove() iterator.remove()
} }
} }
updateDate = System.currentTimeMillis()
} }
open fun getShortcut(actionId: Any): List<Shortcut> { open fun getShortcut(actionId: Any): List<Shortcut> {
@@ -102,6 +111,7 @@ open class Keymap(
return buildJsonObject { return buildJsonObject {
put("name", name) put("name", name)
put("readonly", isReadonly) put("readonly", isReadonly)
put("updateDate", updateDate)
parent?.let { put("parent", it.name) } parent?.let { put("parent", it.name) }
put("shortcuts", buildJsonArray { put("shortcuts", buildJsonArray {
for (entry in shortcuts.entries) { for (entry in shortcuts.entries) {

View File

@@ -645,9 +645,13 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
return return
} }
ohKeyPair = ohKeyPair.copy(
name = nameTextField.text,
remark = remarkTextField.text,
)
if (ohKeyPair.remark.isEmpty()) { if (ohKeyPair.remark.isEmpty()) {
ohKeyPair = ohKeyPair.copy( ohKeyPair = ohKeyPair.copy(
name = nameTextField.text,
remark = "Import on " + DateFormatUtils.format(Date(), I18n.getString("termora.date-format")) remark = "Import on " + DateFormatUtils.format(Date(), I18n.getString("termora.date-format"))
) )
} }

View File

@@ -39,7 +39,7 @@ class MacroDialog(owner: Window) : DialogWrapper(owner) {
initEvents() initEvents()
init() init()
setLocationRelativeTo(null) setLocationRelativeTo(owner)
} }
private fun initView() { private fun initView() {

View File

@@ -0,0 +1,9 @@
package app.termora.sftp
import org.apache.commons.vfs2.FileSystem
interface FileSystemProvider {
fun getFileSystem(): FileSystem
fun setFileSystem(fileSystem: FileSystem)
}

View File

@@ -27,7 +27,7 @@ import javax.swing.filechooser.FileSystemView
import kotlin.io.path.absolutePathString import kotlin.io.path.absolutePathString
class FileSystemViewNav( class FileSystemViewNav(
private val fileSystem: org.apache.commons.vfs2.FileSystem, private val fileSystemProvider: FileSystemProvider,
private val homeDirectory: FileObject private val homeDirectory: FileObject
) : JPanel(BorderLayout()) { ) : JPanel(BorderLayout()) {
@@ -103,7 +103,7 @@ class FileSystemViewNav(
add(layeredPane, BorderLayout.CENTER) add(layeredPane, BorderLayout.CENTER)
if (SystemInfo.isWindows && fileSystem is LocalFileSystem) { if (SystemInfo.isWindows && fileSystemProvider.getFileSystem() is LocalFileSystem) {
try { try {
for (root in fileSystemView.roots) { for (root in fileSystemView.roots) {
history.add(root.absolutePath) history.add(root.absolutePath)
@@ -174,9 +174,14 @@ class FileSystemViewNav(
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(e: ActionEvent) {
val name = textField.text.trim() val name = textField.text.trim()
if (name.isBlank()) return if (name.isBlank()) return
val fileSystem = fileSystemProvider.getFileSystem()
try { try {
if (fileSystem is LocalFileSystem && SystemUtils.IS_OS_WINDOWS) { if (fileSystem is LocalFileSystem && SystemUtils.IS_OS_WINDOWS) {
changeSelectedPath(fileSystem.resolveFile("file://${name}")) val file = VFS.getManager().resolveFile("file://${name}")
if (!StringUtils.equals(file.fileSystem.rootURI, fileSystemProvider.getFileSystem().rootURI)) {
fileSystemProvider.setFileSystem(file.fileSystem)
}
changeSelectedPath(file)
} else { } else {
changeSelectedPath(fileSystem.resolveFile(name)) changeSelectedPath(fileSystem.resolveFile(name))
} }
@@ -192,6 +197,7 @@ class FileSystemViewNav(
private fun showComboBoxPopup() { private fun showComboBoxPopup() {
comboBox.removeAllItems() comboBox.removeAllItems()
val fileSystem = fileSystemProvider.getFileSystem()
for (text in history) { for (text in history) {
val path = if (SystemInfo.isWindows && fileSystem is LocalFileSystem) { val path = if (SystemInfo.isWindows && fileSystem is LocalFileSystem) {
@@ -244,6 +250,13 @@ class FileSystemViewNav(
textField.text = formatDisplayPath(file) textField.text = formatDisplayPath(file)
textField.putClientProperty(PATH, file) textField.putClientProperty(PATH, file)
val fileSystem = fileSystemProvider.getFileSystem()
if (SystemInfo.isWindows && fileSystem is LocalFileSystem) {
if (!StringUtils.equals(fileSystem.rootURI, file.fileSystem.rootURI)) {
fileSystemProvider.setFileSystem(file.fileSystem)
}
}
for (listener in listenerList.getListeners(ActionListener::class.java)) { for (listener in listenerList.getListeners(ActionListener::class.java)) {
listener.actionPerformed(ActionEvent(this, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY)) listener.actionPerformed(ActionEvent(this, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
} }

View File

@@ -5,14 +5,19 @@ import app.termora.actions.DataProvider
import app.termora.terminal.DataKey import app.termora.terminal.DataKey
import app.termora.vfs2.sftp.MySftpFileSystem import app.termora.vfs2.sftp.MySftpFileSystem
import com.formdev.flatlaf.extras.components.FlatToolBar import com.formdev.flatlaf.extras.components.FlatToolBar
import com.formdev.flatlaf.util.SystemInfo
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.SystemUtils import org.apache.commons.lang3.SystemUtils
import org.apache.commons.lang3.exception.ExceptionUtils import org.apache.commons.lang3.exception.ExceptionUtils
import org.apache.commons.vfs2.FileObject import org.apache.commons.vfs2.FileObject
import org.apache.commons.vfs2.FileSystem
import org.apache.commons.vfs2.VFS
import org.apache.commons.vfs2.provider.local.LocalFileSystem
import org.jdesktop.swingx.JXBusyLabel import org.jdesktop.swingx.JXBusyLabel
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.event.* import java.awt.event.*
@@ -22,14 +27,14 @@ import javax.swing.*
class FileSystemViewPanel( class FileSystemViewPanel(
val host: Host, val host: Host,
val fileSystem: org.apache.commons.vfs2.FileSystem, private var fileSystem: FileSystem,
private val transportManager: TransportManager, private val transportManager: TransportManager,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
) : JPanel(BorderLayout()), Disposable, DataProvider { ) : JPanel(BorderLayout()), Disposable, DataProvider, FileSystemProvider {
private val properties get() = Database.getDatabase().properties private val properties get() = Database.getDatabase().properties
private val sftp get() = Database.getDatabase().sftp private val sftp get() = Database.getDatabase().sftp
private val table = FileSystemViewTable(fileSystem, transportManager, coroutineScope) private val table = FileSystemViewTable(this, transportManager, coroutineScope)
private val disposed = AtomicBoolean(false) private val disposed = AtomicBoolean(false)
private var nextReloadTicks = emptyArray<Consumer<Unit>>() private var nextReloadTicks = emptyArray<Consumer<Unit>>()
private val isLoading = AtomicBoolean(false) private val isLoading = AtomicBoolean(false)
@@ -37,7 +42,7 @@ class FileSystemViewPanel(
private val loadingPanel = LoadingPanel() private val loadingPanel = LoadingPanel()
private val layeredPane = LayeredPane() private val layeredPane = LayeredPane()
private val homeDirectory = getHomeDirectory() private val homeDirectory = getHomeDirectory()
private val nav = FileSystemViewNav(fileSystem, homeDirectory) private val nav = FileSystemViewNav(this, homeDirectory)
private var workdir = homeDirectory private var workdir = homeDirectory
private val model get() = table.model as FileSystemViewTableModel private val model get() = table.model as FileSystemViewTableModel
private val showHiddenFilesKey = "termora.transport.host.${host.id}.show-hidden-files" private val showHiddenFilesKey = "termora.transport.host.${host.id}.show-hidden-files"
@@ -173,7 +178,15 @@ class FileSystemViewPanel(
} }
bookmarkBtn.isBookmark = !bookmarkBtn.isBookmark bookmarkBtn.isBookmark = !bookmarkBtn.isBookmark
} else { } else {
changeWorkdir(fileSystem.resolveFile(e.actionCommand)) if (fileSystem is LocalFileSystem && SystemUtils.IS_OS_WINDOWS) {
val file = VFS.getManager().resolveFile("file://${e.actionCommand}")
if (!StringUtils.equals(file.fileSystem.rootURI, fileSystem.rootURI)) {
fileSystem = file.fileSystem
}
changeWorkdir(file)
} else {
changeWorkdir(fileSystem.resolveFile(e.actionCommand))
}
} }
} }
@@ -192,8 +205,7 @@ class FileSystemViewPanel(
button.addActionListener(object : AbstractAction() { button.addActionListener(object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(e: ActionEvent) {
if (model.rowCount < 1) return if (model.rowCount < 1) return
if (model.hasParent) return if (model.hasParent) enterTableSelectionFolder(0)
enterTableSelectionFolder(0)
} }
}) })
@@ -373,6 +385,7 @@ class FileSystemViewPanel(
} }
private fun getHomeDirectory(): FileObject { private fun getHomeDirectory(): FileObject {
val fileSystem = this.fileSystem
if (fileSystem is MySftpFileSystem) { if (fileSystem is MySftpFileSystem) {
val host = fileSystem.getClientSession().getAttribute(SshClients.HOST_KEY) val host = fileSystem.getClientSession().getAttribute(SshClients.HOST_KEY)
?: return fileSystem.resolveFile(fileSystem.getDefaultDir()) ?: return fileSystem.resolveFile(fileSystem.getDefaultDir())
@@ -384,8 +397,13 @@ class FileSystemViewPanel(
} }
if (sftp.defaultDirectory.isNotBlank()) { if (sftp.defaultDirectory.isNotBlank()) {
val resolveFile = fileSystem.resolveFile("file://${sftp.defaultDirectory}") val resolveFile = if (fileSystem is LocalFileSystem && SystemInfo.isWindows) {
VFS.getManager().resolveFile("file://${sftp.defaultDirectory}")
} else {
fileSystem.resolveFile("file://${sftp.defaultDirectory}")
}
if (resolveFile.exists()) { if (resolveFile.exists()) {
setFileSystem(resolveFile.fileSystem)
return resolveFile return resolveFile
} }
} }
@@ -430,6 +448,14 @@ class FileSystemViewPanel(
return if (dataKey == SFTPDataProviders.FileSystemViewTable) table as T else null return if (dataKey == SFTPDataProviders.FileSystemViewTable) table as T else null
} }
override fun getFileSystem(): FileSystem {
return fileSystem
}
override fun setFileSystem(fileSystem: FileSystem) {
this.fileSystem = fileSystem
}
private class LoadingPanel : JPanel() { private class LoadingPanel : JPanel() {
private val busyLabel = JXBusyLabel() private val busyLabel = JXBusyLabel()

View File

@@ -4,6 +4,7 @@ import app.termora.*
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
import app.termora.actions.SettingsAction import app.termora.actions.SettingsAction
import app.termora.sftp.FileSystemViewTable.AskTransfer.Action import app.termora.sftp.FileSystemViewTable.AskTransfer.Action
import app.termora.vfs2.VFSWalker
import app.termora.vfs2.sftp.MySftpFileObject import app.termora.vfs2.sftp.MySftpFileObject
import app.termora.vfs2.sftp.MySftpFileSystem import app.termora.vfs2.sftp.MySftpFileSystem
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
@@ -37,7 +38,6 @@ import java.nio.file.FileVisitor
import java.nio.file.Paths import java.nio.file.Paths
import java.nio.file.StandardOpenOption import java.nio.file.StandardOpenOption
import java.nio.file.attribute.BasicFileAttributes import java.nio.file.attribute.BasicFileAttributes
import java.nio.file.attribute.FileTime
import java.text.MessageFormat import java.text.MessageFormat
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@@ -45,6 +45,21 @@ import java.util.regex.Pattern
import javax.swing.* import javax.swing.*
import javax.swing.table.DefaultTableCellRenderer import javax.swing.table.DefaultTableCellRenderer
import kotlin.collections.ArrayDeque import kotlin.collections.ArrayDeque
import kotlin.collections.List
import kotlin.collections.all
import kotlin.collections.contains
import kotlin.collections.filter
import kotlin.collections.filterIsInstance
import kotlin.collections.find
import kotlin.collections.forEach
import kotlin.collections.isEmpty
import kotlin.collections.isNotEmpty
import kotlin.collections.last
import kotlin.collections.listOf
import kotlin.collections.map
import kotlin.collections.mapOf
import kotlin.collections.mutableListOf
import kotlin.collections.sortedArray
import kotlin.io.path.absolutePathString import kotlin.io.path.absolutePathString
import kotlin.math.max import kotlin.math.max
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@@ -52,7 +67,7 @@ import kotlin.time.Duration.Companion.milliseconds
@Suppress("DuplicatedCode", "CascadeIf") @Suppress("DuplicatedCode", "CascadeIf")
class FileSystemViewTable( class FileSystemViewTable(
private val fileSystem: org.apache.commons.vfs2.FileSystem, private val fileSystemProvider: FileSystemProvider,
private val transportManager: TransportManager, private val transportManager: TransportManager,
private val coroutineScope: CoroutineScope private val coroutineScope: CoroutineScope
) : JTable(), Disposable { ) : JTable(), Disposable {
@@ -184,7 +199,7 @@ class FileSystemViewTable(
val data = support.transferable.getTransferData(FileSystemTableRowTransferable.dataFlavor) val data = support.transferable.getTransferData(FileSystemTableRowTransferable.dataFlavor)
return data is FileSystemTableRowTransferable && data.source != table return data is FileSystemTableRowTransferable && data.source != table
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { } else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
return fileSystem !is LocalFileSystem return fileSystemProvider.getFileSystem() !is LocalFileSystem
} }
return false return false
@@ -218,7 +233,7 @@ class FileSystemViewTable(
val localTarget = sftpPanel.getLocalTarget() val localTarget = sftpPanel.getLocalTarget()
val table = localTarget.getData(SFTPDataProviders.FileSystemViewTable) ?: return false val table = localTarget.getData(SFTPDataProviders.FileSystemViewTable) ?: return false
// 委托最左侧的本地文件系统传输 // 委托最左侧的本地文件系统传输
table.transfer(paths, true, targetWorkdir) table.transfer(paths, true, targetWorkdir, fileSystemViewPanel)
return true return true
} }
return false return false
@@ -261,6 +276,7 @@ class FileSystemViewTable(
private fun showContextMenu(rows: IntArray, e: MouseEvent) { private fun showContextMenu(rows: IntArray, e: MouseEvent) {
val files = rows.map { model.getFileObject(it) } val files = rows.map { model.getFileObject(it) }
val hasParent = rows.contains(0) val hasParent = rows.contains(0)
val fileSystem = fileSystemProvider.getFileSystem()
val popupMenu = FlatPopupMenu() val popupMenu = FlatPopupMenu()
val newMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.new")) val newMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.new"))
@@ -359,34 +375,7 @@ class FileSystemViewTable(
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(e: ActionEvent) {
val last = files.last() val last = files.last()
if (last !is MySftpFileObject) return if (last !is MySftpFileObject) return
changePermission(last)
val dialog = PosixFilePermissionDialog(
SwingUtilities.getWindowAncestor(table),
model.getFilePermissions(last)
)
val permissions = dialog.open() ?: return
if (fileSystemViewPanel.requestLoading()) {
coroutineScope.launch(Dispatchers.IO) {
val c = runCatching { last.setPosixFilePermissions(permissions) }.onFailure {
withContext(Dispatchers.Swing) {
OptionPane.showMessageDialog(
owner,
ExceptionUtils.getMessage(it),
messageType = JOptionPane.ERROR_MESSAGE
)
}
}
// stop loading
fileSystemViewPanel.stopLoading()
// reload
if (c.isSuccess) {
fileSystemViewPanel.reload(true)
}
}
}
} }
}) })
refresh.addActionListener { fileSystemViewPanel.reload() } refresh.addActionListener { fileSystemViewPanel.reload() }
@@ -408,6 +397,80 @@ class FileSystemViewTable(
popupMenu.show(table, e.x, e.y) popupMenu.show(table, e.x, e.y)
} }
private fun changePermission(file: MySftpFileObject) {
val dialog = PosixFilePermissionDialog(
SwingUtilities.getWindowAncestor(table),
model.getFilePermissions(file)
)
val permissions = dialog.open() ?: return
val isIncludeSubdirectories = dialog.isIncludeSubdirectories()
if (fileSystemViewPanel.requestLoading()) {
coroutineScope.launch(Dispatchers.IO) {
val c = runCatching {
file.setPosixFilePermissions(permissions)
if (isIncludeSubdirectories && file.isFolder) {
file.refresh()
VFSWalker.walk(file, object : FileVisitor<FileObject> {
override fun preVisitDirectory(
dir: FileObject,
attrs: BasicFileAttributes
): FileVisitResult {
dir.refresh()
if (dir is MySftpFileObject) {
dir.setPosixFilePermissions(permissions)
}
return FileVisitResult.CONTINUE
}
override fun visitFile(
file: FileObject,
attrs: BasicFileAttributes
): FileVisitResult {
if (file is MySftpFileObject) {
file.setPosixFilePermissions(permissions)
}
return FileVisitResult.CONTINUE
}
override fun visitFileFailed(
file: FileObject,
exc: IOException
): FileVisitResult {
return FileVisitResult.TERMINATE
}
override fun postVisitDirectory(
dir: FileObject,
exc: IOException?
): FileVisitResult {
return FileVisitResult.CONTINUE
}
})
}
}.onFailure {
withContext(Dispatchers.Swing) {
OptionPane.showMessageDialog(
owner,
ExceptionUtils.getMessage(it),
messageType = JOptionPane.ERROR_MESSAGE
)
}
}
// stop loading
fileSystemViewPanel.stopLoading()
// reload
if (c.isSuccess) {
fileSystemViewPanel.reload(true)
}
}
}
}
private fun renameSelection() { private fun renameSelection() {
val index = selectedRow val index = selectedRow
if (index < 0) return if (index < 0) return
@@ -571,7 +634,7 @@ class FileSystemViewTable(
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
runCatching { runCatching {
if (fileSystem is MySftpFileSystem) { if (fileSystemProvider.getFileSystem() is MySftpFileSystem) {
deleteSftpPaths(paths, rm) deleteSftpPaths(paths, rm)
} else { } else {
deleteRecursively(paths) deleteRecursively(paths)
@@ -594,7 +657,7 @@ class FileSystemViewTable(
private fun deleteSftpPaths(files: List<FileObject>, rm: Boolean = false) { private fun deleteSftpPaths(files: List<FileObject>, rm: Boolean = false) {
if (rm) { if (rm) {
val session = (this.fileSystem as MySftpFileSystem).getClientSession() val session = (this.fileSystemProvider.getFileSystem() as MySftpFileSystem).getClientSession()
for (path in files) { for (path in files) {
session.executeRemoteCommand( session.executeRemoteCommand(
"rm -rf '${path.absolutePathString()}'", "rm -rf '${path.absolutePathString()}'",
@@ -618,12 +681,13 @@ class FileSystemViewTable(
private fun transfer( private fun transfer(
files: List<FileObject>, files: List<FileObject>,
fromLocalSystem: Boolean = false, fromLocalSystem: Boolean = false,
targetWorkdir: FileObject? = null targetWorkdir: FileObject? = null,
target: FileSystemViewPanel? = null,
) { ) {
assertEventDispatchThread() assertEventDispatchThread()
val target = sftpPanel.getTarget(table) ?: return val target = (target ?: sftpPanel.getTarget(table)) ?: return
val table = target.getData(SFTPDataProviders.FileSystemViewTable) ?: return val table = target.getData(SFTPDataProviders.FileSystemViewTable) ?: return
var isApplyAll = false var isApplyAll = false
var lastAction = Action.Overwrite var lastAction = Action.Overwrite
@@ -649,7 +713,7 @@ class FileSystemViewTable(
coroutineScope.launch { coroutineScope.launch {
try { try {
doTransfer(file, lastAction, fromLocalSystem, targetWorkdir) doTransfer(file, lastAction, fromLocalSystem, targetWorkdir, target)
} catch (e: Exception) { } catch (e: Exception) {
if (log.isErrorEnabled) { if (log.isErrorEnabled) {
log.error(e.message, e) log.error(e.message, e)
@@ -800,10 +864,11 @@ class FileSystemViewTable(
file: FileObject, file: FileObject,
action: Action, action: Action,
fromLocalSystem: Boolean, fromLocalSystem: Boolean,
targetWorkdir: FileObject? targetWorkdir: FileObject?,
target: FileSystemViewPanel? = null
) { ) {
val sftpPanel = this.sftpPanel val sftpPanel = this.sftpPanel
val target = sftpPanel.getTarget(table) ?: return val target = (target ?: sftpPanel.getTarget(table)) ?: return
/** /**
* 定义一个添加器,它可以自动的判断导入/拖拽行为 * 定义一个添加器,它可以自动的判断导入/拖拽行为
@@ -885,36 +950,7 @@ class FileSystemViewTable(
dir: FileObject, dir: FileObject,
visitor: FileVisitor<FileObject>, visitor: FileVisitor<FileObject>,
): FileVisitResult { ): FileVisitResult {
return VFSWalker.walk(dir, visitor)
// clear cache
if (visitor.preVisitDirectory(dir, EmptyBasicFileAttributes.INSTANCE) == FileVisitResult.TERMINATE) {
return FileVisitResult.TERMINATE
}
for (e in dir.children) {
if (e.name.baseName == ".." || e.name.baseName == ".") continue
if (e.isFolder) {
if (walk(dir.resolveFile(e.name.baseName), visitor) == FileVisitResult.TERMINATE) {
return FileVisitResult.TERMINATE
}
} else {
val result = visitor.visitFile(
dir.resolveFile(e.name.baseName),
EmptyBasicFileAttributes.INSTANCE
)
if (result == FileVisitResult.TERMINATE) {
return FileVisitResult.TERMINATE
} else if (result == FileVisitResult.SKIP_SUBTREE) {
break
}
}
}
if (visitor.postVisitDirectory(dir, null) == FileVisitResult.TERMINATE) {
return FileVisitResult.TERMINATE
}
return FileVisitResult.CONTINUE
} }
private fun addTransport( private fun addTransport(
@@ -973,47 +1009,5 @@ class FileSystemViewTable(
} }
private class EmptyBasicFileAttributes : BasicFileAttributes {
companion object {
val INSTANCE = EmptyBasicFileAttributes()
}
override fun lastModifiedTime(): FileTime {
TODO("Not yet implemented")
}
override fun lastAccessTime(): FileTime {
TODO("Not yet implemented")
}
override fun creationTime(): FileTime {
TODO("Not yet implemented")
}
override fun isRegularFile(): Boolean {
TODO("Not yet implemented")
}
override fun isDirectory(): Boolean {
TODO("Not yet implemented")
}
override fun isSymbolicLink(): Boolean {
TODO("Not yet implemented")
}
override fun isOther(): Boolean {
TODO("Not yet implemented")
}
override fun size(): Long {
TODO("Not yet implemented")
}
override fun fileKey(): Any {
TODO("Not yet implemented")
}
}
} }

View File

@@ -157,7 +157,11 @@ class FileSystemViewTableModel : DefaultTableModel() {
fun getPathNames(): Set<String> { fun getPathNames(): Set<String> {
val names = linkedSetOf<String>() val names = linkedSetOf<String>()
for (i in 0 until rowCount) { for (i in 0 until rowCount) {
names.add(getFileObject(i).name.baseName) if (hasParent && i == 0) {
names.add("..")
} else {
names.add(getFileObject(i).name.baseName)
}
} }
return names return names
} }

View File

@@ -25,6 +25,7 @@ class PosixFilePermissionDialog(
private val otherRead = JCheckBox(I18n.getString("termora.transport.permissions.read")) private val otherRead = JCheckBox(I18n.getString("termora.transport.permissions.read"))
private val otherWrite = JCheckBox(I18n.getString("termora.transport.permissions.write")) private val otherWrite = JCheckBox(I18n.getString("termora.transport.permissions.write"))
private val otherExecute = JCheckBox(I18n.getString("termora.transport.permissions.execute")) private val otherExecute = JCheckBox(I18n.getString("termora.transport.permissions.execute"))
private val includeSubFolder = JCheckBox(I18n.getString("termora.transport.permissions.include-subfolder"))
private var isCancelled = false private var isCancelled = false
@@ -60,13 +61,14 @@ class PosixFilePermissionDialog(
otherRead.isFocusable = false otherRead.isFocusable = false
otherWrite.isFocusable = false otherWrite.isFocusable = false
otherExecute.isFocusable = false otherExecute.isFocusable = false
includeSubFolder.isFocusable = false
} }
override fun createCenterPanel(): JComponent { override fun createCenterPanel(): JComponent {
val formMargin = "7dlu" val formMargin = "7dlu"
val layout = FormLayout( val layout = FormLayout(
"default:grow, $formMargin, default:grow, $formMargin, default:grow", "default:grow, $formMargin, default:grow, $formMargin, default:grow",
"pref, $formMargin, pref, $formMargin, pref" "pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
) )
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin") val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
@@ -95,6 +97,8 @@ class PosixFilePermissionDialog(
otherBox.border = BorderFactory.createTitledBorder(I18n.getString("termora.transport.permissions.others")) otherBox.border = BorderFactory.createTitledBorder(I18n.getString("termora.transport.permissions.others"))
builder.add(otherBox).xy(5, 3) builder.add(otherBox).xy(5, 3)
builder.add(includeSubFolder).xyw(1, 5, 5)
return builder.build() return builder.build()
} }
@@ -103,6 +107,10 @@ class PosixFilePermissionDialog(
super.doCancelAction() super.doCancelAction()
} }
fun isIncludeSubdirectories(): Boolean {
return includeSubFolder.isSelected
}
/** /**
* @return 返回空表示取消了 * @return 返回空表示取消了
*/ */

View File

@@ -56,6 +56,16 @@ class SFTPAction : AnAction("SFTP", Icons.folder) {
} }
val host = hostManager.getHost(hostId) ?: return val host = hostManager.getHost(hostId) ?: return
for (i in 0 until tabbed.tabCount) {
val c = tabbed.getComponentAt(i)
if (c is SFTPFileSystemViewPanel) {
if (c.state == SFTPFileSystemViewPanel.State.Initialized) {
c.selectHost(host)
return
}
}
}
tabbed.addSFTPFileSystemViewPanelTab(host) tabbed.addSFTPFileSystemViewPanelTab(host)
} }

View File

@@ -27,6 +27,8 @@ import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.* import javax.swing.*
import javax.swing.event.TreeExpansionEvent
import javax.swing.event.TreeExpansionListener
class SFTPFileSystemViewPanel( class SFTPFileSystemViewPanel(
var host: Host? = null, var host: Host? = null,
@@ -35,17 +37,18 @@ class SFTPFileSystemViewPanel(
companion object { companion object {
private val log = LoggerFactory.getLogger(SFTPFileSystemViewPanel::class.java) private val log = LoggerFactory.getLogger(SFTPFileSystemViewPanel::class.java)
}
private enum class State { enum class State {
Initialized, Initialized,
Connecting, Connecting,
Connected, Connected,
ConnectFailed, ConnectFailed,
}
} }
@Volatile @Volatile
private var state = State.Initialized var state = State.Initialized
private set
private val cardLayout = CardLayout() private val cardLayout = CardLayout()
private val cardPanel = JPanel(cardLayout) private val cardPanel = JPanel(cardLayout)
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@@ -283,12 +286,20 @@ class SFTPFileSystemViewPanel(
val node = tree.getLastSelectedPathNode() ?: return val node = tree.getLastSelectedPathNode() ?: return
if (node.isFolder) return if (node.isFolder) return
val host = node.data as Host val host = node.data as Host
that.setTabTitle(host.name) selectHost(host)
that.host = host
that.connect()
} }
} }
}) })
tree.addTreeExpansionListener(object : TreeExpansionListener {
override fun treeExpanded(event: TreeExpansionEvent) {
properties.putString("SFTPTabbed.Tree.state", TreeUtils.saveExpansionState(tree))
}
override fun treeCollapsed(event: TreeExpansionEvent) {
properties.putString("SFTPTabbed.Tree.state", TreeUtils.saveExpansionState(tree))
}
})
} }
override fun dispose() { override fun dispose() {
@@ -305,6 +316,12 @@ class SFTPFileSystemViewPanel(
} }
} }
fun selectHost(host: Host) {
that.setTabTitle(host.name)
that.host = host
that.connect()
}
private fun setTabTitle(title: String) { private fun setTabTitle(title: String) {
val tabbed = SwingUtilities.getAncestorOfClass(JTabbedPane::class.java, that) val tabbed = SwingUtilities.getAncestorOfClass(JTabbedPane::class.java, that)
if (tabbed is JTabbedPane) { if (tabbed is JTabbedPane) {

View File

@@ -127,7 +127,7 @@ class SFTPPanel : JPanel(BorderLayout()), DataProvider, Disposable {
return return
} }
val fs = c.fileSystem val fs = c.getFileSystem()
val root = transportManager.root val root = transportManager.root
transportManager.lock.withLock { transportManager.lock.withLock {

View File

@@ -5,7 +5,6 @@ import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.extras.components.FlatTabbedPane import com.formdev.flatlaf.extras.components.FlatTabbedPane
import java.awt.Point
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@@ -13,7 +12,6 @@ import javax.swing.JButton
import javax.swing.JToolBar import javax.swing.JToolBar
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
import javax.swing.UIManager import javax.swing.UIManager
import kotlin.math.max
@Suppress("DuplicatedCode") @Suppress("DuplicatedCode")
class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPane(), Disposable { class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPane(), Disposable {
@@ -43,23 +41,20 @@ class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPan
private fun initEvents() { private fun initEvents() {
addBtn.addActionListener(object : AnAction() { addBtn.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
val dialog = NewHostTreeDialog(SwingUtilities.getWindowAncestor(tabbed)) for (i in 0 until tabCount) {
dialog.location = Point( val c = getComponentAt(i)
max(0, addBtn.locationOnScreen.x - dialog.width / 2 + addBtn.width / 2), if (c !is SFTPFileSystemViewPanel) continue
addBtn.locationOnScreen.y + max(tabHeight, addBtn.height) if (c.state != SFTPFileSystemViewPanel.State.Initialized) continue
) selectedIndex = i
dialog.setFilter { it.host.protocol == Protocol.SSH } return
dialog.setTreeName("SFTPTabbed.Tree")
dialog.allowMulti = true
dialog.isVisible = true
val hosts = dialog.hosts
if (hosts.isEmpty()) return
for (host in hosts) {
addSFTPFileSystemViewPanelTab(host)
} }
// 添加一个新的
addTab(
I18n.getString("termora.transport.sftp.select-host"),
SFTPFileSystemViewPanel(transportManager = transportManager)
)
selectedIndex = tabCount - 1
} }
}) })

View File

@@ -6,7 +6,10 @@ import app.termora.Icons
import app.termora.actions.AnAction import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
import app.termora.terminal.ControlCharacters import app.termora.terminal.ControlCharacters
import app.termora.terminal.Null
import app.termora.terminal.panel.TerminalWriter import app.termora.terminal.panel.TerminalWriter
import org.apache.commons.lang3.StringUtils
import org.apache.commons.text.StringEscapeUtils
class SnippetAction private constructor() : AnAction(I18n.getString("termora.snippet.title"), Icons.codeSpan) { class SnippetAction private constructor() : AnAction(I18n.getString("termora.snippet.title"), Icons.codeSpan) {
companion object { companion object {
@@ -15,6 +18,16 @@ class SnippetAction private constructor() : AnAction(I18n.getString("termora.sni
} }
const val SNIPPET = "SnippetAction" const val SNIPPET = "SnippetAction"
// \r \n \t \a \e \b
private val SpecialChars = mutableMapOf(
'r' to '\r',
'n' to '\n',
't' to '\t',
'a' to ControlCharacters.BEL,
'e' to ControlCharacters.ESC,
'b' to ControlCharacters.BS
)
} }
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
@@ -24,19 +37,39 @@ class SnippetAction private constructor() : AnAction(I18n.getString("termora.sni
fun runSnippet(snippet: Snippet, writer: TerminalWriter) { fun runSnippet(snippet: Snippet, writer: TerminalWriter) {
if (snippet.type != SnippetType.Snippet) return if (snippet.type != SnippetType.Snippet) return
val map = mapOf( writer.write(TerminalWriter.WriteRequest.fromBytes(unescape(snippet.snippet).toByteArray(writer.getCharset())))
"\\r" to ControlCharacters.CR, }
"\\n" to ControlCharacters.LF,
"\\t" to ControlCharacters.TAB,
"\\a" to ControlCharacters.BEL,
"\\e" to ControlCharacters.ESC,
"\\b" to ControlCharacters.BS,
)
var text = snippet.snippet private fun unescape(text: String): String {
for (e in map.entries) { val chars = text.toCharArray()
text = text.replace(e.key, e.value.toString()) val sb = StringBuilder()
for (i in chars.indices) {
val c = chars[i]
// 不是特殊字符不处理
if (SpecialChars.containsKey(c).not()) {
sb.append(c)
continue
}
// 特殊字符前面不是 `\` 不处理
if (chars.getOrNull(i - 1) != '\\') {
sb.append(c)
continue
}
// 如果构成的字符串是:\\r 就会生成 \r 字符串并非转译成CR
if (chars.getOrNull(i - 2) == '\\') {
sb.deleteCharAt(sb.length - 1)
sb.append(c)
continue
}
// 命中条件之后,那么 sb 最后一个字符肯定是 \
sb.deleteCharAt(sb.length - 1)
sb.append(SpecialChars.getValue(c))
} }
writer.write(TerminalWriter.WriteRequest.fromBytes(text.toByteArray(writer.getCharset())))
return sb.toString()
} }
} }

View File

@@ -41,8 +41,10 @@ class SnippetPanel : JPanel(BorderLayout()), Disposable {
private fun initViews() { private fun initViews() {
val splitPane = JSplitPane() val splitPane = JSplitPane()
splitPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor) splitPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
val scrollPane = JScrollPane(snippetTree)
scrollPane.border = BorderFactory.createEmptyBorder()
leftPanel.add(snippetTree, BorderLayout.CENTER) leftPanel.add(scrollPane, BorderLayout.CENTER)
leftPanel.border = BorderFactory.createCompoundBorder( leftPanel.border = BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor), BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor),
BorderFactory.createEmptyBorder(4, 4, 4, 4) BorderFactory.createEmptyBorder(4, 4, 4, 4)
@@ -51,6 +53,7 @@ class SnippetPanel : JPanel(BorderLayout()), Disposable {
properties.getString("SnippetPanel.LeftPanel.width", "180").toIntOrNull() ?: 180, properties.getString("SnippetPanel.LeftPanel.width", "180").toIntOrNull() ?: 180,
-1 -1
) )
leftPanel.minimumSize = Dimension(leftPanel.preferredSize.width, leftPanel.preferredSize.height)
rightPanel.border = BorderFactory.createCompoundBorder( rightPanel.border = BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 1, 0, 0, DynamicColor.BorderColor), BorderFactory.createMatteBorder(0, 1, 0, 0, DynamicColor.BorderColor),

View File

@@ -1,5 +1,6 @@
package app.termora.snippet package app.termora.snippet
import app.termora.I18n
import app.termora.SimpleTreeModel import app.termora.SimpleTreeModel
import javax.swing.tree.MutableTreeNode import javax.swing.tree.MutableTreeNode
import javax.swing.tree.TreeNode import javax.swing.tree.TreeNode
@@ -8,7 +9,7 @@ class SnippetTreeModel : SimpleTreeModel<Snippet>(
SnippetTreeNode( SnippetTreeNode(
Snippet( Snippet(
id = "0", id = "0",
name = "全部片段", name = I18n.getString("termora.snippet.title"),
type = SnippetType.Folder type = SnippetType.Folder
) )
) )

View File

@@ -390,7 +390,15 @@ abstract class SafetySyncer : Syncer {
protected fun decodeKeymaps(text: String, deletedData: List<DeletedData>, config: SyncConfig) { protected fun decodeKeymaps(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
for (keymap in ohMyJson.decodeFromString<List<JsonObject>>(text).mapNotNull { Keymap.fromJSON(it) }) { val localKeymaps = keymapManager.getKeymaps().associateBy { it.name }
val remoteKeymaps = ohMyJson.decodeFromString<List<JsonObject>>(text).mapNotNull { Keymap.fromJSON(it) }
for (keymap in remoteKeymaps) {
val localKeymap = localKeymaps[keymap.name]
if (localKeymap != null) {
if (localKeymap.updateDate > keymap.updateDate) {
continue
}
}
keymapManager.addKeymap(keymap) keymapManager.addKeymap(keymap)
} }

View File

@@ -67,6 +67,8 @@ class SyncManager private constructor() : Disposable {
sync(config) sync(config)
sync.lastSyncTime = System.currentTimeMillis()
if (log.isInfoEnabled) { if (log.isInfoEnabled) {
log.info("Automatic synchronisation end") log.info("Automatic synchronisation end")
} }

View File

@@ -360,8 +360,9 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
} }
} }
// TODO Send Device Attributes (Primary DA). // Send Device Attributes (Primary DA).
'c' -> { 'c' -> {
sendDeviceAttributes()
} }
// CSI Ps M Delete Ps Line(s) (default = 1) (DL). // CSI Ps M Delete Ps Line(s) (default = 1) (DL).
@@ -399,6 +400,16 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
} }
} }
// CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).
'Z' -> {
val count = args.toInt(1)
val cursorModel = terminal.getCursorModel()
for (i in 0 until count) {
val x = terminal.getTabulator().previousTab(cursorModel.getPosition().x - 1) + 1
terminal.getCursorModel().move(cursorModel.getPosition().y, x)
}
}
// split // split
';' -> { ';' -> {
args.append(ch) args.append(ch)
@@ -495,6 +506,22 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
} }
private fun sendDeviceAttributes() {
assertEventDispatchThread()
if (!terminalModel.hasData(DataKey.TerminalWriter)) {
return
}
val writer = terminalModel.getData(DataKey.TerminalWriter)
// VT102_RESPONSE
val bytes = "${ControlCharacters.ESC}[?6c".toByteArray(writer.getCharset())
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
}
/** /**
* https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-?-Pm-h.1D0E * https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-?-Pm-h.1D0E
*/ */

View File

@@ -6,7 +6,7 @@ import java.io.InputStreamReader
import java.nio.charset.Charset import java.nio.charset.Charset
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
class PtyProcessConnector(private val process: PtyProcess, private val charset: Charset = StandardCharsets.UTF_8) : class PtyProcessConnector(val process: PtyProcess, private val charset: Charset = StandardCharsets.UTF_8) :
StreamPtyConnector(process.inputStream, process.outputStream) { StreamPtyConnector(process.inputStream, process.outputStream) {
private val reader = InputStreamReader(input) private val reader = InputStreamReader(input)

View File

@@ -21,6 +21,16 @@ interface SelectionModel {
*/ */
fun setSelection(startPosition: Position, endPosition: Position) fun setSelection(startPosition: Position, endPosition: Position)
/**
* 设置块选中模式
*/
fun setBlockSelection(block: Boolean)
/**
* 是否是块选中模式
*/
fun isBlockSelection(): Boolean
/** /**
* 获取开始选中的位置 * 获取开始选中的位置
*/ */

View File

@@ -7,6 +7,7 @@ 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 val document = terminal.getDocument() private val document = terminal.getDocument()
internal companion object { internal companion object {
@@ -67,29 +68,60 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
return sb.toString() return sb.toString()
} }
val iterator = getChars(getSelectionStartPosition(), getSelectionEndPosition()) val start = getSelectionStartPosition()
while (iterator.hasNext()) { val end = getSelectionEndPosition()
val line = iterator.next()
val chars = line.chars()
if (chars.isEmpty() || chars.first().first.isNull) {
continue
}
for (e in chars) { if (isBlockSelection()) {
if (e.first.isSoftHyphen) { val left = min(start.x, end.x)
continue val right = max(start.x, end.x)
} else if (e.first.isNull) { val top = min(start.y, end.y)
break val bottom = max(start.y, end.y)
for (lineNum in top..bottom) {
val line = document.getLine(lineNum)
val chars = line.chars()
// 块选中要处理超出边界
val from = (left - 1).coerceAtLeast(0)
val to = right.coerceAtMost(chars.size)
if (from < to) {
val selected = chars.subList(from, to)
.filter { !it.first.isNull && !it.first.isSoftHyphen }
.joinToString("") { it.first.toString() }
sb.append(selected)
}
if (lineNum != bottom) {
sb.appendLine()
} }
sb.append(e.first)
} }
if (line.wrapped) { } else {
continue val iterator = getChars(start, end)
} while (iterator.hasNext()) {
val line = iterator.next()
val chars = line.chars()
if (chars.isEmpty() || chars.first().first.isNull) {
continue
}
if (iterator.hasNext()) { for (e in chars) {
sb.appendLine() if (e.first.isSoftHyphen) {
continue
} else if (e.first.isNull) {
break
}
sb.append(e.first)
}
if (line.wrapped) {
continue
}
if (iterator.hasNext()) {
sb.appendLine()
}
} }
} }
@@ -171,6 +203,12 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
fireSelectionChanged() fireSelectionChanged()
} }
override fun setBlockSelection(block: Boolean) {
this.block = block
}
override fun isBlockSelection() = block
override fun getSelectionStartPosition(): Position { override fun getSelectionStartPosition(): Position {
return startPosition return startPosition
} }
@@ -202,13 +240,20 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
} }
override fun hasSelection(x: Int, y: Int): Boolean { override fun hasSelection(x: Int, y: Int): Boolean {
return hasSelection() && isPointInsideArea(
startPosition, if (hasSelection().not()) return false
endPosition,
x, // 如果是块选中
y, if (isBlockSelection()) {
terminal.getTerminalModel().getCols() val left = min(startPosition.x, endPosition.x)
) val right = max(startPosition.x, endPosition.x)
val top = min(startPosition.y, endPosition.y)
val bottom = max(startPosition.y, endPosition.y)
return x in left..right && y in top..bottom
}
return isPointInsideArea(startPosition, endPosition, x, y, terminal.getTerminalModel().getCols())
} }

View File

@@ -14,11 +14,16 @@ import com.formdev.flatlaf.extras.components.FlatToolBar
import com.formdev.flatlaf.ui.FlatRoundBorder import com.formdev.flatlaf.ui.FlatRoundBorder
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import java.awt.event.ActionListener import java.awt.event.ActionListener
import java.beans.PropertyChangeEvent
import java.beans.PropertyChangeListener
import java.util.*
import javax.swing.JButton import javax.swing.JButton
import javax.swing.SwingUtilities
class FloatingToolbarPanel : FlatToolBar(), Disposable { class FloatingToolbarPanel : FlatToolBar(), Disposable {
private val floatingToolbarEnable get() = Database.getDatabase().terminal.floatingToolbar private val floatingToolbarEnable get() = Database.getDatabase().terminal.floatingToolbar
private var closed = false private var closed = false
private val anEvent get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
companion object { companion object {
@@ -72,6 +77,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
} }
initActions() initActions()
initEvents()
} }
override fun updateUI() { override fun updateUI() {
@@ -123,12 +129,38 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
add(initCloseActionButton()) add(initCloseActionButton())
} }
private fun initEvents() {
// 被添加到组件后
addPropertyChangeListener("ancestor", object : PropertyChangeListener {
override fun propertyChange(evt: PropertyChangeEvent) {
removePropertyChangeListener("ancestor", this)
SwingUtilities.invokeLater { resumeVisualWindows() }
}
})
}
@Suppress("UNCHECKED_CAST")
private fun resumeVisualWindows() {
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
if (tab !is SSHTerminalTab) return
val terminalPanel = tab.getData(DataProviders.TerminalPanel) ?: return
terminalPanel.resumeVisualWindows(tab.host.id, object : DataProvider {
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
if (dataKey == DataProviders.TerminalTab) {
return tab as T
}
return super.getData(dataKey)
}
})
}
private fun initServerInfoActionButton(): JButton { private fun initServerInfoActionButton(): JButton {
val btn = JButton(Icons.infoOutline) val btn = JButton(Icons.infoOutline)
btn.toolTipText = I18n.getString("termora.visual-window.system-information") btn.toolTipText = I18n.getString("termora.visual-window.system-information")
btn.addActionListener(object : AnAction() { btn.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
val tab = evt.getData(DataProviders.TerminalTab) ?: return val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
if (tab !is SSHTerminalTab) { if (tab !is SSHTerminalTab) {
@@ -156,7 +188,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
btn.toolTipText = I18n.getString("termora.snippet.title") btn.toolTipText = I18n.getString("termora.snippet.title")
btn.addActionListener(object : AnAction() { btn.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
val tab = evt.getData(DataProviders.TerminalTab) ?: return val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
val writer = tab.getData(DataProviders.TerminalWriter) ?: return val writer = tab.getData(DataProviders.TerminalWriter) ?: return
val dialog = SnippetTreeDialog(evt.window) val dialog = SnippetTreeDialog(evt.window)
dialog.setLocationRelativeTo(btn) dialog.setLocationRelativeTo(btn)
@@ -174,7 +206,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi") btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi")
btn.addActionListener(object : AnAction() { btn.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
val tab = evt.getData(DataProviders.TerminalTab) ?: return val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
if (tab !is SSHTerminalTab) { if (tab !is SSHTerminalTab) {
@@ -233,7 +265,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
btn.addActionListener(object : AnAction() { btn.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
val tab = evt.getData(DataProviders.TerminalTab) ?: return val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
if (tab.canReconnect()) { if (tab.canReconnect()) {
tab.reconnect() tab.reconnect()
} }
@@ -242,8 +274,4 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
return btn return btn
} }
override fun dispose() {
}
} }

View File

@@ -2,6 +2,7 @@ package app.termora.terminal.panel
import app.termora.Application import app.termora.Application
import app.termora.ApplicationScope import app.termora.ApplicationScope
import app.termora.Database
import app.termora.terminal.* import app.termora.terminal.*
import java.awt.Graphics import java.awt.Graphics
import java.net.URI import java.net.URI
@@ -16,6 +17,7 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
} }
private val regex = Regex("https?://\\S*[^.\\s'\",()<>\\[\\]]") private val regex = Regex("https?://\\S*[^.\\s'\",()<>\\[\\]]")
private val isEnableHyperlink get() = Database.getDatabase().terminal.hyperlink
override fun before( override fun before(
offset: Int, offset: Int,
@@ -25,6 +27,9 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
terminalDisplay: TerminalDisplay, terminalDisplay: TerminalDisplay,
terminal: Terminal terminal: Terminal
) { ) {
if (isEnableHyperlink.not()) return
val document = terminal.getDocument() val document = terminal.getDocument()
var startOffset = offset var startOffset = offset
var endOffset = startOffset + count var endOffset = startOffset + count
@@ -91,4 +96,18 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
} }
} }
override fun after(
offset: Int,
count: Int,
g: Graphics,
terminalPanel: TerminalPanel,
terminalDisplay: TerminalDisplay,
terminal: Terminal
) {
if (isEnableHyperlink.not()) {
// 删除之前的
terminal.getMarkupModel().removeAllHighlighters(Highlighter.HYPERLINK)
}
}
} }

View File

@@ -1,13 +1,14 @@
package app.termora.terminal.panel package app.termora.terminal.panel
import app.termora.Database
import app.termora.Disposable import app.termora.Disposable
import app.termora.Disposer import app.termora.Disposer
import app.termora.SSHTerminalTab
import app.termora.actions.DataProvider import app.termora.actions.DataProvider
import app.termora.actions.DataProviderSupport import app.termora.actions.DataProviderSupport
import app.termora.actions.DataProviders import app.termora.actions.DataProviders
import app.termora.terminal.* import app.termora.terminal.*
import app.termora.terminal.panel.vw.VisualWindow import app.termora.terminal.panel.vw.*
import app.termora.terminal.panel.vw.VisualWindowManager
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import org.apache.commons.lang3.ArrayUtils import org.apache.commons.lang3.ArrayUtils
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
@@ -44,15 +45,15 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
val SelectCopy = DataKey(Boolean::class) val SelectCopy = DataKey(Boolean::class)
} }
private val properties get() = Database.getDatabase().properties
private val terminalBlink = TerminalBlink(terminal) private val terminalBlink = TerminalBlink(terminal)
private val terminalFindPanel = TerminalFindPanel(this, terminal) private val terminalFindPanel = TerminalFindPanel(this, terminal)
private val floatingToolbar = FloatingToolbarPanel() private val floatingToolbar = FloatingToolbarPanel()
private val terminalDisplay = TerminalDisplay(this, terminal, terminalBlink) private val terminalDisplay = TerminalDisplay(this, terminal, terminalBlink)
private val dataProviderSupport = DataProviderSupport()
private val layeredPane = TerminalLayeredPane() private val layeredPane = TerminalLayeredPane()
private var visualWindows = emptyArray<VisualWindow>() private var visualWindows = emptyArray<VisualWindow>()
val scrollBar = TerminalScrollBar(this@TerminalPanel, terminalFindPanel, terminal) val scrollBar = TerminalScrollBar(this, terminalFindPanel, terminal)
var enableFloatingToolbar = true var enableFloatingToolbar = true
set(value) { set(value) {
field = value field = value
@@ -63,6 +64,8 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
} }
} }
val dataProviderSupport = DataProviderSupport()
/** /**
* 键盘事件 * 键盘事件
@@ -585,6 +588,37 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
requestFocusInWindow() requestFocusInWindow()
} }
override fun resumeVisualWindows(id: String, dataProvider: DataProvider) {
val windows = properties.getString("VisualWindow.${id}.store") ?: return
for (name in windows.split(",")) {
if (name == "NVIDIA-SMI") {
addVisualWindow(
NvidiaSMIVisualWindow(
dataProvider.getData(DataProviders.TerminalTab) as SSHTerminalTab,
this
)
)
} else if (name == "SystemInformation") {
addVisualWindow(
SystemInformationVisualWindow(
dataProvider.getData(DataProviders.TerminalTab) as SSHTerminalTab,
this
)
)
}
}
}
override fun storeVisualWindows(id: String) {
val windows = mutableListOf<String>()
for (window in getVisualWindows()) {
if (window is Resumeable) {
windows.add(window.getWindowName())
}
}
properties.putString("VisualWindow.${id}.store", windows.joinToString(","))
}
override fun getDimension(): Dimension { override fun getDimension(): Dimension {
return Dimension( return Dimension(
terminalDisplay.size.width + padding.left + padding.right, terminalDisplay.size.width + padding.left + padding.right,

View File

@@ -1,7 +1,9 @@
package app.termora.terminal.panel package app.termora.terminal.panel
import app.termora.actions.TerminalCopyAction
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.terminal.Terminal import app.termora.terminal.Terminal
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -68,6 +70,7 @@ class TerminalPanelKeyAdapter(
} }
val keyStroke = KeyStroke.getKeyStrokeForEvent(e) val keyStroke = KeyStroke.getKeyStrokeForEvent(e)
val keymapActions = activeKeymap.getActionIds(KeyShortcut(keyStroke))
for (action in terminalPanel.getTerminalActions()) { for (action in terminalPanel.getTerminalActions()) {
if (action.test(keyStroke, e)) { if (action.test(keyStroke, e)) {
action.actionPerformed(e) action.actionPerformed(e)
@@ -78,6 +81,8 @@ class TerminalPanelKeyAdapter(
val encode = terminal.getKeyEncoder().encode(AWTTerminalKeyEvent(e)) val encode = terminal.getKeyEncoder().encode(AWTTerminalKeyEvent(e))
if (encode.isNotEmpty()) { if (encode.isNotEmpty()) {
writer.write(TerminalWriter.WriteRequest.fromBytes(encode.toByteArray(writer.getCharset()))) writer.write(TerminalWriter.WriteRequest.fromBytes(encode.toByteArray(writer.getCharset())))
// scroll to bottom
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
e.consume() e.consume()
} }
@@ -90,20 +95,25 @@ class TerminalPanelKeyAdapter(
if (isAltPressedOnly(e) && Character.isDefined(e.keyChar)) { if (isAltPressedOnly(e) && Character.isDefined(e.keyChar)) {
val c = String(charArrayOf(ASCII_ESC, simpleMapKeyCodeToChar(e))) val c = String(charArrayOf(ASCII_ESC, simpleMapKeyCodeToChar(e)))
writer.write(TerminalWriter.WriteRequest.fromBytes(c.toByteArray(writer.getCharset()))) writer.write(TerminalWriter.WriteRequest.fromBytes(c.toByteArray(writer.getCharset())))
// scroll to bottom
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
e.consume() e.consume()
return return
} }
// 如果命中了全局快捷键,那么不处理 // 如果命中了全局快捷键,那么不处理
if (keyStroke.modifiers != 0 && activeKeymap.getActionIds(KeyShortcut(keyStroke)).isNotEmpty()) { val copyShortcutWithoutSelection =
keymapActions.contains(TerminalCopyAction.COPY) && terminal.getSelectionModel().hasSelection().not()
if (keyStroke.modifiers != 0 && keymapActions.isNotEmpty() && !copyShortcutWithoutSelection) {
return return
} }
if (Character.isISOControl(e.keyChar)) { val keyChar = mapKeyChar(e)
if (Character.isISOControl(keyChar)) {
terminal.getSelectionModel().clearSelection() terminal.getSelectionModel().clearSelection()
// 如果不为空表示已经发送过了,所以这里为空的时候再发送 // 如果不为空表示已经发送过了,所以这里为空的时候再发送
if (encode.isEmpty()) { if (encode.isEmpty()) {
writer.write(TerminalWriter.WriteRequest.fromBytes("${e.keyChar}".toByteArray(writer.getCharset()))) writer.write(TerminalWriter.WriteRequest.fromBytes("$keyChar".toByteArray(writer.getCharset())))
e.consume() e.consume()
} }
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE) terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
@@ -111,6 +121,21 @@ class TerminalPanelKeyAdapter(
} }
private fun mapKeyChar(e: KeyEvent): Char {
if (Character.isISOControl(e.keyChar)) {
return e.keyChar
}
val isCtrlPressedOnly = isCtrlPressedOnly(e)
// https://github.com/TermoraDev/termora/issues/478
if (isCtrlPressedOnly && e.keyCode == KeyEvent.VK_OPEN_BRACKET) {
return ControlCharacters.ESC
}
return e.keyChar
}
private fun isCtrlPressedOnly(e: KeyEvent): Boolean { private fun isCtrlPressedOnly(e: KeyEvent): Boolean {
val modifiersEx = e.modifiersEx val modifiersEx = e.modifiersEx
return (modifiersEx and InputEvent.ALT_DOWN_MASK) == 0 return (modifiersEx and InputEvent.ALT_DOWN_MASK) == 0

View File

@@ -134,6 +134,8 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
// 如果不判断的话可能会导致移动了一点点就就进入选择状态了 // 如果不判断的话可能会导致移动了一点点就就进入选择状态了
val diff = terminalPanel.getAverageCharWidth() / 5.0 val diff = terminalPanel.getAverageCharWidth() / 5.0
if (abs(mousePressedPoint.y - e.y) >= diff || abs(mousePressedPoint.x - e.x) >= diff) { if (abs(mousePressedPoint.y - e.y) >= diff || abs(mousePressedPoint.x - e.x) >= diff) {
// 设置选中模式
terminal.getSelectionModel().setBlockSelection(isOnlyAltDown(e))
beginSelect( beginSelect(
Position(x = mousePressedPoint.x, y = mousePressedPoint.y), Position(x = mousePressedPoint.x, y = mousePressedPoint.y),
) )
@@ -141,6 +143,13 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
} }
} }
private fun isOnlyAltDown(e: MouseEvent): Boolean {
return e.isAltDown &&
e.isMetaDown.not() &&
e.isControlDown.not() &&
e.isShiftDown.not() &&
e.isAltGraphDown.not()
}
private fun beginSelect(position: Position) { private fun beginSelect(position: Position) {

View File

@@ -47,6 +47,7 @@ class NvidiaSMIVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWind
private val percentageBtn by lazy { JButton(if (isPercentage) Icons.text else Icons.percentage) } private val percentageBtn by lazy { JButton(if (isPercentage) Icons.text else Icons.percentage) }
init { init {
Disposer.register(tab, this)
initViews() initViews()
initEvents() initEvents()
initVisualWindowPanel() initVisualWindowPanel()

View File

@@ -0,0 +1,3 @@
package app.termora.terminal.panel.vw
interface Resumeable

View File

@@ -1,6 +1,5 @@
package app.termora.terminal.panel.vw package app.termora.terminal.panel.vw
import app.termora.Disposer
import app.termora.SSHTerminalTab import app.termora.SSHTerminalTab
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders import app.termora.actions.DataProviders
@@ -11,11 +10,7 @@ abstract class SSHVisualWindow(
protected val tab: SSHTerminalTab, protected val tab: SSHTerminalTab,
id: String, id: String,
visualWindowManager: VisualWindowManager visualWindowManager: VisualWindowManager
) : VisualWindowPanel(id, visualWindowManager) { ) : VisualWindowPanel(id, visualWindowManager), Resumeable {
init {
Disposer.register(tab, this)
}
override fun toggleWindow() { override fun toggleWindow() {
val evt = AnActionEvent(tab.getJComponent(), StringUtils.EMPTY, EventObject(this)) val evt = AnActionEvent(tab.getJComponent(), StringUtils.EMPTY, EventObject(this))

View File

@@ -25,6 +25,7 @@ class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
private val systemInformationPanel by lazy { SystemInformationPanel() } private val systemInformationPanel by lazy { SystemInformationPanel() }
init { init {
Disposer.register(tab, this)
initViews() initViews()
initEvents() initEvents()
initVisualWindowPanel() initVisualWindowPanel()
@@ -137,7 +138,7 @@ class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
private suspend fun refreshCPUAndMem(session: ClientSession) { private suspend fun refreshCPUAndMem(session: ClientSession) {
// top // top
var pair = SshClients.execChannel(session, "top -bn1") val pair = SshClients.execChannel(session, "top -bn1")
if (pair.first != 0) { if (pair.first != 0) {
return return
} }
@@ -236,7 +237,7 @@ class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
private suspend fun refreshDisk(session: ClientSession) { private suspend fun refreshDisk(session: ClientSession) {
// df -h // df -h
var pair = SshClients.execChannel(session, "df -B1") val pair = SshClients.execChannel(session, "df -B1")
if (pair.first != 0) { if (pair.first != 0) {
return return
} }

View File

@@ -28,4 +28,9 @@ interface VisualWindow : Disposable {
* 切换独立模式 * 切换独立模式
*/ */
fun toggleWindow() fun toggleWindow()
/**
* 同一个类,返回的相同
*/
fun getWindowName(): String
} }

View File

@@ -1,5 +1,6 @@
package app.termora.terminal.panel.vw package app.termora.terminal.panel.vw
import app.termora.actions.DataProvider
import java.awt.Dimension import java.awt.Dimension
interface VisualWindowManager { interface VisualWindowManager {
@@ -33,4 +34,14 @@ interface VisualWindowManager {
* 获取管理器的宽高 * 获取管理器的宽高
*/ */
fun getDimension(): Dimension fun getDimension(): Dimension
/**
* 恢复所有窗口
*/
fun resumeVisualWindows(id: String, dataProvider: DataProvider)
/**
* 存储所有窗口
*/
fun storeVisualWindows(id: String)
} }

View File

@@ -2,10 +2,12 @@ package app.termora.terminal.panel.vw
import app.termora.* import app.termora.*
import com.formdev.flatlaf.extras.components.FlatToolBar import com.formdev.flatlaf.extras.components.FlatToolBar
import com.formdev.flatlaf.util.SystemInfo
import java.awt.* import java.awt.*
import java.awt.event.* import java.awt.event.*
import java.beans.PropertyChangeEvent import java.beans.PropertyChangeEvent
import java.beans.PropertyChangeListener import java.beans.PropertyChangeListener
import javax.imageio.ImageIO
import javax.swing.* import javax.swing.*
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@@ -333,6 +335,15 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
title = getWindowTitle() title = getWindowTitle()
isAlwaysOnTop = isAlwaysTop isAlwaysOnTop = isAlwaysTop
if (SystemInfo.isWindows || SystemInfo.isLinux) {
val sizes = listOf(16, 20, 24, 28, 32, 48, 64)
val loader = TermoraFrame::class.java.classLoader
val images = sizes.mapNotNull { e ->
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
}
iconImages = images
}
initEvents() initEvents()
init() init()
@@ -374,4 +385,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
return null return null
} }
} }
override fun getWindowName(): String {
return id
}
} }

View File

@@ -0,0 +1,88 @@
package app.termora.vfs2
import org.apache.commons.vfs2.FileObject
import java.nio.file.FileVisitResult
import java.nio.file.FileVisitor
import java.nio.file.attribute.BasicFileAttributes
import java.nio.file.attribute.FileTime
object VFSWalker {
fun walk(
dir: FileObject,
visitor: FileVisitor<FileObject>,
): FileVisitResult {
// clear cache
if (visitor.preVisitDirectory(dir, EmptyBasicFileAttributes.INSTANCE) == FileVisitResult.TERMINATE) {
return FileVisitResult.TERMINATE
}
for (e in dir.children) {
if (e.name.baseName == ".." || e.name.baseName == ".") continue
if (e.isFolder) {
if (walk(dir.resolveFile(e.name.baseName), visitor) == FileVisitResult.TERMINATE) {
return FileVisitResult.TERMINATE
}
} else {
val result = visitor.visitFile(
dir.resolveFile(e.name.baseName),
EmptyBasicFileAttributes.INSTANCE
)
if (result == FileVisitResult.TERMINATE) {
return FileVisitResult.TERMINATE
} else if (result == FileVisitResult.SKIP_SUBTREE) {
break
}
}
}
if (visitor.postVisitDirectory(dir, null) == FileVisitResult.TERMINATE) {
return FileVisitResult.TERMINATE
}
return FileVisitResult.CONTINUE
}
private class EmptyBasicFileAttributes : BasicFileAttributes {
companion object {
val INSTANCE = EmptyBasicFileAttributes()
}
override fun lastModifiedTime(): FileTime {
TODO("Not yet implemented")
}
override fun lastAccessTime(): FileTime {
TODO("Not yet implemented")
}
override fun creationTime(): FileTime {
TODO("Not yet implemented")
}
override fun isRegularFile(): Boolean {
TODO("Not yet implemented")
}
override fun isDirectory(): Boolean {
TODO("Not yet implemented")
}
override fun isSymbolicLink(): Boolean {
TODO("Not yet implemented")
}
override fun isOther(): Boolean {
TODO("Not yet implemented")
}
override fun size(): Long {
TODO("Not yet implemented")
}
override fun fileKey(): Any {
TODO("Not yet implemented")
}
}
}

View File

@@ -55,7 +55,9 @@ termora.settings.appearance.language=Language
termora.settings.appearance.i-want-to-translate=I want to translate 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-image=BG Image
termora.settings.appearance.background-running=Backgrounding termora.settings.appearance.background-running=Backgrounding
termora.settings.appearance.confirm-tab-close=Confirm tab close
termora.setting.security=Security termora.setting.security=Security
termora.setting.security.enter-password=Enter password termora.setting.security.enter-password=Enter password
@@ -72,6 +74,7 @@ termora.settings.terminal.size=Size
termora.settings.terminal.max-rows=Max rows termora.settings.terminal.max-rows=Max rows
termora.settings.terminal.debug=Debug mode termora.settings.terminal.debug=Debug mode
termora.settings.terminal.beep=Beep termora.settings.terminal.beep=Beep
termora.settings.terminal.hyperlink=Hyperlink
termora.settings.terminal.select-copy=Select copy termora.settings.terminal.select-copy=Select 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
@@ -230,6 +233,8 @@ termora.tabbed.contextmenu.close=Close
termora.tabbed.contextmenu.close-other-tabs=Close Other Tabs termora.tabbed.contextmenu.close-other-tabs=Close Other Tabs
termora.tabbed.contextmenu.close-all-tabs=Close All Tabs termora.tabbed.contextmenu.close-all-tabs=Close All Tabs
termora.tabbed.contextmenu.reconnect=Reconnect termora.tabbed.contextmenu.reconnect=Reconnect
termora.tabbed.local-tab.close-prompt=Do you want to terminal a running process in this terminal?
termora.tabbed.tab.close-prompt=Are you sure you want to close this tab?
# Terminal logger # Terminal logger
termora.terminal-logger=Terminal Logger termora.terminal-logger=Terminal Logger
@@ -307,6 +312,7 @@ termora.transport.permissions.execute=Execute
termora.transport.permissions.owner=Owner termora.transport.permissions.owner=Owner
termora.transport.permissions.group=Group termora.transport.permissions.group=Group
termora.transport.permissions.others=Others termora.transport.permissions.others=Others
termora.transport.permissions.include-subfolder=Include subdirectories
termora.transport.sftp.retry=Retry termora.transport.sftp.retry=Retry
termora.transport.sftp.select-another-host=Select another host termora.transport.sftp.select-another-host=Select another host

View File

@@ -52,7 +52,9 @@ termora.settings.appearance.language=语言
termora.settings.appearance.i-want-to-translate=我想要翻译 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-image=背景图
termora.settings.appearance.background-running=后台运行 termora.settings.appearance.background-running=后台运行
termora.settings.appearance.confirm-tab-close=标签关闭前确认
termora.setting.security=安全 termora.setting.security=安全
termora.setting.security.enter-password=请输入密码 termora.setting.security.enter-password=请输入密码
@@ -76,6 +78,7 @@ 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=超链接
termora.settings.terminal.select-copy=选中复制 termora.settings.terminal.select-copy=选中复制
termora.settings.terminal.cursor-style=光标样式 termora.settings.terminal.cursor-style=光标样式
termora.settings.terminal.cursor-blink=光标闪烁 termora.settings.terminal.cursor-blink=光标闪烁
@@ -219,7 +222,8 @@ termora.tabbed.contextmenu.close=关闭
termora.tabbed.contextmenu.close-other-tabs=关闭其他标签页 termora.tabbed.contextmenu.close-other-tabs=关闭其他标签页
termora.tabbed.contextmenu.close-all-tabs=关闭所有标签页 termora.tabbed.contextmenu.close-all-tabs=关闭所有标签页
termora.tabbed.contextmenu.reconnect=重新连接 termora.tabbed.contextmenu.reconnect=重新连接
termora.tabbed.local-tab.close-prompt=你想要终止这个终端中正在运行的进程吗?
termora.tabbed.tab.close-prompt=你确定要关闭这个标签页吗?
# Terminal logger # Terminal logger
@@ -298,7 +302,7 @@ termora.transport.sftp.status.done=已完成
termora.transport.sftp.status.failed=已失败 termora.transport.sftp.status.failed=已失败
termora.transport.sftp.already-exists.message1=此文件夹已包含下名称的对象 termora.transport.sftp.already-exists.message1=此文件夹已包含下名称的对象
termora.transport.sftp.already-exists.message2=请选择要执行的操作 termora.transport.sftp.already-exists.message2=请选择要执行的操作
termora.transport.sftp.already-exists.overwrite=覆盖 termora.transport.sftp.already-exists.overwrite=覆盖
termora.transport.sftp.already-exists.append=追加 termora.transport.sftp.already-exists.append=追加
@@ -321,6 +325,7 @@ termora.transport.permissions.execute=执行
termora.transport.permissions.owner=所有者 termora.transport.permissions.owner=所有者
termora.transport.permissions.group= termora.transport.permissions.group=
termora.transport.permissions.others=其他 termora.transport.permissions.others=其他
termora.transport.permissions.include-subfolder=包含子目录
# transport job # transport job
termora.transport.jobs.table.name=名称 termora.transport.jobs.table.name=名称

View File

@@ -53,7 +53,9 @@ termora.settings.appearance.language=語言
termora.settings.appearance.i-want-to-translate=我想要翻譯 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-image=背景圖
termora.settings.appearance.background-running=後台運行 termora.settings.appearance.background-running=後台運行
termora.settings.appearance.confirm-tab-close=關閉分頁確認
termora.setting.security=安全 termora.setting.security=安全
termora.setting.security.enter-password=請輸入密碼 termora.setting.security.enter-password=請輸入密碼
@@ -87,7 +89,8 @@ termora.settings.terminal.font=字體
termora.settings.terminal.size=大小 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.select-copy=選取複製 termora.settings.terminal.select-copy=選取複製
termora.settings.terminal.cursor-style=遊標風格 termora.settings.terminal.cursor-style=遊標風格
termora.settings.terminal.cursor-blink=遊標閃爍 termora.settings.terminal.cursor-blink=遊標閃爍
@@ -215,7 +218,8 @@ termora.tabbed.contextmenu.close=關閉
termora.tabbed.contextmenu.close-other-tabs=關閉其他標籤頁 termora.tabbed.contextmenu.close-other-tabs=關閉其他標籤頁
termora.tabbed.contextmenu.close-all-tabs=關閉所有標籤 termora.tabbed.contextmenu.close-all-tabs=關閉所有標籤
termora.tabbed.contextmenu.reconnect=重新連接 termora.tabbed.contextmenu.reconnect=重新連接
termora.tabbed.local-tab.close-prompt=你想要終止這個終端機中正在運作的進程嗎?
termora.tabbed.tab.close-prompt=你確定要關閉這個分頁嗎?
# Terminal logger # Terminal logger
@@ -292,7 +296,7 @@ termora.transport.sftp.status.waiting=等待中
termora.transport.sftp.status.done=已完成 termora.transport.sftp.status.done=已完成
termora.transport.sftp.status.failed=已失敗 termora.transport.sftp.status.failed=已失敗
termora.transport.sftp.already-exists.message1=此資料夾已包含下名稱的對象 termora.transport.sftp.already-exists.message1=此資料夾已包含下名稱的對象
termora.transport.sftp.already-exists.message2=請選擇要執行的操作 termora.transport.sftp.already-exists.message2=請選擇要執行的操作
termora.transport.sftp.already-exists.overwrite=覆蓋 termora.transport.sftp.already-exists.overwrite=覆蓋
termora.transport.sftp.already-exists.append=追加 termora.transport.sftp.already-exists.append=追加
@@ -303,6 +307,17 @@ termora.transport.sftp.already-exists.destination=目標文件
termora.transport.sftp.already-exists.source=原始檔 termora.transport.sftp.already-exists.source=原始檔
termora.transport.sftp.already-exists.actions=操作 termora.transport.sftp.already-exists.actions=操作
# permissions
termora.transport.permissions=更改權限
termora.transport.permissions.file-folder-permissions=檔案/資料夾權限
termora.transport.permissions.read=讀取
termora.transport.permissions.write=寫入
termora.transport.permissions.execute=執行
termora.transport.permissions.owner=所有者
termora.transport.permissions.group=群組
termora.transport.permissions.others=其他
termora.transport.permissions.include-subfolder=包含子目錄
# transport job # transport job
termora.transport.jobs.table.name=名稱 termora.transport.jobs.table.name=名稱
termora.transport.jobs.table.status=狀態 termora.transport.jobs.table.status=狀態

View File

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.00098 2H7.00195L7.00098 7H2L2.00098 2ZM8.00293 2H13V7H8.00293V2ZM2 7.99902L7 8V13.001L2 13V7.99902ZM8.00195 8H12.999L12.998 13.001H8.00195" fill="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 399 B

View File

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.00098 2H7.00195L7.00098 7H2L2.00098 2ZM8.00293 2H13V7H8.00293V2ZM2 7.99902L7 8V13.001L2 13V7.99902ZM8.00195 8H12.999L12.998 13.001H8.00195" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 399 B

View File

@@ -0,0 +1,7 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.5 9V8C3.5 4.96243 5.96243 2.5 9 2.5C10.1068 2.5 11.1372 2.82692 12 3.38947" stroke="#6C707E" stroke-linecap="round"/>
<path d="M6 12.6105C6.86278 13.1731 7.89321 13.5 9 13.5C12.0376 13.5 14.5 11.0376 14.5 8V7" stroke="#6C707E" stroke-linecap="round"/>
<path d="M1.37868 7.32133L3.5 9.44265L5.62132 7.32133" stroke="#6C707E" stroke-linecap="round"/>
<path d="M12.3787 8.67867L14.5 6.55735L16.6213 8.67867" stroke="#6C707E" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 689 B

View File

@@ -0,0 +1,7 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.5 9V8C3.5 4.96243 5.96243 2.5 9 2.5C10.1068 2.5 11.1372 2.82692 12 3.38947" stroke="#CED0D6" stroke-linecap="round"/>
<path d="M6 12.6105C6.86278 13.1731 7.89321 13.5 9 13.5C12.0376 13.5 14.5 11.0376 14.5 8V7" stroke="#CED0D6" stroke-linecap="round"/>
<path d="M1.37868 7.32133L3.5 9.44265L5.62132 7.32133" stroke="#CED0D6" stroke-linecap="round"/>
<path d="M12.3787 8.67867L14.5 6.55735L16.6213 8.67867" stroke="#CED0D6" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 689 B

View File

@@ -55,6 +55,10 @@ Source: "{#MySourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdir
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[InstallDelete]
Type: files; Name: "{app}\app\*.jar"
Type: filesandordirs; Name: "{app}\runtime\*"
[Run] [Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall; Check: ShouldPromptStart Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall; Check: ShouldPromptStart
Filename: "{app}\{#MyAppExeName}"; Flags: nowait runhidden; Check: ShouldAutoStart Filename: "{app}\{#MyAppExeName}"; Flags: nowait runhidden; Check: ShouldAutoStart

View File

@@ -0,0 +1,29 @@
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Shanghai
# 安装基础包 + sshd + nvim 依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
openssh-server curl ca-certificates tzdata git unzip \
libfuse2 locales && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# 安装 nvim 最新版AppImage 提取)
RUN curl -LO https://github.com/neovim/neovim/releases/download/v0.11.1/nvim-linux-arm64.appimage && \
mv nvim-linux-arm64.appimage nvim.appimage && chmod u+x nvim.appimage && ./nvim.appimage --appimage-extract && \
mv squashfs-root/usr/bin/nvim /usr/local/bin/nvim && \
rm -rf squashfs-root nvim.appimage
# 配置 SSH
RUN mkdir /var/run/sshd && \
echo 'root:root' | chpasswd && \
echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config && \
echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config
# 设置语言环境(可选)
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \
apt-get update && apt-get install -y locales && \
locale-gen en_US.UTF-8 && \
update-locale LANG=en_US.UTF-8
ENV LANG=en_US.UTF-8 \
LANGUAGE=en_US:en \
LC_ALL=en_US.UTF-8
# 启动 SSHD
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]

View File

@@ -1,6 +1,6 @@
FROM linuxserver/openssh-server FROM linuxserver/openssh-server
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update && apk add wget gcc g++ git make zsh htop stress-ng inetutils-telnet xclock xcalc xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \ && apk update && apk add wget tmux gcc g++ git make zsh htop stress-ng inetutils-telnet xclock xcalc xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
&& tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \ && tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \
&& ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz && ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz
RUN sed -i 's/#AllowAgentForwarding yes/AllowAgentForwarding yes/g' /etc/ssh/sshd_config RUN sed -i 's/#AllowAgentForwarding yes/AllowAgentForwarding yes/g' /etc/ssh/sshd_config