Compare commits

...

42 Commits

Author SHA1 Message Date
hstyi
f329ef60df release: 2.0.0-beta.8 2025-07-14 14:20:44 +08:00
hstyi
8acfdb8bca feat: transfer supports copy and paste 2025-07-14 13:50:12 +08:00
hstyi
a7aec52f2a fix: password text field status 2025-07-14 11:56:59 +08:00
hstyi
7f1317a9a7 chore: improve terminal options 2025-07-14 11:11:20 +08:00
hstyi
a8a1fea91b feat: support focus mode 2025-07-14 11:02:40 +08:00
hstyi
675ad4608a chore: improve keymap refresh 2025-07-11 14:57:24 +08:00
hstyi
72ba3757e2 chore: discussion group 2025-07-11 11:24:29 +08:00
hstyi
c58e84d2ae chore: windows action 2025-07-11 10:05:38 +08:00
hstyi
18a7a5059b feat: keyword highlight support import and export 2025-07-11 09:20:24 +08:00
hstyi
f0102b6f13 fix: windows tray icon size 2025-07-10 16:13:50 +08:00
hstyi
0cf8eb3c17 chore: README 2025-07-10 12:14:33 +08:00
hstyi
c08a9f2b18 release: 2.0.0-beta.7 2025-07-10 11:50:43 +08:00
hstyi
728f1f2802 fix: xterm ScrollRegion 2025-07-10 11:30:18 +08:00
hstyi
7310211fba feat: telnet support login scripts 2025-07-10 11:30:05 +08:00
dependabot[bot]
1f3267de0a chore(deps): bump org.apache.commons:commons-lang3 from 3.17.0 to 3.18.0
Bumps org.apache.commons:commons-lang3 from 3.17.0 to 3.18.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-10 11:25:28 +08:00
hstyi
8ddad59c70 chore: use docker jbr 2025-07-10 09:30:12 +08:00
hstyi
9ff6d0afa1 feat: telnet character mode 2025-07-09 19:57:56 +08:00
hstyi
2341b09f81 fix: osx.yml 2025-07-09 19:57:22 +08:00
hstyi
5830aa937a feat: improve GitHub actions 2025-07-09 18:15:36 +08:00
hstyi
56a9361e86 release: 2.0.0-beta.7 2025-07-09 12:22:34 +08:00
hstyi
5868aa4d2f fix: unable to update automatically 2025-07-09 12:18:19 +08:00
dependabot[bot]
45135b7299 chore(deps): bump exposed from 1.0.0-beta-3 to 1.0.0-beta-4
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-09 12:14:23 +08:00
hstyi
a0020fede1 release: 2.0.0-beta.6 2025-07-09 10:27:28 +08:00
hstyi
6f1eaab456 chore: ru_RU i18n 2025-07-09 09:52:02 +08:00
ForumLiker
6173eae772 chore: messages_ru_RU.properties 2025-07-09 09:23:09 +08:00
hstyi
0bb366b1f7 chore: quick connect typo 2025-07-08 21:30:45 +08:00
hstyi
9a4d6f7f4d chore: temporary host disabled editing 2025-07-08 21:22:34 +08:00
hstyi
a4ae11e301 feat: quick connect 2025-07-08 18:21:19 +08:00
dependabot[bot]
5af0acb619 chore(deps): bump cn.hutool:hutool-all from 5.8.37 to 5.8.39
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 16:16:42 +08:00
hstyi
042434b8f8 chore: find everywhere 2025-07-08 16:10:14 +08:00
hstyi
eddc7ef0c6 refactor: frame toolbar 2025-07-08 16:05:31 +08:00
dependabot[bot]
c96ca2d424 chore(deps): bump com.github.mwiede:jsch from 0.2.26 to 2.27.2
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 16:05:01 +08:00
hstyi
45be9008fd refactor: key shortcuts 2025-07-08 14:47:37 +08:00
hstyi
057da4e297 chore: okhttp 5.1.0 2025-07-08 14:01:11 +08:00
hstyi
e4e41667ff chore: osx actions 2025-07-08 12:24:25 +08:00
dependabot[bot]
95ca0a4af7 chore(deps): bump com.github.hstyi:geolite2 from v1.0-202506280327 to v1.0-202507040118
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hstyi <hstyi@foxmail.com>
2025-07-08 12:09:42 +08:00
hstyi
702dee7983 feat: serial plugin 2025-07-08 12:00:02 +08:00
hstyi
165d544448 feat: ftp plugin 2025-07-08 11:38:40 +08:00
hstyi
ecf61bedc4 chore: remove button 2025-07-07 15:50:48 +08:00
hstyi
66a81a5da3 feat: transfer support compress and extract 2025-07-07 15:48:04 +08:00
hstyi
574c816ebb fix: system disk cannot be entered 2025-07-07 11:06:53 +08:00
hstyi
e7cafb74e4 chore: clone session order 2025-07-07 10:34:01 +08:00
152 changed files with 4228 additions and 2140 deletions

View File

