mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
11 Commits
2.0.0-beta
...
2.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c08a9f2b18 | ||
|
|
728f1f2802 | ||
|
|
7310211fba | ||
|
|
1f3267de0a | ||
|
|
8ddad59c70 | ||
|
|
9ff6d0afa1 | ||
|
|
2341b09f81 | ||
|
|
5830aa937a | ||
|
|
56a9361e86 | ||
|
|
5868aa4d2f | ||
|
|
45135b7299 |
52
.github/workflows/linux-aarch64.yml
vendored
52
.github/workflows/linux-aarch64.yml
vendored
@@ -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
|
||||
52
.github/workflows/linux-x86-64.yml
vendored
52
.github/workflows/linux-x86-64.yml
vendored
@@ -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
|
||||
64
.github/workflows/linux.yml
vendored
Normal file
64
.github/workflows/linux.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Linux
|
||||
|
||||
on: [ push, pull_request ]
|
||||
|
||||
env:
|
||||
DOCKER_NAME: hstyi/jbr:21.0.7b1038.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: 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
|
||||
89
.github/workflows/osx-aarch64.yml
vendored
89
.github/workflows/osx-aarch64.yml
vendored
@@ -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 != ''
|
||||
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 != ''"
|
||||
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
|
||||
@@ -1,17 +1,27 @@
|
||||
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 }}
|
||||
|
||||
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' && env.BUILD_CERTIFICATE_BASE64 != ''
|
||||
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 }}
|
||||
@@ -43,8 +53,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-21.0.7-osx-$ARCH-b1034.51.tar.gz
|
||||
|
||||
# install jdk
|
||||
- name: Installing Java
|
||||
@@ -53,8 +69,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 +79,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
|
||||
18
.github/workflows/windows-x86-64.yml
vendored
18
.github/workflows/windows-x86-64.yml
vendored
@@ -34,15 +34,17 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||
|
||||
# test build
|
||||
- run: |
|
||||
.\gradlew classes -x test --no-daemon
|
||||
.\gradlew clean --no-daemon
|
||||
- name: Compile
|
||||
run: .\gradlew :check-license && .\gradlew classes -x test
|
||||
|
||||
# dist
|
||||
- run: |
|
||||
.\gradlew.bat dist --no-daemon
|
||||
.\gradlew.bat --stop
|
||||
- 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
|
||||
|
||||
@@ -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
|
||||
@@ -185,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
|
||||
@@ -519,31 +519,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -574,26 +563,6 @@ 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
|
||||
@@ -654,7 +623,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
|
||||
@@ -667,7 +636,7 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
|
||||
// @formatter:on
|
||||
|
||||
// sign zip
|
||||
if (macOSSign) signMacOSLocalFile(zipFile)
|
||||
signMacOSLocalFile(zipFile)
|
||||
|
||||
// 公证
|
||||
if (macOSNotary) {
|
||||
|
||||
@@ -7,7 +7,7 @@ kotlinx-coroutines = "1.10.2"
|
||||
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"
|
||||
@@ -41,7 +41,7 @@ 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"
|
||||
|
||||
@@ -9,7 +9,7 @@ dependencies {
|
||||
compileOnly(project(":"))
|
||||
implementation("com.maxmind.geoip2:geoip2:4.3.1")
|
||||
// https://github.com/hstyi/geolite2
|
||||
implementation("com.github.hstyi:geolite2:v1.0-202507040118")
|
||||
implementation("com.github.hstyi:geolite2:v1.0-202507070058")
|
||||
}
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
|
||||
@@ -12,12 +12,6 @@ object Actions {
|
||||
*/
|
||||
const val KEY_MANAGER = "KeyManagerAction"
|
||||
|
||||
/**
|
||||
* 更新
|
||||
*/
|
||||
const val APP_UPDATE = "AppUpdateAction"
|
||||
|
||||
|
||||
/**
|
||||
* 宏
|
||||
*/
|
||||
|
||||
215
src/main/kotlin/app/termora/LoginScriptPanel.kt
Normal file
215
src/main/kotlin/app/termora/LoginScriptPanel.kt
Normal 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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,19 +2,19 @@ package app.termora
|
||||
|
||||
import app.termora.actions.StateAction
|
||||
import app.termora.findeverywhere.FindEverywhereAction
|
||||
import app.termora.plugin.internal.badge.Badge
|
||||
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.AWTEventListener
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.*
|
||||
import java.beans.PropertyChangeEvent
|
||||
import java.beans.PropertyChangeListener
|
||||
import javax.swing.*
|
||||
|
||||
internal class MyTermoraToolbar(private val windowScope: WindowScope) : FlatToolBar() {
|
||||
internal class MyTermoraToolbar(private val windowScope: WindowScope, private val frame: TermoraFrame) : FlatToolBar() {
|
||||
|
||||
|
||||
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
|
||||
@@ -56,6 +56,14 @@ internal class MyTermoraToolbar(private val windowScope: WindowScope) : FlatTool
|
||||
}
|
||||
}).let { Disposer.register(windowScope, it) }
|
||||
|
||||
// 监听窗口大小变动,然后修改边距避开控制按钮
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentResized(e: ComponentEvent) {
|
||||
adjust()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshActions() {
|
||||
@@ -76,16 +84,70 @@ internal class MyTermoraToolbar(private val windowScope: WindowScope) : FlatTool
|
||||
|
||||
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
|
||||
@@ -100,16 +162,7 @@ internal class MyTermoraToolbar(private val windowScope: WindowScope) : FlatTool
|
||||
})
|
||||
|
||||
val listener = object : PropertyChangeListener, Disposable {
|
||||
private val badge get() = Badge.getInstance(windowScope)
|
||||
override fun propertyChange(evt: PropertyChangeEvent) {
|
||||
if (evt.propertyName == "Badge") {
|
||||
if (action.getValue("Badge") == true) {
|
||||
badge.addBadge(button)
|
||||
} else {
|
||||
badge.removeBadge(button)
|
||||
}
|
||||
}
|
||||
|
||||
if (action is StateAction) {
|
||||
button.isSelected = action.isSelected(windowScope)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ 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 = MyTermoraToolbar(windowScope)
|
||||
private val toolbar = MyTermoraToolbar(windowScope, this)
|
||||
private val terminalTabbed = TerminalTabbed(windowScope, tabbedPane, layout)
|
||||
private val dataProviderSupport = DataProviderSupport()
|
||||
private var notifyListeners = emptyArray<NotifyListener>()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
||||
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())
|
||||
@@ -42,7 +41,7 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
||||
|
||||
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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,9 +42,12 @@ class SettingsAction : AnAction(
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
if (isShowing) {
|
||||
return
|
||||
}
|
||||
if (isShowing) return
|
||||
showSettingsDialog(evt)
|
||||
}
|
||||
|
||||
|
||||
private fun showSettingsDialog(evt: AnActionEvent) {
|
||||
|
||||
isShowing = true
|
||||
|
||||
@@ -46,10 +55,12 @@ class SettingsAction : AnAction(
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import app.termora.plugin.internal.rdp.RDPInternalPlugin
|
||||
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
|
||||
@@ -108,6 +109,8 @@ 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))
|
||||
|
||||
// ssh plugin
|
||||
plugins.add(PluginDescriptor(SSHInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||
|
||||
@@ -11,8 +11,6 @@ import app.termora.tree.NewHostTreeDialog
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import com.formdev.flatlaf.extras.components.FlatTable
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
@@ -27,7 +25,6 @@ import java.nio.charset.Charset
|
||||
import javax.swing.*
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
import javax.swing.table.DefaultTableModel
|
||||
import kotlin.math.max
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() {
|
||||
@@ -496,35 +493,7 @@ open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsP
|
||||
val environmentTextArea = FixedLengthTextArea(2048)
|
||||
val loginScripts = mutableListOf<LoginScript>()
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val loginScriptPanel = LoginScriptPanel(loginScripts)
|
||||
private val tabbed = FlatTabbedPane()
|
||||
|
||||
init {
|
||||
@@ -533,12 +502,7 @@ open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsP
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
addBtn.isFocusable = false
|
||||
editBtn.isFocusable = false
|
||||
deleteBtn.isFocusable = false
|
||||
|
||||
deleteBtn.isEnabled = false
|
||||
editBtn.isEnabled = false
|
||||
|
||||
tabbed.styleMap = mapOf(
|
||||
"focusColor" to DynamicColor("TabbedPane.background"),
|
||||
@@ -547,7 +511,7 @@ open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsP
|
||||
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"), getLoginScriptsComponent())
|
||||
tabbed.addTab(I18n.getString("termora.new-host.terminal.login-scripts"), loginScriptPanel)
|
||||
add(tabbed, BorderLayout.CENTER)
|
||||
|
||||
|
||||
@@ -575,39 +539,7 @@ open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsP
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -648,123 +580,6 @@ open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsP
|
||||
return panel
|
||||
}
|
||||
|
||||
private fun getLoginScriptsComponent(): JComponent {
|
||||
val panel = JPanel(BorderLayout())
|
||||
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)
|
||||
|
||||
panel.add(scrollPane, BorderLayout.CENTER)
|
||||
panel.add(box, BorderLayout.SOUTH)
|
||||
panel.border = BorderFactory.createEmptyBorder(6, 8, 6, 8)
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||
|
||||
@@ -2,18 +2,14 @@ package app.termora.plugin.internal.telnet
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.account.AccountOwner
|
||||
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.extras.components.FlatTabbedPane
|
||||
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.Window
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import java.nio.charset.Charset
|
||||
@@ -25,7 +21,6 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
||||
// telnet 不支持代理密码
|
||||
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
|
||||
private val terminalOption = TerminalOption()
|
||||
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||
|
||||
init {
|
||||
addOption(generalOption)
|
||||
@@ -39,16 +34,7 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
||||
val protocol = TelnetProtocolProvider.PROTOCOL
|
||||
val host = generalOption.hostTextField.text
|
||||
val port = (generalOption.portTextField.value ?: 23) as Int
|
||||
var authentication = Authentication.No
|
||||
var proxy = Proxy.Companion.No
|
||||
val authenticationType = generalOption.authenticationTypeComboBox.selectedItem as AuthenticationType
|
||||
|
||||
if (authenticationType == AuthenticationType.Password) {
|
||||
authentication = authentication.copy(
|
||||
type = authenticationType,
|
||||
password = String(generalOption.passwordTextField.password)
|
||||
)
|
||||
}
|
||||
|
||||
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||
proxy = proxy.copy(
|
||||
@@ -68,8 +54,12 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
||||
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
||||
env = terminalOption.environmentTextArea.text,
|
||||
startupCommand = terminalOption.startupCommandTextField.text,
|
||||
loginScripts = terminalOption.loginScripts,
|
||||
serialComm = serialComm,
|
||||
extras = mutableMapOf("backspace" to (terminalOption.backspaceComboBox.selectedItem as Backspace).name)
|
||||
extras = mutableMapOf(
|
||||
"backspace" to (terminalOption.backspaceComboBox.selectedItem as Backspace).name,
|
||||
"character-at-a-time" to (terminalOption.characterAtATimeTextField.selectedItem?.toString() ?: "false")
|
||||
)
|
||||
)
|
||||
|
||||
return Host(
|
||||
@@ -77,8 +67,6 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
||||
protocol = protocol,
|
||||
host = host,
|
||||
port = port,
|
||||
username = generalOption.usernameTextField.text,
|
||||
authentication = authentication,
|
||||
proxy = proxy,
|
||||
sort = System.currentTimeMillis(),
|
||||
remark = generalOption.remarkTextArea.text,
|
||||
@@ -89,13 +77,9 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
||||
fun setHost(host: Host) {
|
||||
generalOption.portTextField.value = host.port
|
||||
generalOption.nameTextField.text = host.name
|
||||
generalOption.usernameTextField.text = host.username
|
||||
generalOption.hostTextField.text = host.host
|
||||
generalOption.remarkTextArea.text = host.remark
|
||||
generalOption.authenticationTypeComboBox.selectedItem = host.authentication.type
|
||||
if (host.authentication.type == AuthenticationType.Password) {
|
||||
generalOption.passwordTextField.text = host.authentication.password
|
||||
}
|
||||
|
||||
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||
proxyOption.proxyHostTextField.text = host.proxy.host
|
||||
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
||||
@@ -108,7 +92,11 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
||||
terminalOption.startupCommandTextField.text = host.options.startupCommand
|
||||
terminalOption.backspaceComboBox.selectedItem =
|
||||
Backspace.valueOf(host.options.extras["backspace"] ?: Backspace.Delete.name)
|
||||
terminalOption.characterAtATimeTextField.selectedItem =
|
||||
host.options.extras["character-at-a-time"]?.toBooleanStrictOrNull() ?: false
|
||||
|
||||
terminalOption.loginScripts.clear()
|
||||
terminalOption.loginScripts.addAll(host.options.loginScripts)
|
||||
}
|
||||
|
||||
fun validateFields(): Boolean {
|
||||
@@ -121,15 +109,6 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
||||
return false
|
||||
}
|
||||
|
||||
if (host.authentication.type == AuthenticationType.Password) {
|
||||
if (validateField(generalOption.usernameTextField)) {
|
||||
return false
|
||||
}
|
||||
if (validateField(generalOption.passwordTextField)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// proxy
|
||||
if (host.proxy.type != ProxyType.No) {
|
||||
if (validateField(proxyOption.proxyHostTextField)
|
||||
@@ -166,29 +145,11 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
||||
textField.requestFocusInWindow()
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 true 表示有错误
|
||||
*/
|
||||
private fun validateField(comboBox: JComboBox<*>): Boolean {
|
||||
val selectedItem = comboBox.selectedItem
|
||||
if (comboBox.isEnabled && (selectedItem == null || (selectedItem is String && selectedItem.isBlank()))) {
|
||||
selectOptionJComponent(comboBox)
|
||||
comboBox.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||
comboBox.requestFocusInWindow()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
protected inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||
inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||
val portTextField = PortSpinner(23)
|
||||
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()
|
||||
@@ -197,68 +158,6 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
||||
|
||||
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() {
|
||||
@@ -285,7 +184,7 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
||||
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"
|
||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
remarkTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||
@@ -314,15 +213,6 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
||||
.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 }
|
||||
@@ -340,8 +230,11 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
||||
val charsetComboBox = JComboBox<String>()
|
||||
val backspaceComboBox = JComboBox<Backspace>()
|
||||
val startupCommandTextField = OutlineTextField()
|
||||
val characterAtATimeTextField = YesOrNoComboBox()
|
||||
val environmentTextArea = FixedLengthTextArea(2048)
|
||||
val loginScripts = mutableListOf<LoginScript>()
|
||||
|
||||
private val loginScriptPanel = LoginScriptPanel(loginScripts)
|
||||
|
||||
init {
|
||||
initView()
|
||||
@@ -349,12 +242,12 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
|
||||
backspaceComboBox.addItem(Backspace.Delete)
|
||||
backspaceComboBox.addItem(Backspace.Backspace)
|
||||
backspaceComboBox.addItem(Backspace.VT220)
|
||||
|
||||
characterAtATimeTextField.selectedItem = false
|
||||
|
||||
environmentTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||
@@ -377,6 +270,17 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
||||
|
||||
charsetComboBox.selectedItem = "UTF-8"
|
||||
|
||||
val tabbed = FlatTabbedPane()
|
||||
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)
|
||||
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
@@ -405,10 +309,13 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
.border(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
||||
.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.backspace")}:").xy(1, rows)
|
||||
.add(backspaceComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.new-host.terminal.character-mode")}:").xy(1, rows)
|
||||
.add(characterAtATimeTextField).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 }
|
||||
.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.termora.plugin.internal.telnet
|
||||
|
||||
import app.termora.terminal.StreamPtyConnector
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.net.telnet.TelnetClient
|
||||
import org.apache.commons.net.telnet.TelnetOption
|
||||
import org.apache.commons.net.telnet.WindowSizeOptionHandler
|
||||
@@ -9,18 +10,27 @@ import java.nio.charset.Charset
|
||||
|
||||
class TelnetStreamPtyConnector(
|
||||
private val telnet: TelnetClient,
|
||||
private val charset: Charset
|
||||
) :
|
||||
StreamPtyConnector(telnet.inputStream, telnet.outputStream) {
|
||||
private val reader = InputStreamReader(telnet.inputStream, getCharset())
|
||||
private val charset: Charset,
|
||||
private val characterMode: Boolean,
|
||||
) : StreamPtyConnector(telnet.inputStream, telnet.outputStream) {
|
||||
|
||||
private val reader = InputStreamReader(telnet.inputStream, charset)
|
||||
|
||||
|
||||
override fun read(buffer: CharArray): Int {
|
||||
return reader.read(buffer)
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteArray, offset: Int, len: Int) {
|
||||
output.write(buffer, offset, len)
|
||||
output.flush()
|
||||
if (characterMode) {
|
||||
for (i in offset until len + offset) {
|
||||
output.write(byteArrayOf(buffer[i]))
|
||||
output.flush()
|
||||
}
|
||||
} else {
|
||||
output.write(buffer, offset, len)
|
||||
output.flush()
|
||||
}
|
||||
}
|
||||
|
||||
override fun resize(rows: Int, cols: Int) {
|
||||
@@ -33,10 +43,13 @@ class TelnetStreamPtyConnector(
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(input)
|
||||
IOUtils.closeQuietly(output)
|
||||
telnet.disconnect()
|
||||
}
|
||||
|
||||
override fun getCharset(): Charset {
|
||||
return charset
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package app.termora.plugin.internal.telnet
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.Host
|
||||
import app.termora.ProxyType
|
||||
import app.termora.PtyHostTerminalTab
|
||||
import app.termora.WindowScope
|
||||
import app.termora.terminal.ControlCharacters
|
||||
import app.termora.terminal.KeyEncoderImpl
|
||||
import app.termora.terminal.PtyConnector
|
||||
@@ -11,6 +14,7 @@ import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.nio.charset.Charset
|
||||
|
||||
|
||||
class TelnetTerminalTab(
|
||||
windowScope: WindowScope, host: Host,
|
||||
) : PtyHostTerminalTab(windowScope, host) {
|
||||
@@ -32,12 +36,18 @@ class TelnetTerminalTab(
|
||||
)
|
||||
}
|
||||
|
||||
val characterMode = host.options.extras["character-at-a-time"]?.toBooleanStrictOrNull() ?: false
|
||||
|
||||
val termtype = host.options.envs()["TERM"] ?: "xterm-256color"
|
||||
val ttopt = TerminalTypeOptionHandler(termtype, false, false, true, false)
|
||||
val echoopt = EchoOptionHandler(false, true, false, true)
|
||||
val gaopt = SuppressGAOptionHandler(true, true, true, true)
|
||||
val wsopt = WindowSizeOptionHandler(winSize.cols, winSize.rows, true, false, true, false)
|
||||
val bopt = SimpleOptionHandler(TelnetOption.BINARY, true, false, true, false)
|
||||
val fcopt = SimpleOptionHandler(TelnetOption.REMOTE_FLOW_CONTROL, true, true, false, false)
|
||||
|
||||
telnet.addOptionHandler(bopt)
|
||||
telnet.addOptionHandler(fcopt)
|
||||
telnet.addOptionHandler(ttopt)
|
||||
telnet.addOptionHandler(echoopt)
|
||||
telnet.addOptionHandler(gaopt)
|
||||
@@ -45,6 +55,7 @@ class TelnetTerminalTab(
|
||||
|
||||
telnet.connect(host.host, host.port)
|
||||
telnet.keepAlive = true
|
||||
telnet.tcpNoDelay = characterMode
|
||||
|
||||
val encoder = terminal.getKeyEncoder()
|
||||
if (encoder is KeyEncoderImpl) {
|
||||
@@ -56,38 +67,9 @@ class TelnetTerminalTab(
|
||||
}
|
||||
}
|
||||
|
||||
return ptyConnectorFactory.decorate(TelnetStreamPtyConnector(telnet, telnet.charset))
|
||||
|
||||
return ptyConnectorFactory.decorate(TelnetStreamPtyConnector(telnet, telnet.charset, characterMode))
|
||||
}
|
||||
|
||||
|
||||
override fun loginScriptsPtyConnector(host: Host, ptyConnector: PtyConnector): PtyConnector {
|
||||
if (host.authentication.type != AuthenticationType.Password) {
|
||||
return ptyConnector
|
||||
}
|
||||
|
||||
val scripts = mutableListOf<LoginScript>()
|
||||
scripts.add(
|
||||
LoginScript(
|
||||
expect = "login:",
|
||||
send = host.username,
|
||||
regex = false,
|
||||
matchCase = false
|
||||
)
|
||||
)
|
||||
|
||||
scripts.add(
|
||||
LoginScript(
|
||||
expect = "password:",
|
||||
send = host.authentication.password,
|
||||
regex = false,
|
||||
matchCase = false
|
||||
)
|
||||
)
|
||||
|
||||
return super.loginScriptsPtyConnector(
|
||||
host.copy(options = host.options.copy(loginScripts = scripts)),
|
||||
ptyConnector
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package app.termora.plugin.internal.update
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
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 org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.JXEditorPane
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Dimension
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.net.URI
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.JScrollPane
|
||||
import javax.swing.UIManager
|
||||
import javax.swing.event.HyperlinkEvent
|
||||
|
||||
internal class AppUpdateAction private constructor() : AnAction(StringUtils.EMPTY, Icons.ideUpdate) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(AppUpdateAction::class.java)
|
||||
|
||||
fun getInstance(): AppUpdateAction {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(AppUpdateAction::class) { AppUpdateAction() }
|
||||
}
|
||||
}
|
||||
|
||||
private val updaterManager get() = UpdaterManager.getInstance()
|
||||
|
||||
init {
|
||||
isEnabled = false
|
||||
}
|
||||
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
showUpdateDialog()
|
||||
}
|
||||
|
||||
|
||||
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.OK_CANCEL_OPTION,
|
||||
options = arrayOf(
|
||||
I18n.getString("termora.update.update"),
|
||||
I18n.getString("termora.cancel")
|
||||
),
|
||||
initialValue = I18n.getString("termora.update.update")
|
||||
)
|
||||
if (option == JOptionPane.CANCEL_OPTION) {
|
||||
return
|
||||
} else if (option == JOptionPane.OK_OPTION) {
|
||||
updateSelf(lastVersion)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelf(latestVersion: UpdaterManager.LatestVersion) {
|
||||
val pkg = Updater.getInstance().getLatestPkg()
|
||||
if (SystemInfo.isLinux || pkg == null) {
|
||||
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${latestVersion.version}"))
|
||||
return
|
||||
}
|
||||
val file = pkg.file
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package app.termora.plugin.internal.update
|
||||
|
||||
import app.termora.ApplicationRunnerExtension
|
||||
|
||||
internal class MyApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
|
||||
companion object {
|
||||
val instance = MyApplicationRunnerExtension()
|
||||
}
|
||||
|
||||
override fun ready() {
|
||||
Updater.getInstance().scheduleUpdate()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package app.termora.plugin.internal.update
|
||||
|
||||
import app.termora.ApplicationRunnerExtension
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.InternalPlugin
|
||||
|
||||
internal class UpdatePlugin : InternalPlugin() {
|
||||
|
||||
init {
|
||||
support.addExtension(ApplicationRunnerExtension::class.java) { MyApplicationRunnerExtension.instance }
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
return "Update"
|
||||
}
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
}
|
||||
157
src/main/kotlin/app/termora/plugin/internal/update/Updater.kt
Normal file
157
src/main/kotlin/app/termora/plugin/internal/update/Updater.kt
Normal file
@@ -0,0 +1,157 @@
|
||||
package app.termora.plugin.internal.update
|
||||
|
||||
import app.termora.Application
|
||||
import app.termora.Application.httpClient
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.Disposable
|
||||
import app.termora.UpdaterManager
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import kotlinx.coroutines.*
|
||||
import okhttp3.Request
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.semver4j.Semver
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.net.ProxySelector
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
internal class Updater private constructor() : Disposable {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(Updater::class.java)
|
||||
fun getInstance(): Updater {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(Updater::class) { Updater() }
|
||||
}
|
||||
}
|
||||
|
||||
private val updaterManager get() = UpdaterManager.getInstance()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private var isRemindMeNextTime = false
|
||||
|
||||
/**
|
||||
* 安装包位置
|
||||
*/
|
||||
private var pkg: LatestPkg? = null
|
||||
|
||||
fun scheduleUpdate() {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
// 启动 3 分钟后才是检查
|
||||
if (Application.isUnknownVersion().not()) {
|
||||
delay(3.seconds)
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun downloadLatestPkg(latestVersion: UpdaterManager.LatestVersion) {
|
||||
if (SystemInfo.isLinux) return
|
||||
|
||||
setLatestPkg(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")
|
||||
}
|
||||
|
||||
setLatestPkg(LatestPkg(latestVersion.version, file))
|
||||
}
|
||||
|
||||
private fun setLatestPkg(pkg: LatestPkg?) {
|
||||
this.pkg = pkg
|
||||
SwingUtilities.invokeLater { AppUpdateAction.getInstance().isEnabled = pkg != null }
|
||||
}
|
||||
|
||||
fun getLatestPkg(): LatestPkg? {
|
||||
return pkg
|
||||
}
|
||||
|
||||
data class LatestPkg(val version: String, val file: File)
|
||||
}
|
||||
@@ -342,7 +342,10 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
||||
}
|
||||
|
||||
// 设置滚动区域
|
||||
terminal.getTerminalModel().setData(DataKey.ScrollingRegion, ScrollingRegion(top, bottom))
|
||||
terminal.getTerminalModel().setData(
|
||||
DataKey.ScrollingRegion,
|
||||
ScrollingRegion(top, min(bottom, terminalModel.getRows()))
|
||||
)
|
||||
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Set Scrolling Region [${top}; ${bottom}]")
|
||||
@@ -715,6 +718,13 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
||||
}
|
||||
}
|
||||
|
||||
// Alternate Screen Buffer
|
||||
47, 1047 -> {
|
||||
// clear selection
|
||||
terminal.getSelectionModel().clearSelection()
|
||||
terminalModel.setData(DataKey.AlternateScreenBuffer, enable)
|
||||
}
|
||||
|
||||
// Alternate Screen Buffer
|
||||
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
|
||||
1049 -> {
|
||||
|
||||
@@ -21,7 +21,6 @@ termora.optional=Optional
|
||||
# update
|
||||
termora.update.title=New version
|
||||
termora.update.update=Update
|
||||
termora.update.ignore=Remind me next time
|
||||
|
||||
# Hosts
|
||||
termora.host.modified-server-key.title=HOST [{0}] IDENTIFICATION HAS CHANGED
|
||||
@@ -183,6 +182,7 @@ termora.new-host.proxy=Proxy
|
||||
termora.new-host.terminal=${termora.settings.terminal}
|
||||
termora.new-host.terminal.encoding=Encoding
|
||||
termora.new-host.terminal.backspace=Backspace
|
||||
termora.new-host.terminal.character-mode=Character-at-a-time
|
||||
termora.new-host.terminal.heartbeat-interval=Heartbeat Interval
|
||||
termora.new-host.terminal.startup-commands=Startup Command
|
||||
termora.new-host.terminal.env=Environment
|
||||
|
||||
@@ -17,7 +17,6 @@ termora.quit-confirm=Выйти {0}?
|
||||
# update
|
||||
termora.update.title=Новая версия
|
||||
termora.update.update=Обновить
|
||||
termora.update.ignore=Напомнить в следующий раз
|
||||
|
||||
|
||||
# Hosts
|
||||
|
||||
@@ -22,7 +22,6 @@ termora.optional=可选的
|
||||
# update
|
||||
termora.update.title=新版本
|
||||
termora.update.update=更新
|
||||
termora.update.ignore=下次提醒我
|
||||
|
||||
|
||||
# Hosts
|
||||
@@ -175,6 +174,7 @@ termora.new-host.proxy=代理
|
||||
termora.new-host.terminal=${termora.settings.terminal}
|
||||
termora.new-host.terminal.encoding=编码
|
||||
termora.new-host.terminal.backspace=退格键
|
||||
termora.new-host.terminal.character-mode=单字符模式
|
||||
termora.new-host.terminal.heartbeat-interval=心跳间隔
|
||||
termora.new-host.terminal.startup-commands=启动命令
|
||||
termora.new-host.terminal.env=环境
|
||||
|
||||
@@ -19,7 +19,6 @@ termora.optional=可選的
|
||||
# update
|
||||
termora.update.title=新版本
|
||||
termora.update.update=更新
|
||||
termora.update.ignore=下次提醒我
|
||||
|
||||
|
||||
|
||||
@@ -173,6 +172,7 @@ termora.new-host.proxy=代理
|
||||
termora.new-host.terminal=${termora.settings.terminal}
|
||||
termora.new-host.terminal.encoding=編碼
|
||||
termora.new-host.terminal.backspace=退格鍵
|
||||
termora.new-host.terminal.character-mode=單字元模式
|
||||
termora.new-host.terminal.startup-commands=啟動命令
|
||||
termora.new-host.terminal.heartbeat-interval=心跳間隔
|
||||
termora.new-host.terminal.env=環境
|
||||
|
||||
20
src/test/resources/deb/Dockerfile
Normal file
20
src/test/resources/deb/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM debian:12.11
|
||||
|
||||
RUN apt-get clean && apt-get update && apt-get install -y curl tar zip binutils fakeroot wget libfuse2 fuse libglib2.0-0 file ca-certificates libstdc++6
|
||||
|
||||
RUN ARCH=$(dpkg --print-architecture) && \
|
||||
if [ "$ARCH" = "amd64" ]; then \
|
||||
URL="https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-x64-b1034.51.tar.gz"; \
|
||||
elif [ "$ARCH" = "arm64" ]; then \
|
||||
URL="https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-aarch64-b1034.51.tar.gz"; \
|
||||
else \
|
||||
echo "Unsupported architecture: $ARCH" && exit 1; \
|
||||
fi && \
|
||||
curl -L "$URL" -o jbr.tar.gz && \
|
||||
mkdir -p /opt/jbr && \
|
||||
tar -xzf jbr.tar.gz -C /opt/jbr --strip-components=1 && \
|
||||
rm jbr.tar.gz
|
||||
|
||||
ENV JAVA_HOME=/opt/jbr
|
||||
ENV PATH="${JAVA_HOME}/bin:${PATH}"
|
||||
|
||||
Reference in New Issue
Block a user