mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
22 Commits
2.0.0-beta
...
2.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f329ef60df | ||
|
|
8acfdb8bca | ||
|
|
a7aec52f2a | ||
|
|
7f1317a9a7 | ||
|
|
a8a1fea91b | ||
|
|
675ad4608a | ||
|
|
72ba3757e2 | ||
|
|
c58e84d2ae | ||
|
|
18a7a5059b | ||
|
|
f0102b6f13 | ||
|
|
0cf8eb3c17 | ||
|
|
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
|
|
||||||
69
.github/workflows/linux.yml
vendored
Normal file
69
.github/workflows/linux.yml
vendored
Normal 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
|
||||||
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,29 @@
|
|||||||
name: macOS x86-64
|
name: macOS
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
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:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: macos-13
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos-15, macos-13 ]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install the Apple certificate
|
- 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:
|
env:
|
||||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
@@ -43,8 +55,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||||
|
|
||||||
# download jdk
|
- name: Download Java
|
||||||
- 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
|
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
|
# install jdk
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
@@ -53,8 +71,6 @@ jobs:
|
|||||||
distribution: 'jdkfile'
|
distribution: 'jdkfile'
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
java-version: '21.0.7'
|
java-version: '21.0.7'
|
||||||
architecture: x64
|
|
||||||
|
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -65,27 +81,22 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
- name: Compile
|
||||||
|
shell: bash
|
||||||
|
run: ./gradlew :check-license && ./gradlew classes -x test
|
||||||
|
|
||||||
# test build
|
- name: JLink
|
||||||
- run: |
|
shell: bash
|
||||||
./gradlew classes -x test --no-daemon
|
run: ./gradlew :jar :copy-dependencies :plugins:migration:build :jlink
|
||||||
./gradlew clean --no-daemon
|
|
||||||
|
|
||||||
# dist
|
- name: Package
|
||||||
- name: Dist
|
shell: bash
|
||||||
env:
|
run: ./gradlew :jpackage && ./gradlew :dist
|
||||||
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
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: termora-osx-x86-64
|
name: termora-osx-${{ runner.arch }}
|
||||||
path: |
|
path: |
|
||||||
build/distributions/*.zip
|
build/distributions/*.zip
|
||||||
build/distributions/*.dmg
|
build/distributions/*.dmg
|
||||||
53
.github/workflows/windows-x86-64.yml
vendored
53
.github/workflows/windows-x86-64.yml
vendored
@@ -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
75
.github/workflows/windows.yml
vendored
Normal 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
|
||||||
@@ -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.
|
We recommend using the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK for development.
|
||||||
|
|
||||||
- Run locally: `./gradlew :run`
|
- Run locally: `./gradlew :run`
|
||||||
- Build for current OS: `./gradlew :dist`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|||||||
@@ -88,8 +88,6 @@ Termora 使用 [**Kotlin/JVM**](https://kotlinlang.org/) 开发,支持(正
|
|||||||
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK 运行环境。
|
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK 运行环境。
|
||||||
|
|
||||||
- 本地运行:`./gradlew :run`
|
- 本地运行:`./gradlew :run`
|
||||||
- 构建当前系统安装包:`./gradlew :dist`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 📄 授权协议
|
## 📄 授权协议
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ version = rootProject.projectDir.resolve("VERSION").readText().trim()
|
|||||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||||
val appVersion = project.version.toString().split("-")[0]
|
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 签名信息
|
// macOS 签名信息
|
||||||
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
||||||
@@ -185,7 +185,7 @@ tasks.register<Copy>("copy-dependencies") {
|
|||||||
|
|
||||||
// 对 JNA 和 PTY4J 的本地库提取
|
// 对 JNA 和 PTY4J 的本地库提取
|
||||||
// 提取出来是为了单独签名,不然无法通过公证
|
// 提取出来是为了单独签名,不然无法通过公证
|
||||||
if (os.isMacOsX && macOSSign) {
|
if (os.isMacOsX) {
|
||||||
doLast {
|
doLast {
|
||||||
val archName = if (arch.isArm) "aarch64" else "x86_64"
|
val archName = if (arch.isArm) "aarch64" else "x86_64"
|
||||||
val dylib = dir.get().dir("dylib").asFile
|
val dylib = dir.get().dir("dylib").asFile
|
||||||
@@ -480,10 +480,6 @@ tasks.register<Exec>("jpackage") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (os.isWindows) {
|
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"))
|
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.ico"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,7 +492,7 @@ tasks.register<Exec>("jpackage") {
|
|||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
arguments.add("dmg")
|
arguments.add("dmg")
|
||||||
} else if (os.isWindows) {
|
} else if (os.isWindows) {
|
||||||
arguments.add("msi")
|
arguments.add("app-image")
|
||||||
} else if (os.isLinux) {
|
} else if (os.isLinux) {
|
||||||
arguments.add(if (isDeb) "deb" else "app-image")
|
arguments.add(if (isDeb) "deb" else "app-image")
|
||||||
if (isDeb) {
|
if (isDeb) {
|
||||||
@@ -519,31 +515,20 @@ tasks.register<Exec>("jpackage") {
|
|||||||
|
|
||||||
tasks.register("dist") {
|
tasks.register("dist") {
|
||||||
doLast {
|
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
|
if (os.isWindows) {
|
||||||
|
packOnWindows(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||||
// 清空目录
|
} else if (os.isLinux) {
|
||||||
exec { commandLine(gradlew, "clean") }
|
packOnLinux(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||||
|
} else if (os.isMacOsX) {
|
||||||
// 构建自带的插件
|
packOnMac(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||||
exec { commandLine(gradlew, ":plugins:migration:build") }
|
} else {
|
||||||
|
throw GradleException("${os.name} is not supported")
|
||||||
// 打包并复制依赖
|
|
||||||
exec {
|
|
||||||
commandLine(gradlew, ":jar", ":copy-dependencies")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查依赖的开源协议
|
|
||||||
exec { commandLine(gradlew, ":check-license") }
|
|
||||||
|
|
||||||
// jlink
|
|
||||||
exec { commandLine(gradlew, ":jlink") }
|
|
||||||
|
|
||||||
// 打包
|
|
||||||
exec { commandLine(gradlew, ":jpackage", "-Dtype=${System.getProperty("type")}") }
|
|
||||||
|
|
||||||
// 根据不同的系统构建不同的二进制包
|
|
||||||
pack()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,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
|
* 创建 zip、msi
|
||||||
*/
|
*/
|
||||||
fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
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 cfg = FileUtils.getFile(dir, projectName, "app", "${projectName}.cfg")
|
||||||
val configText = cfg.readText()
|
val configText = cfg.readText()
|
||||||
|
|
||||||
@@ -624,21 +589,12 @@ fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: Str
|
|||||||
"/DMyAppVersion=${appVersion}",
|
"/DMyAppVersion=${appVersion}",
|
||||||
"/DMyOutputDir=${distributionDir.asFile.absolutePath}",
|
"/DMyOutputDir=${distributionDir.asFile.absolutePath}",
|
||||||
"/DMySetupIconFile=${FileUtils.getFile(projectDir, "src", "main", "resources", "icons", "termora.ico")}",
|
"/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}",
|
"/F${finalFilenameWithoutExtension}",
|
||||||
FileUtils.getFile(projectDir, "src", "main", "resources", "termora.iss")
|
FileUtils.getFile(projectDir, "src", "main", "resources", "termora.iss")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// msi
|
|
||||||
exec {
|
|
||||||
commandLine(
|
|
||||||
"cmd", "/c", "move",
|
|
||||||
"${projectName}-${appVersion}.msi",
|
|
||||||
"${finalFilenameWithoutExtension}.msi"
|
|
||||||
)
|
|
||||||
workingDir = distributionDir.asFile
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -654,7 +610,7 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
|
|||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
// sign dmg
|
// sign dmg
|
||||||
if (macOSSign) signMacOSLocalFile(dmgFile)
|
signMacOSLocalFile(dmgFile)
|
||||||
|
|
||||||
// 找到 .app
|
// 找到 .app
|
||||||
val imageFile = layout.buildDirectory.dir("jpackage/images/").get().asFile
|
val imageFile = layout.buildDirectory.dir("jpackage/images/").get().asFile
|
||||||
@@ -667,7 +623,7 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
|
|||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
// sign zip
|
// sign zip
|
||||||
if (macOSSign) signMacOSLocalFile(zipFile)
|
signMacOSLocalFile(zipFile)
|
||||||
|
|
||||||
// 公证
|
// 公证
|
||||||
if (macOSNotary) {
|
if (macOSNotary) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ kotlinx-coroutines = "1.10.2"
|
|||||||
flatlaf = "3.6.1-SNAPSHOT"
|
flatlaf = "3.6.1-SNAPSHOT"
|
||||||
kotlinx-serialization-json = "1.9.0"
|
kotlinx-serialization-json = "1.9.0"
|
||||||
commons-codec = "1.18.0"
|
commons-codec = "1.18.0"
|
||||||
commons-lang3 = "3.17.0"
|
commons-lang3 = "3.18.0"
|
||||||
commons-csv = "1.14.0"
|
commons-csv = "1.14.0"
|
||||||
commons-net = "3.11.1"
|
commons-net = "3.11.1"
|
||||||
commons-text = "1.13.1"
|
commons-text = "1.13.1"
|
||||||
@@ -41,7 +41,7 @@ jSerialComm = "2.11.2"
|
|||||||
ini4j = "0.5.5-2"
|
ini4j = "0.5.5-2"
|
||||||
restart4j = "0.0.1"
|
restart4j = "0.0.1"
|
||||||
eddsa = "0.3.0"
|
eddsa = "0.3.0"
|
||||||
exposed = "1.0.0-beta-3"
|
exposed = "1.0.0-beta-4"
|
||||||
h2 = "2.3.232"
|
h2 = "2.3.232"
|
||||||
sqlite = "3.50.2.0"
|
sqlite = "3.50.2.0"
|
||||||
jug = "5.1.0"
|
jug = "5.1.0"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
}
|
}
|
||||||
|
|
||||||
project.version = "0.0.1"
|
project.version = "0.0.2"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import java.awt.Component
|
|||||||
import java.awt.KeyboardFocusManager
|
import java.awt.KeyboardFocusManager
|
||||||
import java.awt.event.ComponentAdapter
|
import java.awt.event.ComponentAdapter
|
||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
|
import java.awt.event.ItemEvent
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
@@ -246,6 +247,12 @@ class FTPHostOptionsPane : OptionsPane() {
|
|||||||
removeComponentListener(this)
|
removeComponentListener(this)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
authenticationTypeComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
passwordTextField.isEnabled = authenticationTypeComboBox.selectedItem == AuthenticationType.Password
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(isSelected: Boolean): Icon {
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ dependencies {
|
|||||||
compileOnly(project(":"))
|
compileOnly(project(":"))
|
||||||
implementation("com.maxmind.geoip2:geoip2:4.3.1")
|
implementation("com.maxmind.geoip2:geoip2:4.3.1")
|
||||||
// https://github.com/hstyi/geolite2
|
// 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")
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ plugins {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
project.version = "0.0.1"
|
project.version = "0.0.2"
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package app.termora.plugins.serial
|
package app.termora.plugins.serial
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
|
import app.termora.plugin.internal.AltKeyModifier
|
||||||
import app.termora.plugin.internal.BasicGeneralOption
|
import app.termora.plugin.internal.BasicGeneralOption
|
||||||
|
import app.termora.plugin.internal.BasicTerminalOption
|
||||||
import com.fazecast.jSerialComm.SerialPort
|
import com.fazecast.jSerialComm.SerialPort
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
@@ -15,12 +17,15 @@ import java.awt.BorderLayout
|
|||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.event.ComponentAdapter
|
import java.awt.event.ComponentAdapter
|
||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
import java.nio.charset.Charset
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
class SerialHostOptionsPane : OptionsPane() {
|
class SerialHostOptionsPane : OptionsPane() {
|
||||||
private val generalOption = BasicGeneralOption()
|
private val generalOption = BasicGeneralOption()
|
||||||
private val terminalOption = TerminalOption()
|
private val terminalOption = BasicTerminalOption().apply {
|
||||||
|
showCharsetComboBox = true
|
||||||
|
showStartupCommandTextField = true
|
||||||
|
init()
|
||||||
|
}
|
||||||
private val serialCommOption = SerialCommOption()
|
private val serialCommOption = SerialCommOption()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -48,6 +53,10 @@ class SerialHostOptionsPane : OptionsPane() {
|
|||||||
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
||||||
startupCommand = terminalOption.startupCommandTextField.text,
|
startupCommand = terminalOption.startupCommandTextField.text,
|
||||||
serialComm = serialComm,
|
serialComm = serialComm,
|
||||||
|
extras = mutableMapOf(
|
||||||
|
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
|
||||||
|
?: AltKeyModifier.EightBit.name),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return Host(
|
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 {
|
protected inner class SerialCommOption : JPanel(BorderLayout()), Option {
|
||||||
val serialPortComboBox = OutlineComboBox<String>()
|
val serialPortComboBox = OutlineComboBox<String>()
|
||||||
val baudRateComboBox = OutlineComboBox<Int>()
|
val baudRateComboBox = OutlineComboBox<Int>()
|
||||||
|
|||||||
@@ -12,12 +12,6 @@ object Actions {
|
|||||||
*/
|
*/
|
||||||
const val KEY_MANAGER = "KeyManagerAction"
|
const val KEY_MANAGER = "KeyManagerAction"
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新
|
|
||||||
*/
|
|
||||||
const val APP_UPDATE = "AppUpdateAction"
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 宏
|
* 宏
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -24,12 +24,13 @@ import java.awt.*
|
|||||||
import java.awt.desktop.AppReopenedEvent
|
import java.awt.desktop.AppReopenedEvent
|
||||||
import java.awt.desktop.AppReopenedListener
|
import java.awt.desktop.AppReopenedListener
|
||||||
import java.awt.desktop.SystemEventListener
|
import java.awt.desktop.SystemEventListener
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.*
|
||||||
import java.awt.event.WindowEvent
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import javax.imageio.ImageIO
|
import javax.imageio.ImageIO
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
import javax.swing.event.PopupMenuEvent
|
||||||
|
import javax.swing.event.PopupMenuListener
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class ApplicationRunner {
|
class ApplicationRunner {
|
||||||
@@ -112,16 +113,63 @@ class ApplicationRunner {
|
|||||||
if (!SystemInfo.isWindows || !SystemTray.isSupported()) return
|
if (!SystemInfo.isWindows || !SystemTray.isSupported()) return
|
||||||
|
|
||||||
val tray = SystemTray.getSystemTray()
|
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 trayIcon = TrayIcon(image)
|
||||||
val popupMenu = PopupMenu()
|
val dialog = JDialog()
|
||||||
trayIcon.popupMenu = popupMenu
|
val trayPopup = JPopupMenu()
|
||||||
|
|
||||||
|
dialog.isUndecorated = true
|
||||||
|
dialog.isModal = false
|
||||||
|
dialog.size = Dimension(0, 0)
|
||||||
|
|
||||||
|
trayIcon.isImageAutoSize = true
|
||||||
trayIcon.toolTip = Application.getName()
|
trayIcon.toolTip = Application.getName()
|
||||||
|
|
||||||
// PopupMenu 不支持中文
|
trayPopup.add(I18n.getString("termora.exit")).addActionListener { quitHandler() }
|
||||||
val exitMenu = MenuItem("Exit")
|
trayPopup.addPopupMenuListener(object : PopupMenuListener {
|
||||||
exitMenu.addActionListener { SwingUtilities.invokeLater { quitHandler() } }
|
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent?) {
|
||||||
popupMenu.add(exitMenu)
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// double click
|
||||||
trayIcon.addActionListener(object : AbstractAction() {
|
trayIcon.addActionListener(object : AbstractAction() {
|
||||||
|
|||||||
21
src/main/kotlin/app/termora/FramePlugin.kt
Normal file
21
src/main/kotlin/app/termora/FramePlugin.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/main/kotlin/app/termora/KeymapRefresher.kt
Normal file
65
src/main/kotlin/app/termora/KeymapRefresher.kt
Normal 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) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
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.actions.StateAction
|
||||||
import app.termora.findeverywhere.FindEverywhereAction
|
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.FlatPopupMenu
|
||||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import java.awt.AWTEvent
|
import java.awt.AWTEvent
|
||||||
import java.awt.Rectangle
|
import java.awt.Rectangle
|
||||||
import java.awt.event.AWTEventListener
|
import java.awt.event.*
|
||||||
import java.awt.event.ActionEvent
|
|
||||||
import java.awt.event.MouseEvent
|
|
||||||
import java.beans.PropertyChangeEvent
|
import java.beans.PropertyChangeEvent
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import javax.swing.*
|
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()
|
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
|
||||||
@@ -56,6 +56,14 @@ internal class MyTermoraToolbar(private val windowScope: WindowScope) : FlatTool
|
|||||||
}
|
}
|
||||||
}).let { Disposer.register(windowScope, it) }
|
}).let { Disposer.register(windowScope, it) }
|
||||||
|
|
||||||
|
// 监听窗口大小变动,然后修改边距避开控制按钮
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
addComponentListener(object : ComponentAdapter() {
|
||||||
|
override fun componentResized(e: ComponentEvent) {
|
||||||
|
adjust()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshActions() {
|
private fun refreshActions() {
|
||||||
@@ -76,16 +84,70 @@ internal class MyTermoraToolbar(private val windowScope: WindowScope) : FlatTool
|
|||||||
|
|
||||||
add(Box.createHorizontalGlue())
|
add(Box.createHorizontalGlue())
|
||||||
|
|
||||||
|
// update
|
||||||
|
add(redirectUpdateAction(disposable))
|
||||||
|
|
||||||
for (action in model.getActions()) {
|
for (action in model.getActions()) {
|
||||||
if (action.visible.not()) continue
|
if (action.visible.not()) continue
|
||||||
val action = actionManager.getAction(action.id) ?: continue
|
val action = actionManager.getAction(action.id) ?: continue
|
||||||
add(redirectAction(action, disposable))
|
add(redirectAction(action, disposable))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
adjust()
|
||||||
|
}
|
||||||
|
|
||||||
revalidate()
|
revalidate()
|
||||||
repaint()
|
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 {
|
private fun redirectAction(action: Action, disposable: Disposable): AbstractButton {
|
||||||
val button = if (action is StateAction) JToggleButton() else JButton()
|
val button = if (action is StateAction) JToggleButton() else JButton()
|
||||||
button.toolTipText = action.getValue(Action.SHORT_DESCRIPTION) as? String
|
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 {
|
val listener = object : PropertyChangeListener, Disposable {
|
||||||
private val badge get() = Badge.getInstance(windowScope)
|
|
||||||
override fun propertyChange(evt: PropertyChangeEvent) {
|
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) {
|
if (action is StateAction) {
|
||||||
button.isSelected = action.isSelected(windowScope)
|
button.isSelected = action.isSelected(windowScope)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
|
import app.termora.plugin.internal.AltKeyModifier
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
@@ -46,6 +47,9 @@ abstract class PtyHostTerminalTab(
|
|||||||
// 开启 reader
|
// 开启 reader
|
||||||
startPtyConnectorReader()
|
startPtyConnectorReader()
|
||||||
|
|
||||||
|
// 修饰
|
||||||
|
terminalKeyModifiers()
|
||||||
|
|
||||||
// 启动命令
|
// 启动命令
|
||||||
if (host.options.startupCommand.isNotBlank()) {
|
if (host.options.startupCommand.isNotBlank()) {
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
@@ -155,6 +159,15 @@ abstract class PtyHostTerminalTab(
|
|||||||
ptyConnector.write(bytes)
|
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 {
|
override fun canReconnect(): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import javax.swing.JComponent
|
|||||||
import javax.swing.JPanel
|
import javax.swing.JPanel
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
|
|
||||||
class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
internal class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
||||||
private val optionsPane = SettingsOptionsPane()
|
private val optionsPane = SettingsOptionsPane()
|
||||||
private val properties get() = DatabaseManager.getInstance().properties
|
private val properties get() = DatabaseManager.getInstance().properties
|
||||||
|
|
||||||
|
|||||||
@@ -839,7 +839,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private fun p(): JPanel {
|
private fun p(): JPanel {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $FORM_MARGIN, default:grow",
|
"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()
|
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)
|
.layout(layout).debug(false)
|
||||||
.add(I18n.getString("termora.settings.about.termora", Application.getVersion()))
|
.add(I18n.getString("termora.settings.about.termora", Application.getVersion()))
|
||||||
.xyw(1, rows, 3, "center, fill").apply { rows += step }
|
.xyw(1, rows, 3, "center, fill").apply { rows += step }
|
||||||
@@ -870,8 +870,14 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
"Open-source software"
|
"Open-source software"
|
||||||
)
|
)
|
||||||
).xy(3, rows).apply { rows += step }
|
).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()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.TerminalFocusModeAction
|
||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import app.termora.terminal.panel.TerminalPanel
|
import app.termora.terminal.panel.TerminalPanel
|
||||||
import app.termora.tlog.TerminalLoggerDataListener
|
import app.termora.tlog.TerminalLoggerDataListener
|
||||||
import java.awt.Color
|
import java.awt.Color
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
|
import kotlin.reflect.cast
|
||||||
|
|
||||||
class TerminalFactory private constructor() : Disposable {
|
class TerminalFactory private constructor() : Disposable {
|
||||||
private val terminals = mutableListOf<Terminal>()
|
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 {
|
override fun <T : Any> getData(key: DataKey<T>, defaultValue: T): T {
|
||||||
if (key == TerminalPanel.SelectCopy) {
|
if (key == TerminalPanel.SelectCopy) {
|
||||||
return config.selectCopy as T
|
return config.selectCopy as T
|
||||||
|
} else if (key == TerminalPanel.FocusMode) {
|
||||||
|
return key.clazz.cast(TerminalFocusModeAction.getInstance().isSelected)
|
||||||
}
|
}
|
||||||
return super.getData(key, defaultValue)
|
return super.getData(key, defaultValue)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package app.termora
|
|||||||
|
|
||||||
|
|
||||||
import app.termora.actions.*
|
import app.termora.actions.*
|
||||||
import app.termora.database.DatabaseChangedExtension
|
|
||||||
import app.termora.database.DatabasePropertiesChangedExtension
|
|
||||||
import app.termora.findeverywhere.FindEverywhereProvider
|
import app.termora.findeverywhere.FindEverywhereProvider
|
||||||
import app.termora.findeverywhere.FindEverywhereProviderExtension
|
import app.termora.findeverywhere.FindEverywhereProviderExtension
|
||||||
import app.termora.findeverywhere.FindEverywhereResult
|
import app.termora.findeverywhere.FindEverywhereResult
|
||||||
@@ -42,7 +40,7 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
private val id = UUID.randomUUID().toString()
|
private val id = UUID.randomUUID().toString()
|
||||||
private val windowScope = ApplicationScope.forWindowScope(this)
|
private val windowScope = ApplicationScope.forWindowScope(this)
|
||||||
private val tabbedPane = MyTabbedPane().apply { tabHeight = titleBarHeight }
|
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 terminalTabbed = TerminalTabbed(windowScope, tabbedPane, layout)
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
private var notifyListeners = emptyArray<NotifyListener>()
|
private var notifyListeners = emptyArray<NotifyListener>()
|
||||||
@@ -73,12 +71,8 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 快捷键变动时重新监听
|
// 快捷键变动时重新监听
|
||||||
val refresher = KeymapRefresher()
|
KeymapRefresher.getInstance().addRefreshListener { initKeymap() }
|
||||||
dynamicExtensionHandler.register(DatabasePropertiesChangedExtension::class.java, refresher)
|
|
||||||
.let { Disposer.register(windowScope, it) }
|
.let { Disposer.register(windowScope, it) }
|
||||||
dynamicExtensionHandler.register(DatabaseChangedExtension::class.java, refresher)
|
|
||||||
.let { Disposer.register(windowScope, it) }
|
|
||||||
|
|
||||||
|
|
||||||
// FindEverywhere
|
// FindEverywhere
|
||||||
dynamicExtensionHandler
|
dynamicExtensionHandler
|
||||||
@@ -418,29 +412,6 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
return object : MouseAdapter() {}
|
return object : MouseAdapter() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class KeymapRefresher : DatabasePropertiesChangedExtension, DatabaseChangedExtension {
|
|
||||||
|
|
||||||
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() {
|
|
||||||
initKeymap()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class RedirectAnActionEvent(
|
private inner class RedirectAnActionEvent(
|
||||||
source: Any,
|
source: Any,
|
||||||
|
|||||||
@@ -86,8 +86,8 @@ class UpdaterManager private constructor() {
|
|||||||
return LatestVersion.self
|
return LatestVersion.self
|
||||||
}
|
}
|
||||||
|
|
||||||
val text = response.use { resp -> resp.body?.use { it.string() } }
|
val text = response.use { resp -> resp.body.use { it.string() } }
|
||||||
if (text.isNullOrBlank()) {
|
if (text.isBlank()) {
|
||||||
return LatestVersion.self
|
return LatestVersion.self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
|||||||
addAction(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
|
addAction(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
|
||||||
addAction(QuickConnectAction.QUICK_CONNECT, QuickConnectAction.instance)
|
addAction(QuickConnectAction.QUICK_CONNECT, QuickConnectAction.instance)
|
||||||
|
|
||||||
addAction(Actions.APP_UPDATE, AppUpdateAction.getInstance())
|
|
||||||
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())
|
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())
|
||||||
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
|
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
|
||||||
addAction(Actions.SFTP, TransferAnAction())
|
addAction(Actions.SFTP, TransferAnAction())
|
||||||
@@ -42,7 +41,7 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
|||||||
|
|
||||||
addAction(SwitchTabAction.SWITCH_TAB, SwitchTabAction())
|
addAction(SwitchTabAction.SWITCH_TAB, SwitchTabAction())
|
||||||
addAction(TabReconnectAction.RECONNECT_TAB, TabReconnectAction())
|
addAction(TabReconnectAction.RECONNECT_TAB, TabReconnectAction())
|
||||||
addAction(SettingsAction.SETTING, SettingsAction())
|
addAction(SettingsAction.SETTING, SettingsAction.getInstance())
|
||||||
|
|
||||||
addAction(NewHostAction.NEW_HOST, NewHostAction())
|
addAction(NewHostAction.NEW_HOST, NewHostAction())
|
||||||
addAction(OpenHostAction.OPEN_HOST, OpenHostAction())
|
addAction(OpenHostAction.OPEN_HOST, OpenHostAction())
|
||||||
@@ -54,6 +53,7 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
|||||||
addAction(TerminalClearScreenAction.CLEAR_SCREEN, TerminalClearScreenAction())
|
addAction(TerminalClearScreenAction.CLEAR_SCREEN, TerminalClearScreenAction())
|
||||||
addAction(OpenLocalTerminalAction.LOCAL_TERMINAL, OpenLocalTerminalAction())
|
addAction(OpenLocalTerminalAction.LOCAL_TERMINAL, OpenLocalTerminalAction())
|
||||||
addAction(TerminalSelectAllAction.SELECT_ALL, TerminalSelectAllAction())
|
addAction(TerminalSelectAllAction.SELECT_ALL, TerminalSelectAllAction())
|
||||||
|
addAction(TerminalFocusModeAction.FocusMode, TerminalFocusModeAction.getInstance())
|
||||||
|
|
||||||
addAction(TerminalZoomInAction.ZOOM_IN, TerminalZoomInAction())
|
addAction(TerminalZoomInAction.ZOOM_IN, TerminalZoomInAction())
|
||||||
addAction(TerminalZoomOutAction.ZOOM_OUT, TerminalZoomOutAction())
|
addAction(TerminalZoomOutAction.ZOOM_OUT, TerminalZoomOutAction())
|
||||||
|
|||||||
@@ -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.WindowAdapter
|
||||||
import java.awt.event.WindowEvent
|
import java.awt.event.WindowEvent
|
||||||
|
|
||||||
class SettingsAction : AnAction(
|
class SettingsAction private constructor() : AnAction(
|
||||||
I18n.getString("termora.setting"),
|
I18n.getString("termora.setting"),
|
||||||
Icons.settings
|
Icons.settings
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开设置
|
* 打开设置
|
||||||
*/
|
*/
|
||||||
const val SETTING = "SettingAction"
|
const val SETTING = "SettingAction"
|
||||||
|
|
||||||
|
fun getInstance(): SettingsAction {
|
||||||
|
return ApplicationScope.forApplicationScope().getOrCreate(SettingsAction::class) { SettingsAction() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isShowing = false
|
private var isShowing = false
|
||||||
|
private val action get() = this
|
||||||
|
|
||||||
init {
|
init {
|
||||||
FlatDesktop.setPreferencesHandler {
|
FlatDesktop.setPreferencesHandler {
|
||||||
@@ -36,20 +42,25 @@ class SettingsAction : AnAction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
if (isShowing) {
|
if (isShowing) return
|
||||||
return
|
showSettingsDialog(evt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun showSettingsDialog(evt: AnActionEvent) {
|
||||||
|
|
||||||
isShowing = true
|
isShowing = true
|
||||||
|
|
||||||
val owner = evt.window
|
val owner = evt.window
|
||||||
val dialog = SettingsDialog(owner)
|
val dialog = SettingsDialog(owner)
|
||||||
dialog.addWindowListener(object : WindowAdapter() {
|
dialog.addWindowListener(object : WindowAdapter() {
|
||||||
override fun windowClosed(e: WindowEvent) {
|
override fun windowClosed(e: WindowEvent) {
|
||||||
this@SettingsAction.isShowing = false
|
action.isShowing = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
dialog.setLocationRelativeTo(owner)
|
dialog.setLocationRelativeTo(owner)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import app.termora.I18n
|
|||||||
import app.termora.Scope
|
import app.termora.Scope
|
||||||
import app.termora.WindowScope
|
import app.termora.WindowScope
|
||||||
import app.termora.actions.MultipleAction
|
import app.termora.actions.MultipleAction
|
||||||
|
import app.termora.actions.TerminalFocusModeAction
|
||||||
|
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ class QuickActionsFindEverywhereProvider(private val windowScope: WindowScope) :
|
|||||||
Actions.KEY_MANAGER,
|
Actions.KEY_MANAGER,
|
||||||
Actions.KEYWORD_HIGHLIGHT,
|
Actions.KEYWORD_HIGHLIGHT,
|
||||||
MultipleAction.MULTIPLE,
|
MultipleAction.MULTIPLE,
|
||||||
|
TerminalFocusModeAction.FocusMode,
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun find(pattern: String, scope: Scope): List<FindEverywhereResult> {
|
override fun find(pattern: String, scope: Scope): List<FindEverywhereResult> {
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
package app.termora.highlight
|
package app.termora.highlight
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.account.AccountOwner
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.terminal.TerminalColor
|
import app.termora.terminal.TerminalColor
|
||||||
import com.formdev.flatlaf.extras.components.FlatTable
|
import com.formdev.flatlaf.extras.components.FlatTable
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Color
|
import java.awt.Color
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.border.EmptyBorder
|
import javax.swing.border.EmptyBorder
|
||||||
import javax.swing.table.DefaultTableCellRenderer
|
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 addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
|
||||||
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
|
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
|
||||||
private val deleteBtn = JButton(I18n.getString("termora.remove"))
|
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 {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -213,6 +219,29 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
|
|||||||
deleteBtn.isEnabled = editBtn.isEnabled
|
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 {
|
Disposer.register(this, object : Disposable {
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
terminal.close()
|
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 {
|
private fun createCenterPanel(): JComponent {
|
||||||
|
|
||||||
val panel = JPanel(BorderLayout())
|
val panel = JPanel(BorderLayout())
|
||||||
@@ -232,13 +278,15 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
|
|||||||
val formMargin = "4dlu"
|
val formMargin = "4dlu"
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"default:grow",
|
"default:grow",
|
||||||
"pref, $formMargin, pref, $formMargin, pref"
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
)
|
)
|
||||||
panel.add(
|
panel.add(
|
||||||
FormBuilder.create().layout(layout).padding(EmptyBorder(0, 12, 0, 0))
|
FormBuilder.create().layout(layout).padding(EmptyBorder(0, 12, 0, 0))
|
||||||
.add(addBtn).xy(1, rows).apply { rows += step }
|
.add(addBtn).xy(1, rows).apply { rows += step }
|
||||||
.add(editBtn).xy(1, rows).apply { rows += step }
|
.add(editBtn).xy(1, rows).apply { rows += step }
|
||||||
.add(deleteBtn).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(),
|
.build(),
|
||||||
BorderLayout.EAST)
|
BorderLayout.EAST)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app.termora.plugin
|
|||||||
|
|
||||||
import app.termora.Application
|
import app.termora.Application
|
||||||
import app.termora.ApplicationScope
|
import app.termora.ApplicationScope
|
||||||
|
import app.termora.FramePlugin
|
||||||
import app.termora.account.AccountPlugin
|
import app.termora.account.AccountPlugin
|
||||||
import app.termora.plugin.internal.badge.BadgePlugin
|
import app.termora.plugin.internal.badge.BadgePlugin
|
||||||
import app.termora.plugin.internal.extension.DynamicExtensionPlugin
|
import app.termora.plugin.internal.extension.DynamicExtensionPlugin
|
||||||
@@ -11,6 +12,7 @@ import app.termora.plugin.internal.rdp.RDPInternalPlugin
|
|||||||
import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin
|
import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin
|
||||||
import app.termora.plugin.internal.ssh.SSHInternalPlugin
|
import app.termora.plugin.internal.ssh.SSHInternalPlugin
|
||||||
import app.termora.plugin.internal.telnet.TelnetInternalPlugin
|
import app.termora.plugin.internal.telnet.TelnetInternalPlugin
|
||||||
|
import app.termora.plugin.internal.update.UpdatePlugin
|
||||||
import app.termora.plugin.internal.wsl.WSLInternalPlugin
|
import app.termora.plugin.internal.wsl.WSLInternalPlugin
|
||||||
import app.termora.swingCoroutineScope
|
import app.termora.swingCoroutineScope
|
||||||
import app.termora.terminal.panel.vw.FloatingToolbarPlugin
|
import app.termora.terminal.panel.vw.FloatingToolbarPlugin
|
||||||
@@ -108,6 +110,10 @@ internal class PluginManager private constructor() {
|
|||||||
plugins.add(PluginDescriptor(AccountPlugin(), origin = PluginOrigin.Internal, version = version))
|
plugins.add(PluginDescriptor(AccountPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
// badge plugin
|
// badge plugin
|
||||||
plugins.add(PluginDescriptor(BadgePlugin(), origin = PluginOrigin.Internal, version = version))
|
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
|
// ssh plugin
|
||||||
plugins.add(PluginDescriptor(SSHInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
plugins.add(PluginDescriptor(SSHInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package app.termora.plugin.internal
|
||||||
|
|
||||||
|
enum class AltKeyModifier {
|
||||||
|
EightBit,
|
||||||
|
CharactersPrecededByESC,
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,20 +1,25 @@
|
|||||||
package app.termora.plugin.internal.local
|
package app.termora.plugin.internal.local
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.Host
|
||||||
|
import app.termora.Options
|
||||||
|
import app.termora.OptionsPane
|
||||||
|
import app.termora.SerialComm
|
||||||
|
import app.termora.plugin.internal.AltKeyModifier
|
||||||
import app.termora.plugin.internal.BasicGeneralOption
|
import app.termora.plugin.internal.BasicGeneralOption
|
||||||
|
import app.termora.plugin.internal.BasicTerminalOption
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
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.KeyboardFocusManager
|
|
||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
import java.nio.charset.Charset
|
import javax.swing.JTextField
|
||||||
import javax.swing.*
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
internal open class LocalHostOptionsPane : OptionsPane() {
|
internal open class LocalHostOptionsPane : OptionsPane() {
|
||||||
protected val generalOption = BasicGeneralOption()
|
protected val generalOption = BasicGeneralOption()
|
||||||
protected val terminalOption = TerminalOption()
|
private val terminalOption = BasicTerminalOption().apply {
|
||||||
|
showCharsetComboBox = true
|
||||||
|
showEnvironmentTextArea = true
|
||||||
|
showStartupCommandTextField = true
|
||||||
|
init()
|
||||||
|
}
|
||||||
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -35,6 +40,10 @@ internal open class LocalHostOptionsPane : OptionsPane() {
|
|||||||
env = terminalOption.environmentTextArea.text,
|
env = terminalOption.environmentTextArea.text,
|
||||||
startupCommand = terminalOption.startupCommandTextField.text,
|
startupCommand = terminalOption.startupCommandTextField.text,
|
||||||
serialComm = serialComm,
|
serialComm = serialComm,
|
||||||
|
extras = mutableMapOf(
|
||||||
|
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
|
||||||
|
?: AltKeyModifier.EightBit.name),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return Host(
|
return Host(
|
||||||
@@ -77,83 +86,4 @@ internal open class LocalHostOptionsPane : OptionsPane() {
|
|||||||
textField.requestFocusInWindow()
|
textField.requestFocusInWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected inner class TerminalOption : JPanel(BorderLayout()), Option {
|
|
||||||
val charsetComboBox = JComboBox<String>()
|
|
||||||
val startupCommandTextField = OutlineTextField()
|
|
||||||
val environmentTextArea = FixedLengthTextArea(2048)
|
|
||||||
|
|
||||||
|
|
||||||
init {
|
|
||||||
initView()
|
|
||||||
initEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
add(getCenterComponent(), BorderLayout.CENTER)
|
|
||||||
|
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 }
|
|
||||||
.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
|
|
||||||
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
|
|
||||||
.apply { rows += step }
|
|
||||||
.build()
|
|
||||||
|
|
||||||
|
|
||||||
return panel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ import java.awt.KeyboardFocusManager
|
|||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
import java.awt.event.ComponentAdapter
|
import java.awt.event.ComponentAdapter
|
||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
|
import java.awt.event.ItemEvent
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
internal open class RDPHostOptionsPane : OptionsPane() {
|
internal open class RDPHostOptionsPane : OptionsPane() {
|
||||||
@@ -223,6 +224,12 @@ internal open class RDPHostOptionsPane : OptionsPane() {
|
|||||||
removeComponentListener(this)
|
removeComponentListener(this)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
authenticationTypeComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
passwordTextField.isEnabled = authenticationTypeComboBox.selectedItem == AuthenticationType.Password
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,14 @@ import app.termora.*
|
|||||||
import app.termora.account.AccountOwner
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.keymgr.KeyManager
|
import app.termora.keymgr.KeyManager
|
||||||
import app.termora.keymgr.KeyManagerDialog
|
import app.termora.keymgr.KeyManagerDialog
|
||||||
|
import app.termora.plugin.internal.AltKeyModifier
|
||||||
import app.termora.plugin.internal.BasicProxyOption
|
import app.termora.plugin.internal.BasicProxyOption
|
||||||
|
import app.termora.plugin.internal.BasicTerminalOption
|
||||||
import app.termora.tree.Filter
|
import app.termora.tree.Filter
|
||||||
import app.termora.tree.HostTreeNode
|
import app.termora.tree.HostTreeNode
|
||||||
import app.termora.tree.NewHostTreeDialog
|
import app.termora.tree.NewHostTreeDialog
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
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.ui.FlatTextBorder
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
@@ -23,21 +22,26 @@ import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocket
|
|||||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.WinPipeConnector
|
import org.eclipse.jgit.internal.transport.sshd.agent.connector.WinPipeConnector
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
import java.awt.event.*
|
import java.awt.event.*
|
||||||
import java.nio.charset.Charset
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.table.DefaultTableCellRenderer
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
import javax.swing.table.DefaultTableModel
|
import javax.swing.table.DefaultTableModel
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
@Suppress("CascadeIf")
|
@Suppress("CascadeIf")
|
||||||
open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() {
|
internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() {
|
||||||
protected val tunnelingOption = TunnelingOption()
|
private val tunnelingOption = TunnelingOption()
|
||||||
protected val generalOption = GeneralOption()
|
private val generalOption = GeneralOption()
|
||||||
protected val proxyOption = BasicProxyOption()
|
private val proxyOption = BasicProxyOption()
|
||||||
protected val terminalOption = TerminalOption()
|
private val terminalOption = BasicTerminalOption().apply {
|
||||||
protected val jumpHostsOption = JumpHostsOption()
|
showCharsetComboBox = true
|
||||||
protected val sftpOption = SFTPOption()
|
showLoginScripts = true
|
||||||
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
showEnvironmentTextArea = true
|
||||||
|
showStartupCommandTextField = true
|
||||||
|
showHeartbeatIntervalTextField = true
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
private val jumpHostsOption = JumpHostsOption()
|
||||||
|
private val sftpOption = SFTPOption()
|
||||||
|
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
addOption(generalOption)
|
addOption(generalOption)
|
||||||
@@ -50,7 +54,7 @@ open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsP
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
open fun getHost(): Host {
|
fun getHost(): Host {
|
||||||
val name = generalOption.nameTextField.text
|
val name = generalOption.nameTextField.text
|
||||||
val protocol = SSHProtocolProvider.PROTOCOL
|
val protocol = SSHProtocolProvider.PROTOCOL
|
||||||
val host = generalOption.hostTextField.text
|
val host = generalOption.hostTextField.text
|
||||||
@@ -101,6 +105,10 @@ open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsP
|
|||||||
enableX11Forwarding = tunnelingOption.x11ForwardingCheckBox.isSelected,
|
enableX11Forwarding = tunnelingOption.x11ForwardingCheckBox.isSelected,
|
||||||
x11Forwarding = tunnelingOption.x11ServerTextField.text,
|
x11Forwarding = tunnelingOption.x11ServerTextField.text,
|
||||||
loginScripts = terminalOption.loginScripts,
|
loginScripts = terminalOption.loginScripts,
|
||||||
|
extras = mutableMapOf(
|
||||||
|
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
|
||||||
|
?: AltKeyModifier.EightBit.name),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return Host(
|
return Host(
|
||||||
@@ -489,284 +497,6 @@ open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsP
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected inner class TerminalOption : JPanel(BorderLayout()), Option {
|
|
||||||
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>()
|
|
||||||
|
|
||||||
|
|
||||||
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 tabbed = FlatTabbedPane()
|
|
||||||
|
|
||||||
init {
|
|
||||||
initView()
|
|
||||||
initEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
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"),
|
|
||||||
"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"), getLoginScriptsComponent())
|
|
||||||
add(tabbed, BorderLayout.CENTER)
|
|
||||||
|
|
||||||
|
|
||||||
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() {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
.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.heartbeat-interval")}:").xy(1, rows)
|
|
||||||
.add(heartbeatIntervalTextField).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)
|
|
||||||
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
|
|
||||||
.apply { rows += step }
|
|
||||||
.build()
|
|
||||||
|
|
||||||
|
|
||||||
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 {
|
protected inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||||
val defaultDirectoryField = OutlineTextField(255)
|
val defaultDirectoryField = OutlineTextField(255)
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,17 @@ package app.termora.plugin.internal.telnet
|
|||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.account.AccountOwner
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.keymgr.KeyManager
|
import app.termora.plugin.internal.AltKeyModifier
|
||||||
import app.termora.plugin.internal.BasicProxyOption
|
import app.termora.plugin.internal.BasicProxyOption
|
||||||
|
import app.termora.plugin.internal.BasicTerminalOption
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
|
||||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Component
|
|
||||||
import java.awt.KeyboardFocusManager
|
import java.awt.KeyboardFocusManager
|
||||||
import java.awt.Window
|
|
||||||
import java.awt.event.ComponentAdapter
|
import java.awt.event.ComponentAdapter
|
||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
import java.nio.charset.Charset
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() {
|
class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() {
|
||||||
@@ -24,8 +20,16 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
|||||||
|
|
||||||
// telnet 不支持代理密码
|
// telnet 不支持代理密码
|
||||||
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
|
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
|
||||||
private val terminalOption = TerminalOption()
|
private val terminalOption = BasicTerminalOption().apply {
|
||||||
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
showCharsetComboBox = true
|
||||||
|
showBackspaceComboBox = true
|
||||||
|
showStartupCommandTextField = true
|
||||||
|
showCharacterAtATimeTextField = true
|
||||||
|
showEnvironmentTextArea = true
|
||||||
|
showLoginScripts = true
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
addOption(generalOption)
|
addOption(generalOption)
|
||||||
@@ -39,16 +43,7 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
|||||||
val protocol = TelnetProtocolProvider.PROTOCOL
|
val protocol = TelnetProtocolProvider.PROTOCOL
|
||||||
val host = generalOption.hostTextField.text
|
val host = generalOption.hostTextField.text
|
||||||
val port = (generalOption.portTextField.value ?: 23) as Int
|
val port = (generalOption.portTextField.value ?: 23) as Int
|
||||||
var authentication = Authentication.No
|
|
||||||
var proxy = Proxy.Companion.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) {
|
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||||
proxy = proxy.copy(
|
proxy = proxy.copy(
|
||||||
@@ -68,8 +63,14 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
|||||||
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
||||||
env = terminalOption.environmentTextArea.text,
|
env = terminalOption.environmentTextArea.text,
|
||||||
startupCommand = terminalOption.startupCommandTextField.text,
|
startupCommand = terminalOption.startupCommandTextField.text,
|
||||||
|
loginScripts = terminalOption.loginScripts,
|
||||||
serialComm = serialComm,
|
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"),
|
||||||
|
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
|
||||||
|
?: AltKeyModifier.EightBit.name),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return Host(
|
return Host(
|
||||||
@@ -77,8 +78,6 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
|||||||
protocol = protocol,
|
protocol = protocol,
|
||||||
host = host,
|
host = host,
|
||||||
port = port,
|
port = port,
|
||||||
username = generalOption.usernameTextField.text,
|
|
||||||
authentication = authentication,
|
|
||||||
proxy = proxy,
|
proxy = proxy,
|
||||||
sort = System.currentTimeMillis(),
|
sort = System.currentTimeMillis(),
|
||||||
remark = generalOption.remarkTextArea.text,
|
remark = generalOption.remarkTextArea.text,
|
||||||
@@ -89,13 +88,9 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
|||||||
fun setHost(host: Host) {
|
fun setHost(host: Host) {
|
||||||
generalOption.portTextField.value = host.port
|
generalOption.portTextField.value = host.port
|
||||||
generalOption.nameTextField.text = host.name
|
generalOption.nameTextField.text = host.name
|
||||||
generalOption.usernameTextField.text = host.username
|
|
||||||
generalOption.hostTextField.text = host.host
|
generalOption.hostTextField.text = host.host
|
||||||
generalOption.remarkTextArea.text = host.remark
|
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.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||||
proxyOption.proxyHostTextField.text = host.proxy.host
|
proxyOption.proxyHostTextField.text = host.proxy.host
|
||||||
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
||||||
@@ -108,7 +103,11 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
|||||||
terminalOption.startupCommandTextField.text = host.options.startupCommand
|
terminalOption.startupCommandTextField.text = host.options.startupCommand
|
||||||
terminalOption.backspaceComboBox.selectedItem =
|
terminalOption.backspaceComboBox.selectedItem =
|
||||||
Backspace.valueOf(host.options.extras["backspace"] ?: Backspace.Delete.name)
|
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 {
|
fun validateFields(): Boolean {
|
||||||
@@ -121,15 +120,6 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (host.authentication.type == AuthenticationType.Password) {
|
|
||||||
if (validateField(generalOption.usernameTextField)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (validateField(generalOption.passwordTextField)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// proxy
|
// proxy
|
||||||
if (host.proxy.type != ProxyType.No) {
|
if (host.proxy.type != ProxyType.No) {
|
||||||
if (validateField(proxyOption.proxyHostTextField)
|
if (validateField(proxyOption.proxyHostTextField)
|
||||||
@@ -166,29 +156,11 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
|||||||
textField.requestFocusInWindow()
|
textField.requestFocusInWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||||
* 返回 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 {
|
|
||||||
val portTextField = PortSpinner(23)
|
val portTextField = PortSpinner(23)
|
||||||
val nameTextField = OutlineTextField(128)
|
val nameTextField = OutlineTextField(128)
|
||||||
val usernameTextField = OutlineTextField(128)
|
|
||||||
val hostTextField = OutlineTextField(255)
|
val hostTextField = OutlineTextField(255)
|
||||||
val passwordTextField = OutlinePasswordField(255)
|
|
||||||
val publicKeyComboBox = OutlineComboBox<String>()
|
|
||||||
val remarkTextArea = FixedLengthTextArea(512)
|
val remarkTextArea = FixedLengthTextArea(512)
|
||||||
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -197,68 +169,6 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
|||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
add(getCenterComponent(), BorderLayout.CENTER)
|
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() {
|
private fun initEvents() {
|
||||||
@@ -285,7 +195,7 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
|||||||
private fun getCenterComponent(): JComponent {
|
private fun getCenterComponent(): JComponent {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
|
"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(
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||||
@@ -314,15 +224,6 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
|||||||
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||||
.add(portTextField).xy(7, rows).apply { rows += step }
|
.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("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
||||||
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
||||||
.xyw(3, rows, 5).apply { rows += step }
|
.xyw(3, rows, 5).apply { rows += step }
|
||||||
@@ -336,91 +237,6 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private inner class TerminalOption : JPanel(BorderLayout()), Option {
|
|
||||||
val charsetComboBox = JComboBox<String>()
|
|
||||||
val backspaceComboBox = JComboBox<Backspace>()
|
|
||||||
val startupCommandTextField = OutlineTextField()
|
|
||||||
val environmentTextArea = FixedLengthTextArea(2048)
|
|
||||||
|
|
||||||
|
|
||||||
init {
|
|
||||||
initView()
|
|
||||||
initEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
add(getCenterComponent(), BorderLayout.CENTER)
|
|
||||||
|
|
||||||
backspaceComboBox.addItem(Backspace.Delete)
|
|
||||||
backspaceComboBox.addItem(Backspace.Backspace)
|
|
||||||
backspaceComboBox.addItem(Backspace.VT220)
|
|
||||||
|
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
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.backspace")}:").xy(1, rows)
|
|
||||||
.add(backspaceComboBox).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)
|
|
||||||
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
|
|
||||||
.apply { rows += step }
|
|
||||||
.build()
|
|
||||||
|
|
||||||
|
|
||||||
return panel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Backspace {
|
enum class Backspace {
|
||||||
/**
|
/**
|
||||||
* 0x08
|
* 0x08
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.termora.plugin.internal.telnet
|
package app.termora.plugin.internal.telnet
|
||||||
|
|
||||||
import app.termora.terminal.StreamPtyConnector
|
import app.termora.terminal.StreamPtyConnector
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
import org.apache.commons.net.telnet.TelnetClient
|
import org.apache.commons.net.telnet.TelnetClient
|
||||||
import org.apache.commons.net.telnet.TelnetOption
|
import org.apache.commons.net.telnet.TelnetOption
|
||||||
import org.apache.commons.net.telnet.WindowSizeOptionHandler
|
import org.apache.commons.net.telnet.WindowSizeOptionHandler
|
||||||
@@ -9,19 +10,28 @@ import java.nio.charset.Charset
|
|||||||
|
|
||||||
class TelnetStreamPtyConnector(
|
class TelnetStreamPtyConnector(
|
||||||
private val telnet: TelnetClient,
|
private val telnet: TelnetClient,
|
||||||
private val charset: Charset
|
private val charset: Charset,
|
||||||
) :
|
private val characterMode: Boolean,
|
||||||
StreamPtyConnector(telnet.inputStream, telnet.outputStream) {
|
) : StreamPtyConnector(telnet.inputStream, telnet.outputStream) {
|
||||||
private val reader = InputStreamReader(telnet.inputStream, getCharset())
|
|
||||||
|
private val reader = InputStreamReader(telnet.inputStream, charset)
|
||||||
|
|
||||||
|
|
||||||
override fun read(buffer: CharArray): Int {
|
override fun read(buffer: CharArray): Int {
|
||||||
return reader.read(buffer)
|
return reader.read(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun write(buffer: ByteArray, offset: Int, len: Int) {
|
override fun write(buffer: ByteArray, offset: Int, len: Int) {
|
||||||
|
if (characterMode) {
|
||||||
|
for (i in offset until len + offset) {
|
||||||
|
output.write(byteArrayOf(buffer[i]))
|
||||||
|
output.flush()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
output.write(buffer, offset, len)
|
output.write(buffer, offset, len)
|
||||||
output.flush()
|
output.flush()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun resize(rows: Int, cols: Int) {
|
override fun resize(rows: Int, cols: Int) {
|
||||||
telnet.deleteOptionHandler(TelnetOption.WINDOW_SIZE)
|
telnet.deleteOptionHandler(TelnetOption.WINDOW_SIZE)
|
||||||
@@ -33,10 +43,13 @@ class TelnetStreamPtyConnector(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
|
IOUtils.closeQuietly(input)
|
||||||
|
IOUtils.closeQuietly(output)
|
||||||
telnet.disconnect()
|
telnet.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCharset(): Charset {
|
override fun getCharset(): Charset {
|
||||||
return charset
|
return charset
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package app.termora.plugin.internal.telnet
|
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.ControlCharacters
|
||||||
import app.termora.terminal.KeyEncoderImpl
|
import app.termora.terminal.KeyEncoderImpl
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
@@ -11,6 +14,7 @@ import java.net.InetSocketAddress
|
|||||||
import java.net.Proxy
|
import java.net.Proxy
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
|
|
||||||
class TelnetTerminalTab(
|
class TelnetTerminalTab(
|
||||||
windowScope: WindowScope, host: Host,
|
windowScope: WindowScope, host: Host,
|
||||||
) : PtyHostTerminalTab(windowScope, 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 termtype = host.options.envs()["TERM"] ?: "xterm-256color"
|
||||||
val ttopt = TerminalTypeOptionHandler(termtype, false, false, true, false)
|
val ttopt = TerminalTypeOptionHandler(termtype, false, false, true, false)
|
||||||
val echoopt = EchoOptionHandler(false, true, false, true)
|
val echoopt = EchoOptionHandler(false, true, false, true)
|
||||||
val gaopt = SuppressGAOptionHandler(true, true, true, true)
|
val gaopt = SuppressGAOptionHandler(true, true, true, true)
|
||||||
val wsopt = WindowSizeOptionHandler(winSize.cols, winSize.rows, true, false, true, false)
|
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(ttopt)
|
||||||
telnet.addOptionHandler(echoopt)
|
telnet.addOptionHandler(echoopt)
|
||||||
telnet.addOptionHandler(gaopt)
|
telnet.addOptionHandler(gaopt)
|
||||||
@@ -45,6 +55,7 @@ class TelnetTerminalTab(
|
|||||||
|
|
||||||
telnet.connect(host.host, host.port)
|
telnet.connect(host.host, host.port)
|
||||||
telnet.keepAlive = true
|
telnet.keepAlive = true
|
||||||
|
telnet.tcpNoDelay = characterMode
|
||||||
|
|
||||||
val encoder = terminal.getKeyEncoder()
|
val encoder = terminal.getKeyEncoder()
|
||||||
if (encoder is KeyEncoderImpl) {
|
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)
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package app.termora.plugin.internal.wsl
|
package app.termora.plugin.internal.wsl
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
|
import app.termora.plugin.internal.AltKeyModifier
|
||||||
|
import app.termora.plugin.internal.BasicTerminalOption
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
@@ -12,12 +14,17 @@ import java.awt.KeyboardFocusManager
|
|||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
import java.awt.event.ComponentAdapter
|
import java.awt.event.ComponentAdapter
|
||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
import java.nio.charset.Charset
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
internal open class WSLHostOptionsPane : OptionsPane() {
|
internal open class WSLHostOptionsPane : OptionsPane() {
|
||||||
protected val generalOption = GeneralOption()
|
protected val generalOption = GeneralOption()
|
||||||
protected val terminalOption = TerminalOption()
|
protected val terminalOption = BasicTerminalOption().apply {
|
||||||
|
showCharsetComboBox = true
|
||||||
|
showStartupCommandTextField = true
|
||||||
|
showEnvironmentTextArea = true
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -36,7 +43,11 @@ internal open class WSLHostOptionsPane : OptionsPane() {
|
|||||||
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
||||||
env = terminalOption.environmentTextArea.text,
|
env = terminalOption.environmentTextArea.text,
|
||||||
startupCommand = terminalOption.startupCommandTextField.text,
|
startupCommand = terminalOption.startupCommandTextField.text,
|
||||||
extras = mutableMapOf("wsl-guid" to wsl.guid, "wsl-flavor" to wsl.flavor)
|
extras = mutableMapOf(
|
||||||
|
"wsl-guid" to wsl.guid, "wsl-flavor" to wsl.flavor,
|
||||||
|
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
|
||||||
|
?: AltKeyModifier.EightBit.name),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return Host(
|
return Host(
|
||||||
@@ -216,85 +227,5 @@ internal open class WSLHostOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected inner class TerminalOption : JPanel(BorderLayout()), Option {
|
|
||||||
val charsetComboBox = JComboBox<String>()
|
|
||||||
val startupCommandTextField = OutlineTextField()
|
|
||||||
val environmentTextArea = FixedLengthTextArea(2048)
|
|
||||||
|
|
||||||
|
|
||||||
init {
|
|
||||||
initView()
|
|
||||||
initEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
add(getCenterComponent(), BorderLayout.CENTER)
|
|
||||||
|
|
||||||
startupCommandTextField.placeholderText = "--cd ~"
|
|
||||||
|
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 }
|
|
||||||
.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
|
|
||||||
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
|
|
||||||
.apply { rows += step }
|
|
||||||
.build()
|
|
||||||
|
|
||||||
|
|
||||||
return panel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Set Scrolling Region [${top}; ${bottom}]")
|
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
|
// Alternate Screen Buffer
|
||||||
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
|
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
|
||||||
1049 -> {
|
1049 -> {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.terminal
|
package app.termora.terminal
|
||||||
|
|
||||||
|
import app.termora.plugin.internal.AltKeyModifier
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
|
||||||
@@ -192,6 +193,11 @@ class DataKey<T : Any>(val clazz: KClass<T>) {
|
|||||||
* TerminalWriter
|
* TerminalWriter
|
||||||
*/
|
*/
|
||||||
val TerminalWriter = DataKey(app.termora.terminal.panel.TerminalWriter::class)
|
val TerminalWriter = DataKey(app.termora.terminal.panel.TerminalWriter::class)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [app.termora.plugin.internal.AltKeyModifier]
|
||||||
|
*/
|
||||||
|
val AltModifier = DataKey(AltKeyModifier::class)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.UIManager
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -263,9 +264,8 @@ class TerminalDisplay(
|
|||||||
var j = 1
|
var j = 1
|
||||||
while (j <= cols) {
|
while (j <= cols) {
|
||||||
val position = Position(row + 1, j)
|
val position = Position(row + 1, j)
|
||||||
val caret = showCursor && j == cursorPosition.x + inputMethodData.offset
|
val isCursorLine = i == cursorPosition.y + (maxVerticalScrollOffset - verticalScrollOffset)
|
||||||
&& i == cursorPosition.y + (maxVerticalScrollOffset - verticalScrollOffset)
|
val caret = showCursor && j == cursorPosition.x + inputMethodData.offset && isCursorLine
|
||||||
|
|
||||||
val (text, style, length) = if (characters.hasNext()) characters.next() else triple
|
val (text, style, length) = if (characters.hasNext()) characters.next() else triple
|
||||||
var textStyle = style
|
var textStyle = style
|
||||||
val hasSelection = selectionModel.hasSelection(y = i + verticalScrollOffset, x = j)
|
val hasSelection = selectionModel.hasSelection(y = i + verticalScrollOffset, x = j)
|
||||||
@@ -307,6 +307,16 @@ class TerminalDisplay(
|
|||||||
length * averageCharWidth
|
length * averageCharWidth
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Focus Mode
|
||||||
|
if (terminalModel.getData(TerminalPanel.FocusMode, false)) {
|
||||||
|
if (terminalModel.isAlternateScreenBuffer().not()) {
|
||||||
|
if (isCursorLine.not()) {
|
||||||
|
background = colorPalette.getColor(TerminalColor.Basic.BACKGROUND)
|
||||||
|
foreground = UIManager.getColor("textInactiveText").rgb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果没有颜色反转并且与渲染的背景色一致,那么无需渲染背景
|
// 如果没有颜色反转并且与渲染的背景色一致,那么无需渲染背景
|
||||||
if (textStyle.inverse || background != colorPalette.getColor(TerminalColor.Basic.BACKGROUND)) {
|
if (textStyle.inverse || background != colorPalette.getColor(TerminalColor.Basic.BACKGROUND)) {
|
||||||
g.color = Color(background)
|
g.color = Color(background)
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class TerminalPanel(val tab: TerminalTab?, val terminal: Terminal, private val w
|
|||||||
val Finding = DataKey(Boolean::class)
|
val Finding = DataKey(Boolean::class)
|
||||||
val Focused = DataKey(Boolean::class)
|
val Focused = DataKey(Boolean::class)
|
||||||
val SelectCopy = DataKey(Boolean::class)
|
val SelectCopy = DataKey(Boolean::class)
|
||||||
|
val FocusMode = DataKey(Boolean::class)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val properties get() = DatabaseManager.getInstance().properties
|
private val properties get() = DatabaseManager.getInstance().properties
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package app.termora.terminal.panel
|
|||||||
|
|
||||||
import app.termora.keymap.KeyShortcut
|
import app.termora.keymap.KeyShortcut
|
||||||
import app.termora.keymap.KeymapManager
|
import app.termora.keymap.KeymapManager
|
||||||
|
import app.termora.plugin.internal.AltKeyModifier
|
||||||
import app.termora.terminal.ControlCharacters
|
import app.termora.terminal.ControlCharacters
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.terminal.Terminal
|
import app.termora.terminal.Terminal
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -89,8 +91,10 @@ class TerminalPanelKeyAdapter(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/TermoraDev/termora/issues/865
|
||||||
|
val modifier = terminal.getTerminalModel().getData(DataKey.AltModifier, AltKeyModifier.EightBit)
|
||||||
// https://github.com/TermoraDev/termora/issues/331
|
// https://github.com/TermoraDev/termora/issues/331
|
||||||
if (isAltPressedOnly(e) && Character.isDefined(e.keyChar)) {
|
if (isAltPressedOnly(e) && Character.isDefined(e.keyChar) && modifier == AltKeyModifier.CharactersPrecededByESC) {
|
||||||
val c = String(charArrayOf(ASCII_ESC, simpleMapKeyCodeToChar(e)))
|
val c = String(charArrayOf(ASCII_ESC, simpleMapKeyCodeToChar(e)))
|
||||||
writer.write(TerminalWriter.WriteRequest.fromBytes(c.toByteArray(writer.getCharset())))
|
writer.write(TerminalWriter.WriteRequest.fromBytes(c.toByteArray(writer.getCharset())))
|
||||||
// scroll to bottom
|
// scroll to bottom
|
||||||
|
|||||||
@@ -467,6 +467,15 @@ internal class TransportPanel(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
table.actionMap.put("copy", object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val rows = table.selectedRows.map { sorter.convertRowIndexToModel(it) }.toTypedArray()
|
||||||
|
val files = rows.map { model.getPath(it) to model.getAttributes(it) }
|
||||||
|
if (files.any { it.second.isParent }) return
|
||||||
|
toolkit.systemClipboard.setContents(TransferTransferable(panel, files), null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
table.actionMap.put("Reload", object : AbstractAction() {
|
table.actionMap.put("Reload", object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
reload()
|
reload()
|
||||||
@@ -514,7 +523,6 @@ internal class TransportPanel(
|
|||||||
data class TransferData(
|
data class TransferData(
|
||||||
// true 就是本地拖拽上传
|
// true 就是本地拖拽上传
|
||||||
val locally: Boolean,
|
val locally: Boolean,
|
||||||
val row: Int,
|
|
||||||
val insertRow: Boolean,
|
val insertRow: Boolean,
|
||||||
val workdir: Path,
|
val workdir: Path,
|
||||||
val files: List<Pair<Path, Attributes>>
|
val files: List<Pair<Path, Attributes>>
|
||||||
@@ -540,18 +548,22 @@ internal class TransportPanel(
|
|||||||
|
|
||||||
private fun getTransferData(support: TransferSupport, load: Boolean): TransferData? {
|
private fun getTransferData(support: TransferSupport, load: Boolean): TransferData? {
|
||||||
val workdir = workdir ?: return null
|
val workdir = workdir ?: return null
|
||||||
val dropLocation = support.dropLocation as? JTable.DropLocation ?: return null
|
|
||||||
val row = if (dropLocation.isInsertRow) 0 else sorter.convertRowIndexToModel(dropLocation.row)
|
|
||||||
if (dropLocation.isInsertRow.not() && dropLocation.column != TransportTableModel.COLUMN_NAME) return null
|
|
||||||
if (dropLocation.isInsertRow.not() && model.getAttributes(row).isDirectory.not()) return null
|
|
||||||
if (hasParent && dropLocation.row == 0) return null
|
|
||||||
val paths = mutableListOf<Pair<Path, Attributes>>()
|
val paths = mutableListOf<Pair<Path, Attributes>>()
|
||||||
var locally = false
|
var locally = false
|
||||||
|
|
||||||
if (support.isDataFlavorSupported(TransferTransferable.FLAVOR)) {
|
if (support.isDataFlavorSupported(TransferTransferable.FLAVOR)) {
|
||||||
val transferTransferable = support.transferable.getTransferData(TransferTransferable.FLAVOR)
|
val transferTransferable = support.transferable.getTransferData(TransferTransferable.FLAVOR)
|
||||||
as? TransferTransferable ?: return null
|
as? TransferTransferable ?: return null
|
||||||
|
if (support.isDrop) {
|
||||||
if (transferTransferable.component == panel) return null
|
if (transferTransferable.component == panel) return null
|
||||||
|
} else {
|
||||||
|
// 如果在一个目录,那么是不允许粘贴的
|
||||||
|
for (pair in transferTransferable.files) {
|
||||||
|
if (pair.first.parent?.pathString == workdir.pathString) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
paths.addAll(transferTransferable.files)
|
paths.addAll(transferTransferable.files)
|
||||||
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||||
if (loader.isLoaded() && loader.getSyncTransportSupport().getFileSystem().isLocallyFileSystem())
|
if (loader.isLoaded() && loader.getSyncTransportSupport().getFileSystem().isLocallyFileSystem())
|
||||||
@@ -569,15 +581,29 @@ internal class TransportPanel(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (support.isDrop) {
|
||||||
|
val dropLocation = support.dropLocation as? JTable.DropLocation ?: return null
|
||||||
|
val row = if (dropLocation.isInsertRow) 0 else sorter.convertRowIndexToModel(dropLocation.row)
|
||||||
|
if (dropLocation.isInsertRow.not() && dropLocation.column != TransportTableModel.COLUMN_NAME) return null
|
||||||
|
if (dropLocation.isInsertRow.not() && model.getAttributes(row).isDirectory.not()) return null
|
||||||
|
if (hasParent && dropLocation.row == 0) return null
|
||||||
return TransferData(
|
return TransferData(
|
||||||
locally = locally,
|
locally = locally,
|
||||||
row = row,
|
|
||||||
insertRow = dropLocation.isInsertRow,
|
insertRow = dropLocation.isInsertRow,
|
||||||
workdir = if (dropLocation.isInsertRow) workdir else model.getPath(row),
|
workdir = if (dropLocation.isInsertRow) workdir else model.getPath(row),
|
||||||
files = paths
|
files = paths
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return TransferData(
|
||||||
|
locally = locally,
|
||||||
|
insertRow = false,
|
||||||
|
workdir = workdir,
|
||||||
|
files = paths
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
override fun getSourceActions(c: JComponent?): Int {
|
override fun getSourceActions(c: JComponent?): Int {
|
||||||
return COPY
|
return COPY
|
||||||
}
|
}
|
||||||
@@ -899,7 +925,7 @@ internal class TransportPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TransferTransferable(val component: TransportPanel, val files: List<Pair<Path, Attributes>>) :
|
class TransferTransferable(val component: TransportPanel, val files: List<Pair<Path, Attributes>>) :
|
||||||
Transferable {
|
Transferable {
|
||||||
companion object {
|
companion object {
|
||||||
val FLAVOR = DataFlavor("termora/transfers", "Termora transfers")
|
val FLAVOR = DataFlavor("termora/transfers", "Termora transfers")
|
||||||
@@ -1041,7 +1067,6 @@ internal class TransportPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private inner class PopupMenuActionListener(private val files: List<Pair<Path, Attributes>>) : ActionListener {
|
private inner class PopupMenuActionListener(private val files: List<Pair<Path, Attributes>>) : ActionListener {
|
||||||
@Suppress("CascadeIf")
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
val actionCommand = TransportPopupMenu.ActionCommand.valueOf(e.actionCommand)
|
val actionCommand = TransportPopupMenu.ActionCommand.valueOf(e.actionCommand)
|
||||||
if (actionCommand == TransportPopupMenu.ActionCommand.Transfer) {
|
if (actionCommand == TransportPopupMenu.ActionCommand.Transfer) {
|
||||||
@@ -1089,6 +1114,12 @@ internal class TransportPanel(
|
|||||||
Files.setPosixFilePermissions(path, c.permissions)
|
Files.setPosixFilePermissions(path, c.permissions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (actionCommand == TransportPopupMenu.ActionCommand.Copy) {
|
||||||
|
val transferable = TransferTransferable(panel, files)
|
||||||
|
toolkit.systemClipboard.setContents(transferable, null)
|
||||||
|
} else if (actionCommand == TransportPopupMenu.ActionCommand.Paste) {
|
||||||
|
val transferable = toolkit.systemClipboard.getContents(null) ?: return
|
||||||
|
table.transferHandler.importData(TransferHandler.TransferSupport(table, transferable))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import javax.swing.JMenu
|
|||||||
import javax.swing.JMenuItem
|
import javax.swing.JMenuItem
|
||||||
import javax.swing.JOptionPane
|
import javax.swing.JOptionPane
|
||||||
import javax.swing.event.EventListenerList
|
import javax.swing.event.EventListenerList
|
||||||
|
import javax.swing.event.PopupMenuEvent
|
||||||
|
import javax.swing.event.PopupMenuListener
|
||||||
import kotlin.io.path.absolutePathString
|
import kotlin.io.path.absolutePathString
|
||||||
import kotlin.io.path.name
|
import kotlin.io.path.name
|
||||||
|
|
||||||
@@ -39,6 +41,8 @@ internal class TransportPopupMenu(
|
|||||||
private val transferMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.transfer"))
|
private val transferMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.transfer"))
|
||||||
private val editMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.edit"))
|
private val editMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.edit"))
|
||||||
private val copyPathMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.copy-path"))
|
private val copyPathMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.copy-path"))
|
||||||
|
private val copyMenu = JMenuItem(I18n.getString("termora.copy"))
|
||||||
|
private val pasteMenu = JMenuItem(I18n.getString("termora.paste"))
|
||||||
private val openInFinderMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.open-in-folder"))
|
private val openInFinderMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.open-in-folder"))
|
||||||
private val renameMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.rename"))
|
private val renameMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.rename"))
|
||||||
private val deleteMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.delete"))
|
private val deleteMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.delete"))
|
||||||
@@ -82,6 +86,9 @@ internal class TransportPopupMenu(
|
|||||||
add(transferMenu)
|
add(transferMenu)
|
||||||
add(editMenu)
|
add(editMenu)
|
||||||
addSeparator()
|
addSeparator()
|
||||||
|
add(copyMenu)
|
||||||
|
add(pasteMenu)
|
||||||
|
addSeparator()
|
||||||
add(copyPathMenu)
|
add(copyPathMenu)
|
||||||
if (fileSystem?.isLocallyFileSystem() == true) {
|
if (fileSystem?.isLocallyFileSystem() == true) {
|
||||||
add(openInFinderMenu)
|
add(openInFinderMenu)
|
||||||
@@ -133,6 +140,7 @@ internal class TransportPopupMenu(
|
|||||||
renameMenu.isEnabled = hasParent.not() && files.size == 1
|
renameMenu.isEnabled = hasParent.not() && files.size == 1
|
||||||
deleteMenu.isEnabled = hasParent.not() && files.isNotEmpty()
|
deleteMenu.isEnabled = hasParent.not() && files.isNotEmpty()
|
||||||
changePermissionsMenu.isVisible = hasParent.not() && fileSystem is SftpFileSystem && files.size == 1
|
changePermissionsMenu.isVisible = hasParent.not() && fileSystem is SftpFileSystem && files.size == 1
|
||||||
|
copyMenu.isEnabled = hasParent.not() && files.isNotEmpty()
|
||||||
|
|
||||||
for ((item, mnemonic) in mnemonics) {
|
for ((item, mnemonic) in mnemonics) {
|
||||||
item.text = "${item.text}(${KeyEvent.getKeyText(mnemonic)})"
|
item.text = "${item.text}(${KeyEvent.getKeyText(mnemonic)})"
|
||||||
@@ -166,6 +174,22 @@ internal class TransportPopupMenu(
|
|||||||
sb.deleteCharAt(sb.length - 1)
|
sb.deleteCharAt(sb.length - 1)
|
||||||
toolkit.systemClipboard.setContents(StringSelection(sb.toString()), null)
|
toolkit.systemClipboard.setContents(StringSelection(sb.toString()), null)
|
||||||
}
|
}
|
||||||
|
copyMenu.addActionListener { fireActionPerformed(it, ActionCommand.Copy) }
|
||||||
|
pasteMenu.addActionListener { fireActionPerformed(it, ActionCommand.Paste) }
|
||||||
|
|
||||||
|
addPopupMenuListener(object : PopupMenuListener {
|
||||||
|
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent?) {
|
||||||
|
pasteMenu.isEnabled = toolkit.systemClipboard
|
||||||
|
.isDataFlavorAvailable(TransportPanel.TransferTransferable.FLAVOR)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popupMenuCanceled(e: PopupMenuEvent?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fireActionPerformed(evt: ActionEvent, command: ActionCommand) {
|
fun fireActionPerformed(evt: ActionEvent, command: ActionCommand) {
|
||||||
@@ -241,6 +265,8 @@ internal class TransportPopupMenu(
|
|||||||
ChangePermissions,
|
ChangePermissions,
|
||||||
Rmrf,
|
Rmrf,
|
||||||
Reconnect,
|
Reconnect,
|
||||||
|
Paste,
|
||||||
|
Copy,
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ChangePermission(val permissions: Set<PosixFilePermission>, val includeSubFolder: Boolean)
|
data class ChangePermission(val permissions: Set<PosixFilePermission>, val includeSubFolder: Boolean)
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
termora.title=Termora
|
termora.title=Termora
|
||||||
termora.confirm=OK
|
termora.confirm=OK
|
||||||
|
termora.exit=退出
|
||||||
termora.cancel=Cancel
|
termora.cancel=Cancel
|
||||||
termora.copy=Copy
|
termora.copy=Copy
|
||||||
|
termora.paste=Paste
|
||||||
termora.apply=Apply
|
termora.apply=Apply
|
||||||
termora.save=Save
|
termora.save=Save
|
||||||
termora.remove=Delete
|
termora.remove=Delete
|
||||||
@@ -21,7 +23,6 @@ termora.optional=Optional
|
|||||||
# update
|
# update
|
||||||
termora.update.title=New version
|
termora.update.title=New version
|
||||||
termora.update.update=Update
|
termora.update.update=Update
|
||||||
termora.update.ignore=Remind me next time
|
|
||||||
|
|
||||||
# Hosts
|
# Hosts
|
||||||
termora.host.modified-server-key.title=HOST [{0}] IDENTIFICATION HAS CHANGED
|
termora.host.modified-server-key.title=HOST [{0}] IDENTIFICATION HAS CHANGED
|
||||||
@@ -183,8 +184,12 @@ termora.new-host.proxy=Proxy
|
|||||||
termora.new-host.terminal=${termora.settings.terminal}
|
termora.new-host.terminal=${termora.settings.terminal}
|
||||||
termora.new-host.terminal.encoding=Encoding
|
termora.new-host.terminal.encoding=Encoding
|
||||||
termora.new-host.terminal.backspace=Backspace
|
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.heartbeat-interval=Heartbeat Interval
|
||||||
termora.new-host.terminal.startup-commands=Startup Command
|
termora.new-host.terminal.startup-commands=Startup Command
|
||||||
|
termora.new-host.terminal.alt-modifier=Alt modifier
|
||||||
|
termora.new-host.terminal.alt-modifier.eight-bit=8-bit characters
|
||||||
|
termora.new-host.terminal.alt-modifier.by-esc=Characters preceded by ESC
|
||||||
termora.new-host.terminal.env=Environment
|
termora.new-host.terminal.env=Environment
|
||||||
termora.new-host.terminal.login-scripts=Login Scripts
|
termora.new-host.terminal.login-scripts=Login Scripts
|
||||||
termora.new-host.terminal.expect=Expect
|
termora.new-host.terminal.expect=Expect
|
||||||
@@ -391,6 +396,7 @@ termora.toolbar.customize-toolbar=Customize Toolbar...
|
|||||||
|
|
||||||
# Actions
|
# Actions
|
||||||
termora.actions.copy-from-terminal=Copy from Terminal
|
termora.actions.copy-from-terminal=Copy from Terminal
|
||||||
|
termora.actions.focus-mode=Focus Mode
|
||||||
termora.actions.paste-to-terminal=Paste to Terminal
|
termora.actions.paste-to-terminal=Paste to Terminal
|
||||||
termora.actions.select-all-in-terminal=Select All in Terminal
|
termora.actions.select-all-in-terminal=Select All in Terminal
|
||||||
termora.actions.open-terminal-find=Open Terminal Find
|
termora.actions.open-terminal-find=Open Terminal Find
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
termora.title=Termora
|
termora.title=Termora
|
||||||
termora.confirm=Ок
|
termora.confirm=Ок
|
||||||
|
termora.exit=покидать
|
||||||
termora.cancel=Отмена
|
termora.cancel=Отмена
|
||||||
termora.copy=Копировать
|
termora.copy=Копировать
|
||||||
|
termora.paste=Вставить
|
||||||
termora.apply=Применить
|
termora.apply=Применить
|
||||||
termora.save=Сохранить
|
termora.save=Сохранить
|
||||||
termora.remove=Удалить
|
termora.remove=Удалить
|
||||||
@@ -17,7 +19,6 @@ termora.quit-confirm=Выйти {0}?
|
|||||||
# update
|
# update
|
||||||
termora.update.title=Новая версия
|
termora.update.title=Новая версия
|
||||||
termora.update.update=Обновить
|
termora.update.update=Обновить
|
||||||
termora.update.ignore=Напомнить в следующий раз
|
|
||||||
|
|
||||||
|
|
||||||
# Hosts
|
# Hosts
|
||||||
@@ -342,6 +343,7 @@ termora.toolbar.customize-toolbar=Настроить Панель Инструм
|
|||||||
|
|
||||||
# Actions
|
# Actions
|
||||||
termora.actions.copy-from-terminal=Копировать из Терминала
|
termora.actions.copy-from-terminal=Копировать из Терминала
|
||||||
|
termora.actions.focus-mode=Режим фокусировки
|
||||||
termora.actions.paste-to-terminal=Вставить в Терминала
|
termora.actions.paste-to-terminal=Вставить в Терминала
|
||||||
termora.actions.select-all-in-terminal=Выделить Все в Терминале
|
termora.actions.select-all-in-terminal=Выделить Все в Терминале
|
||||||
termora.actions.open-terminal-find=Открыть Поиск Терминала
|
termora.actions.open-terminal-find=Открыть Поиск Терминала
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
termora.confirm=确认
|
termora.confirm=确认
|
||||||
|
termora.exit=退出
|
||||||
termora.cancel=取消
|
termora.cancel=取消
|
||||||
termora.copy=复制
|
termora.copy=复制
|
||||||
|
termora.paste=粘贴
|
||||||
termora.apply=应用
|
termora.apply=应用
|
||||||
termora.save=保存
|
termora.save=保存
|
||||||
termora.remove=删除
|
termora.remove=删除
|
||||||
@@ -22,7 +24,6 @@ termora.optional=可选的
|
|||||||
# update
|
# update
|
||||||
termora.update.title=新版本
|
termora.update.title=新版本
|
||||||
termora.update.update=更新
|
termora.update.update=更新
|
||||||
termora.update.ignore=下次提醒我
|
|
||||||
|
|
||||||
|
|
||||||
# Hosts
|
# Hosts
|
||||||
@@ -175,8 +176,12 @@ termora.new-host.proxy=代理
|
|||||||
termora.new-host.terminal=${termora.settings.terminal}
|
termora.new-host.terminal=${termora.settings.terminal}
|
||||||
termora.new-host.terminal.encoding=编码
|
termora.new-host.terminal.encoding=编码
|
||||||
termora.new-host.terminal.backspace=退格键
|
termora.new-host.terminal.backspace=退格键
|
||||||
|
termora.new-host.terminal.character-mode=单字符模式
|
||||||
termora.new-host.terminal.heartbeat-interval=心跳间隔
|
termora.new-host.terminal.heartbeat-interval=心跳间隔
|
||||||
termora.new-host.terminal.startup-commands=启动命令
|
termora.new-host.terminal.startup-commands=启动命令
|
||||||
|
termora.new-host.terminal.alt-modifier=Alt 键修饰
|
||||||
|
termora.new-host.terminal.alt-modifier.eight-bit=8 位字符
|
||||||
|
termora.new-host.terminal.alt-modifier.by-esc=ESC 键作为前缀
|
||||||
termora.new-host.terminal.env=环境
|
termora.new-host.terminal.env=环境
|
||||||
termora.new-host.terminal.login-scripts=登录脚本
|
termora.new-host.terminal.login-scripts=登录脚本
|
||||||
termora.new-host.terminal.expect=预期
|
termora.new-host.terminal.expect=预期
|
||||||
@@ -395,6 +400,7 @@ termora.protocol.not-supported=不支持 {0} 协议,你可能需要安装插
|
|||||||
|
|
||||||
# Actions
|
# Actions
|
||||||
termora.actions.copy-from-terminal=从终端复制
|
termora.actions.copy-from-terminal=从终端复制
|
||||||
|
termora.actions.focus-mode=专注模式
|
||||||
termora.actions.paste-to-terminal=粘贴到终端
|
termora.actions.paste-to-terminal=粘贴到终端
|
||||||
termora.actions.select-all-in-terminal=在终端中全选
|
termora.actions.select-all-in-terminal=在终端中全选
|
||||||
termora.actions.open-terminal-find=打开终端查找
|
termora.actions.open-terminal-find=打开终端查找
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
termora.confirm=確定
|
termora.confirm=確定
|
||||||
|
termora.exit=Exit
|
||||||
termora.cancel=取消
|
termora.cancel=取消
|
||||||
|
termora.copy=複製
|
||||||
|
termora.paste=貼上
|
||||||
termora.apply=应用
|
termora.apply=应用
|
||||||
termora.save=儲存
|
termora.save=儲存
|
||||||
termora.remove=刪除
|
termora.remove=刪除
|
||||||
@@ -19,7 +22,6 @@ termora.optional=可選的
|
|||||||
# update
|
# update
|
||||||
termora.update.title=新版本
|
termora.update.title=新版本
|
||||||
termora.update.update=更新
|
termora.update.update=更新
|
||||||
termora.update.ignore=下次提醒我
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -173,7 +175,11 @@ termora.new-host.proxy=代理
|
|||||||
termora.new-host.terminal=${termora.settings.terminal}
|
termora.new-host.terminal=${termora.settings.terminal}
|
||||||
termora.new-host.terminal.encoding=編碼
|
termora.new-host.terminal.encoding=編碼
|
||||||
termora.new-host.terminal.backspace=退格鍵
|
termora.new-host.terminal.backspace=退格鍵
|
||||||
|
termora.new-host.terminal.character-mode=單字元模式
|
||||||
termora.new-host.terminal.startup-commands=啟動命令
|
termora.new-host.terminal.startup-commands=啟動命令
|
||||||
|
termora.new-host.terminal.alt-modifier=Alt 鍵修飾
|
||||||
|
termora.new-host.terminal.alt-modifier.eight-bit=8 位元字符
|
||||||
|
termora.new-host.terminal.alt-modifier.by-esc=ESC 鍵作為前綴
|
||||||
termora.new-host.terminal.heartbeat-interval=心跳間隔
|
termora.new-host.terminal.heartbeat-interval=心跳間隔
|
||||||
termora.new-host.terminal.env=環境
|
termora.new-host.terminal.env=環境
|
||||||
termora.new-host.terminal.login-scripts=登入腳本
|
termora.new-host.terminal.login-scripts=登入腳本
|
||||||
@@ -382,6 +388,7 @@ termora.protocol.not-supported=不支援 {0} 協議,你可能需要安裝插
|
|||||||
|
|
||||||
# Actions
|
# Actions
|
||||||
termora.actions.copy-from-terminal=從終端複製
|
termora.actions.copy-from-terminal=從終端複製
|
||||||
|
termora.actions.focus-mode=專注模式
|
||||||
termora.actions.paste-to-terminal=貼上到終端
|
termora.actions.paste-to-terminal=貼上到終端
|
||||||
termora.actions.select-all-in-terminal=在終端中全選
|
termora.actions.select-all-in-terminal=在終端中全選
|
||||||
termora.actions.open-terminal-find=開啟終端搜尋
|
termora.actions.open-terminal-find=開啟終端搜尋
|
||||||
|
|||||||
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