@@ -1,52 +0,0 @@
name: Linux aarch64
on: [ push, pull_request ]
jobs:
build:
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-aarch64-b1034.51.tar.gz
# appimagetool
- run: sudo apt install libfuse2
# install jdk
- name: Installing Java
uses: actions/setup-java@v4
with:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.7'
architecture: aarch64
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
# test build
- run: |
./gradlew classes -x test --no-daemon
./gradlew clean --no-daemon
# dist
- run: |
./gradlew dist --no-daemon
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-linux-aarch64
path: |
build/distributions/*.tar.gz
build/distributions/*.AppImage

View File

@@ -1,52 +0,0 @@
name: Linux x86-64
on: [ push, pull_request ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-x64-b1034.51.tar.gz
# appimagetool
- run: sudo apt install libfuse2
# install jdk
- name: Installing Java
uses: actions/setup-java@v4
with:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.7'
architecture: x64
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
# test build
- run: |
./gradlew classes -x test --no-daemon
./gradlew clean --no-daemon
# dist
- run: |
./gradlew dist --no-daemon
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-linux-x86-64
path: |
build/distributions/*.tar.gz
build/distributions/*.AppImage

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

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

View File

@@ -1,89 +0,0 @@
name: macOS aarch64
on: [ push, pull_request ]
jobs:
build:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install the Apple certificate
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
# create variables
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# import certificate from secrets
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
# create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Setup the Notary information
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
STORE_CREDENTIALS: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
run: |
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
# download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-osx-aarch64-b1034.51.tar.gz
# install jdk
- name: Installing Java
uses: actions/setup-java@v4
with:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.7'
architecture: aarch64
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
# test build
- run: |
./gradlew classes -x test --no-daemon
./gradlew clean --no-daemon
# dist
- name: Dist
env:
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
# 只有发布版本时才需要公证
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
run: |
./gradlew dist --no-daemon
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-osx-aarch64
path: |
build/distributions/*.zip
build/distributions/*.dmg

View File

@@ -1,17 +1,29 @@
name: macOS x86-64
name: macOS
on: [ push, pull_request ]
env:
TERMORA_MAC_SIGN: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
# 只有发布版本时才需要公证
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
JBR_MAJOR: 21.0.7
JBR_PATCH: b1038.58
jobs:
build:
runs-on: macos-13
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ macos-15, macos-13 ]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install the Apple certificate
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.BUILD_CERTIFICATE_BASE64 != ''"
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
@@ -34,7 +46,7 @@ jobs:
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Setup the Notary information
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.APPLE_ID != ''"
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
@@ -43,8 +55,14 @@ jobs:
run: |
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
# download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-osx-x64-b1034.51.tar.gz
- name: Download Java
run: |
if [[ "$(uname -m)" == "arm64" ]]; then
ARCH="aarch64"
else
ARCH="x64"
fi
wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-${{ env.JBR_MAJOR }}-osx-$ARCH-${{ env.JBR_PATCH }}.tar.gz
# install jdk
- name: Installing Java
@@ -53,8 +71,6 @@ jobs:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.7'
architecture: x64
- uses: actions/cache@v4
with:
@@ -65,27 +81,22 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
- name: Compile
shell: bash
run: ./gradlew :check-license && ./gradlew classes -x test
# test build
- run: |
./gradlew classes -x test --no-daemon
./gradlew clean --no-daemon
- name: JLink
shell: bash
run: ./gradlew :jar :copy-dependencies :plugins:migration:build :jlink
# dist
- name: Dist
env:
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
# 只有发布版本时才需要公证
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
run: |
./gradlew dist --no-daemon
- name: Package
shell: bash
run: ./gradlew :jpackage && ./gradlew :dist
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-osx-x86-64
name: termora-osx-${{ runner.arch }}
path: |
build/distributions/*.zip
build/distributions/*.dmg

View File

@@ -1,53 +0,0 @@
name: Windows x86-64
on: [ push, pull_request ]
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install zip
run: |
$system32 = [System.Environment]::GetEnvironmentVariable("WINDIR") + "\System32"
Invoke-WebRequest -Uri "http://stahlworks.com/dev/zip.exe" -OutFile "$system32\zip.exe"
Invoke-WebRequest -Uri "http://stahlworks.com/dev/unzip.exe" -OutFile "$system32\unzip.exe"
- name: Install 7z
uses: milliewalky/setup-7-zip@v2
- name: Installing Java
run: |
curl -s --output ${{ runner.temp }}\java_package.zip -L https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-windows-x64-b1034.51.zip
unzip -q ${{ runner.temp }}\java_package.zip -d ${{ runner.temp }}\jbr
echo "JAVA_HOME=${{ runner.temp }}\jbr\jbrsdk-21.0.7-windows-x64-b1034.51" >> $env:GITHUB_ENV
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
# test build
- run: |
.\gradlew classes -x test --no-daemon
.\gradlew clean --no-daemon
# dist
- run: |
.\gradlew.bat dist --no-daemon
.\gradlew.bat --stop
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-windows-x86-64
path: |
build/distributions/*.zip
build/distributions/*.exe

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

@@ -0,0 +1,75 @@
name: Windows
on: [ push, pull_request ]
env:
JBR_MAJOR: 21.0.7
JBR_PATCH: b1038.58
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ windows-11-arm, windows-latest ]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set architecture
id: set-arch
run: |
if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
echo "ARCH=aarch64" >> $env:GITHUB_ENV
} else {
echo "ARCH=x64" >> $env:GITHUB_ENV
}
- name: Install zip
run: |
$system32 = [System.Environment]::GetEnvironmentVariable("WINDIR") + "\System32"
Invoke-WebRequest -Uri "http://stahlworks.com/dev/zip.exe" -OutFile "$system32\zip.exe"
Invoke-WebRequest -Uri "http://stahlworks.com/dev/unzip.exe" -OutFile "$system32\unzip.exe"
- name: Install 7z
uses: milliewalky/setup-7-zip@v2
- name: Installing Java
run: |
$zipPath = "${{ runner.temp }}\java_package.zip"
$extractDir = "${{ runner.temp }}\jbr"
$url = "https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-${{ env.JBR_MAJOR }}-windows-${{ env.ARCH }}-${{ env.JBR_PATCH }}.zip"
curl -s --output $zipPath -L $url
unzip -q $zipPath -d $extractDir
$jbrDir = Get-ChildItem $extractDir | Select-Object -First 1
echo "JAVA_HOME=$($jbrDir.FullName)" >> $env:GITHUB_ENV
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
- name: Compile
run: .\gradlew :check-license && .\gradlew classes -x test
- name: JLink
run: .\gradlew :jar :copy-dependencies :plugins:migration:build :jlink
- name: Package
run: .\gradlew :jpackage && .\gradlew :dist
- name: Stop Gradle
run: .\gradlew.bat --stop
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-windows-${{ runner.arch }}
path: |
build/distributions/*.zip
build/distributions/*.exe

View File

@@ -90,8 +90,6 @@ Termora is developed using [**Kotlin/JVM**](https://kotlinlang.org/) and partial
We recommend using the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK for development.
- Run locally: `./gradlew :run`
- Build for current OS: `./gradlew :dist`
## 📄 License

View File

@@ -88,8 +88,6 @@ Termora 使用 [**Kotlin/JVM**](https://kotlinlang.org/) 开发,支持(正
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK 运行环境。
- 本地运行:`./gradlew :run`
- 构建当前系统安装包:`./gradlew :dist`
## 📄 授权协议

View File

@@ -1 +1 @@
2.0.0-beta.5
2.0.0-beta.8

View File

@@ -28,7 +28,7 @@ version = rootProject.projectDir.resolve("VERSION").readText().trim()
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
val appVersion = project.version.toString().split("-")[0]
val isDeb = os.isLinux && System.getProperty("type") == "deb"
val isDeb = os.isLinux && System.getenv("TERMORA_TYPE") == "deb"
// macOS 签名信息
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
@@ -62,9 +62,6 @@ dependencies {
testImplementation(libs.h2)
testImplementation(libs.exposed.migration)
// implementation(platform(libs.koin.bom))
// implementation(libs.koin.core)
api(kotlin("reflect"))
api(libs.slf4j.api)
api(libs.pty4j)
@@ -105,7 +102,6 @@ dependencies {
api(libs.colorpicker)
api(libs.mixpanel)
api(libs.jSerialComm)
api(libs.ini4j)
api(libs.restart4j)
api(libs.exposed.core)
@@ -189,7 +185,7 @@ tasks.register<Copy>("copy-dependencies") {
// 对 JNA 和 PTY4J 的本地库提取
// 提取出来是为了单独签名,不然无法通过公证
if (os.isMacOsX && macOSSign) {
if (os.isMacOsX) {
doLast {
val archName = if (arch.isArm) "aarch64" else "x86_64"
val dylib = dir.get().dir("dylib").asFile
@@ -484,10 +480,6 @@ tasks.register<Exec>("jpackage") {
}
if (os.isWindows) {
arguments.add("--win-dir-chooser")
arguments.add("--win-shortcut")
arguments.add("--win-shortcut-prompt")
arguments.addAll(listOf("--win-upgrade-uuid", "E1D93CAD-5BF8-442E-93BA-6E90DE601E4C"))
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.ico"))
}
@@ -500,7 +492,7 @@ tasks.register<Exec>("jpackage") {
if (os.isMacOsX) {
arguments.add("dmg")
} else if (os.isWindows) {
arguments.add("msi")
arguments.add("app-image")
} else if (os.isLinux) {
arguments.add(if (isDeb) "deb" else "app-image")
if (isDeb) {
@@ -523,31 +515,20 @@ tasks.register<Exec>("jpackage") {
tasks.register("dist") {
doLast {
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
val distributionDir = layout.buildDirectory.dir("distributions").get()
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
val projectName = project.name.uppercaseFirstChar()
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
// 清空目录
exec { commandLine(gradlew, "clean") }
// 构建自带的插件
exec { commandLine(gradlew, ":plugins:migration:build") }
// 打包并复制依赖
exec {
commandLine(gradlew, ":jar", ":copy-dependencies")
if (os.isWindows) {
packOnWindows(distributionDir, finalFilenameWithoutExtension, projectName)
} else if (os.isLinux) {
packOnLinux(distributionDir, finalFilenameWithoutExtension, projectName)
} else if (os.isMacOsX) {
packOnMac(distributionDir, finalFilenameWithoutExtension, projectName)
} else {
throw GradleException("${os.name} is not supported")
}
// 检查依赖的开源协议
exec { commandLine(gradlew, ":check-license") }
// jlink
exec { commandLine(gradlew, ":jlink") }
// 打包
exec { commandLine(gradlew, ":jpackage", "-Dtype=${System.getProperty("type")}") }
// 根据不同的系统构建不同的二进制包
pack()
}
}
@@ -578,32 +559,12 @@ tasks.register("check-license") {
}
}
/**
* 构建包
*/
fun pack() {
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
val distributionDir = layout.buildDirectory.dir("distributions").get()
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
val projectName = project.name.uppercaseFirstChar()
if (os.isWindows) {
packOnWindows(distributionDir, finalFilenameWithoutExtension, projectName)
} else if (os.isLinux) {
packOnLinux(distributionDir, finalFilenameWithoutExtension, projectName)
} else if (os.isMacOsX) {
packOnMac(distributionDir, finalFilenameWithoutExtension, projectName)
} else {
throw GradleException("${os.name} is not supported")
}
}
/**
* 创建 zip、msi
*/
fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
val dir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
val dir = layout.buildDirectory.dir("distributions").get().asFile
val cfg = FileUtils.getFile(dir, projectName, "app", "${projectName}.cfg")
val configText = cfg.readText()
@@ -628,21 +589,12 @@ fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: Str
"/DMyAppVersion=${appVersion}",
"/DMyOutputDir=${distributionDir.asFile.absolutePath}",
"/DMySetupIconFile=${FileUtils.getFile(projectDir, "src", "main", "resources", "icons", "termora.ico")}",
"/DMySourceDir=${layout.buildDirectory.dir("jpackage/images/win-msi.image/${projectName}").get().asFile}",
"/DMySourceDir=${FileUtils.getFile(dir, projectName).absolutePath}",
"/F${finalFilenameWithoutExtension}",
FileUtils.getFile(projectDir, "src", "main", "resources", "termora.iss")
)
}
// msi
exec {
commandLine(
"cmd", "/c", "move",
"${projectName}-${appVersion}.msi",
"${finalFilenameWithoutExtension}.msi"
)
workingDir = distributionDir.asFile
}
}
/**
@@ -658,7 +610,7 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
// @formatter:on
// sign dmg
if (macOSSign) signMacOSLocalFile(dmgFile)
signMacOSLocalFile(dmgFile)
// 找到 .app
val imageFile = layout.buildDirectory.dir("jpackage/images/").get().asFile
@@ -671,7 +623,7 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
// @formatter:on
// sign zip
if (macOSSign) signMacOSLocalFile(zipFile)
signMacOSLocalFile(zipFile)
// 公证
if (macOSNotary) {

View File

@@ -4,10 +4,10 @@ slf4j = "2.0.17"
pty4j = "0.13.6"
tinylog = "2.7.0"
kotlinx-coroutines = "1.10.2"
flatlaf = "3.6"
flatlaf = "3.6.1-SNAPSHOT"
kotlinx-serialization-json = "1.9.0"
commons-codec = "1.18.0"
commons-lang3 = "3.17.0"
commons-lang3 = "3.18.0"
commons-csv = "1.14.0"
commons-net = "3.11.1"
commons-text = "1.13.1"
@@ -22,9 +22,9 @@ jna = "5.17.0"
jSystemThemeDetector = "3.9.1"
commons-io = "2.19.0"
jbr-api = "17.1.10.1"
hutool = "5.8.37"
jsch = "0.2.26"
okhttp = "4.12.0"
hutool = "5.8.39"
jsch = "2.27.2"
okhttp = "5.1.0"
sshj = "0.39.0"
sshd-core = "2.15.0"
jgit = "7.2.0.202503040940-r"
@@ -41,12 +41,12 @@ jSerialComm = "2.11.2"
ini4j = "0.5.5-2"
restart4j = "0.0.1"
eddsa = "0.3.0"
exposed = "1.0.0-beta-3"
exposed = "1.0.0-beta-4"
h2 = "2.3.232"
sqlite = "3.50.2.0"
jug = "5.1.0"
semver4j = "6.0.0"
jsvg = "1.4.0"
jsvg = "2.0.0"
dom4j = "2.2.0"
[libraries]

View File

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

View File

@@ -1,7 +1,6 @@
package app.termora.plugins.bg
import app.termora.*
import app.termora.database.DatabaseManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
@@ -96,9 +95,7 @@ internal class BackgroundManager private constructor() : Disposable, GlassPaneAw
return
}
val body = response.body
if (body != null) {
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
}
IOUtils.closeQuietly(body)
return@use tempFile
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,393 @@
package app.termora.plugins.ftp
import app.termora.*
import app.termora.keymgr.KeyManager
import app.termora.plugin.internal.BasicProxyOption
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatComboBox
import com.formdev.flatlaf.ui.FlatTextBorder
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import org.apache.commons.lang3.StringUtils
import java.awt.BorderLayout
import java.awt.Component
import java.awt.KeyboardFocusManager
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.awt.event.ItemEvent
import java.nio.charset.Charset
import javax.swing.*
class FTPHostOptionsPane : OptionsPane() {
private val generalOption = GeneralOption()
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
private val sftpOption = SFTPOption()
init {
addOption(generalOption)
addOption(proxyOption)
addOption(sftpOption)
}
fun getHost(): Host {
val name = generalOption.nameTextField.text
val protocol = FTPProtocolProvider.PROTOCOL
val port = generalOption.portTextField.value as Int
var authentication = Authentication.Companion.No
var proxy = Proxy.Companion.No
val authenticationType = AuthenticationType.Password
authentication = authentication.copy(
type = authenticationType,
password = String(generalOption.passwordTextField.password)
)
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
proxy = proxy.copy(
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
host = proxyOption.proxyHostTextField.text,
username = proxyOption.proxyUsernameTextField.text,
password = String(proxyOption.proxyPasswordTextField.password),
port = proxyOption.proxyPortTextField.value as Int,
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
)
}
val options = Options.Default.copy(
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
encoding = sftpOption.charsetComboBox.selectedItem as String,
extras = mutableMapOf("passive" to (sftpOption.passiveComboBox.selectedItem as PassiveMode).name)
)
return Host(
name = name,
protocol = protocol,
port = port,
host = generalOption.hostTextField.text,
username = generalOption.usernameTextField.text,
authentication = authentication,
proxy = proxy,
sort = System.currentTimeMillis(),
remark = generalOption.remarkTextArea.text,
options = options,
)
}
fun setHost(host: Host) {
generalOption.nameTextField.text = host.name
generalOption.usernameTextField.text = host.username
generalOption.remarkTextArea.text = host.remark
generalOption.passwordTextField.text = host.authentication.password
generalOption.hostTextField.text = host.host
generalOption.portTextField.value = host.port
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
proxyOption.proxyHostTextField.text = host.proxy.host
proxyOption.proxyPasswordTextField.text = host.proxy.password
proxyOption.proxyUsernameTextField.text = host.proxy.username
proxyOption.proxyPortTextField.value = host.proxy.port
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
val passive = host.options.extras["passive"] ?: PassiveMode.Local.name
sftpOption.charsetComboBox.selectedItem = host.options.encoding
sftpOption.passiveComboBox.selectedItem = runCatching { PassiveMode.valueOf(passive) }
.getOrNull() ?: PassiveMode.Local
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
}
fun validateFields(): Boolean {
val host = getHost()
// general
if (validateField(generalOption.nameTextField)) {
return false
}
if (validateField(generalOption.hostTextField)) {
return false
}
if (StringUtils.isNotBlank(generalOption.usernameTextField.text) || generalOption.passwordTextField.password.isNotEmpty()) {
if (validateField(generalOption.usernameTextField)) {
return false
}
if (validateField(generalOption.passwordTextField)) {
return false
}
}
// proxy
if (host.proxy.type != ProxyType.No) {
if (validateField(proxyOption.proxyHostTextField)
) {
return false
}
if (host.proxy.authenticationType != AuthenticationType.No) {
if (validateField(proxyOption.proxyUsernameTextField)
|| validateField(proxyOption.proxyPasswordTextField)
) {
return false
}
}
}
return true
}
/**
* 返回 true 表示有错误
*/
private fun validateField(textField: JTextField): Boolean {
if (textField.isEnabled && (if (textField is JPasswordField) textField.password.isEmpty() else textField.text.isBlank())) {
setOutlineError(textField)
return true
}
return false
}
private fun setOutlineError(c: JComponent) {
selectOptionJComponent(c)
c.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
c.requestFocusInWindow()
}
inner class GeneralOption : JPanel(BorderLayout()), Option {
val portTextField = PortSpinner(21)
val nameTextField = OutlineTextField(128)
val usernameTextField = OutlineTextField(128)
val hostTextField = OutlineTextField(255)
val passwordTextField = OutlinePasswordField(255)
val publicKeyComboBox = OutlineComboBox<String>()
val remarkTextArea = FixedLengthTextArea(512)
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
init {
initView()
initEvents()
}
private fun initView() {
add(getCenterComponent(), BorderLayout.CENTER)
publicKeyComboBox.isEditable = false
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component {
var text = StringUtils.EMPTY
if (value is String) {
text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text
}
return super.getListCellRendererComponent(
list,
text,
index,
isSelected,
cellHasFocus
)
}
}
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component {
var text = value?.toString() ?: ""
when (value) {
AuthenticationType.Password -> {
text = "Password"
}
AuthenticationType.PublicKey -> {
text = "Public Key"
}
AuthenticationType.KeyboardInteractive -> {
text = "Keyboard Interactive"
}
}
return super.getListCellRendererComponent(
list,
text,
index,
isSelected,
cellHasFocus
)
}
}
authenticationTypeComboBox.addItem(AuthenticationType.No)
authenticationTypeComboBox.addItem(AuthenticationType.Password)
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
}
private fun initEvents() {
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
removeComponentListener(this)
}
})
authenticationTypeComboBox.addItemListener {
if (it.stateChange == ItemEvent.SELECTED) {
passwordTextField.isEnabled = authenticationTypeComboBox.selectedItem == AuthenticationType.Password
}
}
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.settings
}
override fun getTitle(): String {
return I18n.getString("termora.new-host.general")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
remarkTextArea.setFocusTraversalKeys(
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
)
remarkTextArea.setFocusTraversalKeys(
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
)
remarkTextArea.rows = 8
remarkTextArea.lineWrap = true
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
var rows = 1
val step = 2
val panel = FormBuilder.create().layout(layout)
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
.add(hostTextField).xy(3, rows)
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
.add(portTextField).xy(7, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
.add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
.xyw(3, rows, 5).apply { rows += step }
.build()
return panel
}
}
private inner class SFTPOption : JPanel(BorderLayout()), Option {
val defaultDirectoryField = OutlineTextField(255)
val charsetComboBox = JComboBox<String>()
val passiveComboBox = JComboBox<PassiveMode>()
init {
initView()
initEvents()
}
private fun initView() {
for (e in Charset.availableCharsets()) {
charsetComboBox.addItem(e.key)
}
charsetComboBox.selectedItem = "UTF-8"
passiveComboBox.addItem(PassiveMode.Local)
passiveComboBox.addItem(PassiveMode.Remote)
add(getCenterComponent(), BorderLayout.CENTER)
}
private fun initEvents() {
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.folder
}
override fun getTitle(): String {
return I18n.getString("termora.transport.sftp")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
var rows = 1
val step = 2
val panel = FormBuilder.create().layout(layout)
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
.add(charsetComboBox).xy(3, rows).apply { rows += step }
.add("${FTPI18n.getString("termora.plugins.ftp.passive")}:").xy(1, rows)
.add(passiveComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
.build()
return panel
}
}
enum class PassiveMode {
Local,
Remote,
}
}

View File

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

View File

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

View File

