Compare commits

..

22 Commits

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-10 11:25:28 +08:00
hstyi
8ddad59c70 chore: use docker jbr 2025-07-10 09:30:12 +08:00
hstyi
9ff6d0afa1 feat: telnet character mode 2025-07-09 19:57:56 +08:00
hstyi
2341b09f81 fix: osx.yml 2025-07-09 19:57:22 +08:00
hstyi
5830aa937a feat: improve GitHub actions 2025-07-09 18:15:36 +08:00
hstyi
56a9361e86 release: 2.0.0-beta.7 2025-07-09 12:22:34 +08:00
hstyi
5868aa4d2f fix: unable to update automatically 2025-07-09 12:18:19 +08:00
dependabot[bot]
45135b7299 chore(deps): bump exposed from 1.0.0-beta-3 to 1.0.0-beta-4
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-09 12:14:23 +08:00
61 changed files with 1582 additions and 1484 deletions

View File

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

View File

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

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

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

View File

@@ -1,89 +0,0 @@
name: macOS aarch64
on: [ push, pull_request ]
jobs:
build:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install the Apple certificate
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora' && env.BUILD_CERTIFICATE_BASE64 != ''
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

View File

@@ -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

View File

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

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

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

View File

@@ -90,8 +90,6 @@ Termora is developed using [**Kotlin/JVM**](https://kotlinlang.org/) and partial
We recommend using the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK for development. 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

View File

@@ -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`
## 📄 授权协议 ## 📄 授权协议

View File

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

View File

@@ -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) {

View File

@@ -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"

View File

@@ -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"))

View File

@@ -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 {

View File

@@ -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")

View File

@@ -4,7 +4,7 @@ plugins {
project.version = "0.0.1" project.version = "0.0.2"
dependencies { dependencies {

View File

@@ -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>()

View File

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

View File

@@ -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() {

View File

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

View File

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

View File

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

View File

@@ -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)
} }

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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()
} }

View File

@@ -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)
} }

View File

@@ -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,

View File

@@ -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
} }

View File

@@ -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())

View File

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

View File

@@ -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
} }
} }

View File

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

View File

@@ -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> {

View File

@@ -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)

View File

@@ -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))

View File

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

View File

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

View File

@@ -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
}
}
} }

View File

@@ -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
}
}
} }

View File

@@ -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)

View File

@@ -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

View File

@@ -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
} }
} }

View File

@@ -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
)
}
} }

View File

@@ -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
}
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View 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)
}

View 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
}
}
} }

View File

@@ -342,7 +342,10 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
} }
// 设置滚动区域 // 设置滚动区域
terminal.getTerminalModel().setData(DataKey.ScrollingRegion, ScrollingRegion(top, bottom)) terminal.getTerminalModel().setData(
DataKey.ScrollingRegion,
ScrollingRegion(top, min(bottom, terminalModel.getRows()))
)
if (log.isDebugEnabled) { 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 -> {

View File

@@ -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)
} }
} }

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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))
} }
} }

View File

@@ -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)

View File

@@ -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

View File

@@ -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=Открыть Поиск Терминала

View File

@@ -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=打开终端查找

View File

@@ -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=開啟終端搜尋

View 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}"