@@ -1,8 +1,5 @@
package app.termora.plugins.ftp
import app.termora.DynamicIcon
import app.termora.I18n
import app.termora.Icons
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.PaidPlugin
@@ -27,6 +24,7 @@ class FTPPlugin : PaidPlugin {
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,9 @@
package app.termora.plugin.internal.serial
package app.termora.plugins.serial
import app.termora.*
import app.termora.plugin.internal.AltKeyModifier
import app.termora.plugin.internal.BasicGeneralOption
import app.termora.plugin.internal.BasicTerminalOption
import com.fazecast.jSerialComm.SerialPort
import com.formdev.flatlaf.FlatClientProperties
import com.jgoodies.forms.builder.FormBuilder
@@ -15,12 +17,15 @@ import java.awt.BorderLayout
import java.awt.Component
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.nio.charset.Charset
import javax.swing.*
class SerialHostOptionsPane : OptionsPane() {
private val generalOption = BasicGeneralOption()
private val terminalOption = TerminalOption()
private val terminalOption = BasicTerminalOption().apply {
showCharsetComboBox = true
showStartupCommandTextField = true
init()
}
private val serialCommOption = SerialCommOption()
init {
@@ -48,6 +53,10 @@ class SerialHostOptionsPane : OptionsPane() {
encoding = terminalOption.charsetComboBox.selectedItem as String,
startupCommand = terminalOption.startupCommandTextField.text,
serialComm = serialComm,
extras = mutableMapOf(
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
?: AltKeyModifier.EightBit.name),
)
)
return Host(
@@ -128,67 +137,6 @@ class SerialHostOptionsPane : OptionsPane() {
}
protected inner class TerminalOption : JPanel(BorderLayout()), Option {
val charsetComboBox = JComboBox<String>()
val startupCommandTextField = OutlineTextField()
init {
initView()
initEvents()
}
private fun initView() {
add(getCenterComponent(), BorderLayout.CENTER)
for (e in Charset.availableCharsets()) {
charsetComboBox.addItem(e.key)
}
charsetComboBox.selectedItem = "UTF-8"
}
private fun initEvents() {
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.terminal
}
override fun getTitle(): String {
return I18n.getString("termora.new-host.terminal")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
var rows = 1
val step = 2
val panel = FormBuilder.create().layout(layout)
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
.add(charsetComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows)
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
.apply { rows += step }
.build()
return panel
}
}
protected inner class SerialCommOption : JPanel(BorderLayout()), Option {
val serialPortComboBox = OutlineComboBox<String>()
val baudRateComboBox = OutlineComboBox<Int>()

View File

@@ -1,11 +1,18 @@
package app.termora.plugin.internal.serial
package app.termora.plugins.serial
import app.termora.plugin.Extension
import app.termora.plugin.InternalPlugin
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.Plugin
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProviderExtension
internal class SerialInternalPlugin : InternalPlugin() {
internal class SerialPlugin : Plugin {
private val support = ExtensionSupport()
override fun getAuthor(): String {
return "TermoraDev"
}
init {
support.addExtension(ProtocolProviderExtension::class.java) { SerialProtocolProviderExtension.instance }
support.addExtension(ProtocolHostPanelExtension::class.java) { SerialProtocolHostPanelExtension.instance }
@@ -13,7 +20,7 @@ internal class SerialInternalPlugin : InternalPlugin() {
override fun getName(): String {
return "Serial Protocol"
return "Serial Comm"
}

View File

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

View File

@@ -1,4 +1,4 @@
package app.termora.plugin.internal.serial
package app.termora.plugins.serial
import app.termora.Disposer
import app.termora.Host

View File

@@ -1,4 +1,4 @@
package app.termora.plugin.internal.serial
package app.termora.plugins.serial
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel

View File

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

View File

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

View File

@@ -1,6 +1,9 @@
package app.termora.plugin.internal.serial
package app.termora.plugins.serial
import app.termora.*
import app.termora.Host
import app.termora.Icons
import app.termora.PtyHostTerminalTab
import app.termora.WindowScope
import app.termora.terminal.PtyConnector
import org.apache.commons.io.Charsets
import java.nio.charset.StandardCharsets

View File

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

View File

@@ -0,0 +1,22 @@
<termora-plugin>
<id>serial</id>
<name>Serial Comm</name>
<version>${projectVersion}</version>
<termora-version since=">=${rootProjectVersion}" until=""/>
<entry>app.termora.plugins.serial.SerialPlugin</entry>
<descriptions>
<description>Supports access to serial ports</description>
<description language="zh_CN">支持访问串口</description>
<description language="zh_TW">支援訪問串口</description>
</descriptions>
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
</termora-plugin>

View File

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

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

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

View File

@@ -1,12 +1,22 @@
package app.termora.plugins.smb
import app.termora.transfer.s3.S3FileSystem
import app.termora.transfer.s3.S3Path
import com.hierynomus.smbj.session.Session
import com.hierynomus.smbj.share.DiskShare
class SMBFileSystem(private val share: DiskShare, session: Session) :
S3FileSystem(SMBFileSystemProvider(share, session)) {
override fun create(root: String?, names: List<String>): S3Path {
val path = SMBPath(this, root, names)
if (names.isEmpty()) {
path.attributes = path.attributes.copy(directory = true)
}
return path
}
override fun close() {
share.close()
super.close()

View File

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

View File

@@ -7,7 +7,7 @@ include("plugins:s3")
include("plugins:oss")
include("plugins:cos")
include("plugins:obs")
//include("plugins:ftp")
include("plugins:ftp")
include("plugins:bg")
include("plugins:sync")
include("plugins:migration")
@@ -15,3 +15,4 @@ include("plugins:editor")
include("plugins:geo")
include("plugins:webdav")
include("plugins:smb")
include("plugins:serial")

View File

@@ -12,12 +12,6 @@ object Actions {
*/
const val KEY_MANAGER = "KeyManagerAction"
/**
* 更新
*/
const val APP_UPDATE = "AppUpdateAction"
/**
* 宏
*/

View File

@@ -1,8 +1,6 @@
package app.termora
import app.termora.actions.ActionManager
import app.termora.database.DatabaseManager
import app.termora.keymap.KeymapManager
import app.termora.plugin.ExtensionManager
import app.termora.plugin.PluginManager
import com.formdev.flatlaf.FlatClientProperties
@@ -26,12 +24,13 @@ import java.awt.*
import java.awt.desktop.AppReopenedEvent
import java.awt.desktop.AppReopenedListener
import java.awt.desktop.SystemEventListener
import java.awt.event.ActionEvent
import java.awt.event.WindowEvent
import java.awt.event.*
import java.util.*
import java.util.concurrent.CountDownLatch
import javax.imageio.ImageIO
import javax.swing.*
import javax.swing.event.PopupMenuEvent
import javax.swing.event.PopupMenuListener
import kotlin.system.exitProcess
class ApplicationRunner {
@@ -54,12 +53,6 @@ class ApplicationRunner {
// 统计
enableAnalytics()
// init ActionManager、KeymapManager、VFS
swingCoroutineScope.launch(Dispatchers.IO) {
ActionManager.getInstance()
KeymapManager.getInstance()
}
// 设置 LAF
setupLaf()
@@ -120,16 +113,63 @@ class ApplicationRunner {
if (!SystemInfo.isWindows || !SystemTray.isSupported()) return
val tray = SystemTray.getSystemTray()
val image = ImageIO.read(TermoraFrame::class.java.getResourceAsStream("/icons/termora_16x16.png"))
val image = ImageIO.read(TermoraFrame::class.java.getResourceAsStream("/icons/termora_32x32.png"))
val trayIcon = TrayIcon(image)
val popupMenu = PopupMenu()
trayIcon.popupMenu = popupMenu
val dialog = JDialog()
val trayPopup = JPopupMenu()
dialog.isUndecorated = true
dialog.isModal = false
dialog.size = Dimension(0, 0)
trayIcon.isImageAutoSize = true
trayIcon.toolTip = Application.getName()
// PopupMenu 不支持中文
val exitMenu = MenuItem("Exit")
exitMenu.addActionListener { SwingUtilities.invokeLater { quitHandler() } }
popupMenu.add(exitMenu)
trayPopup.add(I18n.getString("termora.exit")).addActionListener { quitHandler() }
trayPopup.addPopupMenuListener(object : PopupMenuListener {
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent?) {
}
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent?) {
SwingUtilities.invokeLater {
if (dialog.isVisible) {
dialog.isVisible = false
}
}
}
override fun popupMenuCanceled(e: PopupMenuEvent?) {
popupMenuWillBecomeInvisible(e)
}
})
trayIcon.addMouseListener(object : MouseAdapter() {
override fun mouseReleased(e: MouseEvent) {
maybeShowPopup(e)
}
override fun mousePressed(e: MouseEvent) {
maybeShowPopup(e)
}
private fun maybeShowPopup(e: MouseEvent) {
if (e.isPopupTrigger) {
val mouseLocation = MouseInfo.getPointerInfo().location
trayPopup.setLocation(mouseLocation.x, mouseLocation.y)
trayPopup.setInvoker(dialog)
dialog.isVisible = true
trayPopup.isVisible = true
}
}
})
dialog.addWindowFocusListener(object : WindowAdapter() {
override fun windowLostFocus(e: WindowEvent) {
dialog.isVisible = false
}
})
// double click
trayIcon.addActionListener(object : AbstractAction() {

View File

@@ -1,8 +1,5 @@
package app.termora
import app.termora.Application.ohMyJson
import app.termora.actions.MultipleAction
import app.termora.database.DatabaseManager
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import org.apache.commons.lang3.StringUtils
@@ -18,10 +15,10 @@ import javax.swing.event.ListDataListener
import kotlin.math.max
import kotlin.math.min
class CustomizeToolBarDialog(
internal class CustomizeToolBarDialog(
owner: Window,
private val windowScope: WindowScope,
private val toolbar: TermoraToolBar
private val model: TermoraToolbarModel,
) : DialogWrapper(owner) {
private val moveTopBtn = JButton(Icons.moveUp)
@@ -40,6 +37,7 @@ class CustomizeToolBarDialog(
private val actionManager get() = ActionManager.getInstance()
private var isOk = false
private val actions = mutableListOf<ToolBarAction>()
init {
size = Dimension(UIManager.getInt("Dialog.width") - 150, UIManager.getInt("Dialog.height") - 100)
@@ -147,7 +145,7 @@ class CustomizeToolBarDialog(
resetBtn.addActionListener {
leftList.model.removeAllElements()
rightList.model.removeAllElements()
for (action in toolbar.getAllActions()) {
for (action in model.getAllActions()) {
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
}
}
@@ -258,7 +256,7 @@ class CustomizeToolBarDialog(
override fun windowOpened(e: WindowEvent) {
removeWindowListener(this)
for (action in toolbar.getActions()) {
for (action in model.getActions()) {
if (action.visible) {
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
} else {
@@ -271,12 +269,7 @@ class CustomizeToolBarDialog(
}
private fun getActionHolder(actionId: String): ActionHolder? {
var action = actionManager.getAction(actionId)
if (action == null) {
if (actionId == MultipleAction.MULTIPLE) {
action = MultipleAction.getInstance(windowScope)
}
}
val action = actionManager.getAction(actionId)
if (action == null) return null
return ActionHolder(actionId, action)
}
@@ -365,12 +358,14 @@ class CustomizeToolBarDialog(
actions.add(ToolBarAction(leftList.model.getElementAt(i).id, false))
}
DatabaseManager.getInstance()
.properties.putString("Termora.ToolBar.Actions", ohMyJson.encodeToString(actions))
this.actions.clear()
this.actions.addAll(actions)
super.doOKAction()
}
fun getActions()=actions
fun open(): Boolean {
isModal = true
isVisible = true

View File

@@ -0,0 +1,21 @@
package app.termora
import app.termora.database.DatabaseChangedExtension
import app.termora.database.DatabasePropertiesChangedExtension
import app.termora.plugin.Extension
import app.termora.plugin.InternalPlugin
internal class FramePlugin : InternalPlugin() {
init {
support.addExtension(DatabasePropertiesChangedExtension::class.java) { KeymapRefresher.getInstance() }
support.addExtension(DatabaseChangedExtension::class.java) { KeymapRefresher.getInstance() }
}
override fun getName(): String {
return "Frame"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -344,6 +344,11 @@ data class Host(
val isFolder get() = StringUtils.equalsIgnoreCase(protocol, "Folder")
/**
* 临时的 SSH 不可以保存
*/
val isTemporary get() = options.extras["Temporary"] != null
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

View File

@@ -21,9 +21,13 @@ class HostManager private constructor() : Disposable {
*/
fun addHost(host: Host, source: DatabaseChangedExtension.Source = DatabaseChangedExtension.Source.User) {
assertEventDispatchThread()
if (host.ownerType.isBlank()) {
if (host.isTemporary)
throw IllegalArgumentException("Temporary host")
if (host.ownerType.isBlank())
throw IllegalArgumentException("Owner type cannot be null")
}
databaseManager.saveAndIncrementVersion(
Data(
id = host.id,

View File

@@ -20,6 +20,7 @@ object I18n : AbstractI18n() {
"en_US" to "English",
"zh_CN" to "简体中文",
"zh_TW" to "繁體中文",
"ru_RU" to "Русский",
)
fun containsLanguage(locale: Locale): String? {

View File

@@ -0,0 +1,65 @@
package app.termora
import app.termora.database.DatabaseChangedExtension
import app.termora.database.DatabasePropertiesChangedExtension
import app.termora.keymap.KeymapManager
internal class KeymapRefresher private constructor() : DatabasePropertiesChangedExtension, DatabaseChangedExtension {
companion object {
fun getInstance(): KeymapRefresher {
return ApplicationScope.forApplicationScope()
.getOrCreate(KeymapRefresher::class) { KeymapRefresher() }
}
}
private val listeners = mutableListOf<() -> Unit>()
private var currentKeymap: String? = null
private val keymapManager get() = KeymapManager.getInstance()
private val activeKeymapName get() = keymapManager.getActiveKeymap().name
override fun onDataChanged(
id: String,
type: String,
action: DatabaseChangedExtension.Action,
source: DatabaseChangedExtension.Source
) {
if (type != "Keymap") return
refresh()
}
override fun onPropertyChanged(name: String, key: String, value: String) {
if (name != "Setting.Properties") return
if (key != "Keymap.Active") return
refresh()
}
private fun refresh() {
synchronized(this) {
if (currentKeymap == activeKeymapName) {
return
}
currentKeymap = activeKeymapName
for (function in listeners) {
function.invoke()
}
}
}
fun addRefreshListener(listener: () -> Unit): Disposable {
synchronized(this) {
listeners.add(listener)
return object : Disposable {
override fun dispose() {
removeRefreshListener(listener)
}
}
}
}
fun removeRefreshListener(listener: () -> Unit) {
synchronized(this) { listeners.remove(listener) }
}
}

View File

@@ -0,0 +1,215 @@
package app.termora
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatTable
import com.formdev.flatlaf.extras.components.FlatToolBar
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.Window
import java.awt.event.ActionEvent
import javax.swing.*
import javax.swing.table.DefaultTableCellRenderer
import javax.swing.table.DefaultTableModel
import kotlin.math.max
internal class LoginScriptPanel(private val loginScripts: MutableList<LoginScript>) : JPanel(BorderLayout()) {
private val owner get() = SwingUtilities.getWindowAncestor(this)
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
private val editBtn = JButton(I18n.getString("termora.new-host.tunneling.edit"))
private val deleteBtn = JButton(I18n.getString("termora.new-host.tunneling.delete"))
private val table = FlatTable()
private val model = object : DefaultTableModel() {
override fun getRowCount(): Int {
return loginScripts.size
}
override fun isCellEditable(row: Int, column: Int): Boolean {
return false
}
fun addRow(loginScript: LoginScript) {
val rowCount = super.getRowCount()
loginScripts.add(loginScript)
super.fireTableRowsInserted(rowCount, rowCount + 1)
}
override fun getValueAt(row: Int, column: Int): Any {
val loginScript = loginScripts[row]
return when (column) {
0 -> loginScript.expect
1 -> loginScript.send
else -> super.getValueAt(row, column)
}
}
}
init {
initView()
initEvents()
}
private fun initView() {
addBtn.isFocusable = false
editBtn.isFocusable = false
deleteBtn.isFocusable = false
deleteBtn.isEnabled = false
editBtn.isEnabled = false
val scrollPane = JScrollPane(table)
model.addColumn(I18n.getString("termora.new-host.terminal.expect"))
model.addColumn(I18n.getString("termora.new-host.terminal.send"))
table.putClientProperty(
FlatClientProperties.STYLE, mapOf(
"showHorizontalLines" to true,
"showVerticalLines" to true,
)
)
table.model = model
table.autoResizeMode = JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS
table.setDefaultRenderer(
Any::class.java,
DefaultTableCellRenderer().apply { horizontalAlignment = SwingConstants.CENTER })
table.fillsViewportHeight = true
scrollPane.border = BorderFactory.createCompoundBorder(
BorderFactory.createEmptyBorder(4, 0, 4, 0),
BorderFactory.createMatteBorder(1, 1, 1, 1, DynamicColor.Companion.BorderColor)
)
table.border = BorderFactory.createEmptyBorder()
val box = Box.createHorizontalBox()
box.add(addBtn)
box.add(Box.createHorizontalStrut(4))
box.add(editBtn)
box.add(Box.createHorizontalStrut(4))
box.add(deleteBtn)
add(scrollPane, BorderLayout.CENTER)
add(box, BorderLayout.SOUTH)
border = BorderFactory.createEmptyBorder(6, 8, 6, 8)
}
private fun initEvents() {
addBtn.addActionListener(object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
val dialog = LoginScriptDialog(owner)
dialog.isVisible = true
model.addRow(dialog.loginScript ?: return)
}
})
editBtn.addActionListener(object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
val dialog = LoginScriptDialog(owner, loginScripts[table.selectedRow])
dialog.isVisible = true
loginScripts[table.selectedRow] = dialog.loginScript ?: return
model.fireTableRowsUpdated(table.selectedRow, table.selectedRow)
}
})
deleteBtn.addActionListener(object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
val rows = table.selectedRows
if (rows.isEmpty()) return
rows.sortDescending()
for (row in rows) {
loginScripts.removeAt(row)
model.fireTableRowsDeleted(row, row)
}
}
})
table.selectionModel.addListSelectionListener {
deleteBtn.isEnabled = table.selectedRowCount > 0
editBtn.isEnabled = deleteBtn.isEnabled
}
}
private inner class LoginScriptDialog(
owner: Window,
var loginScript: LoginScript? = null
) : DialogWrapper(owner) {
private val formMargin = "4dlu"
private val expectTextField = OutlineTextField()
private val sendTextField = OutlineTextField()
private val regexToggleBtn = JToggleButton(Icons.regex)
.apply { toolTipText = I18n.getString("termora.regex") }
private val matchCaseToggleBtn = JToggleButton(Icons.matchCase)
.apply { toolTipText = I18n.getString("termora.match-case") }
init {
isModal = true
title = I18n.getString("termora.new-host.terminal.login-scripts")
controlsVisible = false
init()
pack()
size = Dimension(max(UIManager.getInt("Dialog.width") - 300, 250), preferredSize.height)
setLocationRelativeTo(owner)
val toolbar = FlatToolBar().apply { isFloatable = false }
toolbar.add(regexToggleBtn)
toolbar.add(matchCaseToggleBtn)
expectTextField.trailingComponent = toolbar
expectTextField.placeholderText = I18n.getString("termora.optional")
val script = loginScript
if (script != null) {
expectTextField.text = script.expect
sendTextField.text = script.send
matchCaseToggleBtn.isSelected = script.matchCase
regexToggleBtn.isSelected = script.regex
}
}
override fun doOKAction() {
if (sendTextField.text.isBlank()) {
sendTextField.outline = "error"
sendTextField.requestFocusInWindow()
return
}
loginScript = LoginScript(
expect = expectTextField.text,
send = sendTextField.text,
matchCase = matchCaseToggleBtn.isSelected,
regex = regexToggleBtn.isSelected,
)
super.doOKAction()
}
override fun doCancelAction() {
loginScript = null
super.doCancelAction()
}
override fun createCenterPanel(): JComponent {
val layout = FormLayout(
"left:pref, $formMargin, default:grow",
"pref, $formMargin, pref"
)
var rows = 1
val step = 2
return FormBuilder.create().layout(layout).padding("0dlu, $formMargin, $formMargin, $formMargin")
.add("${I18n.getString("termora.new-host.terminal.expect")}:").xy(1, rows)
.add(expectTextField).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.send")}:").xy(1, rows)
.add(sendTextField).xy(3, rows).apply { rows += step }
.build()
}
}
}

View File

@@ -27,7 +27,7 @@ class MultipleTerminalListener : TerminalPaintListener {
) {
val windowScope = AnActionEvent(terminalPanel, StringUtils.EMPTY, EventObject(terminalPanel))
.getData(DataProviders.WindowScope) ?: return
if (!MultipleAction.getInstance(windowScope).isSelected) return
if (MultipleAction.getInstance().isSelected(windowScope).not()) return
val oldFont = g.font
val colorPalette = terminal.getTerminalModel().getColorPalette()

View File

@@ -0,0 +1,218 @@
package app.termora
import app.termora.actions.StateAction
import app.termora.findeverywhere.FindEverywhereAction
import app.termora.plugin.internal.update.AppUpdateAction
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.extras.components.FlatToolBar
import com.formdev.flatlaf.util.SystemInfo
import java.awt.AWTEvent
import java.awt.Rectangle
import java.awt.event.*
import java.beans.PropertyChangeEvent
import java.beans.PropertyChangeListener
import javax.swing.*
internal class MyTermoraToolbar(private val windowScope: WindowScope, private val frame: TermoraFrame) : FlatToolBar() {
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
private val model get() = TermoraToolbarModel.getInstance()
private val actionManager get() = model.getActionManager()
private val toolbar get() = this
/**
* 一次性生命周期 每次刷新都会重置
*/
private var disposable = Disposer.newDisposable()
init {
initView()
initEvents()
refreshActions()
}
private fun initView() {
isFloatable = false
}
private fun initEvents() {
Disposer.register(windowScope, object : Disposable {
override fun dispose() {
Disposer.dispose(disposable)
}
})
// 监听全局事件
toolkit.addAWTEventListener(customizeToolBarAWTEventListener, AWTEvent.MOUSE_EVENT_MASK)
Disposer.register(windowScope, customizeToolBarAWTEventListener)
// 监听变化
model.addTermoraToolbarModelListener(object : TermoraToolbarModel.TermoraToolbarModelListener {
override fun onChanged() {
refreshActions()
}
}).let { Disposer.register(windowScope, it) }
// 监听窗口大小变动,然后修改边距避开控制按钮
if (SystemInfo.isWindows || SystemInfo.isLinux) {
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
adjust()
}
})
}
}
private fun refreshActions() {
Disposer.dispose(disposable)
disposable = Disposer.newDisposable()
removeAll()
add(JButton(object : AbstractAction() {
init {
putValue(SMALL_ICON, Icons.add)
}
override fun actionPerformed(evt: ActionEvent) {
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
}
}))
add(Box.createHorizontalGlue())
// update
add(redirectUpdateAction(disposable))
for (action in model.getActions()) {
if (action.visible.not()) continue
val action = actionManager.getAction(action.id) ?: continue
add(redirectAction(action, disposable))
}
if (SystemInfo.isWindows || SystemInfo.isLinux) {
adjust()
}
revalidate()
repaint()
}
private fun adjust() {
val rectangle = frame.rootPane.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
as? Rectangle ?: return
val right = rectangle.width
for (i in 0 until toolbar.componentCount) {
val c = toolbar.getComponent(i)
if (c.name == "spacing") {
if (c.width == right) {
return
}
toolbar.remove(i)
break
}
}
val spacing = Box.createHorizontalStrut(right)
spacing.name = "spacing"
toolbar.add(spacing)
}
private fun redirectUpdateAction(disposable: Disposable): AbstractButton {
val action = AppUpdateAction.getInstance()
val button = JButton(action.smallIcon)
button.isVisible = action.isEnabled
button.addActionListener(object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
action.actionPerformed(e)
}
})
val listener = object : PropertyChangeListener, Disposable {
override fun propertyChange(evt: PropertyChangeEvent) {
button.isVisible = action.isEnabled
}
override fun dispose() {
action.removePropertyChangeListener(this)
}
}
action.addPropertyChangeListener(listener)
Disposer.register(disposable, listener)
return button
}
private fun redirectAction(action: Action, disposable: Disposable): AbstractButton {
val button = if (action is StateAction) JToggleButton() else JButton()
button.toolTipText = action.getValue(Action.SHORT_DESCRIPTION) as? String
button.icon = action.getValue(Action.SMALL_ICON) as? Icon
button.addActionListener(object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
action.actionPerformed(e)
if (action is StateAction) {
button.isSelected = action.isSelected(windowScope)
}
}
})
val listener = object : PropertyChangeListener, Disposable {
override fun propertyChange(evt: PropertyChangeEvent) {
if (action is StateAction) {
button.isSelected = action.isSelected(windowScope)
}
}
override fun dispose() {
action.removePropertyChangeListener(this)
}
}
action.addPropertyChangeListener(listener)
Disposer.register(disposable, listener)
return button
}
/**
* 对着 ToolBar 右键
*/
private inner class CustomizeToolBarAWTEventListener : AWTEventListener, Disposable {
override fun eventDispatched(event: AWTEvent) {
if (event !is MouseEvent || event.id != MouseEvent.MOUSE_CLICKED
|| SwingUtilities.isRightMouseButton(event).not()
) return
// 如果 ToolBar 没有显示
if (toolbar.isShowing.not()) return
// 如果不是作用于在 ToolBar 上面
if (Rectangle(toolbar.locationOnScreen, toolbar.size).contains(event.locationOnScreen).not()) return
// 显示右键菜单
showContextMenu(event)
}
private fun showContextMenu(event: MouseEvent) {
val popupMenu = FlatPopupMenu()
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
val owner = windowScope.window
val dialog = CustomizeToolBarDialog(owner, windowScope, model)
dialog.setLocationRelativeTo(owner)
if (dialog.open()) {
model.setActions(dialog.getActions())
}
}
popupMenu.show(event.component, event.x, event.y)
}
override fun dispose() {
toolkit.removeAWTEventListener(this)
}
}
}

View File

@@ -1,6 +1,7 @@
package app.termora
import app.termora.actions.DataProviders
import app.termora.plugin.internal.AltKeyModifier
import app.termora.terminal.*
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
@@ -46,6 +47,9 @@ abstract class PtyHostTerminalTab(
// 开启 reader
startPtyConnectorReader()
// 修饰
terminalKeyModifiers()
// 启动命令
if (host.options.startupCommand.isNotBlank()) {
coroutineScope.launch(Dispatchers.IO) {
@@ -155,6 +159,15 @@ abstract class PtyHostTerminalTab(
ptyConnector.write(bytes)
}
open fun terminalKeyModifiers() {
val altModifier = host.options.extras["altModifier"]
if (altModifier == AltKeyModifier.CharactersPrecededByESC.name) {
terminalModel.setData(DataKey.AltModifier, AltKeyModifier.CharactersPrecededByESC)
} else {
terminalModel.setData(DataKey.AltModifier, AltKeyModifier.EightBit)
}
}
override fun canReconnect(): Boolean {
return true
}

View File

@@ -37,6 +37,8 @@ class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(own
preferredSize = size
minimumSize = size
rememberCheckBox.isVisible = host.isTemporary.not()
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
@@ -84,7 +86,7 @@ class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(own
switchPasswordComponent()
return FormBuilder.create().padding("$formMargin, $formMargin, $formMargin, $formMargin")
return FormBuilder.create().padding("1dlu, $formMargin, $formMargin, $formMargin")
.layout(layout)
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, 1)
.add(authenticationTypeComboBox).xy(3, 1)

View File

@@ -9,7 +9,7 @@ import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.UIManager
class SettingsDialog(owner: Window) : DialogWrapper(owner) {
internal class SettingsDialog(owner: Window) : DialogWrapper(owner) {
private val optionsPane = SettingsOptionsPane()
private val properties get() = DatabaseManager.getInstance().properties

View File

@@ -839,7 +839,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun p(): JPanel {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, 20dlu, pref, 4dlu, pref, 4dlu, pref, 4dlu, pref"
"pref, 20dlu, pref, 4dlu, pref, 4dlu, pref, 4dlu, pref, 4dlu, pref"
)
@@ -848,7 +848,7 @@ class SettingsOptionsPane : OptionsPane() {
val branch = if (Application.isUnknownVersion()) "main" else Application.getVersion()
return FormBuilder.create().padding("$FORM_MARGIN, $FORM_MARGIN, $FORM_MARGIN, $FORM_MARGIN")
val builder = FormBuilder.create().padding("$FORM_MARGIN, $FORM_MARGIN, $FORM_MARGIN, $FORM_MARGIN")
.layout(layout).debug(false)
.add(I18n.getString("termora.settings.about.termora", Application.getVersion()))
.xyw(1, rows, 3, "center, fill").apply { rows += step }
@@ -870,8 +870,14 @@ class SettingsOptionsPane : OptionsPane() {
"Open-source software"
)
).xy(3, rows).apply { rows += step }
.build()
if (I18n.isChinaMainland()) {
builder.add("交流群:").xy(1, rows)
.add(createHyperlink("https://www.termora.cn/muted/discussion-group", "Discussion Group"))
.xy(3, rows).apply { rows += step }
}
return builder.build()
}

View File

@@ -1,11 +1,13 @@
package app.termora
import app.termora.actions.TerminalFocusModeAction
import app.termora.database.DatabaseManager
import app.termora.terminal.*
import app.termora.terminal.panel.TerminalPanel
import app.termora.tlog.TerminalLoggerDataListener
import java.awt.Color
import javax.swing.UIManager
import kotlin.reflect.cast
class TerminalFactory private constructor() : Disposable {
private val terminals = mutableListOf<Terminal>()
@@ -75,6 +77,8 @@ class TerminalFactory private constructor() : Disposable {
override fun <T : Any> getData(key: DataKey<T>, defaultValue: T): T {
if (key == TerminalPanel.SelectCopy) {
return config.selectCopy as T
} else if (key == TerminalPanel.FocusMode) {
return key.clazz.cast(TerminalFocusModeAction.getInstance().isSelected)
}
return super.getData(key, defaultValue)
}

View File

@@ -133,8 +133,8 @@ class TerminalPanelFactory : Disposable {
return
}
val multipleAction = MultipleAction.getInstance(windowScope)
if (!multipleAction.isSelected) {
val multipleAction = MultipleAction.getInstance()
if (multipleAction.isSelected(windowScope).not()) {
ptyConnector.write(request.buffer)
return
}

View File

@@ -17,7 +17,6 @@ import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import org.apache.commons.lang3.StringUtils
import java.awt.*
import java.awt.event.AWTEventListener
import java.awt.event.ActionEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
@@ -28,13 +27,10 @@ import kotlin.math.min
class TerminalTabbed(
private val windowScope: WindowScope,
private val termoraToolBar: TermoraToolBar,
private val tabbedPane: FlatTabbedPane,
private val layout: TermoraLayout,
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager, DataProvider {
private val tabs = mutableListOf<TerminalTab>()
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
private val toolbar = termoraToolBar.getJToolBar()
private val actionManager = ActionManager.getInstance()
private val dataProviderSupport = DataProviderSupport()
private val appearance get() = DatabaseManager.getInstance().appearance
@@ -60,8 +56,6 @@ class TerminalTabbed(
tabbedPane.isTabsClosable = true
tabbedPane.tabType = FlatTabbedPane.TabType.card
tabbedPane.trailingComponent = toolbar
add(tabbedPane, BorderLayout.CENTER)
windowScope.getOrCreate(TerminalTabbedManager::class) { this }
@@ -72,7 +66,6 @@ class TerminalTabbed(
private fun initEvents() {
Disposer.register(this, customizeToolBarAWTEventListener)
// 关闭 tab
tabbedPane.setTabCloseCallback { _, i -> removeTabAt(i, true) }
@@ -146,9 +139,6 @@ class TerminalTabbed(
}
}).let { Disposer.register(this, it) }
// 监听全局事件
toolkit.addAWTEventListener(customizeToolBarAWTEventListener, AWTEvent.MOUSE_EVENT_MASK)
}
private fun removeTabAt(index: Int, disposable: Boolean = true) {
@@ -301,9 +291,7 @@ class TerminalTabbed(
// 关闭
val close = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close"))
close.addActionListener {
tabbedPane.tabCloseCallback?.accept(tabbedPane, tabIndex)
}
close.addActionListener { tabbedPane.tabCloseCallback?.accept(tabbedPane, tabIndex) }
// 关闭其他标签页
popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close-other-tabs")).addActionListener {
@@ -326,7 +314,7 @@ class TerminalTabbed(
close.isEnabled = tab.canClose()
rename.isEnabled = close.isEnabled
clone.isEnabled = close.isEnabled
edit.isEnabled = tab is HostTerminalTab && tab.host.id != "local"
edit.isEnabled = tab is HostTerminalTab && tab.host.id != "local" && tab.host.isTemporary.not()
openInNewWindow.isEnabled = close.isEnabled
// 如果不允许克隆
@@ -337,12 +325,7 @@ class TerminalTabbed(
if (close.isEnabled) {
popupMenu.addSeparator()
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
reconnect.addActionListener {
if (tabIndex > 0) {
tabs[tabIndex].reconnect()
}
}
reconnect.addActionListener { tabs[tabIndex].reconnect() }
reconnect.isEnabled = tabs[tabIndex].canReconnect()
}
@@ -384,60 +367,6 @@ class TerminalTabbed(
}
}
/**
* 对着 ToolBar 右键
*/
private inner class CustomizeToolBarAWTEventListener : AWTEventListener, Disposable {
override fun eventDispatched(event: AWTEvent) {
if (event !is MouseEvent || event.id != MouseEvent.MOUSE_CLICKED || !SwingUtilities.isRightMouseButton(event)) return
// 如果 ToolBar 没有显示
if (!toolbar.isShowing) return
// 如果不是作用于在 ToolBar 上面
if (!Rectangle(toolbar.locationOnScreen, toolbar.size).contains(event.locationOnScreen)) return
// 显示右键菜单
showContextMenu(event)
}
private fun showContextMenu(event: MouseEvent) {
val popupMenu = FlatPopupMenu()
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
val owner = SwingUtilities.getWindowAncestor(this@TerminalTabbed)
val dialog = CustomizeToolBarDialog(owner, windowScope, termoraToolBar)
dialog.setLocationRelativeTo(owner)
if (dialog.open()) {
TermoraToolBar.rebuild()
}
}
popupMenu.show(event.component, event.x, event.y)
}
override fun dispose() {
toolkit.removeAWTEventListener(this)
}
}
/*private inner class CustomizeToolBarDialog(owner: Window) : DialogWrapper(owner) {
init {
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
isModal = true
title = I18n.getString("termora.setting")
setLocationRelativeTo(null)
init()
}
override fun createCenterPanel(): JComponent {
val model = DefaultListModel<String>()
val checkBoxList = CheckBoxList(model)
checkBoxList.fixedCellHeight = UIManager.getInt("Tree.rowHeight")
model.addElement("Test")
return checkBoxList
}
}*/
private inner class SwitchFindEverywhereResult(
private val title: String,
private val icon: Icon?,

View File

@@ -107,7 +107,7 @@ class TermoraFencePanel(
label.foreground = UIManager.getColor("textInactiveText")
label.font = label.font.deriveFont(Font.BOLD)
// 与最后一个按钮对冲,使其宽度和谐
box.add(JButton(Icons.empty))
box.add(Box.createHorizontalStrut(24))
box.add(Box.createHorizontalGlue())
if (SystemInfo.isMacOS.not()) {
box.add(label)
@@ -153,6 +153,7 @@ class TermoraFencePanel(
override fun dispose() {
if (leftTreePanel.isVisible)
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
}

View File

@@ -1,13 +1,12 @@
package app.termora
import app.termora.actions.DataProvider
import app.termora.actions.DataProviderSupport
import app.termora.actions.DataProviders
import app.termora.actions.OpenHostAction
import app.termora.actions.*
import app.termora.findeverywhere.FindEverywhereProvider
import app.termora.findeverywhere.FindEverywhereProviderExtension
import app.termora.findeverywhere.FindEverywhereResult
import app.termora.keymap.KeyShortcut
import app.termora.keymap.KeymapManager
import app.termora.plugin.ExtensionManager
import app.termora.plugin.internal.extension.DynamicExtensionHandler
import app.termora.plugin.internal.ssh.SSHProtocolProvider
@@ -21,16 +20,12 @@ import com.formdev.flatlaf.util.SystemInfo
import com.jetbrains.JBR
import org.apache.commons.lang3.ArrayUtils
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionManager
import java.awt.*
import java.awt.event.*
import java.util.*
import javax.imageio.ImageIO
import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.JFrame
import javax.swing.*
import javax.swing.SwingUtilities.isEventDispatchThread
import javax.swing.UIManager
fun assertEventDispatchThread() {
@@ -45,22 +40,25 @@ class TermoraFrame : JFrame(), DataProvider {
private val id = UUID.randomUUID().toString()
private val windowScope = ApplicationScope.forWindowScope(this)
private val tabbedPane = MyTabbedPane().apply { tabHeight = titleBarHeight }
private val toolbar = TermoraToolBar(windowScope, this)
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane, layout)
private val toolbar = MyTermoraToolbar(windowScope, this)
private val terminalTabbed = TerminalTabbed(windowScope, tabbedPane, layout)
private val dataProviderSupport = DataProviderSupport()
private var notifyListeners = emptyArray<NotifyListener>()
private val moveMouseAdapter = createMoveMouseAdaptor()
private val keymapManager get() = KeymapManager.getInstance()
private val actionManager get() = ActionManager.getInstance()
private val dynamicExtensionHandler get() = DynamicExtensionHandler.getInstance()
init {
initView()
initEvents()
initKeymap()
}
private fun initEvents() {
if (SystemInfo.isLinux) {
toolbar.getJToolBar().addMouseListener(moveMouseAdapter)
toolbar.getJToolBar().addMouseMotionListener(moveMouseAdapter)
toolbar.addMouseListener(moveMouseAdapter)
toolbar.addMouseMotionListener(moveMouseAdapter)
} else if (SystemInfo.isMacOS) {
terminalTabbed.addMouseListener(moveMouseAdapter)
terminalTabbed.addMouseMotionListener(moveMouseAdapter)
@@ -68,12 +66,16 @@ class TermoraFrame : JFrame(), DataProvider {
tabbedPane.addMouseListener(moveMouseAdapter)
tabbedPane.addMouseMotionListener(moveMouseAdapter)
toolbar.getJToolBar().addMouseListener(moveMouseAdapter)
toolbar.getJToolBar().addMouseMotionListener(moveMouseAdapter)
toolbar.addMouseListener(moveMouseAdapter)
toolbar.addMouseMotionListener(moveMouseAdapter)
}
// 快捷键变动时重新监听
KeymapRefresher.getInstance().addRefreshListener { initKeymap() }
.let { Disposer.register(windowScope, it) }
// FindEverywhere
DynamicExtensionHandler.getInstance()
dynamicExtensionHandler
.register(FindEverywhereProviderExtension::class.java, object : FindEverywhereProviderExtension {
private val hostTreeModel get() = NewHostTreeModel.getInstance()
@@ -115,8 +117,7 @@ class TermoraFrame : JFrame(), DataProvider {
private val showMoreInfo get() = EnableManager.getInstance().isShowMoreInfo()
override fun actionPerformed(e: ActionEvent) {
ActionManager.getInstance()
.getAction(OpenHostAction.OPEN_HOST)
actionManager.getAction(OpenHostAction.OPEN_HOST)
?.actionPerformed(OpenHostActionEvent(e.source, host, e))
}
@@ -149,7 +150,6 @@ class TermoraFrame : JFrame(), DataProvider {
}
private fun initView() {
// macOS 要避开左边的控制栏
@@ -162,6 +162,8 @@ class TermoraFrame : JFrame(), DataProvider {
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
}
tabbedPane.trailingComponent = toolbar
val height = UIManager.getInt("TabbedPane.tabHeight") + tabbedPane.tabAreaInsets.top
if (SystemInfo.isWindows || SystemInfo.isLinux) {
@@ -209,6 +211,61 @@ class TermoraFrame : JFrame(), DataProvider {
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
}
private fun initKeymap() {
assertEventDispatchThread()
val keymap = keymapManager.getActiveKeymap()
val inputMap = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
val actionMap = rootPane.actionMap
// 移除之前所有的快捷键
inputMap.clear()
actionMap.clear()
for ((shortcut, actionIds) in keymap.getShortcuts()) {
if (shortcut !is KeyShortcut) continue
if (actionIds.contains(SwitchTabAction.SWITCH_TAB)) continue
registerKeyStroke(actionMap, inputMap, shortcut.keyStroke, actionIds)
}
for (shortcut in keymap.getShortcut(SwitchTabAction.SWITCH_TAB)) {
if (shortcut !is KeyShortcut) continue
registerKeyStroke(actionMap, inputMap, shortcut.keyStroke, listOf(SwitchTabAction.SWITCH_TAB))
}
}
private fun registerKeyStroke(
actionMap: ActionMap,
inputMap: InputMap,
keyStroke: KeyStroke,
actionIds: List<String>
) {
val keyShortcutActionId = "KeyShortcutAction_${randomUUID()}"
actionMap.put(keyShortcutActionId, redirectAction(actionIds))
inputMap.put(keyStroke, keyShortcutActionId)
}
private fun redirectAction(actionIds: List<String>): Action {
return object : AbstractAction() {
private val keyboardFocusManager get() = KeyboardFocusManager.getCurrentKeyboardFocusManager()
override fun actionPerformed(e: ActionEvent) {
var source = e.source
if (source == rootPane) {
val focusOwner = keyboardFocusManager.focusOwner
if (focusOwner is JComponent) {
source = focusOwner
}
}
for (actionId in actionIds) {
val action = actionManager.getAction(actionId) ?: continue
action.actionPerformed(RedirectAnActionEvent(source, e.actionCommand, EventQueue.getCurrentEvent()))
}
}
}
}
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return dataProviderSupport.getData(dataKey) ?: terminalTabbed.getData(dataKey)
}
@@ -356,6 +413,12 @@ class TermoraFrame : JFrame(), DataProvider {
}
private inner class RedirectAnActionEvent(
source: Any,
command: String,
event: EventObject
) : AnActionEvent(source, command, event)
private inner class GlassPane : JComponent() {
init {

View File

@@ -1,192 +0,0 @@
package app.termora
import app.termora.Application.ohMyJson
import app.termora.actions.*
import app.termora.database.DatabaseManager
import app.termora.findeverywhere.FindEverywhereAction
import app.termora.snippet.SnippetAction
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.util.SystemInfo
import kotlinx.serialization.Serializable
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionContainerFactory
import java.awt.Rectangle
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.Box
import javax.swing.JToolBar
@Serializable
data class ToolBarAction(
val id: String,
val visible: Boolean,
)
class TermoraToolBar(
private val windowScope: WindowScope,
private val frame: TermoraFrame,
) {
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 get() = DatabaseManager.getInstance().properties
private val toolbar by lazy { MyToolBar().apply { rebuild() } }
fun getJToolBar(): JToolBar {
return toolbar
}
/**
* 获取到所有的 Action
*/
fun getAllActions(): List<ToolBarAction> {
return listOf(
ToolBarAction(SnippetAction.SNIPPET, true),
ToolBarAction(Actions.SFTP, true),
ToolBarAction(Actions.TERMINAL_LOGGER, true),
ToolBarAction(Actions.MACRO, true),
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
ToolBarAction(Actions.KEY_MANAGER, true),
ToolBarAction(MultipleAction.MULTIPLE, true),
ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
ToolBarAction(SettingsAction.SETTING, true),
)
}
/**
* 获取到所有 Action会根据用户个性化排序/显示
*/
fun getActions(): List<ToolBarAction> {
val text = properties.getString(
"Termora.ToolBar.Actions",
StringUtils.EMPTY
)
val actions = getAllActions()
if (text.isBlank()) {
return actions
}
// 存储的 action
val storageActions = (ohMyJson.runCatching {
ohMyJson.decodeFromString<List<ToolBarAction>>(text)
}.getOrNull() ?: return actions).toMutableList()
for (action in actions) {
// 如果存储的 action 不包含这个,那么这个可能是新增的,新增的默认显示出来
if (storageActions.none { it.id == action.id }) {
storageActions.addFirst(ToolBarAction(action.id, true))
}
}
// 如果存储的 Action 在所有 Action 里没有,那么移除
storageActions.removeIf { e -> actions.none { e.id == it.id } }
return storageActions
}
private inner class MyToolBar : JToolBar() {
init {
// 监听窗口大小变动,然后修改边距避开控制按钮
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
adjust()
}
})
}
fun adjust() {
if (SystemInfo.isWindows || SystemInfo.isLinux) {
val rectangle =
frame.rootPane.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
as? Rectangle ?: return
val right = rectangle.width
val toolbar = this@MyToolBar
for (i in 0 until toolbar.componentCount) {
val c = toolbar.getComponent(i)
if (c.name == "spacing") {
if (c.width == right) {
return
}
toolbar.remove(i)
break
}
}
if (right > 0) {
val spacing = Box.createHorizontalStrut(right)
spacing.name = "spacing"
toolbar.add(spacing)
}
}
}
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

@@ -0,0 +1,103 @@
package app.termora
import app.termora.Application.ohMyJson
import app.termora.actions.ActionManager
import app.termora.actions.MultipleAction
import app.termora.actions.SettingsAction
import app.termora.database.DatabaseManager
import app.termora.findeverywhere.FindEverywhereAction
import app.termora.snippet.SnippetAction
import org.apache.commons.lang3.StringUtils
import java.util.*
import javax.swing.event.EventListenerList
internal class TermoraToolbarModel private constructor() {
companion object {
fun getInstance(): TermoraToolbarModel {
return ApplicationScope.forApplicationScope()
.getOrCreate(TermoraToolbarModel::class) { TermoraToolbarModel() }
}
}
private val properties get() = DatabaseManager.getInstance().properties
private val eventListener = EventListenerList()
fun getActionManager() = ActionManager.getInstance()
/**
* 获取到所有的 Action
*/
fun getAllActions(): List<ToolBarAction> {
return listOf(
ToolBarAction(SnippetAction.SNIPPET, true),
ToolBarAction(Actions.SFTP, true),
ToolBarAction(Actions.TERMINAL_LOGGER, true),
ToolBarAction(Actions.MACRO, true),
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
ToolBarAction(Actions.KEY_MANAGER, true),
ToolBarAction(MultipleAction.MULTIPLE, true),
ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
ToolBarAction(SettingsAction.SETTING, true),
)
}
/**
* 获取到所有 Action会根据用户个性化排序/显示
*/
fun getActions(): List<ToolBarAction> {
val text = properties.getString(
"Termora.ToolBar.Actions",
StringUtils.EMPTY
)
val actions = getAllActions()
if (text.isBlank()) {
return actions
}
// 存储的 action
val storageActions = (ohMyJson.runCatching {
ohMyJson.decodeFromString<List<ToolBarAction>>(text)
}.getOrNull() ?: return actions).toMutableList()
for (action in actions) {
// 如果存储的 action 不包含这个,那么这个可能是新增的,新增的默认显示出来
if (storageActions.none { it.id == action.id }) {
storageActions.addFirst(ToolBarAction(action.id, true))
}
}
// 如果存储的 Action 在所有 Action 里没有,那么移除
storageActions.removeIf { e -> actions.none { e.id == it.id } }
return storageActions
}
fun setActions(actions: List<ToolBarAction>) {
assertEventDispatchThread()
properties.putString("Termora.ToolBar.Actions", ohMyJson.encodeToString(actions))
for (listener in eventListener.getListeners(TermoraToolbarModelListener::class.java)) {
listener.onChanged()
}
}
fun addTermoraToolbarModelListener(listener: TermoraToolbarModelListener): Disposable {
eventListener.add(TermoraToolbarModelListener::class.java, listener)
return object : Disposable {
override fun dispose() {
removeTermoraToolbarModelListener(listener)
}
}
}
fun removeTermoraToolbarModelListener(listener: TermoraToolbarModelListener) {
eventListener.remove(TermoraToolbarModelListener::class.java, listener)
}
interface TermoraToolbarModelListener : EventListener {
fun onChanged()
}
}

View File

@@ -0,0 +1,9 @@
package app.termora
import kotlinx.serialization.Serializable
@Serializable
data class ToolBarAction(
val id: String,
val visible: Boolean,
)

View File

@@ -86,8 +86,8 @@ class UpdaterManager private constructor() {
return LatestVersion.self
}
val text = response.use { resp -> resp.body?.use { it.string() } }
if (text.isNullOrBlank()) {
val text = response.use { resp -> resp.body.use { it.string() } }
if (text.isBlank()) {
return LatestVersion.self
}

View File

@@ -3,7 +3,6 @@ package app.termora.account
import app.termora.AES
import app.termora.database.*
import app.termora.database.Data.Companion.toData
import okhttp3.internal.EMPTY_BYTE_ARRAY
import org.apache.commons.codec.binary.Base64
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.lang3.ObjectUtils
@@ -88,7 +87,7 @@ abstract class SyncService {
return accountManager.getSecretKey()
}
val team = accountManager.getTeams().firstOrNull { it.id == ownerId }
return team?.secretKey ?: EMPTY_BYTE_ARRAY
return team?.secretKey ?: byteArrayOf()
}
protected fun decryptData(id: String, data: String, ownerId: String): String {

View File

@@ -28,19 +28,20 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
private fun registerActions() {
addAction(NewWindowAction.NEW_WINDOW, NewWindowAction())
addAction(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
addAction(QuickConnectAction.QUICK_CONNECT, QuickConnectAction.instance)
addAction(Actions.APP_UPDATE, AppUpdateAction.getInstance())
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
addAction(Actions.SFTP, TransferAnAction())
addAction(SFTPCommandAction.SFTP_COMMAND, SFTPCommandAction())
addAction(MultipleAction.MULTIPLE, MultipleAction.getInstance())
addAction(SnippetAction.SNIPPET, SnippetAction.getInstance())
addAction(Actions.MACRO, MacroAction())
addAction(Actions.KEY_MANAGER, KeyManagerAction())
addAction(SwitchTabAction.SWITCH_TAB, SwitchTabAction())
addAction(TabReconnectAction.RECONNECT_TAB, TabReconnectAction())
addAction(SettingsAction.SETTING, SettingsAction())
addAction(SettingsAction.SETTING, SettingsAction.getInstance())
addAction(NewHostAction.NEW_HOST, NewHostAction())
addAction(OpenHostAction.OPEN_HOST, OpenHostAction())
@@ -52,6 +53,7 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
addAction(TerminalClearScreenAction.CLEAR_SCREEN, TerminalClearScreenAction())
addAction(OpenLocalTerminalAction.LOCAL_TERMINAL, OpenLocalTerminalAction())
addAction(TerminalSelectAllAction.SELECT_ALL, TerminalSelectAllAction())
addAction(TerminalFocusModeAction.FocusMode, TerminalFocusModeAction.getInstance())
addAction(TerminalZoomInAction.ZOOM_IN, TerminalZoomInAction())
addAction(TerminalZoomOutAction.ZOOM_OUT, TerminalZoomOutAction())

View File

@@ -1,272 +0,0 @@
package app.termora.actions
import app.termora.*
import app.termora.Application.httpClient
import com.formdev.flatlaf.util.SystemInfo
import com.sun.jna.platform.win32.Advapi32
import com.sun.jna.platform.win32.WinError
import com.sun.jna.platform.win32.WinNT
import com.sun.jna.platform.win32.WinReg
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import okhttp3.Request
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.JXEditorPane
import org.semver4j.Semver
import org.slf4j.LoggerFactory
import java.awt.Dimension
import java.awt.KeyboardFocusManager
import java.io.File
import java.net.ProxySelector
import java.net.URI
import java.util.*
import java.util.concurrent.TimeUnit
import javax.swing.BorderFactory
import javax.swing.JOptionPane
import javax.swing.JScrollPane
import javax.swing.UIManager
import javax.swing.event.HyperlinkEvent
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
class AppUpdateAction private constructor() : AnAction(
StringUtils.EMPTY,
Icons.ideUpdate
) {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Swing)
companion object {
private val log = LoggerFactory.getLogger(AppUpdateAction::class.java)
private const val PKG_FILE_KEY = "pkgFile"
fun getInstance(): AppUpdateAction {
return ApplicationScope.forApplicationScope().getOrCreate(AppUpdateAction::class) { AppUpdateAction() }
}
}
private val updaterManager get() = UpdaterManager.getInstance()
private var isRemindMeNextTime = false
init {
isEnabled = false
scheduleUpdate()
}
override fun actionPerformed(evt: AnActionEvent) {
showUpdateDialog()
}
private fun scheduleUpdate() {
coroutineScope.launch(Dispatchers.IO) {
// 启动 3 分钟后才是检查
if (Application.isUnknownVersion().not()) {
delay(3.minutes)
}
while (coroutineScope.isActive) {
// 下次提醒我
if (isRemindMeNextTime) break
try {
checkUpdate()
} catch (e: Exception) {
if (log.isWarnEnabled) {
log.warn(e.message, e)
}
}
// 之后每 3 小时检查一次
delay(3.hours.inWholeMilliseconds)
}
}
}
private suspend fun checkUpdate() {
val latestVersion = updaterManager.fetchLatestVersion()
if (latestVersion.isSelf) {
return
}
// 之所以放到后面检查是不是开发版本,是需要发起一次检测请求,以方便调试
if (Application.isUnknownVersion()) {
return
}
val newVersion = Semver.parse(latestVersion.version) ?: return
val version = Semver.parse(Application.getVersion()) ?: return
if (newVersion <= version) {
return
}
try {
downloadLatestPkg(latestVersion)
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
withContext(Dispatchers.Swing) { isEnabled = true }
}
private suspend fun downloadLatestPkg(latestVersion: UpdaterManager.LatestVersion) {
if (SystemInfo.isLinux) return
super.putValue(PKG_FILE_KEY, null)
val arch = if (SystemInfo.isAARCH64) "aarch64" else "x86-64"
val osName = if (SystemInfo.isWindows) "windows" else "osx"
val suffix = if (SystemInfo.isWindows) "exe" else "dmg"
val filename = "termora-${latestVersion.version}-${osName}-${arch}.${suffix}"
val asset = latestVersion.assets.find { it.name == filename } ?: return
val response = httpClient
.newBuilder()
.callTimeout(15, TimeUnit.MINUTES)
.readTimeout(15, TimeUnit.MINUTES)
.proxySelector(ProxySelector.getDefault())
.build()
.newCall(Request.Builder().url(asset.downloadUrl).build())
.execute()
if (response.isSuccessful.not()) {
if (log.isErrorEnabled) {
log.warn("Failed to download latest version ${latestVersion.version}, response code ${response.code}")
}
IOUtils.closeQuietly(response)
return
}
val body = response.body
val input = body?.byteStream()
val file = FileUtils.getFile(Application.getTemporaryDir(), "${UUID.randomUUID()}-${filename}")
val output = file.outputStream()
val downloaded = runCatching { IOUtils.copy(input, output) }.isSuccess
IOUtils.closeQuietly(input, output, body, response)
if (!downloaded) {
if (log.isErrorEnabled) {
log.error("Failed to download latest version to $filename")
}
return
}
if (log.isInfoEnabled) {
log.info("Successfully downloaded latest version to $file")
}
withContext(Dispatchers.Swing) { setLatestPkgFile(file) }
}
private fun setLatestPkgFile(file: File) {
putValue(PKG_FILE_KEY, file)
}
private fun getLatestPkgFile(): File? {
return getValue(PKG_FILE_KEY) as? File
}
private fun showUpdateDialog() {
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
val lastVersion = updaterManager.lastVersion
val editorPane = JXEditorPane()
editorPane.contentType = "text/html"
editorPane.text = lastVersion.htmlBody
editorPane.isEditable = false
editorPane.addHyperlinkListener {
if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) {
Application.browse(it.url.toURI())
}
}
editorPane.background = DynamicColor("window")
val scrollPane = JScrollPane(editorPane)
scrollPane.border = BorderFactory.createEmptyBorder()
scrollPane.preferredSize = Dimension(
UIManager.getInt("Dialog.width") - 100,
UIManager.getInt("Dialog.height") - 100
)
val option = OptionPane.showConfirmDialog(
owner,
scrollPane,
title = I18n.getString("termora.update.title"),
messageType = JOptionPane.PLAIN_MESSAGE,
optionType = JOptionPane.YES_NO_CANCEL_OPTION,
options = arrayOf(
I18n.getString("termora.update.update"),
I18n.getString("termora.update.ignore"),
I18n.getString("termora.cancel")
),
initialValue = I18n.getString("termora.update.update")
)
if (option == JOptionPane.CANCEL_OPTION) {
return
} else if (option == JOptionPane.NO_OPTION) {
isEnabled = false
isRemindMeNextTime = true
} else if (option == JOptionPane.YES_OPTION) {
updateSelf(lastVersion)
}
}
private fun updateSelf(latestVersion: UpdaterManager.LatestVersion) {
val file = getLatestPkgFile()
if (SystemInfo.isLinux || file == null) {
isEnabled = false
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${latestVersion.version}"))
return
}
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
val commands = if (SystemInfo.isMacOS) listOf("open", "-n", file.absolutePath)
// 如果安装过,那么直接静默安装和自动启动
else if (isAppInstalled()) listOf(
file.absolutePath,
"/SILENT",
"/AUTOSTART",
"/NORESTART",
"/FORCECLOSEAPPLICATIONS"
)
// 没有安装过 则打开安装向导
else listOf(file.absolutePath)
if (log.isInfoEnabled) {
log.info("restart {}", commands.joinToString(StringUtils.SPACE))
}
TermoraRestarter.getInstance().scheduleRestart(owner, true, commands)
}
private fun isAppInstalled(): Boolean {
val keyPath = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${Application.getName()}_is1"
val phkKey = WinReg.HKEYByReference()
// 尝试打开注册表键
val result = Advapi32.INSTANCE.RegOpenKeyEx(
WinReg.HKEY_LOCAL_MACHINE,
keyPath,
0,
WinNT.KEY_READ,
phkKey
)
if (result == WinError.ERROR_SUCCESS) {
// 键存在,关闭句柄
Advapi32.INSTANCE.RegCloseKey(phkKey.getValue())
return true
} else {
// 键不存在或无权限
return false
}
}
}

View File

@@ -1,14 +1,11 @@
package app.termora.actions
import app.termora.I18n
import app.termora.Icons
import app.termora.TerminalPanelFactory
import app.termora.WindowScope
import app.termora.*
class MultipleAction private constructor() : AnAction(
I18n.getString("termora.tools.multiple"),
Icons.vcs
) {
), StateAction {
companion object {
@@ -17,8 +14,8 @@ class MultipleAction private constructor() : AnAction(
*/
const val MULTIPLE = "MultipleAction"
fun getInstance(windowScope: WindowScope): MultipleAction {
return windowScope.getOrCreate(MultipleAction::class) { MultipleAction() }
fun getInstance(): MultipleAction {
return ApplicationScope.forApplicationScope().getOrCreate(MultipleAction::class) { MultipleAction() }
}
}
@@ -27,6 +24,24 @@ class MultipleAction private constructor() : AnAction(
}
override fun actionPerformed(evt: AnActionEvent) {
super.setSelected(false)
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
setSelected(windowScope, isSelected(windowScope).not())
TerminalPanelFactory.getInstance().repaintAll()
}
override fun isSelected(): Boolean {
throw UnsupportedOperationException()
}
override fun isSelected(windowScope: WindowScope): Boolean {
return windowScope.getBoolean("MultipleAction.isSelected", false)
}
override fun setSelected(windowScope: WindowScope, selected: Boolean) {
windowScope.putBoolean("MultipleAction.isSelected", selected)
putValue("MultipleAction.isSelected", selected)
}
}

View File

@@ -38,7 +38,7 @@ class OpenHostAction : AnAction() {
if (providers.first { StringUtils.equalsIgnoreCase(it.getProtocol(), host.protocol) }
.isTransfer()) {
ActionManager.getInstance().getAction(Actions.SFTP)
.actionPerformed(TransferActionEvent(evt.source, evt.host.id, evt.event))
.actionPerformed(TransferActionEvent(evt.source, host, evt.event))
return
}

View File

@@ -0,0 +1,178 @@
package app.termora.actions
import app.termora.*
import app.termora.Application.ohMyJson
import app.termora.OptionsPane.Companion.FORM_MARGIN
import app.termora.database.DatabaseManager
import app.termora.protocol.ProtocolProvider
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import kotlinx.serialization.Serializable
import org.apache.commons.lang3.exception.ExceptionUtils
import java.awt.Dimension
import java.awt.Window
import java.net.URI
import java.util.*
import javax.swing.*
class QuickConnectAction private constructor() : AnAction(I18n.getString("termora.actions.quick-connect"), Icons.find) {
companion object {
const val QUICK_CONNECT = "QuickConnectAction"
val instance = QuickConnectAction()
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.quick-connect"))
}
override fun actionPerformed(evt: AnActionEvent) {
val scope = evt.getData(DataProviders.WindowScope) ?: return
val dialog = QuickConnectDialog(scope.window)
dialog.isVisible = true
}
private class QuickConnectDialog(owner: Window) : DialogWrapper(owner) {
private val properties get() = DatabaseManager.getInstance().properties
private val hostComboBox = OutlineComboBox<String>()
private val usernameTextField = OutlineTextField(256)
private val passwordTextField = OutlinePasswordField(256)
init {
isModal = true
title = I18n.getString("termora.actions.quick-connect")
isResizable = false
init()
pack()
size = Dimension(UIManager.getInt("Dialog.width") - 250, preferredSize.height)
setLocationRelativeTo(owner)
}
override fun createCenterPanel(): JComponent {
hostComboBox.isEditable = true
hostComboBox.placeholderText = "ssh://127.0.0.1:22"
val histories = getHistories()
for (history in histories) {
if (histories.first() == history) {
usernameTextField.text = history.host.username
passwordTextField.text = history.host.authentication.password
}
hostComboBox.addItem(history.url)
}
usernameTextField.placeholderText = I18n.getString("termora.new-host.general.username")
passwordTextField.placeholderText = I18n.getString("termora.new-host.general.password")
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
return FormBuilder.create().layout(layout)
.border(BorderFactory.createEmptyBorder(0, 8, 8, 8))
.add("${I18n.getString("termora.new-host.general.protocol")}:").xy(1, 1)
.add(hostComboBox).xy(3, 1)
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, 3)
.add(usernameTextField).xy(3, 3)
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, 5)
.add(passwordTextField).xy(3, 5)
.build()
}
override fun doOKAction() {
val host = hostComboBox.selectedItem as? String
if (host.isNullOrBlank()) {
hostComboBox.requestFocusInWindow()
return
}
val historyHost: HistoryHost
try {
historyHost = getHistoryHost(host.trim())
} catch (e: Exception) {
hostComboBox.requestFocusInWindow()
OptionPane.showMessageDialog(
this,
e.message ?: ExceptionUtils.getRootCauseMessage(e),
messageType = JOptionPane.ERROR_MESSAGE
)
return
}
val action = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
if (action is OpenHostAction) {
SwingUtilities.invokeLater {
action.actionPerformed(OpenHostActionEvent(this, historyHost.host, EventObject(this)))
}
}
super.doOKAction()
}
override fun createOkAction(): AbstractAction {
return OkAction(I18n.getString("termora.welcome.contextmenu.connect"))
}
private fun getHistoryHost(host: String): HistoryHost {
val uri = URI.create(host)
val protocolProvider = ProtocolProvider.valueOf(uri.scheme)
if (protocolProvider == null) {
throw UnsupportedOperationException(I18n.getString("termora.protocol.not-supported", uri.scheme))
}
val historyHost = HistoryHost(
host, Host(
name = uri.host,
protocol = uri.scheme,
host = uri.host,
port = uri.port,
username = usernameTextField.text.trim(),
authentication = Authentication.No.copy(
type = AuthenticationType.Password,
password = String(passwordTextField.password)
),
options = Options.Default.copy(
extras = mutableMapOf("Temporary" to "true")
)
)
)
val histories = getHistories().toMutableList()
histories.removeIf { it.url == host }
histories.addFirst(historyHost)
if (histories.size > 20) {
histories.removeLast()
}
properties.putString("QuickConnect.historyHosts", ohMyJson.encodeToString(histories))
return historyHost
}
private fun getHistories(): List<HistoryHost> {
val text = properties.getString("QuickConnect.historyHosts", "[]")
return ohMyJson.runCatching { ohMyJson.decodeFromString<List<HistoryHost>>(text) }
.getOrNull() ?: emptyList()
}
override fun addNotify() {
super.addNotify()
controlsVisible = false
}
}
@Serializable
private data class HistoryHost(
val url: String,
val host: Host,
)
}

View File

@@ -11,19 +11,25 @@ import java.awt.event.ActionEvent
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
class SettingsAction : AnAction(
class SettingsAction private constructor() : AnAction(
I18n.getString("termora.setting"),
Icons.settings
) {
companion object {
/**
* 打开设置
*/
const val SETTING = "SettingAction"
fun getInstance(): SettingsAction {
return ApplicationScope.forApplicationScope().getOrCreate(SettingsAction::class) { SettingsAction() }
}
}
private var isShowing = false
private val action get() = this
init {
FlatDesktop.setPreferencesHandler {
@@ -36,20 +42,25 @@ class SettingsAction : AnAction(
}
override fun actionPerformed(evt: AnActionEvent) {
if (isShowing) {
return
if (isShowing) return
showSettingsDialog(evt)
}
private fun showSettingsDialog(evt: AnActionEvent) {
isShowing = true
val owner = evt.window
val dialog = SettingsDialog(owner)
dialog.addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
this@SettingsAction.isShowing = false
action.isShowing = false
}
})
dialog.setLocationRelativeTo(owner)
dialog.isVisible = true
}
}

View File

@@ -0,0 +1,8 @@
package app.termora.actions
import app.termora.WindowScope
interface StateAction {
fun isSelected(windowScope: WindowScope): Boolean
fun setSelected(windowScope: WindowScope, selected: Boolean)
}

View File

@@ -0,0 +1,37 @@
package app.termora.actions
import app.termora.ApplicationScope
import app.termora.EnableManager
import app.termora.I18n
import app.termora.Icons
import org.slf4j.LoggerFactory
class TerminalFocusModeAction private constructor() : AnAction(
I18n.getString("termora.actions.focus-mode"),
Icons.eye
) {
companion object {
const val FocusMode = "TerminalFocusMode"
private val log = LoggerFactory.getLogger(TerminalFocusModeAction::class.java)
fun getInstance(): TerminalFocusModeAction {
return ApplicationScope.forApplicationScope()
.getOrCreate(TerminalFocusModeAction::class) { TerminalFocusModeAction() }
}
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.focus-mode"))
putValue(ACTION_COMMAND_KEY, FocusMode)
setStateAction()
isSelected = enableManager.getFlag("Terminal.FocusMode", false)
}
private val enableManager get() = EnableManager.getInstance()
override fun actionPerformed(evt: AnActionEvent) {
enableManager.setFlag("Terminal.FocusMode", isSelected)
}
}

View File

@@ -504,6 +504,8 @@ class DatabaseManager private constructor() : Disposable {
protected open fun putString(key: String, value: String) {
databaseManager.setSetting("${name}.$key", value)
// 触发变动
DatabasePropertiesChangedExtension.onPropertyChanged(name, key, value)
}

View File

@@ -0,0 +1,38 @@
package app.termora.database
import app.termora.database.DatabaseManager.Companion.log
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionManager
import javax.swing.SwingUtilities
internal interface DatabasePropertiesChangedExtension : Extension {
companion object {
fun onPropertyChanged(name: String, key: String, value: String) {
if (SwingUtilities.isEventDispatchThread()) {
for (extension in ExtensionManager.getInstance()
.getExtensions(DatabasePropertiesChangedExtension::class.java)) {
try {
extension.onPropertyChanged(name, key, value)
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
}
} else {
SwingUtilities.invokeLater { onPropertyChanged(name, key, value) }
}
}
}
/**
* 属性数据变动
*
* @param name 属性名
* @param key key
*/
fun onPropertyChanged(name: String, key: String, value: String)
}

View File

@@ -5,6 +5,7 @@ import app.termora.I18n
import app.termora.Scope
import app.termora.WindowScope
import app.termora.actions.MultipleAction
import app.termora.actions.TerminalFocusModeAction
import org.jdesktop.swingx.action.ActionManager
@@ -13,6 +14,7 @@ class QuickActionsFindEverywhereProvider(private val windowScope: WindowScope) :
Actions.KEY_MANAGER,
Actions.KEYWORD_HIGHLIGHT,
MultipleAction.MULTIPLE,
TerminalFocusModeAction.FocusMode,
)
override fun find(pattern: String, scope: Scope): List<FindEverywhereResult> {
@@ -22,9 +24,7 @@ class QuickActionsFindEverywhereProvider(private val windowScope: WindowScope) :
for (action in actions) {
val ac = actionManager.getAction(action)
if (ac == null) {
if (action == MultipleAction.MULTIPLE) {
results.add(ActionFindEverywhereResult(MultipleAction.getInstance(windowScope)))
}
continue
} else {
results.add(ActionFindEverywhereResult(ac))
}

View File

@@ -6,6 +6,7 @@ import app.termora.Icons
import app.termora.Scope
import app.termora.actions.NewHostAction
import app.termora.actions.OpenLocalTerminalAction
import app.termora.actions.QuickConnectAction
import app.termora.snippet.SnippetAction
import com.formdev.flatlaf.FlatLaf
import org.jdesktop.swingx.action.ActionManager
@@ -19,19 +20,13 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
actionManager.let { list.add(CreateHostFindEverywhereResult()) }
// Local terminal
actionManager.getAction(OpenLocalTerminalAction.LOCAL_TERMINAL)?.let {
list.add(ActionFindEverywhereResult(it))
}
actionManager.getAction(OpenLocalTerminalAction.LOCAL_TERMINAL)?.let { list.add(ActionFindEverywhereResult(it)) }
// Snippet
actionManager.getAction(SnippetAction.SNIPPET)?.let {
list.add(ActionFindEverywhereResult(it))
}
actionManager.getAction(SnippetAction.SNIPPET)?.let { list.add(ActionFindEverywhereResult(it)) }
// SFTP
actionManager.getAction(Actions.SFTP)?.let {
list.add(ActionFindEverywhereResult(it))
}
actionManager.getAction(Actions.SFTP)?.let { list.add(ActionFindEverywhereResult(it)) }
// quick connect
actionManager.getAction(QuickConnectAction.QUICK_CONNECT)?.let { list.add(ActionFindEverywhereResult(it)) }
return list
}

View File

@@ -1,16 +1,21 @@
package app.termora.highlight
import app.termora.*
import app.termora.Application.ohMyJson
import app.termora.account.AccountOwner
import app.termora.terminal.TerminalColor
import com.formdev.flatlaf.extras.components.FlatTable
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.exception.ExceptionUtils
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Component
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.io.File
import java.nio.charset.StandardCharsets
import javax.swing.*
import javax.swing.border.EmptyBorder
import javax.swing.table.DefaultTableCellRenderer
@@ -29,7 +34,8 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
private val deleteBtn = JButton(I18n.getString("termora.remove"))
private val importBtn = JButton(I18n.getString("termora.keymgr.import"))
private val exportBtn = JButton(I18n.getString("termora.keymgr.export"))
init {
initView()
@@ -213,6 +219,29 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
deleteBtn.isEnabled = editBtn.isEnabled
}
exportBtn.addActionListener {
val fileChooser = FileChooser()
fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY
fileChooser.win32Filters.add(Pair("All files", listOf("*")))
fileChooser.showSaveDialog(owner, "highlights.json").thenAccept { file ->
file?.outputStream()?.use {
val highlights = keywordHighlightManager.getKeywordHighlights(accountOwner.id)
.map { e -> e.copy(id = randomUUID()) }
IOUtils.write(ohMyJson.encodeToString(highlights), it, StandardCharsets.UTF_8)
}
}
}
importBtn.addActionListener {
val chooser = FileChooser()
chooser.osxAllowedFileTypes = listOf("json")
chooser.allowsMultiSelection = false
chooser.win32Filters.add(Pair("JSON files", listOf("json")))
chooser.fileSelectionMode = JFileChooser.FILES_ONLY
chooser.showOpenDialog(owner)
.thenAccept { if (it.isNotEmpty()) SwingUtilities.invokeLater { importKeywordHighlights(it.first()) } }
}
Disposer.register(this, object : Disposable {
override fun dispose() {
terminal.close()
@@ -220,6 +249,23 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
})
}
private fun importKeywordHighlights(file: File) {
try {
val highlights = ohMyJson.decodeFromString<List<KeywordHighlight>>(file.readText())
.map { it.copy(id = randomUUID()) }
for (highlight in highlights) {
keywordHighlightManager.addKeywordHighlight(highlight, accountOwner)
model.fireTableRowsInserted(model.rowCount - 1, model.rowCount)
}
} catch (e: Exception) {
OptionPane.showMessageDialog(
owner,
message = e.message ?: ExceptionUtils.getRootCauseMessage(e),
messageType = JOptionPane.ERROR_MESSAGE,
)
}
}
private fun createCenterPanel(): JComponent {
val panel = JPanel(BorderLayout())
@@ -232,13 +278,15 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
val formMargin = "4dlu"
val layout = FormLayout(
"default:grow",
"pref, $formMargin, pref, $formMargin, pref"
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
)
panel.add(
FormBuilder.create().layout(layout).padding(EmptyBorder(0, 12, 0, 0))
.add(addBtn).xy(1, rows).apply { rows += step }
.add(editBtn).xy(1, rows).apply { rows += step }
.add(deleteBtn).xy(1, rows).apply { rows += step }
.add(importBtn).xy(1, rows).apply { rows += step }
.add(exportBtn).xy(1, rows).apply { rows += step }
.build(),
BorderLayout.EAST)

View File

@@ -1,27 +1,14 @@
package app.termora.keymap
import app.termora.ApplicationScope
import app.termora.DialogWrapper
import app.termora.Disposable
import app.termora.SwingUtils
import app.termora.account.AccountManager
import app.termora.actions.AnActionEvent
import app.termora.database.Data
import app.termora.database.DataType
import app.termora.database.DatabaseManager
import app.termora.database.OwnerType
import com.formdev.flatlaf.util.SystemInfo
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionManager
import org.slf4j.LoggerFactory
import java.awt.Container
import java.awt.KeyEventDispatcher
import java.awt.KeyboardFocusManager
import java.awt.event.KeyEvent
import javax.swing.JComponent
import javax.swing.JDialog
import javax.swing.JPopupMenu
import javax.swing.KeyStroke
class KeymapManager private constructor() : Disposable {
@@ -34,17 +21,13 @@ class KeymapManager private constructor() : Disposable {
}
}
private val keymapKeyEventDispatcher = KeymapKeyEventDispatcher()
private val database get() = DatabaseManager.getInstance()
private val properties get() = DatabaseManager.getInstance().properties
private val keymaps = linkedMapOf<String, Keymap>()
private val accountManager get() = AccountManager.getInstance()
private val activeKeymap get() = properties.getString("Keymap.Active")
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
init {
keyboardFocusManager.addKeyEventDispatcher(keymapKeyEventDispatcher)
try {
for (data in database.rawData(DataType.Keymap)) {
try {
@@ -63,13 +46,8 @@ class KeymapManager private constructor() : Disposable {
}
}
MacOSKeymap.getInstance().let {
keymaps[it.name] = it
}
WindowsKeymap.getInstance().let {
keymaps[it.name] = it
}
MacOSKeymap.getInstance().let { keymaps[it.name] = it }
WindowsKeymap.getInstance().let { keymaps[it.name] = it }
}
@@ -102,7 +80,7 @@ class KeymapManager private constructor() : Disposable {
keymaps.putFirst(keymap.name, keymap)
val accountId = accountManager.getAccountId()
database.save(
database.saveAndIncrementVersion(
Data(
id = keymap.id,
ownerId = accountId,
@@ -122,84 +100,4 @@ class KeymapManager private constructor() : Disposable {
database.delete(id, DataType.Keymap.name)
}
private inner class KeymapKeyEventDispatcher : KeyEventDispatcher {
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
if (e.isConsumed || e.id != KeyEvent.KEY_PRESSED || e.modifiersEx == 0) {
return false
}
val keyStroke = KeyStroke.getKeyStrokeForEvent(e)
val component = e.source
if (component is JComponent) {
// 如果这个键已经被组件注册了,那么忽略
if (getConditionForKeyStroke(component, keyStroke) != JComponent.UNDEFINED_CONDITION) {
return false
}
}
val shortcuts = getActiveKeymap()
val actionIds = shortcuts.getActionIds(KeyShortcut(keyStroke))
if (actionIds.isEmpty()) {
return false
}
val focusedWindow = keyboardFocusManager.focusedWindow
if (focusedWindow is DialogWrapper) {
if (!focusedWindow.processGlobalKeymap) {
return false
}
} else if (focusedWindow is JDialog) {
return false
}
// 如果当前有 Popup ,那么不派发事件
val c = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
if (c is Container) {
val popups: List<JPopupMenu> = SwingUtils.getDescendantsOfType(
JPopupMenu::class.java,
c, true
)
if (popups.isNotEmpty()) {
return false
}
}
val evt = AnActionEvent(e.source, StringUtils.EMPTY, e)
for (actionId in actionIds) {
val action = ActionManager.getInstance().getAction(actionId) ?: continue
if (!action.isEnabled) {
continue
}
action.actionPerformed(evt)
if (evt.isConsumed) {
return true
}
}
return false
}
private fun getConditionForKeyStroke(c: JComponent, keyStroke: KeyStroke): Int {
val condition = c.getConditionForKeyStroke(keyStroke)
// 如果这个键已经被组件注册了,那么忽略
if (condition != JComponent.UNDEFINED_CONDITION) {
return condition
}
if (c.parent is JComponent) {
return getConditionForKeyStroke(c.parent as JComponent, keyStroke)
}
return JComponent.UNDEFINED_CONDITION
}
}
override fun dispose() {
keyboardFocusManager.removeKeyEventDispatcher(keymapKeyEventDispatcher)
}
}

View File

@@ -2,16 +2,17 @@ package app.termora.plugin
import app.termora.Application
import app.termora.ApplicationScope
import app.termora.FramePlugin
import app.termora.account.AccountPlugin
import app.termora.plugin.internal.badge.BadgePlugin
import app.termora.plugin.internal.extension.DynamicExtensionPlugin
import app.termora.plugin.internal.local.LocalInternalPlugin
import app.termora.plugin.internal.plugin.PluginInternalPlugin
import app.termora.plugin.internal.rdp.RDPInternalPlugin
import app.termora.plugin.internal.serial.SerialInternalPlugin
import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin
import app.termora.plugin.internal.ssh.SSHInternalPlugin
import app.termora.plugin.internal.telnet.TelnetInternalPlugin
import app.termora.plugin.internal.update.UpdatePlugin
import app.termora.plugin.internal.wsl.WSLInternalPlugin
import app.termora.swingCoroutineScope
import app.termora.terminal.panel.vw.FloatingToolbarPlugin
@@ -109,6 +110,10 @@ internal class PluginManager private constructor() {
plugins.add(PluginDescriptor(AccountPlugin(), origin = PluginOrigin.Internal, version = version))
// badge plugin
plugins.add(PluginDescriptor(BadgePlugin(), origin = PluginOrigin.Internal, version = version))
// update plugin
plugins.add(PluginDescriptor(UpdatePlugin(), origin = PluginOrigin.Internal, version = version))
// frame plugin
plugins.add(PluginDescriptor(FramePlugin(), origin = PluginOrigin.Internal, version = version))
// ssh plugin
plugins.add(PluginDescriptor(SSHInternalPlugin(), origin = PluginOrigin.Internal, version = version))
@@ -118,8 +123,6 @@ internal class PluginManager private constructor() {
plugins.add(PluginDescriptor(RDPInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// telnet plugin
plugins.add(PluginDescriptor(TelnetInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// serial plugin
plugins.add(PluginDescriptor(SerialInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// wsl plugin
if (SystemUtils.IS_OS_WINDOWS) {
plugins.add(PluginDescriptor(WSLInternalPlugin(), origin = PluginOrigin.Internal, version = version))

View File

@@ -0,0 +1,6 @@
package app.termora.plugin.internal
enum class AltKeyModifier {
EightBit,
CharactersPrecededByESC,
}

View File

@@ -10,7 +10,7 @@ import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.*
internal class BasicGeneralOption : JPanel(BorderLayout()), OptionsPane.Option {
class BasicGeneralOption : JPanel(BorderLayout()), OptionsPane.Option {
val nameTextField = OutlineTextField(128)
val remarkTextArea = FixedLengthTextArea(512)
private val formMargin = "7dlu"

View File

@@ -78,8 +78,6 @@ class BasicProxyOption(
proxyAuthenticationTypeComboBox.addItem(type)
}
proxyUsernameTextField.text = "root"
refreshStates()
}

View File

@@ -0,0 +1,191 @@
package app.termora.plugin.internal
import app.termora.*
import app.termora.OptionsPane.Companion.FORM_MARGIN
import app.termora.OptionsPane.Option
import app.termora.plugin.internal.telnet.TelnetHostOptionsPane.Backspace
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.ui.FlatTextBorder
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import java.awt.BorderLayout
import java.awt.Component
import java.awt.KeyboardFocusManager
import java.nio.charset.Charset
import javax.swing.*
class BasicTerminalOption() : JPanel(BorderLayout()), Option {
var showCharsetComboBox: Boolean = false
var showStartupCommandTextField: Boolean = false
var showHeartbeatIntervalTextField: Boolean = false
var showEnvironmentTextArea: Boolean = false
var showLoginScripts: Boolean = false
var showBackspaceComboBox: Boolean = false
var showCharacterAtATimeTextField: Boolean = false
var showAltModifierComboBox: Boolean = true
val charsetComboBox = JComboBox<String>()
val startupCommandTextField = OutlineTextField()
val heartbeatIntervalTextField = IntSpinner(30, minimum = 3, maximum = Int.MAX_VALUE)
val environmentTextArea = FixedLengthTextArea(2048)
val loginScripts = mutableListOf<LoginScript>()
val backspaceComboBox = JComboBox<Backspace>()
val altModifierComboBox = JComboBox<AltKeyModifier>()
val characterAtATimeTextField = YesOrNoComboBox()
private val loginScriptPanel = LoginScriptPanel(loginScripts)
private val tabbed = FlatTabbedPane()
fun init() {
initView()
initEvents()
}
private fun initView() {
if (showLoginScripts) {
tabbed.styleMap = mapOf(
"focusColor" to DynamicColor("TabbedPane.background"),
"hoverColor" to DynamicColor("TabbedPane.background"),
)
tabbed.tabHeight = UIManager.getInt("TabbedPane.tabHeight") - 4
putClientProperty("ContentPanelBorder", BorderFactory.createEmptyBorder())
tabbed.addTab(I18n.getString("termora.new-host.general"), getCenterComponent())
tabbed.addTab(I18n.getString("termora.new-host.terminal.login-scripts"), loginScriptPanel)
add(tabbed, BorderLayout.CENTER)
} else {
add(getCenterComponent(), BorderLayout.CENTER)
}
if (showAltModifierComboBox) {
altModifierComboBox.addItem(AltKeyModifier.EightBit)
altModifierComboBox.addItem(AltKeyModifier.CharactersPrecededByESC)
altModifierComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component? {
var text = value?.toString() ?: value
if (value == AltKeyModifier.CharactersPrecededByESC) {
text = I18n.getString("termora.new-host.terminal.alt-modifier.by-esc")
} else if (value == AltKeyModifier.EightBit) {
text = I18n.getString("termora.new-host.terminal.alt-modifier.eight-bit")
}
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
}
}
}
if (showBackspaceComboBox) {
backspaceComboBox.addItem(Backspace.Delete)
backspaceComboBox.addItem(Backspace.Backspace)
backspaceComboBox.addItem(Backspace.VT220)
}
if (showCharacterAtATimeTextField) {
characterAtATimeTextField.selectedItem = false
}
environmentTextArea.setFocusTraversalKeys(
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
)
environmentTextArea.setFocusTraversalKeys(
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
)
environmentTextArea.rows = 8
environmentTextArea.lineWrap = true
environmentTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
for (e in Charset.availableCharsets()) {
charsetComboBox.addItem(e.key)
}
charsetComboBox.selectedItem = "UTF-8"
}
private fun initEvents() {
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.terminal
}
override fun getTitle(): String {
return I18n.getString("termora.new-host.terminal")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
var rows = 1
val step = 2
val builder = FormBuilder.create().layout(layout)
if (showLoginScripts) {
builder.border(BorderFactory.createEmptyBorder(6, 8, 6, 8))
}
if (showCharsetComboBox) {
builder.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
.add(charsetComboBox).xy(3, rows).apply { rows += step }
}
if (showAltModifierComboBox) {
builder.add("${I18n.getString("termora.new-host.terminal.alt-modifier")}:").xy(1, rows)
.add(altModifierComboBox).xy(3, rows).apply { rows += step }
}
if (showBackspaceComboBox) {
builder.add("${I18n.getString("termora.new-host.terminal.backspace")}:").xy(1, rows)
.add(backspaceComboBox).xy(3, rows).apply { rows += step }
}
if (showCharacterAtATimeTextField) {
builder
.add("${I18n.getString("termora.new-host.terminal.character-mode")}:").xy(1, rows)
.add(characterAtATimeTextField).xy(3, rows).apply { rows += step }
}
if (showHeartbeatIntervalTextField) {
builder.add("${I18n.getString("termora.new-host.terminal.heartbeat-interval")}:").xy(1, rows)
.add(heartbeatIntervalTextField).xy(3, rows).apply { rows += step }
}
if (showStartupCommandTextField) {
builder.add("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows)
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
}
if (showEnvironmentTextArea) {
builder.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
.apply { rows += step }
}
return builder.build()
}
}

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