Compare commits

...

72 Commits
1.0.1 ... 1.0.5

Author SHA1 Message Date
hstyi
343d11482d release: 1.0.5 2025-01-26 20:35:18 +08:00
hstyi
7ef81a0116 feat: xterm DCS 2025-01-26 14:42:59 +08:00
hstyi
5df62d5d3e fix: possible invalid window creation 2025-01-26 10:24:55 +08:00
hstyi
7db650d69f feat: open in new window 2025-01-26 10:20:26 +08:00
hstyi
8d80d38d63 fix: missing exchange algorithms 2025-01-26 08:44:00 +08:00
hstyi
48f05d4cff feat: ssh insecure key exchange algorithms 2025-01-26 08:44:00 +08:00
hstyi
9a1cf387c0 fix: check-license 2025-01-25 21:20:08 +08:00
hstyi
8b7efefbdb fix: shift to close tabs causes switching 2025-01-25 21:11:54 +08:00
hstyi
75f21db325 fix: theAwtToolkitWindow 2025-01-25 18:06:01 +08:00
hstyi
b094c9d4ff chore: remove tabbed hover background 2025-01-25 17:03:06 +08:00
hstyi
0da3c95759 feat: press and hold Shift to close Tab (#131) 2025-01-25 16:24:36 +08:00
hstyi
fa79473ece chore: optimize key encoder 2025-01-25 15:03:52 +08:00
hstyi
86ccb5e0cc chore: LANG=en_US.UTF-8 2025-01-24 17:27:47 +08:00
hstyi
f385f4b277 feat: support import (#127) 2025-01-24 16:45:36 +08:00
hstyi
3d0ef2a331 feat: shortcut key prediction (#126) 2025-01-24 15:40:14 +08:00
hstyi
96999205a8 fix: host test connection 2025-01-24 10:55:42 +08:00
hstyi
ee7f3871eb fix: sftp symbolic link (#120) 2025-01-24 10:27:15 +08:00
hstyi
df2e9b0743 feat: support drag and drop sorting 2025-01-23 16:23:16 +08:00
hstyi
7964950149 fix: #112 2025-01-23 14:47:39 +08:00
hstyi
e2d77fe881 fix: key manager 2025-01-23 14:43:48 +08:00
hstyi
f5783c8587 feat: support more monospaced fonts 2025-01-23 11:26:24 +08:00
hstyi
346044b1ba fix: shortcut keys lead to terminal input 2025-01-23 11:26:12 +08:00
hstyi
aa6ec8dd43 feat: xcrun stapler staple 2025-01-23 10:17:18 +08:00
hstyi
e0e6a85a81 release: 1.0.4 2025-01-22 21:04:09 +08:00
hstyi
56ba107c87 fix: tab key not working 2025-01-22 20:58:10 +08:00
hstyi
0345848418 release: 1.0.3 2025-01-22 19:17:52 +08:00
hstyi
f1073fb53f fix: deadlock 2025-01-22 15:50:36 +08:00
hstyi
ce1924c422 docs: README 2025-01-22 15:47:36 +08:00
hstyi
d6de0922c6 feat: Device Status Report (DSR) 2025-01-22 15:32:10 +08:00
hstyi
d5157d3a16 feat: left right key 2025-01-22 15:01:40 +08:00
hstyi
63b27a2f83 feat: improved keyPair comboBox (#92) 2025-01-16 18:18:58 +08:00
hstyi
992015c8e5 feat: GitHub actions 2025-01-16 17:31:53 +08:00
hstyi
5d459f9b0d fix: FindEverywhereAction name (#89) 2025-01-16 16:09:23 +08:00
hstyi
88f20c4898 feat: SFTP supports pasting files for upload (#87) 2025-01-16 14:59:01 +08:00
hstyi
314c112d4b feat: Windows keyboard shortcut (#86) 2025-01-16 12:38:43 +08:00
hstyi
0cd818e9a0 feat: support fast reconnect 2025-01-15 23:02:05 +08:00
hstyi
0884486e91 feat: theme sync with OS (#82) 2025-01-15 22:24:19 +08:00
hstyi
e30316eab3 feat: support keymap sync 2025-01-15 20:05:26 +08:00
hstyi
d321e766b1 docs: README 2025-01-15 17:22:02 +08:00
hstyi
6aaed92f2c feat: SFTP 支持不显示隐藏文件 2025-01-15 16:52:58 +08:00
hstyi
21cf22906b fix: 修复可能导致内存泄漏的问题 2025-01-15 15:14:30 +08:00
hstyi
1476368673 feat: support jump hosts 2025-01-15 15:14:30 +08:00
hstyi
45ea822fd6 feat: 改进事件系统与全局快捷键 (#62) 2025-01-15 14:54:39 +08:00
hstyi
a71493e52c release: 1.0.2 2025-01-15 13:59:35 +08:00
hstyi
cb327f218c fix: 修复 macOS 没有对二进制依赖进行签名的问题 2025-01-15 13:57:47 +08:00
hstyi
6881b6376f feat: 支持 macOS 签名以及 Windows MSI 安装包 (#71) 2025-01-14 19:03:41 +08:00
hstyi
5027fd9dfb fix: 修复 SFTP 拖拽单个文件上传失败的问题 2025-01-10 18:42:58 +08:00
hstyi
49cef39b8b fix: 修复在极端情况下可能导致部分样式错乱问题 2025-01-10 18:28:38 +08:00
hstyi
5c4acf85e8 chore: unix shell login 2025-01-10 16:36:26 +08:00
hstyi
07bee64b7f chore: 修改 SFTP 图标 2025-01-10 15:08:52 +08:00
hstyi
923afb7e99 feat: 弹窗位置以父窗口为中心 (#55) 2025-01-10 15:08:01 +08:00
hstyi
68df52bfc0 fix: 修复 Windows 切换标签页导致误输入的问题 (#54) 2025-01-10 14:40:26 +08:00
hstyi
c2ee6fc8ac feat: analytics 2025-01-10 13:28:37 +08:00
hstyi
9d4562e7e3 fix: 修复 SFTP 格式化进度可能报错的问题 2025-01-10 12:32:36 +08:00
hstyi
5733b5f485 fix: 修复在同步配置时 “宏” 没有禁用的问题 (#49) 2025-01-10 11:24:32 +08:00
hstyi
9dbdb5fd7a fix: 修复 ESC[?xm 私有模式导致不正常渲染的问题 2025-01-10 10:57:24 +08:00
hstyi
a1d1821553 feat: vim 支持鼠标滚动 2025-01-10 10:57:05 +08:00
hstyi
4a8faea8c5 chore: 在新窗口中打开时标题跟随之前的 2025-01-09 20:53:22 +08:00
hstyi
cfb841db00 feat: support Dracula 2025-01-09 20:53:07 +08:00
hstyi
a87d4ddf82 fix: 修复在 Linux 环境下无法移动窗口的问题 2025-01-09 16:07:39 +08:00
hstyi
6071b251a4 fix: 修复终端日志重复记录的问题 2025-01-09 14:58:04 +08:00
hstyi
950ff517bb fix: 修复自定义工具栏排序无效的问题 2025-01-09 14:45:40 +08:00
hstyi
70008978d8 feat: 在工具栏添加 SFTP 快速打开 2025-01-09 14:15:49 +08:00
hstyi
7c445bdadb feat: 优化自定义工具栏的存储结构 2025-01-09 14:05:53 +08:00
hstyi
f24151f6d8 feat: 支持自定义工具栏 2025-01-09 13:11:46 +08:00
hstyi
7d65a88d63 fix: 修复在开发环境 “设置 - 关于” 页面地址 404 问题 2025-01-08 17:45:46 +08:00
hstyi
ed57c3e5b4 chore: 改进 SFTP 传输进度 2025-01-08 17:43:57 +08:00
hstyi
00f11c9ed5 feat: 添加非 macOS 系统下的 复制/粘贴 快捷键 (#31) 2025-01-08 15:10:02 +08:00
hstyi
5ebea06a95 feat: 当停止记录终端日志的时候立即关闭文件流 2025-01-08 11:46:40 +08:00
hstyi
3e5df2161b feat: support keyboard-interactive 2025-01-07 23:12:39 +08:00
hstyi
ffcb4d028e feat: 支持 OSC 52 指令 2025-01-07 23:12:07 +08:00
hstyi
022ae402cc feat: 支持终端日志记录 (#7) 2025-01-07 17:43:59 +08:00
212 changed files with 6940 additions and 1498 deletions

33
.github/workflows/linux-x86-64.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Linux x86-64
on: [ push, pull_request ]
jobs:
build:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# download jdk
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.5-linux-x64-b509.30.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.5'
architecture: x64
# 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

35
.github/workflows/osx-aarch64.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: macOS aarch64
on: [ push, pull_request ]
jobs:
build:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# download jdk
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.5-osx-aarch64-b509.30.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.5'
architecture: aarch64
# dist
- run: |
./gradlew dist --no-daemon
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-osx-aarch64
path: build/distributions/*.dmg

34
.github/workflows/osx-x86-64.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: macOS x86-64
on: [ push, pull_request ]
jobs:
build:
runs-on: macos-13
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# download jdk
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.5-osx-x64-b509.30.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.5'
architecture: x64
# dist
- run: |
./gradlew dist --no-daemon
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-osx-x86-64
path: build/distributions/*.dmg

29
.github/workflows/windows-x86-64.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Windows x86-64
on: [ push, pull_request ]
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Installing Java
uses: actions/setup-java@v4
with:
distribution: 'jetbrains'
java-version: '21'
# dist
- run: |
.\gradlew.bat dist --no-daemon
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-windows-x86-64
path: |
build/distributions/*.zip
build/distributions/*.msi

View File

@@ -1,46 +1,48 @@
<div align="center">
<a href="./README.zh_CN.md">🇨🇳 简体中文</a>
</div>
# Termora # Termora
**Termora** 是一个终端模拟器和 SSH 客户端,支持 WindowsmacOS Linux **Termora** is a terminal emulator and SSH client for Windows, macOS and Linux.
<div align="center"> <div align="center">
<img src="./docs/readme.png" alt="termora" /> <img src="./docs/readme.png" alt="termora" />
</div> </div>
**Termora** 采用 [Kotlin/JVM](https://kotlinlang.org/) 开发并实现了 [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) 协议(尚未完全实现),它的最终目标是通过 [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) 实现全平台(含 Android、iOS、iPadOS 等)。 **Termora** is developed using [Kotlin/JVM](https://kotlinlang.org) and partially implements the [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) protocol (with ongoing improvements). Its ultimate vision is to achieve full platform support (including Android, iOS, and iPadOS) through [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html).
## 功能特性 ## Features
- 支持 SSH 和本地终端 - SSH and local terminal support
- 支持 [SFTP](./docs/sftp-zh_CN.png) 文件传输 - [SFTP](./docs/sftp.png?raw=1) file transfer support
- 支持 WindowsmacOSLinux 平台 - Compatible with Windows, macOS, and Linux
- 支持 Zmodem 协议 - Zmodem protocol support
- 支持 SSH 端口转发 - SSH port forwarding
- 支持配置同步到 [Gist](https://gist.github.com) - Configuration synchronization via [Gist](https://gist.github.com)
- 支持宏(录制脚本并回放) - Macro support (record and replay scripts)
- 支持关键词高亮 - Keyword highlighting
- 支持密钥管理器 - Key management
- 支持将命令发送到多个会话 - Broadcast commands to multiple sessions
- 支持 [Find Everywhere](./docs/findeverywhere.png) 快速跳转 - [Find Everywhere](./docs/findeverywhere.png?raw=1) quick navigation
- 支持数据加密 - Data encryption
- ... - ...
## 下载 ## Download
- [releases](https://github.com/TermoraDev/termora/releases/latest) - [Latest release](https://github.com/TermoraDev/termora/releases/latest)
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
### macOS ## Development
由于苹果开发者证书正在申请中,所以 macOS 用户需要执行 `sudo xattr -r -d com.apple.quarantine /Applications/Termora.app` 后才可以运行程序。 It is recommended to use the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) version of the JDK and run the program via `./gradlew :run` to run the program.
## 开发 The program can be run via `./gradlew dist` to automatically build the local version. On macOS: `dmg`, on Windows: `zip`, on Linux: `tar.gz`.
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) 的 JDK 版本,通过 `./gradlew :run` 即可运行程序。
通过 `./gradlew dist` 可以自动构建适用于本机的版本。在 macOS 上是:`dmg`,在 Windows 上是:`zip`,在 Linux 上是:`tar.gz` ## LICENSE
## 协议 This software is distributed under a dual-license model. You may choose one of the following options:
本软件采用双重许可模式,您可以选择以下任意一种许可方式: - AGPL-3.0: Use, distribute, and modify the software under the terms of the [AGPL-3.0](https://opensource.org/license/agpl-v3).
- Proprietary License: For closed-source or proprietary use, please contact the author to obtain a commercial license.
- AGPL-3.0:根据 [AGPL-3.0](https://opensource.org/license/agpl-v3) 的条款,您可以自由使用、分发和修改本软件。
- 专有许可:如果希望在闭源或专有环境中使用,请联系作者获取许可。

43
README.zh_CN.md Normal file
View File

@@ -0,0 +1,43 @@
# Termora
**Termora** 是一个终端模拟器和 SSH 客户端,支持 WindowsmacOS 和 Linux。
<div align="center">
<img src="./docs/readme-zh_CN.png" alt="termora" />
</div>
**Termora** 采用 [Kotlin/JVM](https://kotlinlang.org/) 开发并实现了 [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) 协议(尚未完全实现),它的最终目标是通过 [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) 实现全平台(含 Android、iOS、iPadOS 等)。
## 功能特性
- 支持 SSH 和本地终端
- 支持 [SFTP](./docs/sftp-zh_CN.png?raw=1) 文件传输
- 支持 Windows、macOS、Linux 平台
- 支持 Zmodem 协议
- 支持 SSH 端口转发
- 支持配置同步到 [Gist](https://gist.github.com)
- 支持宏(录制脚本并回放)
- 支持关键词高亮
- 支持密钥管理器
- 支持将命令发送到多个会话
- 支持 [Find Everywhere](./docs/findeverywhere-zh_CN.png?raw=1) 快速跳转
- 支持数据加密
- ...
## 下载
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
## 开发
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) 的 JDK 版本,通过 `./gradlew :run` 即可运行程序。
通过 `./gradlew dist` 可以自动构建适用于本机的版本。在 macOS 上是:`dmg`,在 Windows 上是:`zip`,在 Linux 上是:`tar.gz`
## 协议
本软件采用双重许可模式,您可以选择以下任意一种许可方式:
- AGPL-3.0:根据 [AGPL-3.0](https://opensource.org/license/agpl-v3) 的条款,您可以自由使用、分发和修改本软件。
- 专有许可:如果希望在闭源或专有环境中使用,请联系作者获取许可。

View File

@@ -46,6 +46,10 @@ flatlaf 3.5.4
Apache License 2.0 Apache License 2.0
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
flatlaf 3.5.4-no-natives
Apache License 2.0
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
flatlaf-extras 3.5.4 flatlaf-extras 3.5.4
Apache License 2.0 Apache License 2.0
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
@@ -228,4 +232,12 @@ https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
jediterm jediterm
Apache License 2.0 Apache License 2.0
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
mixpanel-java 1.5.3
Apache License 2.0
https://github.com/mixpanel/mixpanel-java/blob/master/LICENSE
json-20231013
Public Domain.
https://github.com/stleary/JSON-java/blob/master/LICENSE

View File

@@ -1,9 +1,9 @@
import org.gradle.internal.jvm.Jvm import org.gradle.internal.jvm.Jvm
import org.gradle.kotlin.dsl.support.uppercaseFirstChar import org.gradle.kotlin.dsl.support.uppercaseFirstChar
import org.gradle.nativeplatform.platform.internal.ArchitectureInternal
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import org.gradle.nativeplatform.platform.internal.DefaultOperatingSystem import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
import java.nio.file.Files
plugins { plugins {
java java
@@ -14,11 +14,20 @@ plugins {
group = "app.termora" group = "app.termora"
version = "1.0.1" version = "1.0.5"
val os: DefaultOperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem() val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
var arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture() val arch: Architecture = DefaultNativePlatform.getCurrentArchitecture()
// macOS 签名信息
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
val macOSSign = os.isMacOsX && macOSSignUsername.isNotBlank()
&& System.getenv("TERMORA_MAC_SIGN").toBoolean()
// macOS 公证信息
val macOSNotaryKeychainProfile = System.getenv("TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE") ?: StringUtils.EMPTY
val macOSNotary = macOSSign && macOSNotaryKeychainProfile.isNotBlank()
&& System.getenv("TERMORA_MAC_NOTARY").toBoolean()
repositories { repositories {
mavenCentral() mavenCentral()
@@ -27,17 +36,20 @@ repositories {
} }
dependencies { dependencies {
// 由于签名和公证macOS 不携带 natives
val useNoNativesFlatLaf = os.isMacOsX && macOSNotary && System.getenv("ENABLE_BUILD").toBoolean()
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
testImplementation(libs.hutool) testImplementation(libs.hutool)
testImplementation(libs.sshj) testImplementation(libs.sshj)
testImplementation(platform(libs.koin.bom))
testImplementation(libs.koin.core)
testImplementation(libs.jsch) testImplementation(libs.jsch)
testImplementation(libs.rhino) testImplementation(libs.rhino)
testImplementation(libs.delight.rhino.sandbox) testImplementation(libs.delight.rhino.sandbox)
testImplementation(platform(libs.testcontainers.bom)) testImplementation(platform(libs.testcontainers.bom))
testImplementation(libs.testcontainers) testImplementation(libs.testcontainers)
// implementation(platform(libs.koin.bom))
// implementation(libs.koin.core)
implementation(libs.slf4j.api) implementation(libs.slf4j.api)
implementation(libs.pty4j) implementation(libs.pty4j)
implementation(libs.slf4j.tinylog) implementation(libs.slf4j.tinylog)
@@ -50,9 +62,25 @@ dependencies {
implementation(libs.commons.compress) implementation(libs.commons.compress)
implementation(libs.kotlinx.coroutines.swing) implementation(libs.kotlinx.coroutines.swing)
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)
implementation(libs.flatlaf)
implementation(libs.flatlaf.extras) implementation(libs.flatlaf) {
implementation(libs.flatlaf.swingx) artifact {
if (useNoNativesFlatLaf) {
classifier = "no-natives"
}
}
}
implementation(libs.flatlaf.extras) {
if (useNoNativesFlatLaf) {
exclude(group = "com.formdev", module = "flatlaf")
}
}
implementation(libs.flatlaf.swingx) {
if (useNoNativesFlatLaf) {
exclude(group = "com.formdev", module = "flatlaf")
}
}
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.swingx) implementation(libs.swingx)
implementation(libs.jgoodies.forms) implementation(libs.jgoodies.forms)
@@ -75,11 +103,18 @@ dependencies {
implementation(libs.xodus.environment) implementation(libs.xodus.environment)
implementation(libs.bip39) implementation(libs.bip39)
implementation(libs.colorpicker) implementation(libs.colorpicker)
implementation(libs.mixpanel)
} }
application { application {
val args = mutableListOf( val args = mutableListOf(
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED", "--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
"-Xmx2g",
"-XX:+UseZGC",
"-XX:+ZUncommit",
"-XX:+ZGenerational",
"-XX:ZUncommitDelay=60",
"-XX:SoftMaxHeapSize=64m"
) )
if (os.isMacOsX) { if (os.isMacOsX) {
@@ -103,8 +138,53 @@ tasks.test {
} }
tasks.register<Copy>("copy-dependencies") { tasks.register<Copy>("copy-dependencies") {
from(configurations.runtimeClasspath) val dir = layout.buildDirectory.dir("libs")
.into("${layout.buildDirectory.get()}/libs") from(configurations.runtimeClasspath).into(dir)
// 对 JNA 和 PTY4J 的本地库提取
// 提取出来是为了单独签名,不然无法通过公证
if (os.isMacOsX && macOSSign) {
doLast {
val jna = libs.jna.asProvider().get()
val dylib = dir.get().dir("dylib").asFile
val pty4j = libs.pty4j.get()
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
val targetDir = File(dylib, jna.name)
FileUtils.forceMkdir(targetDir)
// @formatter:off
exec { commandLine("unzip","-j","-o", file.absolutePath, "com/sun/jna/darwin-${arch.name}/*", "-d", targetDir.absolutePath) }
// @formatter:on
// 删除所有二进制类库
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/darwin-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/sunos-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/openbsd-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/linux-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/freebsd-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/dragonflybsd-*") }
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
val targetDir = FileUtils.getFile(dylib, pty4j.name, "darwin")
FileUtils.forceMkdir(targetDir)
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/com/pty4j/native/darwin*", "-d", targetDir.absolutePath) }
// @formatter:on
// 删除所有二进制类库
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
}
}
// 对二进制签名
Files.walk(dylib.toPath()).use { paths ->
for (path in paths) {
if (Files.isRegularFile(path)) {
signMacOSLocalFile(path.toFile())
}
}
}
}
}
} }
tasks.register<Exec>("jlink") { tasks.register<Exec>("jlink") {
@@ -136,10 +216,16 @@ tasks.register<Exec>("jlink") {
} }
tasks.register<Exec>("jpackage") { tasks.register<Exec>("jpackage") {
val buildDir = layout.buildDirectory.get() val buildDir = layout.buildDirectory.get()
val options = mutableListOf( val options = mutableListOf(
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED", "--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
"-Xmx2g", "-Xmx2g",
"-XX:+UseZGC",
"-XX:+ZUncommit",
"-XX:+ZGenerational",
"-XX:ZUncommitDelay=60",
"-XX:SoftMaxHeapSize=64m",
"-XX:+HeapDumpOnOutOfMemoryError", "-XX:+HeapDumpOnOutOfMemoryError",
"-Dlogger.console.level=off", "-Dlogger.console.level=off",
"-Dkotlinx.coroutines.debug=off", "-Dkotlinx.coroutines.debug=off",
@@ -164,6 +250,9 @@ tasks.register<Exec>("jpackage") {
arguments.addAll(listOf("--temp", "$buildDir/jpackage")) arguments.addAll(listOf("--temp", "$buildDir/jpackage"))
arguments.addAll(listOf("--dest", "$buildDir/distributions")) arguments.addAll(listOf("--dest", "$buildDir/distributions"))
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE))) arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE)))
arguments.addAll(listOf("--vendor", "TermoraDev"))
arguments.addAll(listOf("--copyright", "TermoraDev"))
arguments.addAll(listOf("--description", "A terminal emulator and SSH client."))
if (os.isMacOsX) { if (os.isMacOsX) {
@@ -193,6 +282,12 @@ tasks.register<Exec>("jpackage") {
throw UnsupportedOperationException() throw UnsupportedOperationException()
} }
if (os.isMacOsX && macOSSign) {
arguments.add("--mac-sign")
arguments.add("--mac-signing-key-user-name")
arguments.add(macOSSignUsername)
}
commandLine(arguments) commandLine(arguments)
} }
@@ -207,12 +302,18 @@ tasks.register("dist") {
val distributionDir = layout.buildDirectory.dir("distributions").get() val distributionDir = layout.buildDirectory.dir("distributions").get()
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
val macOSFinalFilePath = distributionDir.file("${finalFilenameWithoutExtension}.dmg").asFile.absolutePath
// 清空目录 // 清空目录
exec { commandLine(gradlew, "clean") } exec { commandLine(gradlew, "clean") }
// 打包并复制依赖 // 打包并复制依赖
exec { commandLine(gradlew, "jar", "copy-dependencies") } exec {
commandLine(gradlew, "jar", "copy-dependencies")
environment("ENABLE_BUILD" to true)
}
// 检查依赖的开源协议 // 检查依赖的开源协议
exec { commandLine(gradlew, "check-license") } exec { commandLine(gradlew, "check-license") }
@@ -224,29 +325,72 @@ tasks.register("dist") {
exec { commandLine(gradlew, "jpackage") } exec { commandLine(gradlew, "jpackage") }
// pack // pack
exec { if (os.isWindows) { // zip and msi
if (os.isWindows) { // zip // zip
exec {
commandLine( commandLine(
"tar", "-vacf", "tar", "-vacf",
distributionDir.file("${project.name}-${project.version}-windows-${arch.name}.zip").asFile.absolutePath, distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile.absolutePath,
project.name.uppercaseFirstChar() project.name.uppercaseFirstChar()
) )
workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
} else if (os.isLinux) { // tar.gz }
// msi
exec {
commandLine(
"cmd", "/c", "move",
"${project.name.uppercaseFirstChar()}-${project.version}.msi",
"${finalFilenameWithoutExtension}.msi"
)
workingDir = distributionDir.asFile
}
} else if (os.isLinux) { // tar.gz
exec {
commandLine( commandLine(
"tar", "-czvf", "tar", "-czvf",
distributionDir.file("${project.name}-${project.version}-linux-${arch.name}.tar.gz").asFile.absolutePath, distributionDir.file("${finalFilenameWithoutExtension}.tar.gz").asFile.absolutePath,
project.name.uppercaseFirstChar() project.name.uppercaseFirstChar()
) )
workingDir = distributionDir.asFile workingDir = distributionDir.asFile
} else if (os.isMacOsX) { // rename }
} else if (os.isMacOsX) { // rename
exec {
commandLine( commandLine(
"mv", "mv",
distributionDir.file("${project.name.uppercaseFirstChar()}-${project.version}.dmg").asFile.absolutePath, distributionDir.file("${project.name.uppercaseFirstChar()}-${project.version}.dmg").asFile.absolutePath,
distributionDir.file("${project.name}-${project.version}-osx-${arch.name}.dmg").asFile.absolutePath, macOSFinalFilePath,
) )
} else { }
throw GradleException("${os.name} is not supported") } else {
throw GradleException("${os.name} is not supported")
}
// sign dmg
if (os.isMacOsX && macOSSign) {
// sign
signMacOSLocalFile(File(macOSFinalFilePath))
// notary
if (macOSNotary) {
exec {
commandLine(
"/usr/bin/xcrun", "notarytool",
"submit", macOSFinalFilePath,
"--keychain-profile", macOSNotaryKeychainProfile,
"--wait",
)
}
// 绑定公证信息
exec {
commandLine(
"/usr/bin/xcrun",
"stapler", "staple", macOSFinalFilePath,
)
}
} }
} }
} }
@@ -286,6 +430,26 @@ tasks.register("check-license") {
} }
} }
/**
* macOS 对本地文件进行签名
*/
fun signMacOSLocalFile(file: File) {
if (os.isMacOsX && macOSSign) {
if (file.exists() && file.isFile) {
exec {
commandLine(
"/usr/bin/codesign",
"-s", macOSSignUsername,
"--timestamp", "--force",
"-vvvv", "--options", "runtime",
file.absolutePath,
)
}
}
}
}
kotlin { kotlin {
jvmToolchain { jvmToolchain {
languageVersion = JavaLanguageVersion.of(21) languageVersion = JavaLanguageVersion.of(21)

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 61 KiB

BIN
docs/readme-zh_CN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -1,3 +1,4 @@
org.gradle.caching=true org.gradle.caching=true
org.gradle.parallel=true org.gradle.parallel=true
kotlin.code.style=official kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx4g

View File

@@ -40,6 +40,7 @@ colorpicker = "2.0.1"
rhino = "1.7.15" rhino = "1.7.15"
delight-rhino-sandbox = "0.0.17" delight-rhino-sandbox = "0.0.17"
testcontainers = "1.20.4" testcontainers = "1.20.4"
mixpanel = "1.5.3"
[libraries] [libraries]
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
@@ -95,6 +96,7 @@ bip39 = { module = "cash.z.ecc.android:kotlin-bip39-jvm", version.ref = "bip39"
rhino = { module = "org.mozilla:rhino", version.ref = "rhino" } rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" } delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" } colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
[plugins] [plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

View File

@@ -0,0 +1,433 @@
package app.termora;/*
* @(#)SwingUtils.java 1.02 11/15/08
*
*/
//package darrylbu.util;
import javax.swing.*;
import java.awt.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.*;
/**
* A collection of utility methods for Swing.
*
* @author Darryl Burke
*/
public final class SwingUtils {
private SwingUtils() {
throw new Error("SwingUtils is just a container for static methods");
}
/**
* Convenience method for searching below <code>container</code> in the
* component hierarchy and return nested components that are instances of
* class <code>clazz</code> it finds. Returns an empty list if no such
* components exist in the container.
* <P>
* Invoking this method with a class parameter of JComponent.class
* will return all nested components.
* <P>
* This method invokes getDescendantsOfType(clazz, container, true)
*
* @param clazz the class of components whose instances are to be found.
* @param container the container at which to begin the search
* @return the List of components
*/
public static <T extends JComponent> List<T> getDescendantsOfType(
Class<T> clazz, Container container) {
return getDescendantsOfType(clazz, container, true);
}
/**
* Convenience method for searching below <code>container</code> in the
* component hierarchy and return nested components that are instances of
* class <code>clazz</code> it finds. Returns an empty list if no such
* components exist in the container.
* <P>
* Invoking this method with a class parameter of JComponent.class
* will return all nested components.
*
* @param clazz the class of components whose instances are to be found.
* @param container the container at which to begin the search
* @param nested true to list components nested within another listed
* component, false otherwise
* @return the List of components
*/
public static <T extends JComponent> List<T> getDescendantsOfType(
Class<T> clazz, Container container, boolean nested) {
List<T> tList = new ArrayList<T>();
for (Component component : container.getComponents()) {
if (clazz.isAssignableFrom(component.getClass())) {
tList.add(clazz.cast(component));
}
if (nested || !clazz.isAssignableFrom(component.getClass())) {
tList.addAll(SwingUtils.<T>getDescendantsOfType(clazz,
(Container) component, nested));
}
}
return tList;
}
/**
* Convenience method that searches below <code>container</code> in the
* component hierarchy and returns the first found component that is an
* instance of class <code>clazz</code> having the bound property value.
* Returns {@code null} if such component cannot be found.
* <P>
* This method invokes getDescendantOfType(clazz, container, property, value,
* true)
*
* @param clazz the class of component whose instance is to be found.
* @param container the container at which to begin the search
* @param property the className of the bound property, exactly as expressed in
* the accessor e.g. "Text" for getText(), "Value" for getValue().
* @param value the value of the bound property
* @return the component, or null if no such component exists in the
* container
* @throws java.lang.IllegalArgumentException if the bound property does
* not exist for the class or cannot be accessed
*/
public static <T extends JComponent> T getDescendantOfType(
Class<T> clazz, Container container, String property, Object value)
throws IllegalArgumentException {
return getDescendantOfType(clazz, container, property, value, true);
}
/**
* Convenience method that searches below <code>container</code> in the
* component hierarchy and returns the first found component that is an
* instance of class <code>clazz</code> and has the bound property value.
* Returns {@code null} if such component cannot be found.
*
* @param clazz the class of component whose instance to be found.
* @param container the container at which to begin the search
* @param property the className of the bound property, exactly as expressed in
* the accessor e.g. "Text" for getText(), "Value" for getValue().
* @param value the value of the bound property
* @param nested true to list components nested within another component
* which is also an instance of <code>clazz</code>, false otherwise
* @return the component, or null if no such component exists in the
* container
* @throws java.lang.IllegalArgumentException if the bound property does
* not exist for the class or cannot be accessed
*/
public static <T extends JComponent> T getDescendantOfType(Class<T> clazz,
Container container, String property, Object value, boolean nested)
throws IllegalArgumentException {
List<T> list = getDescendantsOfType(clazz, container, nested);
return getComponentFromList(clazz, list, property, value);
}
/**
* Convenience method for searching below <code>container</code> in the
* component hierarchy and return nested components of class
* <code>clazz</code> it finds. Returns an empty list if no such
* components exist in the container.
* <P>
* This method invokes getDescendantsOfClass(clazz, container, true)
*
* @param clazz the class of components to be found.
* @param container the container at which to begin the search
* @return the List of components
*/
public static <T extends JComponent> List<T> getDescendantsOfClass(
Class<T> clazz, Container container) {
return getDescendantsOfClass(clazz, container, true);
}
/**
* Convenience method for searching below <code>container</code> in the
* component hierarchy and return nested components of class
* <code>clazz</code> it finds. Returns an empty list if no such
* components exist in the container.
*
* @param clazz the class of components to be found.
* @param container the container at which to begin the search
* @param nested true to list components nested within another listed
* component, false otherwise
* @return the List of components
*/
public static <T extends JComponent> List<T> getDescendantsOfClass(
Class<T> clazz, Container container, boolean nested) {
List<T> tList = new ArrayList<T>();
for (Component component : container.getComponents()) {
if (clazz.equals(component.getClass())) {
tList.add(clazz.cast(component));
}
if (nested || !clazz.equals(component.getClass())) {
tList.addAll(SwingUtils.<T>getDescendantsOfClass(clazz,
(Container) component, nested));
}
}
return tList;
}
/**
* Convenience method that searches below <code>container</code> in the
* component hierarchy in a depth first manner and returns the first
* found component of class <code>clazz</code> having the bound property
* value.
* <P>
* Returns {@code null} if such component cannot be found.
* <P>
* This method invokes getDescendantOfClass(clazz, container, property,
* value, true)
*
* @param clazz the class of component to be found.
* @param container the container at which to begin the search
* @param property the className of the bound property, exactly as expressed in
* the accessor e.g. "Text" for getText(), "Value" for getValue().
* This parameter is case sensitive.
* @param value the value of the bound property
* @return the component, or null if no such component exists in the
* container's hierarchy.
* @throws java.lang.IllegalArgumentException if the bound property does
* not exist for the class or cannot be accessed
*/
public static <T extends JComponent> T getDescendantOfClass(Class<T> clazz,
Container container, String property, Object value)
throws IllegalArgumentException {
return getDescendantOfClass(clazz, container, property, value, true);
}
/**
* Convenience method that searches below <code>container</code> in the
* component hierarchy in a depth first manner and returns the first
* found component of class <code>clazz</code> having the bound property
* value.
* <P>
* Returns {@code null} if such component cannot be found.
*
* @param clazz the class of component to be found.
* @param container the container at which to begin the search
* @param property the className of the bound property, exactly as expressed
* in the accessor e.g. "Text" for getText(), "Value" for getValue().
* This parameter is case sensitive.
* @param value the value of the bound property
* @param nested true to include components nested within another listed
* component, false otherwise
* @return the component, or null if no such component exists in the
* container's hierarchy
* @throws java.lang.IllegalArgumentException if the bound property does
* not exist for the class or cannot be accessed
*/
public static <T extends JComponent> T getDescendantOfClass(Class<T> clazz,
Container container, String property, Object value, boolean nested)
throws IllegalArgumentException {
List<T> list = getDescendantsOfClass(clazz, container, nested);
return getComponentFromList(clazz, list, property, value);
}
private static <T extends JComponent> T getComponentFromList(Class<T> clazz,
List<T> list, String property, Object value)
throws IllegalArgumentException {
T retVal = null;
Method method = null;
try {
method = clazz.getMethod("get" + property);
} catch (NoSuchMethodException ex) {
try {
method = clazz.getMethod("is" + property);
} catch (NoSuchMethodException ex1) {
throw new IllegalArgumentException("Property " + property +
" not found in class " + clazz.getName());
}
}
try {
for (T t : list) {
Object testVal = method.invoke(t);
if (equals(value, testVal)) {
return t;
}
}
} catch (InvocationTargetException ex) {
throw new IllegalArgumentException(
"Error accessing property " + property +
" in class " + clazz.getName());
} catch (IllegalAccessException ex) {
throw new IllegalArgumentException(
"Property " + property +
" cannot be accessed in class " + clazz.getName());
} catch (SecurityException ex) {
throw new IllegalArgumentException(
"Property " + property +
" cannot be accessed in class " + clazz.getName());
}
return retVal;
}
/**
* Convenience method for determining whether two objects are either
* equal or both null.
*
* @param obj1 the first reference object to compare.
* @param obj2 the second reference object to compare.
* @return true if obj1 and obj2 are equal or if both are null,
* false otherwise
*/
public static boolean equals(Object obj1, Object obj2) {
return obj1 == null ? obj2 == null : obj1.equals(obj2);
}
/**
* Convenience method for mapping a container in the hierarchy to its
* contained components. The keys are the containers, and the values
* are lists of contained components.
* <P>
* Implementation note: The returned value is a HashMap and the values
* are of type ArrayList. This is subject to change, so callers should
* code against the interfaces Map and List.
*
* @param container The JComponent to be mapped
* @param nested true to drill down to nested containers, false otherwise
* @return the Map of the UI
*/
public static Map<JComponent, List<JComponent>> getComponentMap(
JComponent container, boolean nested) {
HashMap<JComponent, List<JComponent>> retVal =
new HashMap<JComponent, List<JComponent>>();
for (JComponent component : getDescendantsOfType(JComponent.class,
container, false)) {
if (!retVal.containsKey(container)) {
retVal.put(container,
new ArrayList<JComponent>());
}
retVal.get(container).add(component);
if (nested) {
retVal.putAll(getComponentMap(component, nested));
}
}
return retVal;
}
/**
* Convenience method for retrieving a subset of the UIDefaults pertaining
* to a particular class.
*
* @param clazz the class of interest
* @return the UIDefaults of the class
*/
public static UIDefaults getUIDefaultsOfClass(Class clazz) {
String name = clazz.getName();
name = name.substring(name.lastIndexOf(".") + 2);
return getUIDefaultsOfClass(name);
}
/**
* Convenience method for retrieving a subset of the UIDefaults pertaining
* to a particular class.
*
* @param className fully qualified name of the class of interest
* @return the UIDefaults of the class named
*/
public static UIDefaults getUIDefaultsOfClass(String className) {
UIDefaults retVal = new UIDefaults();
UIDefaults defaults = UIManager.getLookAndFeelDefaults();
List<?> listKeys = Collections.list(defaults.keys());
for (Object key : listKeys) {
if (key instanceof String && ((String) key).startsWith(className)) {
String stringKey = (String) key;
String property = stringKey;
if (stringKey.contains(".")) {
property = stringKey.substring(stringKey.indexOf(".") + 1);
}
retVal.put(property, defaults.get(key));
}
}
return retVal;
}
/**
* Convenience method for retrieving the UIDefault for a single property
* of a particular class.
*
* @param clazz the class of interest
* @param property the property to query
* @return the UIDefault property, or null if not found
*/
public static Object getUIDefaultOfClass(Class clazz, String property) {
Object retVal = null;
UIDefaults defaults = getUIDefaultsOfClass(clazz);
List<Object> listKeys = Collections.list(defaults.keys());
for (Object key : listKeys) {
if (key.equals(property)) {
return defaults.get(key);
}
if (key.toString().equalsIgnoreCase(property)) {
retVal = defaults.get(key);
}
}
return retVal;
}
/**
* Exclude methods that return values that are meaningless to the user
*/
static Set<String> setExclude = new HashSet<String>();
static {
setExclude.add("getFocusCycleRootAncestor");
setExclude.add("getAccessibleContext");
setExclude.add("getColorModel");
setExclude.add("getGraphics");
setExclude.add("getGraphicsConfiguration");
}
/**
* Convenience method for obtaining most non-null human readable properties
* of a JComponent. Array properties are not included.
* <P>
* Implementation note: The returned value is a HashMap. This is subject
* to change, so callers should code against the interface Map.
*
* @param component the component whose proerties are to be determined
* @return the class and value of the properties
*/
public static Map<Object, Object> getProperties(JComponent component) {
Map<Object, Object> retVal = new HashMap<Object, Object>();
Class<?> clazz = component.getClass();
Method[] methods = clazz.getMethods();
Object value = null;
for (Method method : methods) {
if (method.getName().matches("^(is|get).*") &&
method.getParameterTypes().length == 0) {
try {
Class returnType = method.getReturnType();
if (returnType != void.class &&
!returnType.getName().startsWith("[") &&
!setExclude.contains(method.getName())) {
String key = method.getName();
value = method.invoke(component);
if (value != null && !(value instanceof Component)) {
retVal.put(key, value);
}
}
// ignore exceptions that arise if the property could not be accessed
} catch (IllegalAccessException ex) {
} catch (IllegalArgumentException ex) {
} catch (InvocationTargetException ex) {
}
}
}
return retVal;
}
/**
* Convenience method to obtain the Swing class from which this
* component was directly or indirectly derived.
*
* @param component The component whose Swing superclass is to be
* determined
* @return The nearest Swing class in the inheritance tree
*/
public static <T extends JComponent> Class getJClass(T component) {
Class<?> clazz = component.getClass();
while (!clazz.getName().matches("javax.swing.J[^.]*$")) {
clazz = clazz.getSuperclass();
}
return clazz;
}
}

View File

@@ -2,25 +2,16 @@ package app.termora
object Actions { object Actions {
/**
* 打开设置
*/
const val SETTING = "SettingAction"
/** /**
* 将命令发送到多个会话 * 将命令发送到多个会话
*/ */
const val MULTIPLE = "MultipleAction" const val MULTIPLE = "MultipleAction"
/**
* 查找
*/
const val FIND_EVERYWHERE = "FindEverywhereAction"
/** /**
* 关键词高亮 * 关键词高亮
*/ */
const val KEYWORD_HIGHLIGHT_EVERYWHERE = "KeywordHighlightAction" const val KEYWORD_HIGHLIGHT = "KeywordHighlightAction"
/** /**
* Key manager * Key manager
@@ -38,13 +29,15 @@ object Actions {
*/ */
const val MACRO = "MacroAction" const val MACRO = "MacroAction"
/**
* 添加主机对话框
*/
const val ADD_HOST = "AddHostAction"
/** /**
* 打开一个主机 * 终端日志记录
*/ */
const val OPEN_HOST = "OpenHostAction" const val TERMINAL_LOGGER = "TerminalLogAction"
/**
* 打开 SFTP Tab Action
*/
const val SFTP = "SFTPAction"
} }

View File

@@ -1,16 +0,0 @@
package app.termora
import org.jdesktop.swingx.action.BoundAction
import javax.swing.Icon
abstract class AnAction : BoundAction {
constructor() : super()
constructor(icon: Icon) : super() {
super.putValue(SMALL_ICON, icon)
}
constructor(name: String?) : super(name)
constructor(name: String?, icon: Icon?) : super(name, icon)
}

View File

@@ -16,14 +16,11 @@ import java.awt.Desktop
import java.io.File import java.io.File
import java.net.URI import java.net.URI
import java.time.Duration import java.time.Duration
import java.util.*
import kotlin.math.ln import kotlin.math.ln
import kotlin.math.pow import kotlin.math.pow
import kotlin.reflect.KClass
object Application { object Application {
private val services = Collections.synchronizedMap(mutableMapOf<KClass<*>, Any>())
private lateinit var baseDataDir: File private lateinit var baseDataDir: File
@@ -125,22 +122,6 @@ object Application {
} }
} }
@Suppress("UNCHECKED_CAST")
fun <T : Any> getService(clazz: KClass<T>): T {
if (services.containsKey(clazz)) {
return services[clazz] as T
}
throw IllegalStateException("$clazz does not exist")
}
@Synchronized
fun registerService(clazz: KClass<*>, service: Any) {
if (services.containsKey(clazz)) {
throw IllegalStateException("$clazz already registered")
}
services[clazz] = service
}
private fun tryBrowse(uri: URI) { private fun tryBrowse(uri: URI) {
if (SystemInfo.isWindows) { if (SystemInfo.isWindows) {
ProcessBuilder("explorer", uri.toString()).start() ProcessBuilder("explorer", uri.toString()).start()

View File

@@ -1,10 +0,0 @@
package app.termora
/**
* 将在 JVM 进程退出时释放
*/
class ApplicationDisposable : Disposable {
companion object {
val instance by lazy { ApplicationDisposable() }
}
}

View File

@@ -1,70 +1,102 @@
package app.termora package app.termora
import app.termora.db.Database import app.termora.actions.ActionManager
import app.termora.keymap.KeymapManager
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatSystemProperties import com.formdev.flatlaf.FlatSystemProperties
import com.formdev.flatlaf.extras.FlatInspector import com.formdev.flatlaf.extras.FlatInspector
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import com.jthemedetecor.OsThemeDetector import com.jthemedetecor.OsThemeDetector
import com.sun.jna.platform.WindowUtils import com.mixpanel.mixpanelapi.ClientDelivery
import com.sun.jna.platform.win32.User32 import com.mixpanel.mixpanelapi.MessageBuilder
import com.sun.jna.ptr.IntByReference import com.mixpanel.mixpanelapi.MixpanelAPI
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.LocaleUtils import org.apache.commons.lang3.LocaleUtils
import org.apache.commons.lang3.SystemUtils import org.apache.commons.lang3.SystemUtils
import org.apache.commons.lang3.math.NumberUtils import org.json.JSONObject
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.tinylog.configuration.Configuration import org.tinylog.configuration.Configuration
import java.io.File import java.io.File
import java.io.RandomAccessFile
import java.nio.channels.FileChannel import java.nio.channels.FileChannel
import java.nio.channels.FileLock import java.nio.channels.FileLock
import java.nio.file.Paths
import java.nio.file.StandardOpenOption import java.nio.file.StandardOpenOption
import java.util.* import java.util.*
import javax.swing.* import javax.swing.*
import javax.swing.WindowConstants.DISPOSE_ON_CLOSE
import kotlin.system.exitProcess import kotlin.system.exitProcess
import kotlin.system.measureTimeMillis
class ApplicationRunner { class ApplicationRunner {
private lateinit var singletonChannel: FileChannel
private lateinit var singletonLock: FileLock private lateinit var singletonLock: FileLock
private val log by lazy { private val log by lazy {
if (!::singletonLock.isInitialized) { if (!::singletonLock.isInitialized) {
throw UnsupportedOperationException("Singleton lock is not initialized") throw UnsupportedOperationException("Singleton lock is not initialized")
} }
LoggerFactory.getLogger("Main") LoggerFactory.getLogger(ApplicationRunner::class.java)
} }
fun run() { fun run() {
// 覆盖 tinylog 配置 measureTimeMillis {
setupTinylog() // 覆盖 tinylog 配置
val setupTinylog = measureTimeMillis { setupTinylog() }
// 是否单例 // 是否单例
checkSingleton() val checkSingleton = measureTimeMillis { checkSingleton() }
// 打印系统信息 // 打印系统信息
printSystemInfo() val printSystemInfo = measureTimeMillis { printSystemInfo() }
SwingUtilities.invokeAndWait {
// 打开数据库 // 打开数据库
openDatabase() val openDatabase = measureTimeMillis { openDatabase() }
// 加载设置 // 加载设置
loadSettings() val loadSettings = measureTimeMillis { loadSettings() }
// 统计
val enableAnalytics = measureTimeMillis { enableAnalytics() }
// init ActionManager、KeymapManager
@Suppress("OPT_IN_USAGE")
GlobalScope.launch(Dispatchers.IO) {
ActionManager.getInstance()
KeymapManager.getInstance()
}
// 设置 LAF // 设置 LAF
setupLaf() val setupLaf = measureTimeMillis { setupLaf() }
// 解密数据 // 解密数据
openDoor() val openDoor = measureTimeMillis { openDoor() }
// 启动主窗口 // 启动主窗口
startMainFrame() val startMainFrame = measureTimeMillis { startMainFrame() }
if (log.isDebugEnabled) {
log.debug("setupTinylog: {}ms", setupTinylog)
log.debug("checkSingleton: {}ms", checkSingleton)
log.debug("printSystemInfo: {}ms", printSystemInfo)
log.debug("openDatabase: {}ms", openDatabase)
log.debug("loadSettings: {}ms", loadSettings)
log.debug("enableAnalytics: {}ms", enableAnalytics)
log.debug("setupLaf: {}ms", setupLaf)
log.debug("openDoor: {}ms", openDoor)
log.debug("startMainFrame: {}ms", startMainFrame)
}
}.let {
if (log.isDebugEnabled) {
log.debug("run: {}ms", it)
}
} }
} }
private fun openDoor() { private fun openDoor() {
if (Doorman.instance.isWorking()) { if (Doorman.getInstance().isWorking()) {
if (!DoormanDialog(null).open()) { if (!DoormanDialog(null).open()) {
exitProcess(1) exitProcess(1)
} }
@@ -72,17 +104,11 @@ class ApplicationRunner {
} }
private fun startMainFrame() { private fun startMainFrame() {
val frame = TermoraFrame() TermoraFrameManager.getInstance().createWindow().isVisible = true
frame.title = if (SystemInfo.isLinux) null else Application.getName()
frame.defaultCloseOperation = DISPOSE_ON_CLOSE
frame.setSize(1280, 800)
frame.setLocationRelativeTo(null)
frame.isVisible = true
} }
private fun loadSettings() { private fun loadSettings() {
val language = Database.instance.appearance.language val language = Database.getDatabase().appearance.language
val locale = runCatching { LocaleUtils.toLocale(language) }.getOrElse { Locale.getDefault() } val locale = runCatching { LocaleUtils.toLocale(language) }.getOrElse { Locale.getDefault() }
if (log.isInfoEnabled) { if (log.isInfoEnabled) {
log.info("Language: {} , Locale: {}", language, locale) log.info("Language: {} , Locale: {}", language, locale)
@@ -101,22 +127,22 @@ class ApplicationRunner {
JDialog.setDefaultLookAndFeelDecorated(true) JDialog.setDefaultLookAndFeelDecorated(true)
} }
val themeManager = ThemeManager.instance val themeManager = ThemeManager.getInstance()
val settings = Database.instance val appearance = Database.getDatabase().appearance
var theme = settings.appearance.theme var theme = appearance.theme
// 如果是跟随系统
// 如果是跟随系统或者不存在样式,那么使用默认的 if (appearance.followSystem) {
if (settings.appearance.followSystem || !themeManager.themes.containsKey(theme)) {
theme = if (OsThemeDetector.getDetector().isDark) { theme = if (OsThemeDetector.getDetector().isDark) {
"Dark" appearance.darkTheme
} else { } else {
"Light" appearance.lightTheme
} }
} }
themeManager.change(theme, true) themeManager.change(theme, true)
FlatInspector.install("ctrl shift alt X"); if (Application.isUnknownVersion())
FlatInspector.install("ctrl shift alt X");
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true) UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false) UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false)
@@ -164,21 +190,21 @@ class ApplicationRunner {
} }
private fun printSystemInfo() { private fun printSystemInfo() {
if (log.isInfoEnabled) { if (log.isDebugEnabled) {
log.info("Welcome to ${Application.getName()} ${Application.getVersion()}!") log.debug("Welcome to ${Application.getName()} ${Application.getVersion()}!")
log.info( log.debug(
"JVM name: {} , vendor: {} , version: {}", "JVM name: {} , vendor: {} , version: {}",
SystemUtils.JAVA_VM_NAME, SystemUtils.JAVA_VM_NAME,
SystemUtils.JAVA_VM_VENDOR, SystemUtils.JAVA_VM_VENDOR,
SystemUtils.JAVA_VM_VERSION, SystemUtils.JAVA_VM_VERSION,
) )
log.info( log.debug(
"OS name: {} , version: {} , arch: {}", "OS name: {} , version: {} , arch: {}",
SystemUtils.OS_NAME, SystemUtils.OS_NAME,
SystemUtils.OS_VERSION, SystemUtils.OS_VERSION,
SystemUtils.OS_ARCH SystemUtils.OS_ARCH
) )
log.info("Base config dir: ${Application.getBaseDataDir().absolutePath}") log.debug("Base config dir: ${Application.getBaseDataDir().absolutePath}")
} }
} }
@@ -197,36 +223,14 @@ class ApplicationRunner {
private fun checkSingleton() { private fun checkSingleton() {
val file = File(Application.getBaseDataDir(), "lock") singletonChannel = FileChannel.open(
val pidFile = File(Application.getBaseDataDir(), "pid") Paths.get(Application.getBaseDataDir().absolutePath, "lock"),
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
val raf = RandomAccessFile(file, "rw") )
val lock = raf.channel.tryLock()
if (lock != null) {
pidFile.writeText(ProcessHandle.current().pid().toString())
pidFile.deleteOnExit()
file.deleteOnExit()
} else {
if (SystemInfo.isWindows && pidFile.exists()) {
val pid = NumberUtils.toLong(pidFile.readText())
for (window in WindowUtils.getAllWindows(false)) {
if (pid > 0) {
val processId = IntByReference()
User32.INSTANCE.GetWindowThreadProcessId(window.hwnd, processId)
if (processId.value.toLong() != pid) {
continue
}
} else if (window.title != Application.getName() || window.filePath.endsWith("explorer.exe")) {
continue
}
User32.INSTANCE.ShowWindow(window.hwnd, User32.SW_SHOWNOACTIVATE)
User32.INSTANCE.SetForegroundWindow(window.hwnd)
break
}
}
val lock = singletonChannel.tryLock()
if (lock == null) {
System.err.println("Program is already running") System.err.println("Program is already running")
exitProcess(1) exitProcess(1)
} }
@@ -236,9 +240,8 @@ class ApplicationRunner {
private fun openDatabase() { private fun openDatabase() {
val dir = Application.getDatabaseFile()
try { try {
Database.open(dir) Database.getDatabase()
} catch (e: Exception) { } catch (e: Exception) {
if (log.isErrorEnabled) { if (log.isErrorEnabled) {
log.error(e.message, e) log.error(e.message, e)
@@ -251,4 +254,48 @@ class ApplicationRunner {
} }
} }
/**
* 统计 https://mixpanel.com
*/
@OptIn(DelicateCoroutinesApi::class)
private fun enableAnalytics() {
if (Application.isUnknownVersion()) {
return
}
GlobalScope.launch(Dispatchers.IO) {
try {
val properties = JSONObject()
properties.put("os", SystemUtils.OS_NAME)
if (SystemInfo.isLinux) {
properties.put("platform", "Linux")
} else if (SystemInfo.isWindows) {
properties.put("platform", "Windows")
} else if (SystemInfo.isMacOS) {
properties.put("platform", "macOS")
}
properties.put("version", Application.getVersion())
properties.put("language", Locale.getDefault().toString())
val message = MessageBuilder("0871335f59ee6d0eb246b008a20f9d1c")
.event(getAnalyticsUserID(), "launch", properties)
val delivery = ClientDelivery()
delivery.addMessage(message)
MixpanelAPI().deliver(delivery, true)
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
}
}
private fun getAnalyticsUserID(): String {
var id = Database.getDatabase().properties.getString("AnalyticsUserID")
if (id.isNullOrBlank()) {
id = UUID.randomUUID().toSimpleString()
Database.getDatabase().properties.putString("AnalyticsUserID", id)
}
return id
}
} }

View File

@@ -0,0 +1,374 @@
package app.termora
import app.termora.Application.ohMyJson
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import kotlinx.serialization.encodeToString
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionManager
import java.awt.Component
import java.awt.Dimension
import java.awt.Window
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.swing.*
import javax.swing.event.ListDataEvent
import javax.swing.event.ListDataListener
import kotlin.math.max
import kotlin.math.min
class CustomizeToolBarDialog(
owner: Window,
private val toolbar: TermoraToolBar
) : DialogWrapper(owner) {
private val moveTopBtn = JButton(Icons.moveUp)
private val moveBottomBtn = JButton(Icons.moveDown)
private val upBtn = JButton(Icons.up)
private val downBtn = JButton(Icons.down)
private val leftBtn = JButton(Icons.left)
private val rightBtn = JButton(Icons.right)
private val resetBtn = JButton(Icons.refresh)
private val allToLeftBtn = JButton(Icons.applyNotConflictsRight)
private val allToRightBtn = JButton(Icons.applyNotConflictsLeft)
private val leftList = ToolBarActionList()
private val rightList = ToolBarActionList()
private val actionManager get() = ActionManager.getInstance()
private var isOk = false
init {
size = Dimension(UIManager.getInt("Dialog.width") - 150, UIManager.getInt("Dialog.height") - 100)
isModal = true
controlsVisible = false
isResizable = false
title = I18n.getString("termora.toolbar.customize-toolbar")
setLocationRelativeTo(null)
moveTopBtn.isEnabled = false
moveBottomBtn.isEnabled = false
downBtn.isEnabled = false
upBtn.isEnabled = false
leftBtn.isEnabled = false
rightBtn.isEnabled = false
initEvents()
init()
}
override fun createCenterPanel(): JComponent {
allToLeftBtn.isEnabled = !rightList.model.isEmpty
allToRightBtn.isEnabled = !leftList.model.isEmpty
val box = JToolBar(JToolBar.VERTICAL)
box.add(Box.createVerticalStrut(leftList.fixedCellHeight))
box.add(rightBtn)
box.add(leftBtn)
box.add(Box.createVerticalGlue())
box.add(resetBtn)
box.add(Box.createVerticalGlue())
box.add(allToRightBtn)
box.add(allToLeftBtn)
box.add(Box.createVerticalStrut(leftList.fixedCellHeight))
val box2 = JToolBar(JToolBar.VERTICAL)
box2.add(Box.createVerticalStrut(leftList.fixedCellHeight))
box2.add(moveTopBtn)
box2.add(upBtn)
box2.add(Box.createVerticalGlue())
box2.add(downBtn)
box2.add(moveBottomBtn)
box2.add(Box.createVerticalStrut(leftList.fixedCellHeight))
return FormBuilder.create().debug(false)
.border(BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor))
.layout(FormLayout("default:grow, pref, default:grow, pref", "fill:p:grow"))
.add(JScrollPane(leftList).apply {
border = BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor)
}).xy(1, 1)
.add(box).xy(2, 1)
.add(JScrollPane(rightList).apply {
border = BorderFactory.createMatteBorder(0, 1, 0, 1, DynamicColor.BorderColor)
}).xy(3, 1)
.add(box2).xy(4, 1)
.build()
}
private fun initEvents() {
rightList.addListSelectionListener { resetMoveButtons() }
leftList.addListSelectionListener {
val indices = leftList.selectedIndices
rightBtn.isEnabled = indices.isNotEmpty()
}
leftList.model.addListDataListener(object : ListDataListener {
override fun intervalAdded(e: ListDataEvent) {
contentsChanged(e)
}
override fun intervalRemoved(e: ListDataEvent) {
contentsChanged(e)
}
override fun contentsChanged(e: ListDataEvent) {
allToLeftBtn.isEnabled = !rightList.model.isEmpty
allToRightBtn.isEnabled = !leftList.model.isEmpty
resetMoveButtons()
}
})
rightList.model.addListDataListener(object : ListDataListener {
override fun intervalAdded(e: ListDataEvent) {
contentsChanged(e)
}
override fun intervalRemoved(e: ListDataEvent) {
contentsChanged(e)
}
override fun contentsChanged(e: ListDataEvent) {
allToLeftBtn.isEnabled = !rightList.model.isEmpty
allToRightBtn.isEnabled = !leftList.model.isEmpty
resetMoveButtons()
}
})
resetBtn.addActionListener {
leftList.model.removeAllElements()
rightList.model.removeAllElements()
for (action in toolbar.getAllActions()) {
actionManager.getAction(action.id)?.let {
rightList.model.addElement(ActionHolder(action.id, it))
}
}
}
// move first
moveTopBtn.addActionListener {
val indices = rightList.selectedIndices.sortedDescending()
rightList.clearSelection()
for (index in indices.indices) {
val ele = rightList.model.getElementAt(indices[index])
rightList.model.removeElementAt(indices[index])
rightList.model.add(index, ele)
rightList.selectionModel.addSelectionInterval(index, max(index - 1, 0))
}
}
// move up
upBtn.addActionListener {
val indices = rightList.selectedIndices.sortedDescending()
rightList.clearSelection()
for (index in indices) {
val ele = rightList.model.getElementAt(index)
rightList.model.removeElementAt(index)
rightList.model.add(index - 1, ele)
rightList.selectionModel.addSelectionInterval(max(index - 1, 0), max(index - 1, 0))
}
}
// move down
downBtn.addActionListener {
val indices = rightList.selectedIndices.sortedDescending()
rightList.clearSelection()
for (index in indices) {
val ele = rightList.model.getElementAt(index)
rightList.model.removeElementAt(index)
rightList.model.add(index + 1, ele)
rightList.selectionModel.addSelectionInterval(index + 1, index + 1)
}
}
// move last
moveBottomBtn.addActionListener {
val indices = rightList.selectedIndices.sortedDescending()
val size = rightList.model.size
rightList.clearSelection()
for (index in indices.indices) {
val ele = rightList.model.getElementAt(indices[index])
rightList.model.removeElementAt(indices[index])
rightList.model.add(size - index - 1, ele)
rightList.selectionModel.addSelectionInterval(size - index - 1, size - index - 1)
}
}
allToLeftBtn.addActionListener {
while (!rightList.model.isEmpty) {
val ele = rightList.model.getElementAt(0)
rightList.model.removeElementAt(0)
leftList.model.addElement(ele)
}
}
allToRightBtn.addActionListener {
while (!leftList.model.isEmpty) {
val ele = leftList.model.getElementAt(0)
leftList.model.removeElementAt(0)
rightList.model.addElement(ele)
}
}
leftBtn.addActionListener {
val indices = rightList.selectedIndices.sortedDescending()
for (index in indices) {
val ele = rightList.model.getElementAt(index)
rightList.model.removeElementAt(index)
leftList.model.addElement(ele)
}
rightList.clearSelection()
val index = min(indices.max(), rightList.model.size - 1)
if (!rightList.model.isEmpty) {
rightList.addSelectionInterval(index, index)
}
}
rightBtn.addActionListener {
val indices = leftList.selectedIndices.sortedDescending()
val rightSelectedIndex = if (rightList.selectedIndices.isEmpty()) rightList.model.size else
rightList.selectionModel.maxSelectionIndex + 1
if (indices.isNotEmpty()) {
for (index in indices.indices) {
val ele = leftList.model.getElementAt(indices[index])
leftList.model.removeElementAt(indices[index])
rightList.model.add(rightSelectedIndex + index, ele)
}
leftList.clearSelection()
val index = min(indices.max(), leftList.model.size - 1)
if (!leftList.model.isEmpty) {
leftList.addSelectionInterval(index, index)
}
rightList.clearSelection()
rightList.addSelectionInterval(rightSelectedIndex, rightSelectedIndex)
}
}
addWindowListener(object : WindowAdapter() {
override fun windowOpened(e: WindowEvent) {
removeWindowListener(this)
for (action in toolbar.getActions()) {
if (action.visible) {
actionManager.getAction(action.id)
?.let { rightList.model.addElement(ActionHolder(action.id, it)) }
} else {
actionManager.getAction(action.id)
?.let { leftList.model.addElement(ActionHolder(action.id, it)) }
}
}
}
})
}
private fun resetMoveButtons() {
val indices = rightList.selectedIndices
if (indices.isEmpty()) {
moveTopBtn.isEnabled = false
moveBottomBtn.isEnabled = false
downBtn.isEnabled = false
upBtn.isEnabled = false
} else {
moveTopBtn.isEnabled = !indices.contains(0)
upBtn.isEnabled = moveTopBtn.isEnabled
moveBottomBtn.isEnabled = !indices.contains(rightList.model.size - 1)
downBtn.isEnabled = moveBottomBtn.isEnabled
}
leftBtn.isEnabled = indices.isNotEmpty()
}
private class ToolBarActionList : JList<ActionHolder>() {
private val model = DefaultListModel<ActionHolder>()
init {
initView()
initEvents()
setModel(model)
}
private fun initView() {
border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
background = UIManager.getColor("window")
fixedCellHeight = UIManager.getInt("Tree.rowHeight")
cellRenderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component {
var text = value?.toString() ?: StringUtils.EMPTY
if (value is ActionHolder) {
val action = value.action
text = action.getValue(Action.NAME)?.toString() ?: text
}
val c = super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
if (value is ActionHolder) {
val action = value.action
val icon = action.getValue(Action.SMALL_ICON) as Icon?
if (icon != null) {
this.icon = icon
if (icon is DynamicIcon) {
if (isSelected && cellHasFocus) {
this.icon = icon.dark
}
}
}
}
return c
}
}
}
private fun initEvents() {
}
override fun getModel(): DefaultListModel<ActionHolder> {
return model
}
}
override fun doOKAction() {
isOk = true
val actions = mutableListOf<ToolBarAction>()
for (i in 0 until rightList.model.size()) {
actions.add(ToolBarAction(rightList.model.getElementAt(i).id, true))
}
for (i in 0 until leftList.model.size()) {
actions.add(ToolBarAction(leftList.model.getElementAt(i).id, false))
}
Database.getDatabase()
.properties.putString("Termora.ToolBar.Actions", ohMyJson.encodeToString(actions))
super.doOKAction()
}
fun open(): Boolean {
isModal = true
isVisible = true
return isOk
}
private class ActionHolder(val id: String, val action: Action)
}

View File

@@ -1,8 +1,8 @@
package app.termora.db package app.termora
import app.termora.*
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.highlight.KeywordHighlight import app.termora.highlight.KeywordHighlight
import app.termora.keymap.Keymap
import app.termora.keymgr.OhKeyPair import app.termora.keymgr.OhKeyPair
import app.termora.macro.Macro import app.termora.macro.Macro
import app.termora.sync.SyncType import app.termora.sync.SyncType
@@ -26,24 +26,15 @@ import kotlin.time.Duration.Companion.minutes
class Database private constructor(private val env: Environment) : Disposable { class Database private constructor(private val env: Environment) : Disposable {
companion object { companion object {
private const val KEYMAP_STORE = "Keymap"
private const val HOST_STORE = "Host" private const val HOST_STORE = "Host"
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight" private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
private const val MACRO_STORE = "Macro" private const val MACRO_STORE = "Macro"
private const val KEY_PAIR_STORE = "KeyPair" private const val KEY_PAIR_STORE = "KeyPair"
private val log = LoggerFactory.getLogger(Database::class.java) private val log = LoggerFactory.getLogger(Database::class.java)
private lateinit var database: Database
val instance by lazy {
if (!::database.isInitialized) {
throw UnsupportedOperationException("Database has not been initialized!")
}
database
}
fun open(dir: File) { private fun open(dir: File): Database {
if (::database.isInitialized) {
throw UnsupportedOperationException("Database is already open")
}
val config = EnvironmentConfig() val config = EnvironmentConfig()
// 32MB // 32MB
config.setLogFileSize(1024 * 32) config.setLogFileSize(1024 * 32)
@@ -51,8 +42,12 @@ class Database private constructor(private val env: Environment) : Disposable {
// 5m // 5m
config.setGcStartIn(5.minutes.inWholeMilliseconds.toInt()) config.setGcStartIn(5.minutes.inWholeMilliseconds.toInt())
val environment = Environments.newInstance(dir, config) val environment = Environments.newInstance(dir, config)
database = Database(environment) return Database(environment)
Disposer.register(ApplicationDisposable.instance, database) }
fun getDatabase(): Database {
return ApplicationScope.forApplicationScope()
.getOrCreate(Database::class) { open(Application.getDatabaseFile()) }
} }
} }
@@ -62,7 +57,41 @@ class Database private constructor(private val env: Environment) : Disposable {
val appearance by lazy { Appearance() } val appearance by lazy { Appearance() }
val sync by lazy { Sync() } val sync by lazy { Sync() }
private val doorman get() = Doorman.instance private val doorman get() = Doorman.getInstance()
fun getKeymaps(): Collection<Keymap> {
val array = env.computeInTransaction { tx ->
openCursor<String>(tx, KEYMAP_STORE) { _, value ->
value
}.values
}
val keymaps = mutableListOf<Keymap>()
for (text in array.iterator()) {
keymaps.add(Keymap.fromJSON(text) ?: continue)
}
return keymaps
}
fun addKeymap(keymap: Keymap) {
env.executeInTransaction {
put(it, KEYMAP_STORE, keymap.name, keymap.toJSON())
if (log.isDebugEnabled) {
log.debug("Added Keymap: ${keymap.name}")
}
}
}
fun removeKeymap(name: String) {
env.executeInTransaction {
delete(it, KEYMAP_STORE, name)
if (log.isDebugEnabled) {
log.debug("Removed Keymap: $name")
}
}
}
fun getHosts(): Collection<Host> { fun getHosts(): Collection<Host> {
@@ -413,7 +442,7 @@ class Database private constructor(private val env: Environment) : Disposable {
/** /**
* 字体大小 * 字体大小
*/ */
var fontSize by IntPropertyDelegate(16) var fontSize by IntPropertyDelegate(14)
/** /**
* 最大行数 * 最大行数
@@ -459,7 +488,7 @@ class Database private constructor(private val env: Environment) : Disposable {
* 安全的通用属性 * 安全的通用属性
*/ */
open inner class SafetyProperties(name: String) : Property(name) { open inner class SafetyProperties(name: String) : Property(name) {
private val doorman get() = Doorman.instance private val doorman get() = Doorman.getInstance()
public override fun getString(key: String): String? { public override fun getString(key: String): String? {
var value = super.getString(key) var value = super.getString(key)
@@ -522,6 +551,8 @@ class Database private constructor(private val env: Environment) : Disposable {
* 跟随系统 * 跟随系统
*/ */
var followSystem by BooleanPropertyDelegate(true) var followSystem by BooleanPropertyDelegate(true)
var darkTheme by StringPropertyDelegate("Dark")
var lightTheme by StringPropertyDelegate("Light")
/** /**
* 语言 * 语言
@@ -548,6 +579,7 @@ class Database private constructor(private val env: Environment) : Disposable {
var rangeKeyPairs by BooleanPropertyDelegate(true) var rangeKeyPairs by BooleanPropertyDelegate(true)
var rangeKeywordHighlights by BooleanPropertyDelegate(true) var rangeKeywordHighlights by BooleanPropertyDelegate(true)
var rangeMacros by BooleanPropertyDelegate(true) var rangeMacros by BooleanPropertyDelegate(true)
var rangeKeymap by BooleanPropertyDelegate(true)
/** /**
* Token * Token

View File

@@ -1,18 +1,18 @@
package app.termora package app.termora
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import com.jetbrains.JBR import com.jetbrains.JBR
import java.awt.BorderLayout import java.awt.*
import java.awt.Dimension
import java.awt.Window
import java.awt.event.ActionEvent
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import java.awt.event.WindowAdapter import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent import java.awt.event.WindowEvent
import javax.swing.* import javax.swing.*
abstract class DialogWrapper(owner: Window?) : JDialog(owner) { abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
private val rootPanel = JPanel(BorderLayout()) private val rootPanel = JPanel(BorderLayout())
private val titleLabel = JLabel() private val titleLabel = JLabel()
@@ -21,6 +21,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
companion object { companion object {
const val DEFAULT_ACTION = "DEFAULT_ACTION" const val DEFAULT_ACTION = "DEFAULT_ACTION"
private const val PROCESS_GLOBAL_KEYMAP = "PROCESS_GLOBAL_KEYMAP"
} }
@@ -38,9 +39,21 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
protected var lostFocusDispose = false protected var lostFocusDispose = false
protected var escapeDispose = true protected var escapeDispose = true
var processGlobalKeymap: Boolean
get() {
val v = super.rootPane.getClientProperty(PROCESS_GLOBAL_KEYMAP)
if (v is Boolean) {
return v
}
return false
}
protected set(value) {
super.rootPane.putClientProperty(PROCESS_GLOBAL_KEYMAP, value)
}
protected fun init() { protected fun init() {
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
initTitleBar() initTitleBar()
@@ -132,7 +145,32 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, toolkit.menuShortcutKeyMaskEx), "close") inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, toolkit.menuShortcutKeyMaskEx), "close")
rootPane.actionMap.put("close", object : AnAction() { rootPane.actionMap.put("close", object : AnAction() {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
val c = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
val popups: List<JPopupMenu> = SwingUtils.getDescendantsOfType(
JPopupMenu::class.java,
c as Container, true
)
var openPopup = false
for (p in popups) {
p.isVisible = false
openPopup = true
}
val window = SwingUtilities.windowForComponent(c)
val windows = window.ownedWindows
for (w in windows) {
if (w.isVisible && w.javaClass.getName().endsWith("HeavyWeightWindow")) {
openPopup = true
w.dispose()
}
}
if (openPopup) {
return
}
doCancelAction() doCancelAction()
} }
}) })
@@ -154,12 +192,12 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
if (SystemInfo.isWindows) { if (SystemInfo.isWindows) {
addWindowListener(object : WindowAdapter(), ThemeChangeListener { addWindowListener(object : WindowAdapter(), ThemeChangeListener {
override fun windowClosed(e: WindowEvent) { override fun windowClosed(e: WindowEvent) {
ThemeManager.instance.removeThemeChangeListener(this) ThemeManager.getInstance().removeThemeChangeListener(this)
} }
override fun windowOpened(e: WindowEvent) { override fun windowOpened(e: WindowEvent) {
onChanged() onChanged()
ThemeManager.instance.addThemeChangeListener(this) ThemeManager.getInstance().addThemeChangeListener(this)
} }
override fun onChanged() { override fun onChanged() {
@@ -190,7 +228,8 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
putValue(DEFAULT_ACTION, true) putValue(DEFAULT_ACTION, true)
} }
override fun actionPerformed(e: ActionEvent) {
override fun actionPerformed(evt: AnActionEvent) {
doOKAction() doOKAction()
} }
@@ -198,7 +237,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
protected inner class CancelAction : AnAction(I18n.getString("termora.cancel")) { protected inner class CancelAction : AnAction(I18n.getString("termora.cancel")) {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
doCancelAction() doCancelAction()
} }

View File

@@ -2,16 +2,17 @@ package app.termora
import app.termora.AES.decodeBase64 import app.termora.AES.decodeBase64
import app.termora.AES.encodeBase64String import app.termora.AES.encodeBase64String
import app.termora.db.Database
class PasswordWrongException : RuntimeException() class PasswordWrongException : RuntimeException()
class Doorman private constructor() { class Doorman private constructor() : Disposable {
private val properties get() = Database.instance.properties private val properties get() = Database.getDatabase().properties
private var key = byteArrayOf() private var key = byteArrayOf()
companion object { companion object {
val instance by lazy { Doorman() } fun getInstance(): Doorman {
return ApplicationScope.forApplicationScope().getOrCreate(Doorman::class) { Doorman() }
}
} }
fun isWorking(): Boolean { fun isWorking(): Boolean {
@@ -82,4 +83,8 @@ class Doorman private constructor() {
checkIsWorking() checkIsWorking()
return key.contentEquals(convertKey(password)) return key.contentEquals(convertKey(password))
} }
override fun dispose() {
key = byteArrayOf()
}
} }

View File

@@ -1,7 +1,8 @@
package app.termora package app.termora
import app.termora.AES.decodeBase64 import app.termora.AES.decodeBase64
import app.termora.db.Database import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.terminal.ControlCharacters import app.termora.terminal.ControlCharacters
import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.Mnemonics
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
@@ -17,7 +18,6 @@ import org.slf4j.LoggerFactory
import java.awt.Dimension import java.awt.Dimension
import java.awt.Window import java.awt.Window
import java.awt.datatransfer.DataFlavor import java.awt.datatransfer.DataFlavor
import java.awt.event.ActionEvent
import java.awt.event.KeyAdapter import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import javax.imageio.ImageIO import javax.imageio.ImageIO
@@ -95,7 +95,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
.add(safeBtn).xy(4, rows).apply { rows += step } .add(safeBtn).xy(4, rows).apply { rows += step }
.add(tip).xyw(2, rows, 4, "center, fill").apply { rows += step } .add(tip).xyw(2, rows, 4, "center, fill").apply { rows += step }
.add(JXHyperlink(object : AnAction(I18n.getString("termora.doorman.forget-password")) { .add(JXHyperlink(object : AnAction(I18n.getString("termora.doorman.forget-password")) {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
val option = OptionPane.showConfirmDialog( val option = OptionPane.showConfirmDialog(
this@DoormanDialog, I18n.getString("termora.doorman.forget-password-message"), this@DoormanDialog, I18n.getString("termora.doorman.forget-password-message"),
options = arrayOf( options = arrayOf(
@@ -130,10 +130,11 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
} }
try { try {
val keyBackup = Database.instance.properties.getString("doorman-key-backup") val keyBackup = Database.getDatabase()
.properties.getString("doorman-key-backup")
?: throw IllegalStateException("doorman-key-backup is null") ?: throw IllegalStateException("doorman-key-backup is null")
val key = AES.ECB.decrypt(entropy, keyBackup.decodeBase64()) val key = AES.ECB.decrypt(entropy, keyBackup.decodeBase64())
Doorman.instance.work(key) Doorman.getInstance().work(key)
} catch (e: Exception) { } catch (e: Exception) {
OptionPane.showMessageDialog( OptionPane.showMessageDialog(
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"), this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
@@ -157,7 +158,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
} }
try { try {
Doorman.instance.work(passwordTextField.password) Doorman.getInstance().work(passwordTextField.password)
} catch (e: Exception) { } catch (e: Exception) {
if (e is PasswordWrongException) { if (e is PasswordWrongException) {
OptionPane.showMessageDialog( OptionPane.showMessageDialog(

View File

@@ -1,8 +1,5 @@
package app.termora package app.termora
import app.termora.keymgr.KeyManager
import app.termora.keymgr.OhKeyPair
class EditHostOptionsPane(private val host: Host) : HostOptionsPane() { class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
init { init {
generalOption.portTextField.value = host.port generalOption.portTextField.value = host.port
@@ -10,15 +7,12 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
generalOption.protocolTypeComboBox.selectedItem = host.protocol generalOption.protocolTypeComboBox.selectedItem = host.protocol
generalOption.usernameTextField.text = host.username generalOption.usernameTextField.text = host.username
generalOption.hostTextField.text = host.host generalOption.hostTextField.text = host.host
generalOption.passwordTextField.text = host.authentication.password
generalOption.remarkTextArea.text = host.remark generalOption.remarkTextArea.text = host.remark
generalOption.authenticationTypeComboBox.selectedItem = host.authentication.type generalOption.authenticationTypeComboBox.selectedItem = host.authentication.type
if (host.authentication.type == AuthenticationType.PublicKey) { if (host.authentication.type == AuthenticationType.Password) {
val ohKeyPair = KeyManager.instance.getOhKeyPair(host.authentication.password) generalOption.passwordTextField.text = host.authentication.password
if (ohKeyPair != null) { } else if (host.authentication.type == AuthenticationType.PublicKey) {
generalOption.publicKeyTextField.text = ohKeyPair.name generalOption.publicKeyComboBox.selectedItem = host.authentication.password
generalOption.publicKeyTextField.putClientProperty(OhKeyPair::class, ohKeyPair)
}
} }
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
@@ -34,6 +28,15 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
terminalOption.heartbeatIntervalTextField.value = host.options.heartbeatInterval terminalOption.heartbeatIntervalTextField.value = host.options.heartbeatInterval
tunnelingOption.tunnelings.addAll(host.tunnelings) tunnelingOption.tunnelings.addAll(host.tunnelings)
if (host.options.jumpHosts.isNotEmpty()) {
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
for (id in host.options.jumpHosts) {
jumpHostsOption.jumpHosts.add(hosts[id] ?: continue)
}
}
jumpHostsOption.filter = { it.id != host.id }
} }
override fun getHost(): Host { override fun getHost(): Host {

View File

@@ -1,12 +1,16 @@
package app.termora package app.termora
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.keyboardinteractive.TerminalUserInteraction
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import org.apache.commons.lang3.exception.ExceptionUtils import org.apache.commons.lang3.exception.ExceptionUtils
import org.apache.sshd.client.SshClient import org.apache.sshd.client.SshClient
import org.apache.sshd.client.session.ClientSession import org.apache.sshd.client.session.ClientSession
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Dimension import java.awt.Dimension
import java.awt.Window import java.awt.Window
import java.awt.event.ActionEvent
import javax.swing.* import javax.swing.*
class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) { class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
@@ -40,25 +44,33 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
private fun createTestConnectionAction(): AbstractAction { private fun createTestConnectionAction(): AbstractAction {
return object : AnAction(I18n.getString("termora.new-host.test-connection")) { return object : AnAction(I18n.getString("termora.new-host.test-connection")) {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
if (!pane.validateFields()) { if (!pane.validateFields()) {
return return
} }
putValue(NAME, "${I18n.getString("termora.new-host.test-connection")}...") putValue(NAME, "${I18n.getString("termora.new-host.test-connection")}...")
SwingUtilities.invokeLater { isEnabled = false
testConnection(pane.getHost())
putValue(NAME, I18n.getString("termora.new-host.test-connection"))
}
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) {
testConnection(pane.getHost())
withContext(Dispatchers.Swing) {
putValue(NAME, I18n.getString("termora.new-host.test-connection"))
isEnabled = true
}
}
} }
} }
} }
private fun testConnection(host: Host) { private suspend fun testConnection(host: Host) {
val owner = this
if (host.protocol != Protocol.SSH) { if (host.protocol != Protocol.SSH) {
OptionPane.showMessageDialog(this, I18n.getString("termora.new-host.test-connection-successful")) withContext(Dispatchers.Swing) {
OptionPane.showMessageDialog(owner, I18n.getString("termora.new-host.test-connection-successful"))
}
return return
} }
@@ -66,13 +78,21 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
var session: ClientSession? = null var session: ClientSession? = null
try { try {
client = SshClients.openClient(host) client = SshClients.openClient(host)
client.userInteraction = TerminalUserInteraction(owner)
session = SshClients.openSession(host, client) session = SshClients.openSession(host, client)
OptionPane.showMessageDialog(this, I18n.getString("termora.new-host.test-connection-successful")) withContext(Dispatchers.Swing) {
OptionPane.showMessageDialog(
owner,
I18n.getString("termora.new-host.test-connection-successful")
)
}
} catch (e: Exception) { } catch (e: Exception) {
OptionPane.showMessageDialog( withContext(Dispatchers.Swing) {
this, ExceptionUtils.getRootCauseMessage(e), OptionPane.showMessageDialog(
messageType = JOptionPane.ERROR_MESSAGE owner, ExceptionUtils.getRootCauseMessage(e),
) messageType = JOptionPane.ERROR_MESSAGE
)
}
} finally { } finally {
session?.close() session?.close()
client?.close() client?.close()

View File

@@ -1,6 +1,5 @@
package app.termora package app.termora
import app.termora.db.Database
import java.util.* import java.util.*
interface HostListener : EventListener { interface HostListener : EventListener {
@@ -12,10 +11,12 @@ interface HostListener : EventListener {
class HostManager private constructor() { class HostManager private constructor() {
companion object { companion object {
val instance by lazy { HostManager() } fun getInstance(): HostManager {
return ApplicationScope.forApplicationScope().getOrCreate(HostManager::class) { HostManager() }
}
} }
private val database get() = Database.instance private val database get() = Database.getDatabase()
private val listeners = mutableListOf<HostListener>() private val listeners = mutableListOf<HostListener>()
fun addHost(host: Host, notify: Boolean = true) { fun addHost(host: Host, notify: Boolean = true) {

View File

@@ -1,7 +1,7 @@
package app.termora package app.termora
import app.termora.keymgr.KeyManager
import app.termora.keymgr.KeyManagerDialog import app.termora.keymgr.KeyManagerDialog
import app.termora.keymgr.OhKeyPair
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.ui.FlatTextBorder import com.formdev.flatlaf.ui.FlatTextBorder
@@ -12,6 +12,7 @@ import java.awt.*
import java.awt.event.* import java.awt.event.*
import java.nio.charset.Charset import java.nio.charset.Charset
import javax.swing.* import javax.swing.*
import javax.swing.table.DefaultTableCellRenderer
import javax.swing.table.DefaultTableModel import javax.swing.table.DefaultTableModel
@@ -20,12 +21,14 @@ open class HostOptionsPane : OptionsPane() {
protected val generalOption = GeneralOption() protected val generalOption = GeneralOption()
protected val proxyOption = ProxyOption() protected val proxyOption = ProxyOption()
protected val terminalOption = TerminalOption() protected val terminalOption = TerminalOption()
protected val owner: Window? get() = SwingUtilities.getWindowAncestor(this) protected val jumpHostsOption = JumpHostsOption()
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
init { init {
addOption(generalOption) addOption(generalOption)
addOption(proxyOption) addOption(proxyOption)
addOption(tunnelingOption) addOption(tunnelingOption)
addOption(jumpHostsOption)
addOption(terminalOption) addOption(terminalOption)
setContentBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8)) setContentBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8))
@@ -46,10 +49,9 @@ open class HostOptionsPane : OptionsPane() {
password = String(generalOption.passwordTextField.password) password = String(generalOption.passwordTextField.password)
) )
} else if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) { } else if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
val keyPair = generalOption.publicKeyTextField.getClientProperty(OhKeyPair::class) as OhKeyPair?
authentication = authentication.copy( authentication = authentication.copy(
type = AuthenticationType.PublicKey, type = AuthenticationType.PublicKey,
password = keyPair?.id ?: StringUtils.EMPTY password = generalOption.publicKeyComboBox.selectedItem?.toString() ?: StringUtils.EMPTY
) )
} }
@@ -69,6 +71,7 @@ open class HostOptionsPane : OptionsPane() {
env = terminalOption.environmentTextArea.text, env = terminalOption.environmentTextArea.text,
startupCommand = terminalOption.startupCommandTextField.text, startupCommand = terminalOption.startupCommandTextField.text,
heartbeatInterval = (terminalOption.heartbeatIntervalTextField.value ?: 30) as Int, heartbeatInterval = (terminalOption.heartbeatIntervalTextField.value ?: 30) as Int,
jumpHosts = jumpHostsOption.jumpHosts.map { it.id }
) )
return Host( return Host(
@@ -107,7 +110,7 @@ open class HostOptionsPane : OptionsPane() {
return false return false
} }
} else if (host.authentication.type == AuthenticationType.PublicKey) { } else if (host.authentication.type == AuthenticationType.PublicKey) {
if (validateField(generalOption.publicKeyTextField)) { if (validateField(generalOption.publicKeyComboBox)) {
return false return false
} }
} }
@@ -145,6 +148,19 @@ open class HostOptionsPane : OptionsPane() {
return false return false
} }
/**
* 返回 true 表示有错误
*/
private fun validateField(comboBox: JComboBox<*>): Boolean {
if (comboBox.isEnabled && comboBox.selectedItem == null) {
selectOptionJComponent(comboBox)
comboBox.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
comboBox.requestFocusInWindow()
return true
}
return false
}
protected inner class GeneralOption : JPanel(BorderLayout()), Option { protected inner class GeneralOption : JPanel(BorderLayout()), Option {
val portTextField = PortSpinner() val portTextField = PortSpinner()
val nameTextField = OutlineTextField(128) val nameTextField = OutlineTextField(128)
@@ -154,7 +170,7 @@ open class HostOptionsPane : OptionsPane() {
private val passwordPanel = JPanel(BorderLayout()) private val passwordPanel = JPanel(BorderLayout())
private val chooseKeyBtn = JButton(Icons.greyKey) private val chooseKeyBtn = JButton(Icons.greyKey)
val passwordTextField = OutlinePasswordField(255) val passwordTextField = OutlinePasswordField(255)
val publicKeyTextField = OutlineTextField() val publicKeyComboBox = OutlineComboBox<String>()
val remarkTextArea = FixedLengthTextArea(512) val remarkTextArea = FixedLengthTextArea(512)
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>() val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
@@ -166,7 +182,7 @@ open class HostOptionsPane : OptionsPane() {
private fun initView() { private fun initView() {
add(getCenterComponent(), BorderLayout.CENTER) add(getCenterComponent(), BorderLayout.CENTER)
publicKeyTextField.isEditable = false publicKeyComboBox.isEditable = false
chooseKeyBtn.isFocusable = false chooseKeyBtn.isFocusable = false
protocolTypeComboBox.renderer = object : DefaultListCellRenderer() { protocolTypeComboBox.renderer = object : DefaultListCellRenderer() {
@@ -187,6 +203,28 @@ open class HostOptionsPane : OptionsPane() {
} }
} }
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() { authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent( override fun getListCellRendererComponent(
list: JList<*>?, list: JList<*>?,
@@ -265,14 +303,20 @@ open class HostOptionsPane : OptionsPane() {
dialog.pack() dialog.pack()
dialog.setLocationRelativeTo(null) dialog.setLocationRelativeTo(null)
dialog.isVisible = true dialog.isVisible = true
if (dialog.ok) {
val lastKeyPair = dialog.getLasOhKeyPair() val selectedItem = publicKeyComboBox.selectedItem
if (lastKeyPair != null) {
publicKeyTextField.putClientProperty(OhKeyPair::class, lastKeyPair) publicKeyComboBox.removeAllItems()
publicKeyTextField.text = lastKeyPair.name for (keyPair in KeyManager.getInstance().getOhKeyPairs()) {
publicKeyTextField.outline = null publicKeyComboBox.addItem(keyPair.id)
}
} }
publicKeyComboBox.selectedItem = selectedItem
if (!dialog.ok) {
return
}
publicKeyComboBox.selectedItem = dialog.getLasOhKeyPair()?.id ?: return
} }
private fun refreshStates() { private fun refreshStates() {
@@ -280,6 +324,7 @@ open class HostOptionsPane : OptionsPane() {
portTextField.isEnabled = true portTextField.isEnabled = true
usernameTextField.isEnabled = true usernameTextField.isEnabled = true
authenticationTypeComboBox.isEnabled = true authenticationTypeComboBox.isEnabled = true
publicKeyComboBox.isEnabled = true
passwordTextField.isEnabled = true passwordTextField.isEnabled = true
chooseKeyBtn.isEnabled = true chooseKeyBtn.isEnabled = true
@@ -289,6 +334,7 @@ open class HostOptionsPane : OptionsPane() {
usernameTextField.isEnabled = false usernameTextField.isEnabled = false
authenticationTypeComboBox.isEnabled = false authenticationTypeComboBox.isEnabled = false
passwordTextField.isEnabled = false passwordTextField.isEnabled = false
publicKeyComboBox.isEnabled = false
chooseKeyBtn.isEnabled = false chooseKeyBtn.isEnabled = false
} }
@@ -365,10 +411,16 @@ open class HostOptionsPane : OptionsPane() {
passwordPanel.removeAll() passwordPanel.removeAll()
if (authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) { if (authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
val selectedItem = publicKeyComboBox.selectedItem
publicKeyComboBox.removeAllItems()
for (pair in KeyManager.getInstance().getOhKeyPairs()) {
publicKeyComboBox.addItem(pair.id)
}
publicKeyComboBox.selectedItem = selectedItem
passwordPanel.add( passwordPanel.add(
FormBuilder.create() FormBuilder.create()
.layout(FormLayout("default:grow, 4dlu, left:pref", "pref")) .layout(FormLayout("default:grow, 4dlu, left:pref", "pref"))
.add(publicKeyTextField).xy(1, 1) .add(publicKeyComboBox).xy(1, 1)
.add(chooseKeyBtn).xy(3, 1) .add(chooseKeyBtn).xy(3, 1)
.build(), BorderLayout.CENTER .build(), BorderLayout.CENTER
) )
@@ -635,6 +687,12 @@ open class HostOptionsPane : OptionsPane() {
model.addColumn(I18n.getString("termora.new-host.tunneling.table.destination")) model.addColumn(I18n.getString("termora.new-host.tunneling.table.destination"))
table.putClientProperty(
FlatClientProperties.STYLE, mapOf(
"showHorizontalLines" to true,
"showVerticalLines" to true,
)
)
table.autoResizeMode = JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS table.autoResizeMode = JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS
table.border = BorderFactory.createEmptyBorder() table.border = BorderFactory.createEmptyBorder()
table.fillsViewportHeight = true table.fillsViewportHeight = true
@@ -843,4 +901,169 @@ open class HostOptionsPane : OptionsPane() {
} }
} }
protected inner class JumpHostsOption : JPanel(BorderLayout()), Option {
val jumpHosts = mutableListOf<Host>()
var filter: (host: Host) -> Boolean = { true }
private val model = object : DefaultTableModel() {
override fun isCellEditable(row: Int, column: Int): Boolean {
return false
}
override fun getRowCount(): Int {
return jumpHosts.size
}
override fun getValueAt(row: Int, column: Int): Any {
val host = jumpHosts.getOrNull(row) ?: return StringUtils.EMPTY
return if (column == 0)
host.name
else "${host.host}:${host.port}"
}
}
private val table = JTable(model)
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
private val moveUpBtn = JButton(I18n.getString("termora.transport.bookmarks.up"))
private val moveDownBtn = JButton(I18n.getString("termora.transport.bookmarks.down"))
private val deleteBtn = JButton(I18n.getString("termora.new-host.tunneling.delete"))
init {
initView()
initEvents()
}
private fun initView() {
val scrollPane = JScrollPane(table)
model.addColumn(I18n.getString("termora.new-host.general.name"))
model.addColumn(I18n.getString("termora.new-host.general.host"))
table.putClientProperty(
FlatClientProperties.STYLE, mapOf(
"showHorizontalLines" to true,
"showVerticalLines" to true,
)
)
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.BorderColor)
)
table.border = BorderFactory.createEmptyBorder()
moveUpBtn.isFocusable = false
moveDownBtn.isFocusable = false
deleteBtn.isFocusable = false
moveUpBtn.isEnabled = false
moveDownBtn.isEnabled = false
deleteBtn.isEnabled = false
addBtn.isFocusable = false
val box = Box.createHorizontalBox()
box.add(addBtn)
box.add(Box.createHorizontalStrut(4))
box.add(deleteBtn)
box.add(Box.createHorizontalStrut(4))
box.add(moveUpBtn)
box.add(Box.createHorizontalStrut(4))
box.add(moveDownBtn)
add(JLabel("${getTitle()}:"), BorderLayout.NORTH)
add(scrollPane, BorderLayout.CENTER)
add(box, BorderLayout.SOUTH)
}
private fun initEvents() {
addBtn.addActionListener(object : AbstractAction() {
override fun actionPerformed(e: ActionEvent?) {
val dialog = HostTreeDialog(owner) { host ->
jumpHosts.none { it.id == host.id } && filter.invoke(host)
}
dialog.setLocationRelativeTo(owner)
dialog.isVisible = true
val hosts = dialog.hosts
if (hosts.isEmpty()) {
return
}
hosts.forEach {
val rowCount = model.rowCount
jumpHosts.add(it)
model.fireTableRowsInserted(rowCount, rowCount + 1)
}
}
})
deleteBtn.addActionListener(object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
val rows = table.selectedRows.sortedDescending()
if (rows.isEmpty()) return
for (row in rows) {
jumpHosts.removeAt(row)
model.fireTableRowsDeleted(row, row)
}
}
})
table.selectionModel.addListSelectionListener {
deleteBtn.isEnabled = table.selectedRowCount > 0
moveUpBtn.isEnabled = deleteBtn.isEnabled && !table.selectedRows.contains(0)
moveDownBtn.isEnabled = deleteBtn.isEnabled && !table.selectedRows.contains(table.rowCount - 1)
}
moveUpBtn.addActionListener(object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
val rows = table.selectedRows.sorted()
if (rows.isEmpty()) return
table.clearSelection()
for (row in rows) {
val host = jumpHosts[(row)]
jumpHosts.removeAt(row)
jumpHosts.add(row - 1, host)
table.addRowSelectionInterval(row - 1, row - 1)
}
}
})
moveDownBtn.addActionListener(object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
val rows = table.selectedRows.sortedDescending()
if (rows.isEmpty()) return
table.clearSelection()
for (row in rows) {
val host = jumpHosts[(row)]
jumpHosts.removeAt(row)
jumpHosts.add(row + 1, host)
table.addRowSelectionInterval(row + 1, row + 1)
}
}
})
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.server
}
override fun getTitle(): String {
return I18n.getString("termora.new-host.jump-hosts")
}
override fun getJComponent(): JComponent {
return this
}
}
} }

View File

@@ -8,9 +8,16 @@ import kotlinx.coroutines.swing.Swing
import java.beans.PropertyChangeEvent import java.beans.PropertyChangeEvent
import javax.swing.Icon import javax.swing.Icon
abstract class HostTerminalTab(val host: Host) : PropertyTerminalTab() { abstract class HostTerminalTab(
val windowScope: WindowScope,
val host: Host,
protected val terminal: Terminal = TerminalFactory.getInstance(windowScope).createTerminal()
) : PropertyTerminalTab() {
companion object {
val Host = DataKey(app.termora.Host::class)
}
protected val coroutineScope by lazy { CoroutineScope(Dispatchers.Swing) } protected val coroutineScope by lazy { CoroutineScope(Dispatchers.Swing) }
protected val terminal = TerminalFactory.instance.createTerminal()
protected val terminalModel get() = terminal.getTerminalModel() protected val terminalModel get() = terminal.getTerminalModel()
protected var unread = false protected var unread = false
set(value) { set(value) {
@@ -25,6 +32,7 @@ abstract class HostTerminalTab(val host: Host) : PropertyTerminalTab() {
} }
init { init {
terminal.getTerminalModel().setData(Host, host)
terminal.getTerminalModel().addDataListener(object : DataListener { terminal.getTerminalModel().addDataListener(object : DataListener {
override fun onChanged(key: DataKey<*>, data: Any) { override fun onChanged(key: DataKey<*>, data: Any) {
if (key == VisualTerminal.Written) { if (key == VisualTerminal.Written) {
@@ -51,6 +59,7 @@ abstract class HostTerminalTab(val host: Host) : PropertyTerminalTab() {
} }
override fun dispose() { override fun dispose() {
terminal.close()
coroutineScope.cancel() coroutineScope.cancel()
} }

View File

@@ -1,6 +1,8 @@
package app.termora package app.termora
import app.termora.db.Database
import app.termora.actions.NewHostAction
import app.termora.actions.OpenHostAction
import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.icons.FlatTreeClosedIcon import com.formdev.flatlaf.icons.FlatTreeClosedIcon
import com.formdev.flatlaf.icons.FlatTreeOpenIcon import com.formdev.flatlaf.icons.FlatTreeOpenIcon
@@ -24,7 +26,7 @@ import javax.swing.tree.TreeSelectionModel
class HostTree : JTree(), Disposable { class HostTree : JTree(), Disposable {
private val hostManager get() = HostManager.instance private val hostManager get() = HostManager.getInstance()
private val editor = OutlineTextField(64) private val editor = OutlineTextField(64)
var contextmenu = true var contextmenu = true
@@ -83,7 +85,7 @@ class HostTree : JTree(), Disposable {
}) })
val state = Database.instance.properties.getString("HostTreeExpansionState") val state = Database.getDatabase().properties.getString("HostTreeExpansionState")
if (state != null) { if (state != null) {
TreeUtils.loadExpansionState(this@HostTree, state) TreeUtils.loadExpansionState(this@HostTree, state)
} }
@@ -132,8 +134,8 @@ class HostTree : JTree(), Disposable {
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) { if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
val host = lastSelectedPathComponent val host = lastSelectedPathComponent
if (host is Host && host.protocol != Protocol.Folder) { if (host is Host && host.protocol != Protocol.Folder) {
ActionManager.getInstance().getAction(Actions.OPEN_HOST) ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
?.actionPerformed(OpenHostActionEvent(this, host)) ?.actionPerformed(OpenHostActionEvent(e.source, host, e))
} }
} }
} }
@@ -316,6 +318,7 @@ class HostTree : JTree(), Disposable {
val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host")) val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host"))
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open")) val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open"))
val openInNewWindow = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open-in-new-window"))
popupMenu.addSeparator() popupMenu.addSeparator()
val copy = popupMenu.add(I18n.getString("termora.welcome.contextmenu.copy")) val copy = popupMenu.add(I18n.getString("termora.welcome.contextmenu.copy"))
val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove")) val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove"))
@@ -328,15 +331,8 @@ class HostTree : JTree(), Disposable {
popupMenu.addSeparator() popupMenu.addSeparator()
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property")) val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
open.addActionListener { open.addActionListener { openHosts(it, false) }
getSelectionNodes() openInNewWindow.addActionListener { openHosts(it, true) }
.filter { it.protocol != Protocol.Folder }
.forEach {
ActionManager.getInstance()
.getAction(Actions.OPEN_HOST)
?.actionPerformed(OpenHostActionEvent(this, it))
}
}
rename.addActionListener { rename.addActionListener {
startEditingAtPath(TreePath(model.getPathToRoot(lastHost))) startEditingAtPath(TreePath(model.getPathToRoot(lastHost)))
@@ -412,7 +408,8 @@ class HostTree : JTree(), Disposable {
newHost.addActionListener(object : AbstractAction() { newHost.addActionListener(object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(e: ActionEvent) {
showAddHostDialog() ActionManager.getInstance().getAction(NewHostAction.NEW_HOST)
?.actionPerformed(e)
} }
}) })
@@ -451,30 +448,19 @@ class HostTree : JTree(), Disposable {
popupMenu.show(this, event.x, event.y) popupMenu.show(this, event.x, event.y)
} }
fun showAddHostDialog() { private fun openHosts(evt: EventObject, openInNewWindow: Boolean) {
var lastHost = lastSelectedPathComponent assertEventDispatchThread()
if (lastHost !is Host) { val nodes = getSelectionNodes().filter { it.protocol != Protocol.Folder }
return if (nodes.isEmpty()) return
} val openHostAction = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST) ?: return
val source = if (openInNewWindow)
if (lastHost.protocol != Protocol.Folder) { TermoraFrameManager.getInstance().createWindow().apply { isVisible = true }
val p = model.getParent(lastHost) ?: return else evt.source
lastHost = p
}
val dialog = HostDialog(SwingUtilities.getWindowAncestor(this))
dialog.isVisible = true
val host = (dialog.host ?: return).copy(parentId = lastHost.id)
runCatchingHost(host)
expandNode(lastHost)
selectionPath = TreePath(model.getPathToRoot(host))
nodes.forEach { openHostAction.actionPerformed(OpenHostActionEvent(source, it, evt)) }
} }
fun expandNode(node: Host, including: Boolean = false) {
private fun expandNode(node: Host, including: Boolean = false) {
expandPath(TreePath(model.getPathToRoot(node))) expandPath(TreePath(model.getPathToRoot(node)))
if (including) { if (including) {
model.getChildren(node).forEach { expandNode(it, true) } model.getChildren(node).forEach { expandNode(it, true) }
@@ -541,18 +527,23 @@ class HostTree : JTree(), Disposable {
while (parents.isNotEmpty()) { while (parents.isNotEmpty()) {
val p = parents.removeFirst() val p = parents.removeFirst()
for (i in 0 until model.getChildCount(p)) { for (i in 0 until getModel().getChildCount(p)) {
val child = model.getChild(p, i) as Host val child = getModel().getChild(p, i) as Host
nodes.add(child) nodes.add(child)
parents.add(child) parents.add(child)
} }
} }
// 确保是最新的
for (i in 0 until nodes.size) {
nodes[i] = model.getHost(nodes[i].id) ?: continue
}
return nodes return nodes
} }
override fun dispose() { override fun dispose() {
Database.instance.properties.putString( Database.getDatabase().properties.putString(
"HostTreeExpansionState", "HostTreeExpansionState",
TreeUtils.saveExpansionState(this) TreeUtils.saveExpansionState(this)
) )

View File

@@ -1,6 +1,5 @@
package app.termora package app.termora
import app.termora.db.Database
import java.awt.Dimension import java.awt.Dimension
import java.awt.Window import java.awt.Window
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
@@ -10,7 +9,10 @@ import java.awt.event.WindowEvent
import javax.swing.* import javax.swing.*
import javax.swing.tree.TreeSelectionModel import javax.swing.tree.TreeSelectionModel
class HostTreeDialog(owner: Window) : DialogWrapper(owner) { class HostTreeDialog(
owner: Window,
private val filter: (host: Host) -> Boolean = { true }
) : DialogWrapper(owner) {
private val tree = HostTree() private val tree = HostTree()
@@ -34,7 +36,7 @@ class HostTreeDialog(owner: Window) : DialogWrapper(owner) {
title = I18n.getString("termora.transport.sftp.select-host") title = I18n.getString("termora.transport.sftp.select-host")
tree.setModel(SearchableHostTreeModel(tree.model) { host -> tree.setModel(SearchableHostTreeModel(tree.model) { host ->
host.protocol == Protocol.Folder || host.protocol == Protocol.SSH (host.protocol == Protocol.Folder || host.protocol == Protocol.SSH) && filter.invoke(host)
}) })
tree.contextmenu = true tree.contextmenu = true
tree.doubleClickConnection = false tree.doubleClickConnection = false
@@ -51,7 +53,7 @@ class HostTreeDialog(owner: Window) : DialogWrapper(owner) {
addWindowListener(object : WindowAdapter() { addWindowListener(object : WindowAdapter() {
override fun windowActivated(e: WindowEvent) { override fun windowActivated(e: WindowEvent) {
removeWindowListener(this) removeWindowListener(this)
val state = Database.instance.properties.getString("HostTreeDialog.HostTreeExpansionState") val state = Database.getDatabase().properties.getString("HostTreeDialog.HostTreeExpansionState")
if (state != null) { if (state != null) {
TreeUtils.loadExpansionState(tree, state) TreeUtils.loadExpansionState(tree, state)
} }
@@ -71,7 +73,8 @@ class HostTreeDialog(owner: Window) : DialogWrapper(owner) {
addWindowListener(object : WindowAdapter() { addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) { override fun windowClosed(e: WindowEvent) {
Database.instance.properties.putString( tree.setModel(null)
Database.getDatabase().properties.putString(
"HostTreeDialog.HostTreeExpansionState", "HostTreeDialog.HostTreeExpansionState",
TreeUtils.saveExpansionState(tree) TreeUtils.saveExpansionState(tree)
) )

View File

@@ -10,7 +10,7 @@ class HostTreeModel : TreeModel {
val listeners = mutableListOf<TreeModelListener>() val listeners = mutableListOf<TreeModelListener>()
private val hostManager get() = HostManager.instance private val hostManager get() = HostManager.getInstance()
private val hosts = mutableMapOf<String, Host>() private val hosts = mutableMapOf<String, Host>()
private val myRoot by lazy { private val myRoot by lazy {
Host( Host(

View File

@@ -1,5 +1,6 @@
package app.termora package app.termora
import app.termora.actions.AnAction
import com.formdev.flatlaf.extras.FlatSVGIcon import com.formdev.flatlaf.extras.FlatSVGIcon
import com.formdev.flatlaf.extras.FlatSVGIcon.ColorFilter import com.formdev.flatlaf.extras.FlatSVGIcon.ColorFilter
import org.jdesktop.swingx.JXHyperlink import org.jdesktop.swingx.JXHyperlink

View File

@@ -40,12 +40,17 @@ object I18n {
} }
fun getString(key: String, vararg args: Any): String { fun getString(key: String, vararg args: Any): String {
val text = getString(key)
if (args.isNotEmpty()) {
return MessageFormat.format(text, *args)
}
return text
}
fun getString(key: String): String {
try { try {
val text = substitutor.replace(bundle.getString(key)) return substitutor.replace(bundle.getString(key))
if (args.isNotEmpty()) {
return MessageFormat.format(text, *args)
}
return text
} catch (e: MissingResourceException) { } catch (e: MissingResourceException) {
if (log.isWarnEnabled) { if (log.isWarnEnabled) {
log.warn(e.message, e) log.warn(e.message, e)
@@ -54,4 +59,5 @@ object I18n {
} }
} }
} }

View File

@@ -3,17 +3,25 @@ package app.termora
object Icons { object Icons {
val bulletList by lazy { DynamicIcon("icons/bulletList.svg", "icons/bulletList_dark.svg") } val bulletList by lazy { DynamicIcon("icons/bulletList.svg", "icons/bulletList_dark.svg") }
val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") } val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") }
val moveUp by lazy { DynamicIcon("icons/moveUp.svg", "icons/moveUp_dark.svg") }
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") } val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
val close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") } val close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") }
val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") } val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
val eye by lazy { DynamicIcon("icons/eye.svg", "icons/eye_dark.svg") }
val eyeClose by lazy { DynamicIcon("icons/eyeClose.svg", "icons/eyeClose_dark.svg") }
val matchCase by lazy { DynamicIcon("icons/matchCase.svg", "icons/matchCase_dark.svg") } val matchCase by lazy { DynamicIcon("icons/matchCase.svg", "icons/matchCase_dark.svg") }
val regex by lazy { DynamicIcon("icons/regex.svg", "icons/regex_dark.svg") } val regex by lazy { DynamicIcon("icons/regex.svg", "icons/regex_dark.svg") }
val vcs by lazy { DynamicIcon("icons/vcs.svg", "icons/vcs_dark.svg") } val vcs by lazy { DynamicIcon("icons/vcs.svg", "icons/vcs_dark.svg") }
val dumpThreads by lazy { DynamicIcon("icons/dumpThreads.svg", "icons/dumpThreads_dark.svg") } val dumpThreads by lazy { DynamicIcon("icons/dumpThreads.svg", "icons/dumpThreads_dark.svg") }
val supertypes by lazy { DynamicIcon("icons/supertypes.svg", "icons/supertypes_dark.svg") } val supertypes by lazy { DynamicIcon("icons/supertypes.svg", "icons/supertypes_dark.svg") }
val fitContent by lazy { DynamicIcon("icons/fitContent.svg", "icons/fitContent_dark.svg") }
val settings by lazy { DynamicIcon("icons/settings.svg", "icons/settings_dark.svg") } val settings by lazy { DynamicIcon("icons/settings.svg", "icons/settings_dark.svg") }
val copy by lazy { DynamicIcon("icons/copy.svg", "icons/copy_dark.svg") }
val delete by lazy { DynamicIcon("icons/delete.svg", "icons/delete_dark.svg") }
val pin by lazy { DynamicIcon("icons/pin.svg", "icons/pin_dark.svg") } val pin by lazy { DynamicIcon("icons/pin.svg", "icons/pin_dark.svg") }
val empty by lazy { DynamicIcon("icons/empty.svg") } val empty by lazy { DynamicIcon("icons/empty.svg") }
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") }
val add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") } val add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") }
val errorIntroduction by lazy { DynamicIcon("icons/errorIntroduction.svg", "icons/errorIntroduction_dark.svg") } val errorIntroduction by lazy { DynamicIcon("icons/errorIntroduction.svg", "icons/errorIntroduction_dark.svg") }
val networkPolicy by lazy { DynamicIcon("icons/networkPolicy.svg", "icons/networkPolicy_dark.svg") } val networkPolicy by lazy { DynamicIcon("icons/networkPolicy.svg", "icons/networkPolicy_dark.svg") }
@@ -47,11 +55,11 @@ object Icons {
val google by lazy { DynamicIcon("icons/google-small.svg") } val google by lazy { DynamicIcon("icons/google-small.svg") }
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") } val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }
val yandexCloud by lazy { DynamicIcon("icons/yandexCloud.svg") } val yandexCloud by lazy { DynamicIcon("icons/yandexCloud.svg") }
val aws by lazy { DynamicIcon("icons/aws.svg","icons/aws_dark.svg") } val aws by lazy { DynamicIcon("icons/aws.svg", "icons/aws_dark.svg") }
val huawei by lazy { DynamicIcon("icons/huawei.svg") } val huawei by lazy { DynamicIcon("icons/huawei.svg") }
val baidu by lazy { DynamicIcon("icons/baiduyun.svg") } val baidu by lazy { DynamicIcon("icons/baiduyun.svg") }
val tianyi by lazy { DynamicIcon("icons/tianyiyun.svg") } val tianyi by lazy { DynamicIcon("icons/tianyiyun.svg") }
val digitalocean by lazy { DynamicIcon("icons/digitalocean.svg","icons/digitalocean_dark.svg") } val digitalocean by lazy { DynamicIcon("icons/digitalocean.svg", "icons/digitalocean_dark.svg") }
val terminalUnread by lazy { DynamicIcon("icons/terminalUnread.svg", "icons/terminalUnread_dark.svg") } val terminalUnread by lazy { DynamicIcon("icons/terminalUnread.svg", "icons/terminalUnread_dark.svg") }
val dbPrimitive by lazy { DynamicIcon("icons/dbPrimitive.svg", "icons/dbPrimitive_dark.svg") } val dbPrimitive by lazy { DynamicIcon("icons/dbPrimitive.svg", "icons/dbPrimitive_dark.svg") }
val linux by lazy { DynamicIcon("icons/linux.svg", "icons/linux_dark.svg") } val linux by lazy { DynamicIcon("icons/linux.svg", "icons/linux_dark.svg") }
@@ -73,8 +81,23 @@ object Icons {
val colorPicker by lazy { DynamicIcon("icons/colorPicker.svg", "icons/colorPicker_dark.svg") } val colorPicker by lazy { DynamicIcon("icons/colorPicker.svg", "icons/colorPicker_dark.svg") }
val folder by lazy { DynamicIcon("icons/folder.svg", "icons/folder_dark.svg") } val folder by lazy { DynamicIcon("icons/folder.svg", "icons/folder_dark.svg") }
val listFiles by lazy { DynamicIcon("icons/listFiles.svg", "icons/listFiles_dark.svg") } val listFiles by lazy { DynamicIcon("icons/listFiles.svg", "icons/listFiles_dark.svg") }
val left by lazy { DynamicIcon("icons/left.svg", "icons/left_dark.svg") }
val right by lazy { DynamicIcon("icons/right.svg", "icons/right_dark.svg") }
val dotListFiles by lazy { DynamicIcon("icons/dotListFiles.svg", "icons/dotListFiles_dark.svg") }
val fileTransfer by lazy { DynamicIcon("icons/fileTransfer.svg", "icons/fileTransfer_dark.svg") } val fileTransfer by lazy { DynamicIcon("icons/fileTransfer.svg", "icons/fileTransfer_dark.svg") }
val help by lazy { DynamicIcon("icons/help.svg", "icons/help_dark.svg") } val help by lazy { DynamicIcon("icons/help.svg", "icons/help_dark.svg") }
val applyNotConflictsLeft by lazy {
DynamicIcon(
"icons/applyNotConflictsLeft.svg",
"icons/applyNotConflictsLeft_dark.svg"
)
}
val applyNotConflictsRight by lazy {
DynamicIcon(
"icons/applyNotConflictsRight.svg",
"icons/applyNotConflictsRight_dark.svg"
)
}
val expand by lazy { DynamicIcon("icons/expand.svg", "icons/expand_dark.svg") } val expand by lazy { DynamicIcon("icons/expand.svg", "icons/expand_dark.svg") }
val collapse by lazy { DynamicIcon("icons/collapse.svg", "icons/collapse_dark.svg") } val collapse by lazy { DynamicIcon("icons/collapse.svg", "icons/collapse_dark.svg") }
val expandAll by lazy { DynamicIcon("icons/expandAll.svg", "icons/expandAll_dark.svg") } val expandAll by lazy { DynamicIcon("icons/expandAll.svg", "icons/expandAll_dark.svg") }

View File

@@ -9,7 +9,58 @@ import com.formdev.flatlaf.util.SystemInfo
import java.util.* import java.util.*
class LightLaf : FlatLightLaf(), ColorTheme { interface LafTag
interface LightLafTag : LafTag
interface DarkLafTag : LafTag
class DraculaLaf : FlatPropertiesLaf("Dracula", Properties().apply {
putAll(
mapOf(
"@baseTheme" to "dark",
"@background" to "#282935",
"@windowText" to "#eaeaea",
)
)
}), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Basic.BACKGROUND -> 0x282935
TerminalColor.Basic.FOREGROUND -> 0xeaeaea
TerminalColor.Basic.SELECTION_BACKGROUND -> 0x56596b
TerminalColor.Basic.SELECTION_FOREGROUND -> 0xfeffff
TerminalColor.Basic.HYPERLINK -> 0x255ab4
TerminalColor.Cursor.BACKGROUND -> 0xc7c7c7
TerminalColor.Find.BACKGROUND -> 0xffff00
TerminalColor.Find.FOREGROUND -> 0x282935
TerminalColor.Normal.BLACK -> 0
TerminalColor.Normal.RED -> 0xef766d
TerminalColor.Normal.GREEN -> 0x88f397
TerminalColor.Normal.YELLOW -> 0xf4f8a7
TerminalColor.Normal.BLUE -> 0xc4a9f4
TerminalColor.Normal.MAGENTA -> 0xf297cd
TerminalColor.Normal.CYAN -> 0xaceafb
TerminalColor.Normal.WHITE -> 0xc7c7c7
TerminalColor.Bright.BLACK -> 0x676767
TerminalColor.Bright.RED -> 0xef766d
TerminalColor.Bright.GREEN -> 0x88f397
TerminalColor.Bright.YELLOW -> 0xf4f8a7
TerminalColor.Bright.BLUE -> 0xc4a9f4
TerminalColor.Bright.MAGENTA -> 0xf297cd
TerminalColor.Bright.CYAN -> 0xaceafb
TerminalColor.Bright.WHITE -> 0xfeffff
else -> Int.MAX_VALUE
}
}
}
class LightLaf : FlatLightLaf(), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0 TerminalColor.Normal.BLACK -> 0
@@ -36,7 +87,7 @@ class LightLaf : FlatLightLaf(), ColorTheme {
} }
class DarkLaf : FlatDarkLaf(), ColorTheme { class DarkLaf : FlatDarkLaf(), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0 TerminalColor.Normal.BLACK -> 0
@@ -65,7 +116,7 @@ class DarkLaf : FlatDarkLaf(), ColorTheme {
} }
} }
class iTerm2DarkLaf : FlatDarkLaf(), ColorTheme { class iTerm2DarkLaf : FlatDarkLaf(), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
@@ -113,7 +164,7 @@ class TermiusLightLaf : FlatPropertiesLaf("Termius Light", Properties().apply {
"@windowText" to "#32364a", "@windowText" to "#32364a",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
@@ -156,14 +207,14 @@ class TermiusDarkLaf : FlatPropertiesLaf("Termius Dark", Properties().apply {
"@windowText" to "#21b568", "@windowText" to "#21b568",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Basic.SELECTION_BACKGROUND, TerminalColor.Basic.SELECTION_BACKGROUND,
TerminalColor.Cursor.BACKGROUND -> 0x21b568 TerminalColor.Cursor.BACKGROUND -> 0x21b568
TerminalColor.Basic.SELECTION_FOREGROUND ->0 TerminalColor.Basic.SELECTION_FOREGROUND -> 0
TerminalColor.Basic.FOREGROUND -> 0x21b568 TerminalColor.Basic.FOREGROUND -> 0x21b568
@@ -198,7 +249,7 @@ class NovelLaf : FlatPropertiesLaf("Novel", Properties().apply {
"@windowText" to "#3b2322", "@windowText" to "#3b2322",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -237,7 +288,7 @@ class AtomOneDarkLaf : FlatPropertiesLaf("Atom One Dark", Properties().apply {
"@windowText" to "#abb2bf", "@windowText" to "#abb2bf",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -275,7 +326,7 @@ class AtomOneLightLaf : FlatPropertiesLaf("Atom One Light", Properties().apply {
"@windowText" to "#383a42", "@windowText" to "#383a42",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -313,7 +364,7 @@ class EverforestDarkLaf : FlatPropertiesLaf("Everforest Dark", Properties().appl
"@windowText" to "#d3c6aa", "@windowText" to "#d3c6aa",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x42494e TerminalColor.Normal.BLACK -> 0x42494e
@@ -350,7 +401,7 @@ class EverforestLightLaf : FlatPropertiesLaf("Everforest Light", Properties().ap
"@windowText" to "#5c6a72", "@windowText" to "#5c6a72",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x42494e TerminalColor.Normal.BLACK -> 0x42494e
@@ -387,7 +438,7 @@ class NightOwlLaf : FlatPropertiesLaf("Night Owl", Properties().apply {
"@windowText" to "#d6deeb", "@windowText" to "#d6deeb",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x072945 TerminalColor.Normal.BLACK -> 0x072945
@@ -424,7 +475,7 @@ class LightOwlLaf : FlatPropertiesLaf("Light Owl", Properties().apply {
"@windowText" to "#403f53", "@windowText" to "#403f53",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x403f53 TerminalColor.Normal.BLACK -> 0x403f53
@@ -461,7 +512,7 @@ class AuraLaf : FlatPropertiesLaf("Aura", Properties().apply {
"@windowText" to "#edecee", "@windowText" to "#edecee",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x1c1b22 TerminalColor.Normal.BLACK -> 0x1c1b22
@@ -498,7 +549,7 @@ class Cobalt2Laf : FlatPropertiesLaf("Cobalt2", Properties().apply {
"@windowText" to "#ffffff", "@windowText" to "#ffffff",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -535,7 +586,7 @@ class OctocatDarkLaf : FlatPropertiesLaf("Octocat Dark", Properties().apply {
"@windowText" to "#8b949e", "@windowText" to "#8b949e",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -572,7 +623,7 @@ class OctocatLightLaf : FlatPropertiesLaf("Octocat Light", Properties().apply {
"@windowText" to "#3e3e3e", "@windowText" to "#3e3e3e",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -609,7 +660,7 @@ class AyuDarkLaf : FlatPropertiesLaf("Ayu Dark", Properties().apply {
"@windowText" to "#e6e1cf", "@windowText" to "#e6e1cf",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -646,7 +697,7 @@ class AyuLightLaf : FlatPropertiesLaf("Ayu Light", Properties().apply {
"@windowText" to "#5c6773", "@windowText" to "#5c6773",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -683,7 +734,7 @@ class HomebrewLaf : FlatPropertiesLaf("Homebrew", Properties().apply {
"@windowText" to "#00ff00", "@windowText" to "#00ff00",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x2e2e2e TerminalColor.Normal.BLACK -> 0x2e2e2e
@@ -722,7 +773,7 @@ class ProLaf : FlatPropertiesLaf("Pro", Properties().apply {
"@windowText" to "#f2f2f2", "@windowText" to "#f2f2f2",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x2e2e2e TerminalColor.Normal.BLACK -> 0x2e2e2e
@@ -761,7 +812,7 @@ class NordLightLaf : FlatPropertiesLaf("Nord Light", Properties().apply {
"@windowText" to "#414858", "@windowText" to "#414858",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x2c3344 TerminalColor.Normal.BLACK -> 0x2c3344
@@ -800,7 +851,7 @@ class NordDarkLaf : FlatPropertiesLaf("Nord Dark", Properties().apply {
"@windowText" to "#d8dee9", "@windowText" to "#d8dee9",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x3b4252 TerminalColor.Normal.BLACK -> 0x3b4252
@@ -840,7 +891,7 @@ class GitHubLightLaf : FlatPropertiesLaf("GitHub Light", Properties().apply {
"@windowText" to "#3e3e3e", "@windowText" to "#3e3e3e",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x3e3e3e TerminalColor.Normal.BLACK -> 0x3e3e3e
@@ -879,7 +930,7 @@ class GitHubDarkLaf : FlatPropertiesLaf("GitHub Dark", Properties().apply {
"@windowText" to "#8b949e", "@windowText" to "#8b949e",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -919,7 +970,7 @@ class ChalkLaf : FlatPropertiesLaf("Chalk", Properties().apply {
"@windowText" to "#d2d8d9", "@windowText" to "#d2d8d9",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x7d8b8f TerminalColor.Normal.BLACK -> 0x7d8b8f

View File

@@ -4,11 +4,11 @@ import app.termora.terminal.PtyConnector
import org.apache.commons.io.Charsets import org.apache.commons.io.Charsets
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
class LocalTerminalTab(host: Host) : PtyHostTerminalTab(host) { class LocalTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
override suspend fun openPtyConnector(): PtyConnector { override suspend fun openPtyConnector(): PtyConnector {
val winSize = terminalPanel.winSize() val winSize = terminalPanel.winSize()
val ptyConnector = PtyConnectorFactory.instance.createPtyConnector( val ptyConnector = PtyConnectorFactory.getInstance(windowScope).createPtyConnector(
winSize.rows, winSize.cols, winSize.rows, winSize.cols,
host.options.envs(), host.options.envs(),
Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8), Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8),

View File

@@ -1,6 +1,44 @@
package app.termora package app.termora
import com.pty4j.util.PtyUtil
import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.SystemUtils
import java.io.File
fun main() { fun main() {
// 由于 macOS 签名和公证问题,依赖二进制依赖会单独在一个文件夹
if (SystemUtils.IS_OS_MAC_OSX) {
setupNativeLibraries()
}
ApplicationRunner().run() ApplicationRunner().run()
} }
private fun setupNativeLibraries() {
if (!SystemUtils.IS_OS_MAC_OSX) {
return
}
val appPath = Application.getAppPath()
if (StringUtils.isBlank(appPath)) {
return
}
val contents = File(appPath).parentFile?.parentFile ?: return
val dylib = FileUtils.getFile(contents, "app", "dylib")
if (!dylib.exists()) {
return
}
val jna = FileUtils.getFile(dylib, "jna")
if (jna.exists()) {
System.setProperty("jna.boot.library.path", jna.absolutePath)
}
val pty4j = FileUtils.getFile(dylib, "pty4j")
if (pty4j.exists()) {
System.setProperty(PtyUtil.PREFERRED_NATIVE_FOLDER_KEY, pty4j.absolutePath)
}
}

View File

@@ -1,5 +1,6 @@
package app.termora package app.termora
import app.termora.terminal.PtyConnector import app.termora.terminal.PtyConnector
import app.termora.terminal.PtyConnectorDelegate import app.termora.terminal.PtyConnectorDelegate
import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.action.ActionManager
@@ -7,10 +8,15 @@ import org.jdesktop.swingx.action.ActionManager
/** /**
* 当开启转发时,会获取到所有的 [PtyConnector] 然后跳过中间层,直接找到最近的一个 [MultiplePtyConnector],如果找不到那就以最后一个匹配不到的为准 [getMultiplePtyConnector]。 * 当开启转发时,会获取到所有的 [PtyConnector] 然后跳过中间层,直接找到最近的一个 [MultiplePtyConnector],如果找不到那就以最后一个匹配不到的为准 [getMultiplePtyConnector]。
*/ */
class MultiplePtyConnector(private val myConnector: PtyConnector) : PtyConnectorDelegate(myConnector) { class MultiplePtyConnector(
private val myConnector: PtyConnector
) : PtyConnectorDelegate(myConnector) {
private val isMultiple get() = ActionManager.getInstance().isSelected(Actions.MULTIPLE) private val isMultiple get() = ActionManager.getInstance().isSelected(Actions.MULTIPLE)
private val ptyConnectors get() = PtyConnectorFactory.instance.getPtyConnectors() private val ptyConnectors
get() = ApplicationScope.forApplicationScope()
.windowScopes().map { PtyConnectorFactory.getInstance(it).getPtyConnectors() }
.flatten()
override fun write(buffer: ByteArray, offset: Int, len: Int) { override fun write(buffer: ByteArray, offset: Int, len: Int) {
if (isMultiple) { if (isMultiple) {

View File

@@ -1,12 +1,13 @@
package app.termora package app.termora
import app.termora.actions.ActionManager
import app.termora.terminal.Terminal import app.termora.terminal.Terminal
import app.termora.terminal.TerminalColor import app.termora.terminal.TerminalColor
import app.termora.terminal.TextStyle import app.termora.terminal.TextStyle
import app.termora.terminal.panel.TerminalDisplay import app.termora.terminal.panel.TerminalDisplay
import app.termora.terminal.panel.TerminalPaintListener import app.termora.terminal.panel.TerminalPaintListener
import app.termora.terminal.panel.TerminalPanel import app.termora.terminal.panel.TerminalPanel
import org.jdesktop.swingx.action.ActionManager
import java.awt.Color import java.awt.Color
import java.awt.Graphics import java.awt.Graphics

View File

@@ -1,11 +1,190 @@
package app.termora package app.termora
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
import com.formdev.flatlaf.extras.components.FlatTabbedPane import com.formdev.flatlaf.extras.components.FlatTabbedPane
import org.apache.commons.lang3.StringUtils
import java.awt.*
import java.awt.event.*
import java.awt.image.BufferedImage
import java.util.*
import javax.swing.*
import kotlin.math.abs
class MyTabbedPane : FlatTabbedPane() { class MyTabbedPane : FlatTabbedPane() {
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
private val dragMouseAdaptor = DragMouseAdaptor()
private val terminalTabbedManager
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
.getData(DataProviders.TerminalTabbedManager)
init {
initEvents()
}
override fun updateUI() {
styleMap = mapOf(
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground"),
"hoverColor" to UIManager.getColor("TabbedPane.background"),
)
super.updateUI()
}
private fun initEvents() {
addMouseListener(dragMouseAdaptor)
addMouseMotionListener(dragMouseAdaptor)
}
override fun processMouseEvent(e: MouseEvent) {
// Shift + Click ===> close tab
if (e.id == MouseEvent.MOUSE_CLICKED && SwingUtilities.isLeftMouseButton(e) && isShiftPressedOnly(e.modifiersEx)) {
val index = indexAtLocation(e.x, e.y)
if (index >= 0) {
tabCloseCallback?.accept(this, index)
return
}
} else if (e.id == MouseEvent.MOUSE_PRESSED && isShiftPressedOnly(e.modifiersEx)) {
val index = indexAtLocation(e.x, e.y)
if (index >= 0) {
return
}
}
super.processMouseEvent(e)
}
private fun isShiftPressedOnly(modifiersEx: Int): Boolean {
return (modifiersEx and InputEvent.ALT_DOWN_MASK) == 0
&& (modifiersEx and InputEvent.ALT_GRAPH_DOWN_MASK) == 0
&& (modifiersEx and InputEvent.CTRL_DOWN_MASK) == 0
&& (modifiersEx and InputEvent.SHIFT_DOWN_MASK) != 0
}
override fun setSelectedIndex(index: Int) { override fun setSelectedIndex(index: Int) {
val oldIndex = selectedIndex val oldIndex = selectedIndex
super.setSelectedIndex(index) super.setSelectedIndex(index)
firePropertyChange("selectedIndex", oldIndex,index) firePropertyChange("selectedIndex", oldIndex, index)
} }
private inner class DragMouseAdaptor : MouseAdapter(), KeyEventDispatcher {
private var mousePressedPoint = Point()
private var tabIndex = 0 - 1
private var cancelled = false
private var window: Window? = null
private var terminalTab: TerminalTab? = null
private var isDragging = false
private var lastVisitTabIndex = -1
override fun mousePressed(e: MouseEvent) {
val index = indexAtLocation(e.x, e.y)
if (index < 0 || !isTabClosable(index)) {
return
}
tabIndex = index
mousePressedPoint = e.point
}
override fun mouseDragged(e: MouseEvent) {
// 如果正在拖拽中,那么修改 Window 的位置
if (isDragging) {
window?.location = e.locationOnScreen
lastVisitTabIndex = indexAtLocation(e.x, e.y)
} else if (tabIndex >= 0) { // 这里之所以判断是确保在 mousePressed 时已经确定了 Tab
// 有的时候会太灵敏,这里容错一下
val diff = 5
if (abs(mousePressedPoint.y - e.y) >= diff || abs(mousePressedPoint.x - e.x) >= diff) {
startDrag(e)
}
}
}
private fun startDrag(e: MouseEvent) {
if (isDragging) return
val terminalTabbedManager = terminalTabbedManager ?: return
val window = JDialog(owner).also { this.window = it }
window.isUndecorated = true
val image = createTabImage(tabIndex)
window.size = Dimension(image.width, image.height)
window.add(JLabel(ImageIcon(image)))
window.location = e.locationOnScreen
window.addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.removeKeyEventDispatcher(this@DragMouseAdaptor)
}
override fun windowOpened(e: WindowEvent) {
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.addKeyEventDispatcher(this@DragMouseAdaptor)
}
})
// 暂时关闭 Tab
terminalTabbedManager.closeTerminalTab(terminalTabbedManager.getTerminalTabs()[tabIndex].also {
terminalTab = it
}, false)
window.isVisible = true
isDragging = true
cancelled = false
}
private fun stopDrag() {
if (!isDragging) {
return
}
val tab = this.terminalTab
val terminalTabbedManager = terminalTabbedManager
if (tab != null && terminalTabbedManager != null) {
// 如果是手动取消
if (cancelled) {
terminalTabbedManager.addTerminalTab(tabIndex, tab)
} else if (lastVisitTabIndex > 0) {
terminalTabbedManager.addTerminalTab(lastVisitTabIndex, tab)
} else if (lastVisitTabIndex == 0) {
terminalTabbedManager.addTerminalTab(1, tab)
} else {
terminalTabbedManager.addTerminalTab(tab)
}
}
// reset
window?.dispose()
isDragging = false
tabIndex = -1
cancelled = false
lastVisitTabIndex = -1
}
override fun mouseReleased(e: MouseEvent) {
stopDrag()
}
private fun createTabImage(index: Int): BufferedImage {
val tabBounds = getBoundsAt(index)
val image = BufferedImage(tabBounds.width, tabBounds.height, BufferedImage.TYPE_INT_ARGB)
val g2 = image.createGraphics()
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
g2.translate(-tabBounds.x, -tabBounds.y)
paint(g2)
g2.dispose()
return image
}
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
if (e.keyCode == KeyEvent.VK_ESCAPE) {
cancelled = true
stopDrag()
return true
}
return false
}
}
} }

View File

@@ -1,5 +1,7 @@
package app.termora package app.termora
import java.awt.event.ActionEvent import app.termora.actions.AnActionEvent
import java.util.*
class OpenHostActionEvent(source: Any, val host: Host) : ActionEvent(source, ACTION_PERFORMED, String()) class OpenHostActionEvent(source: Any, val host: Host, event: EventObject) :
AnActionEvent(source, String(), event)

View File

@@ -6,6 +6,7 @@ import com.formdev.flatlaf.util.SystemInfo
import com.jetbrains.JBR import com.jetbrains.JBR
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.JXLabel import org.jdesktop.swingx.JXLabel
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Component import java.awt.Component
@@ -122,7 +123,7 @@ object OptionPane {
if (Desktop.isDesktopSupported() && Desktop.getDesktop() if (Desktop.isDesktopSupported() && Desktop.getDesktop()
.isSupported(Desktop.Action.BROWSE_FILE_DIR) .isSupported(Desktop.Action.BROWSE_FILE_DIR)
) { ) {
if (JOptionPane.YES_OPTION == showConfirmDialog( if (yMessage.isEmpty() || JOptionPane.YES_OPTION == showConfirmDialog(
parentComponent, parentComponent,
yMessage, yMessage,
optionType = JOptionPane.YES_NO_OPTION optionType = JOptionPane.YES_NO_OPTION

View File

@@ -1,22 +1,26 @@
package app.termora package app.termora
import app.termora.db.Database
import app.termora.macro.MacroPtyConnector import app.termora.macro.MacroPtyConnector
import app.termora.terminal.PtyConnector import app.termora.terminal.PtyConnector
import app.termora.terminal.PtyConnectorDelegate import app.termora.terminal.PtyConnectorDelegate
import app.termora.terminal.PtyProcessConnector import app.termora.terminal.PtyProcessConnector
import com.pty4j.PtyProcessBuilder import com.pty4j.PtyProcessBuilder
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.SystemUtils import org.apache.commons.lang3.SystemUtils
import org.slf4j.LoggerFactory
import java.nio.charset.Charset import java.nio.charset.Charset
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.* import java.util.*
class PtyConnectorFactory { class PtyConnectorFactory : Disposable {
private val ptyConnectors = Collections.synchronizedList(mutableListOf<PtyConnector>()) private val ptyConnectors = Collections.synchronizedList(mutableListOf<PtyConnector>())
private val database get() = Database.instance private val database get() = Database.getDatabase()
companion object { companion object {
val instance by lazy { PtyConnectorFactory() } private val log = LoggerFactory.getLogger(PtyConnectorFactory::class.java)
fun getInstance(scope: Scope): PtyConnectorFactory {
return scope.getOrCreate(PtyConnectorFactory::class) { PtyConnectorFactory() }
}
} }
fun createPtyConnector( fun createPtyConnector(
@@ -29,8 +33,28 @@ class PtyConnectorFactory {
envs["TERM"] = "xterm-256color" envs["TERM"] = "xterm-256color"
envs.putAll(env) envs.putAll(env)
if (SystemUtils.IS_OS_UNIX) {
if (!envs.containsKey("LANG")) {
val locale = Locale.getDefault()
if (StringUtils.isNoneBlank(locale.language, locale.country)) {
envs["LANG"] = "${locale.language}_${locale.country}.${Charset.defaultCharset().name()}"
} else {
envs["LANG"] = "en_US.UTF-8"
}
}
}
val command = database.terminal.localShell val command = database.terminal.localShell
val ptyProcess = PtyProcessBuilder(arrayOf(command)) val commands = mutableListOf(command)
if (SystemUtils.IS_OS_UNIX) {
commands.add("-l")
}
if (log.isDebugEnabled) {
log.debug("command: {} , envs: {}", commands.joinToString(" "), envs)
}
val ptyProcess = PtyProcessBuilder(commands.toTypedArray())
.setEnvironment(envs) .setEnvironment(envs)
.setInitialRows(rows) .setInitialRows(rows)
.setInitialColumns(cols) .setInitialColumns(cols)

View File

@@ -3,6 +3,7 @@ package app.termora
import app.termora.terminal.PtyConnector import app.termora.terminal.PtyConnector
import app.termora.terminal.Terminal import app.termora.terminal.Terminal
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.slf4j.LoggerFactory
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@@ -11,9 +12,14 @@ class PtyConnectorReader(
private val terminal: Terminal, private val terminal: Terminal,
) { ) {
companion object {
private val log = LoggerFactory.getLogger(PtyConnectorReader::class.java)
}
suspend fun start() { suspend fun start() {
var i: Int var i: Int
val buffer = CharArray(1024 * 8) val buffer = CharArray(1024 * 8)
while ((ptyConnector.read(buffer).also { i = it }) != -1) { while ((ptyConnector.read(buffer).also { i = it }) != -1) {
if (i == 0) { if (i == 0) {
delay(10.milliseconds) delay(10.milliseconds)
@@ -22,6 +28,10 @@ class PtyConnectorReader(
val text = String(buffer, 0, i) val text = String(buffer, 0, i)
SwingUtilities.invokeLater { terminal.write(text) } SwingUtilities.invokeLater { terminal.write(text) }
} }
if (log.isDebugEnabled) {
log.debug("PtyConnectorReader stopped")
}
} }
} }

View File

@@ -1,9 +1,6 @@
package app.termora package app.termora
import app.termora.terminal.ControlCharacters import app.termora.terminal.*
import app.termora.terminal.PtyConnector
import app.termora.terminal.PtyConnectorDelegate
import app.termora.terminal.TerminalKeyEvent
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import org.apache.commons.lang3.exception.ExceptionUtils import org.apache.commons.lang3.exception.ExceptionUtils
@@ -12,7 +9,12 @@ import java.awt.event.KeyEvent
import javax.swing.JComponent import javax.swing.JComponent
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
abstract class PtyHostTerminalTab(host: Host) : HostTerminalTab(host) { abstract class PtyHostTerminalTab(
windowScope: WindowScope,
host: Host,
terminal: Terminal = TerminalFactory.getInstance(windowScope).createTerminal()
) : HostTerminalTab(windowScope, host, terminal) {
companion object { companion object {
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java) private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
} }
@@ -21,8 +23,13 @@ abstract class PtyHostTerminalTab(host: Host) : HostTerminalTab(host) {
private var readerJob: Job? = null private var readerJob: Job? = null
private val ptyConnectorDelegate = PtyConnectorDelegate() private val ptyConnectorDelegate = PtyConnectorDelegate()
protected val terminalPanel = TerminalPanelFactory.instance.createTerminalPanel(terminal, ptyConnectorDelegate) protected val terminalPanel =
protected val ptyConnectorFactory get() = PtyConnectorFactory.instance TerminalPanelFactory.getInstance(windowScope).createTerminalPanel(terminal, ptyConnectorDelegate)
protected val ptyConnectorFactory get() = PtyConnectorFactory.getInstance(windowScope)
init {
terminal.getTerminalModel().setData(DataKey.PtyConnector, ptyConnectorDelegate)
}
override fun start() { override fun start() {
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
@@ -60,6 +67,10 @@ abstract class PtyHostTerminalTab(host: Host) : HostTerminalTab(host) {
if (log.isErrorEnabled) { if (log.isErrorEnabled) {
log.error(e.message, e) log.error(e.message, e)
} }
// 失败关闭
stop()
withContext(Dispatchers.Swing) { withContext(Dispatchers.Swing) {
terminal.write("\r\n${ControlCharacters.ESC}[31m") terminal.write("\r\n${ControlCharacters.ESC}[31m")
terminal.write(ExceptionUtils.getRootCauseMessage(e)) terminal.write(ExceptionUtils.getRootCauseMessage(e))

View File

@@ -1,5 +1,6 @@
package app.termora package app.termora
import app.termora.transport.TransportDataProviders
import app.termora.transport.TransportPanel import app.termora.transport.TransportPanel
import java.beans.PropertyChangeListener import java.beans.PropertyChangeListener
import javax.swing.Icon import javax.swing.Icon
@@ -20,7 +21,7 @@ class SFTPTerminalTab : Disposable, TerminalTab {
} }
override fun getIcon(): Icon { override fun getIcon(): Icon {
return Icons.fileTransfer return Icons.folder
} }
override fun addPropertyChangeListener(listener: PropertyChangeListener) { override fun addPropertyChangeListener(listener: PropertyChangeListener) {
@@ -34,11 +35,14 @@ class SFTPTerminalTab : Disposable, TerminalTab {
return transportPanel return transportPanel
} }
override fun canClone(): Boolean {
return false
}
override fun canClose(): Boolean { override fun canClose(): Boolean {
assertEventDispatchThread() assertEventDispatchThread()
val transportManager = transportPanel.getData(TransportDataProviders.TransportManager) ?: return true
if (transportPanel.transportManager.getTransports().isEmpty()) { if (transportManager.getTransports().isEmpty()) {
return true return true
} }

View File

@@ -1,6 +1,10 @@
package app.termora package app.termora
import app.termora.actions.TabReconnectAction
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
import app.termora.keyboardinteractive.TerminalUserInteraction
import app.termora.keymap.KeyShortcut
import app.termora.keymap.KeymapManager
import app.termora.terminal.ControlCharacters import app.termora.terminal.ControlCharacters
import app.termora.terminal.DataKey import app.termora.terminal.DataKey
import app.termora.terminal.PtyConnector import app.termora.terminal.PtyConnector
@@ -24,9 +28,10 @@ import org.apache.sshd.common.util.net.SshdSocketAddress
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.SwingUtilities
class SSHTerminalTab(host: Host) : PtyHostTerminalTab(host) { class SSHTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
companion object { companion object {
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java) private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
} }
@@ -76,6 +81,9 @@ class SSHTerminalTab(host: Host) : PtyHostTerminalTab(host) {
} }
val client = SshClients.openClient(host).also { sshClient = it } val client = SshClients.openClient(host).also { sshClient = it }
// keyboard interactive
client.userInteraction = TerminalUserInteraction(SwingUtilities.getWindowAncestor(terminalPanel))
val sessionListener = MySessionListener() val sessionListener = MySessionListener()
val channelListener = MyChannelListener() val channelListener = MyChannelListener()
@@ -104,10 +112,18 @@ class SSHTerminalTab(host: Host) : PtyHostTerminalTab(host) {
channel.addChannelListener(object : ChannelListener { channel.addChannelListener(object : ChannelListener {
private val reconnectShortcut
get() = KeymapManager.getInstance().getActiveKeymap()
.getShortcut(TabReconnectAction.RECONNECT_TAB).firstOrNull()
override fun channelClosed(channel: Channel, reason: Throwable?) { override fun channelClosed(channel: Channel, reason: Throwable?) {
coroutineScope.launch(Dispatchers.Swing) { coroutineScope.launch(Dispatchers.Swing) {
terminal.write("\r\n${ControlCharacters.ESC}[31m") terminal.write("\r\n\r\n${ControlCharacters.ESC}[31m")
terminal.write("Channel has been disconnected.\r\n") terminal.write("Channel has been disconnected.")
if (reconnectShortcut is KeyShortcut) {
terminal.write(" Type $reconnectShortcut to reconnect.")
}
terminal.write("\r\n")
terminal.write("${ControlCharacters.ESC}[0m") terminal.write("${ControlCharacters.ESC}[0m")
terminalModel.setData(DataKey.ShowCursor, false) terminalModel.setData(DataKey.ShowCursor, false)
} }

View File

@@ -0,0 +1,173 @@
package app.termora
import org.slf4j.LoggerFactory
import java.awt.Component
import java.awt.Window
import java.util.concurrent.ConcurrentHashMap
import javax.swing.JPopupMenu
import javax.swing.SwingUtilities
import kotlin.reflect.KClass
@Suppress("UNCHECKED_CAST")
open class Scope(
private val beans: MutableMap<KClass<*>, Any> = ConcurrentHashMap(),
private val properties: MutableMap<String, Any> = ConcurrentHashMap()
) : Disposable {
fun <T : Any> get(clazz: KClass<T>): T {
return beans[clazz] as T
}
fun <T : Any> getOrCreate(clazz: KClass<T>, create: () -> T): T {
if (beans.containsKey(clazz)) {
return get(clazz)
}
synchronized(clazz) {
if (beans.containsKey(clazz)) {
return get(clazz)
}
val instance = create.invoke()
beans[clazz] = instance
if (instance is Disposable) {
Disposer.register(this, instance)
}
return instance
}
}
fun putBoolean(name: String, value: Boolean) {
properties[name] = value
}
fun getBoolean(name: String, defaultValue: Boolean): Boolean {
return properties[name]?.toString()?.toBoolean() ?: defaultValue
}
fun putAny(name: String, value: Any) {
properties[name] = value
}
fun getAny(name: String, defaultValue: Any): Any {
return properties[name]?.toString() ?: defaultValue
}
fun getAnyOrNull(name: String): Any? {
return properties[name]
}
override fun dispose() {
beans.clear()
}
}
class ApplicationScope private constructor() : Scope() {
private val scopes = mutableMapOf<Any, WindowScope>()
companion object {
private val log = LoggerFactory.getLogger(ApplicationScope::class.java)
private val instance by lazy { ApplicationScope() }
fun forApplicationScope(): ApplicationScope {
return instance
}
fun forWindowScope(frame: TermoraFrame): WindowScope {
return forApplicationScope().forWindowScope(frame)
}
fun forWindowScope(container: Component): WindowScope {
val frame = getFrameForComponent(container)
?: throw IllegalStateException("Unexpected owner in $container")
return forWindowScope(frame)
}
fun windowScopes(): List<WindowScope> {
return forApplicationScope().windowScopes()
}
private fun getFrameForComponent(component: Component): TermoraFrame? {
if (component is TermoraFrame) {
return component
}
var owner = SwingUtilities.getWindowAncestor(component) as Component?
if (owner is TermoraFrame) {
return owner
}
if (owner == null) {
owner = component
}
while (owner != null) {
if (owner is JPopupMenu) {
owner = owner.invoker
if (owner is TermoraFrame) {
return owner
}
continue
}
owner = owner.parent
if (owner is TermoraFrame) {
return owner
}
}
return null
}
}
private fun forWindowScope(frame: TermoraFrame): WindowScope {
val windowScope = scopes.getOrPut(frame) { WindowScope(frame) }
Disposer.register(windowScope, object : Disposable {
override fun dispose() {
scopes.remove(frame)
}
})
return windowScope
}
fun windowScopes(): List<WindowScope> {
return scopes.values.toList()
}
override fun dispose() {
if (log.isInfoEnabled) {
log.info("ApplicationScope disposed")
}
super.dispose()
}
}
class WindowScope(
val window: Window,
) : Scope() {
companion object {
private val log = LoggerFactory.getLogger(WindowScope::class.java)
}
override fun dispose() {
if (log.isInfoEnabled) {
log.info("WindowScope disposed")
}
super.dispose()
}
}

View File

@@ -48,10 +48,10 @@ class SearchableHostTreeModel(
val children = model.getChildren(parent) val children = model.getChildren(parent)
if (children.isEmpty()) return emptyList() if (children.isEmpty()) return emptyList()
return children.filter { e -> return children.filter { e ->
filter.invoke(e) && e.name.contains(text, true) || TreeUtils.children(model, e, true) filter.invoke(e)
.filterIsInstance<Host>().any { && e.name.contains(text, true)
it.name.contains(text, true) || e.host.contains(text, true)
} || TreeUtils.children(model, e, true).filterIsInstance<Host>().any { it.name.contains(text, true) || it.host.contains(text, true) }
} }
} }

View File

@@ -1,6 +1,5 @@
package app.termora package app.termora
import app.termora.db.Database
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Dimension import java.awt.Dimension
import java.awt.Window import java.awt.Window
@@ -13,7 +12,7 @@ import javax.swing.UIManager
class SettingsDialog(owner: Window) : DialogWrapper(owner) { class SettingsDialog(owner: Window) : DialogWrapper(owner) {
private val optionsPane = SettingsOptionsPane() private val optionsPane = SettingsOptionsPane()
private val properties get() = Database.instance.properties private val properties get() = Database.getDatabase().properties
init { init {
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height")) size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))

View File

@@ -2,9 +2,16 @@ package app.termora
import app.termora.AES.encodeBase64String import app.termora.AES.encodeBase64String
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.db.Database import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.highlight.KeywordHighlight
import app.termora.highlight.KeywordHighlightManager import app.termora.highlight.KeywordHighlightManager
import app.termora.keymap.Keymap
import app.termora.keymap.KeymapManager
import app.termora.keymap.KeymapPanel
import app.termora.keymgr.KeyManager import app.termora.keymgr.KeyManager
import app.termora.keymgr.OhKeyPair
import app.termora.macro.Macro
import app.termora.macro.MacroManager import app.termora.macro.MacroManager
import app.termora.native.FileChooser import app.termora.native.FileChooser
import app.termora.sync.SyncConfig import app.termora.sync.SyncConfig
@@ -15,11 +22,9 @@ import app.termora.terminal.CursorStyle
import app.termora.terminal.DataKey import app.termora.terminal.DataKey
import app.termora.terminal.panel.TerminalPanel import app.termora.terminal.panel.TerminalPanel
import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.Mnemonics
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.FlatSVGIcon import com.formdev.flatlaf.extras.FlatSVGIcon
import com.formdev.flatlaf.extras.components.FlatButton import com.formdev.flatlaf.extras.components.*
import com.formdev.flatlaf.extras.components.FlatComboBox import com.formdev.flatlaf.util.FontUtils
import com.formdev.flatlaf.extras.components.FlatLabel
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import com.jgoodies.forms.builder.FormBuilder import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout import com.jgoodies.forms.layout.FormLayout
@@ -28,19 +33,17 @@ import com.sun.jna.LastErrorException
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.*
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.put
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.SystemUtils import org.apache.commons.lang3.SystemUtils
import org.apache.commons.lang3.exception.ExceptionUtils
import org.apache.commons.lang3.time.DateFormatUtils import org.apache.commons.lang3.time.DateFormatUtils
import org.jdesktop.swingx.JXEditorPane import org.jdesktop.swingx.JXEditorPane
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Component import java.awt.Component
import java.awt.datatransfer.StringSelection import java.awt.datatransfer.StringSelection
import java.awt.event.ActionEvent
import java.awt.event.ItemEvent import java.awt.event.ItemEvent
import java.io.File import java.io.File
import java.net.URI import java.net.URI
@@ -48,12 +51,19 @@ import java.nio.charset.StandardCharsets
import java.util.* import java.util.*
import javax.swing.* import javax.swing.*
import javax.swing.event.DocumentEvent import javax.swing.event.DocumentEvent
import javax.swing.event.PopupMenuEvent
import javax.swing.event.PopupMenuListener
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
class SettingsOptionsPane : OptionsPane() { class SettingsOptionsPane : OptionsPane() {
private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane) private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane)
private val database get() = Database.instance private val database get() = Database.getDatabase()
private val hostManager get() = HostManager.getInstance()
private val keymapManager get() = KeymapManager.getInstance()
private val macroManager get() = MacroManager.getInstance()
private val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
private val keyManager get() = KeyManager.getInstance()
companion object { companion object {
private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java) private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java)
@@ -96,6 +106,7 @@ class SettingsOptionsPane : OptionsPane() {
init { init {
addOption(AppearanceOption()) addOption(AppearanceOption())
addOption(TerminalOption()) addOption(TerminalOption())
addOption(KeyShortcutsOption())
addOption(CloudSyncOption()) addOption(CloudSyncOption())
addOption(DoormanOption()) addOption(DoormanOption())
addOption(AboutOption()) addOption(AboutOption())
@@ -103,10 +114,11 @@ class SettingsOptionsPane : OptionsPane() {
} }
private inner class AppearanceOption : JPanel(BorderLayout()), Option { private inner class AppearanceOption : JPanel(BorderLayout()), Option {
val themeManager = ThemeManager.instance val themeManager = ThemeManager.getInstance()
val themeComboBox = FlatComboBox<String>() val themeComboBox = FlatComboBox<String>()
val languageComboBox = FlatComboBox<String>() val languageComboBox = FlatComboBox<String>()
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system")) val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
val preferredThemeBtn = JButton(Icons.settings)
private val appearance get() = database.appearance private val appearance get() = database.appearance
init { init {
@@ -117,6 +129,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun initView() { private fun initView() {
followSystemCheckBox.isSelected = appearance.followSystem followSystemCheckBox.isSelected = appearance.followSystem
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
themeComboBox.isEnabled = !followSystemCheckBox.isSelected themeComboBox.isEnabled = !followSystemCheckBox.isSelected
themeManager.themes.keys.forEach { themeComboBox.addItem(it) } themeManager.themes.keys.forEach { themeComboBox.addItem(it) }
@@ -156,19 +169,17 @@ class SettingsOptionsPane : OptionsPane() {
followSystemCheckBox.addActionListener { followSystemCheckBox.addActionListener {
appearance.followSystem = followSystemCheckBox.isSelected appearance.followSystem = followSystemCheckBox.isSelected
themeComboBox.isEnabled = !followSystemCheckBox.isSelected themeComboBox.isEnabled = !followSystemCheckBox.isSelected
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
appearance.theme = themeComboBox.selectedItem as String
if (followSystemCheckBox.isSelected) { if (followSystemCheckBox.isSelected) {
SwingUtilities.invokeLater { SwingUtilities.invokeLater {
if (OsThemeDetector.getDetector().isDark) { if (OsThemeDetector.getDetector().isDark) {
if (!FlatLaf.isLafDark()) { themeManager.change(appearance.darkTheme)
themeManager.change("Dark") themeComboBox.selectedItem = appearance.darkTheme
themeComboBox.selectedItem = "Dark"
}
} else { } else {
if (FlatLaf.isLafDark()) { themeManager.change(appearance.lightTheme)
themeManager.change("Light") themeComboBox.selectedItem = appearance.lightTheme
themeComboBox.selectedItem = "Light"
}
} }
} }
} }
@@ -187,6 +198,8 @@ class SettingsOptionsPane : OptionsPane() {
} }
} }
} }
preferredThemeBtn.addActionListener { showPreferredThemeContextmenu() }
} }
override fun getIcon(isSelected: Boolean): Icon { override fun getIcon(isSelected: Boolean): Icon {
@@ -201,23 +214,78 @@ class SettingsOptionsPane : OptionsPane() {
return this return this
} }
private fun showPreferredThemeContextmenu() {
val popupMenu = FlatPopupMenu()
val dark = JMenu("For Dark OS")
val light = JMenu("For Light OS")
val darkTheme = appearance.darkTheme
val lightTheme = appearance.lightTheme
for (e in themeManager.themes) {
val clazz = Class.forName(e.value)
val item = JCheckBoxMenuItem(e.key)
item.isSelected = e.key == lightTheme || e.key == darkTheme
if (clazz.interfaces.contains(DarkLafTag::class.java)) {
dark.add(item).addActionListener {
if (e.key != darkTheme) {
appearance.darkTheme = e.key
if (OsThemeDetector.getDetector().isDark) {
themeComboBox.selectedItem = e.key
}
}
}
} else if (clazz.interfaces.contains(LightLafTag::class.java)) {
light.add(item).addActionListener {
if (e.key != lightTheme) {
appearance.lightTheme = e.key
if (!OsThemeDetector.getDetector().isDark) {
themeComboBox.selectedItem = e.key
}
}
}
}
}
popupMenu.add(dark)
popupMenu.addSeparator()
popupMenu.add(light)
popupMenu.addPopupMenuListener(object : PopupMenuListener {
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
}
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {
}
override fun popupMenuCanceled(e: PopupMenuEvent) {
}
})
popupMenu.show(preferredThemeBtn, 0, preferredThemeBtn.height + 2)
}
private fun getFormPanel(): JPanel { private fun getFormPanel(): JPanel {
val layout = FormLayout( val layout = FormLayout(
"left:pref, $formMargin, default:grow, $formMargin, default, default:grow", "left:pref, $formMargin, default:grow, $formMargin, default, default:grow",
"pref, $formMargin, pref, $formMargin" "pref, $formMargin, pref, $formMargin"
) )
val box = FlatToolBar()
box.add(followSystemCheckBox)
box.add(Box.createHorizontalStrut(2))
box.add(preferredThemeBtn)
var rows = 1 var rows = 1
val step = 2 val step = 2
return FormBuilder.create().layout(layout) return FormBuilder.create().layout(layout)
.add("${I18n.getString("termora.settings.appearance.theme")}:").xy(1, rows) .add("${I18n.getString("termora.settings.appearance.theme")}:").xy(1, rows)
.add(themeComboBox).xy(3, rows) .add(themeComboBox).xy(3, rows)
.add(followSystemCheckBox).xy(5, rows).apply { rows += step } .add(box).xy(5, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.appearance.language")}:").xy(1, rows) .add("${I18n.getString("termora.settings.appearance.language")}:").xy(1, rows)
.add(languageComboBox).xy(3, rows) .add(languageComboBox).xy(3, rows)
.add(Hyperlink(object : AnAction(I18n.getString("termora.settings.appearance.i-want-to-translate")) { .add(Hyperlink(object : AnAction(I18n.getString("termora.settings.appearance.i-want-to-translate")) {
override fun actionPerformed(evt: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
Application.browse(URI.create("https://github.com/TermoraDev/termora/tree/main/src/main/resources/i18n")) Application.browse(URI.create("https://github.com/TermoraDev/termora/tree/main/src/main/resources/i18n"))
} }
})).xy(5, rows).apply { rows += step } })).xy(5, rows).apply { rows += step }
@@ -234,7 +302,7 @@ class SettingsOptionsPane : OptionsPane() {
private val shellComboBox = FlatComboBox<String>() private val shellComboBox = FlatComboBox<String>()
private val maxRowsTextField = IntSpinner(0, 0) private val maxRowsTextField = IntSpinner(0, 0)
private val fontSizeTextField = IntSpinner(0, 9, 99) private val fontSizeTextField = IntSpinner(0, 9, 99)
private val terminalSetting get() = Database.instance.terminal private val terminalSetting get() = Database.getDatabase().terminal
private val selectCopyComboBox = YesOrNoComboBox() private val selectCopyComboBox = YesOrNoComboBox()
init { init {
@@ -270,7 +338,7 @@ class SettingsOptionsPane : OptionsPane() {
if (it.stateChange == ItemEvent.SELECTED) { if (it.stateChange == ItemEvent.SELECTED) {
val style = cursorStyleComboBox.selectedItem as CursorStyle val style = cursorStyleComboBox.selectedItem as CursorStyle
terminalSetting.cursor = style terminalSetting.cursor = style
TerminalFactory.instance.getTerminals().forEach { e -> TerminalFactory.getInstance(ApplicationScope.forWindowScope(owner)).getTerminals().forEach { e ->
e.getTerminalModel().setData(DataKey.CursorStyle, style) e.getTerminalModel().setData(DataKey.CursorStyle, style)
} }
} }
@@ -280,7 +348,7 @@ class SettingsOptionsPane : OptionsPane() {
debugComboBox.addItemListener { e -> debugComboBox.addItemListener { e ->
if (e.stateChange == ItemEvent.SELECTED) { if (e.stateChange == ItemEvent.SELECTED) {
terminalSetting.debug = debugComboBox.selectedItem as Boolean terminalSetting.debug = debugComboBox.selectedItem as Boolean
TerminalFactory.instance.getTerminals().forEach { TerminalFactory.getInstance(ApplicationScope.forWindowScope(owner)).getTerminals().forEach {
it.getTerminalModel().setData(TerminalPanel.Debug, terminalSetting.debug) it.getTerminalModel().setData(TerminalPanel.Debug, terminalSetting.debug)
} }
} }
@@ -296,7 +364,10 @@ class SettingsOptionsPane : OptionsPane() {
} }
private fun fireFontChanged() { private fun fireFontChanged() {
TerminalPanelFactory.instance.fireResize() ApplicationScope.windowScopes().forEach {
TerminalPanelFactory.getInstance(it)
.fireResize()
}
} }
private fun initView() { private fun initView() {
@@ -318,6 +389,28 @@ class SettingsOptionsPane : OptionsPane() {
} }
} }
fontComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component {
if (value is String) {
return super.getListCellRendererComponent(
list,
"<html><font face='$value'>$value</font></html>",
index,
isSelected,
cellHasFocus
)
}
return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus)
}
}
fontComboBox.maximumSize = fontComboBox.preferredSize
cursorStyleComboBox.addItem(CursorStyle.Block) cursorStyleComboBox.addItem(CursorStyle.Block)
cursorStyleComboBox.addItem(CursorStyle.Bar) cursorStyleComboBox.addItem(CursorStyle.Bar)
cursorStyleComboBox.addItem(CursorStyle.Underline) cursorStyleComboBox.addItem(CursorStyle.Underline)
@@ -330,8 +423,33 @@ class SettingsOptionsPane : OptionsPane() {
shellComboBox.selectedItem = terminalSetting.localShell shellComboBox.selectedItem = terminalSetting.localShell
fontComboBox.addItem("JetBrains Mono") val fonts = linkedSetOf(
fontComboBox.addItem("Source Code Pro") "JetBrains Mono",
"Source Code Pro",
"Monospaced",
"Andale Mono",
"Ayuthaya",
"Courier New",
"Droid Sans Mono",
"Fira Code",
"PCMyungjo",
"Menlo",
"Monaco",
"Osaka",
"PT Mono",
"SimSong",
)
for (font in FontUtils.getAllFonts()) {
if (fonts.contains(font.family)) {
continue
}
fonts.remove(font.family)
}
for (font in fonts) {
fontComboBox.addItem(font)
}
fontComboBox.selectedItem = terminalSetting.font fontComboBox.selectedItem = terminalSetting.font
debugComboBox.selectedItem = terminalSetting.debug debugComboBox.selectedItem = terminalSetting.debug
@@ -390,6 +508,7 @@ class SettingsOptionsPane : OptionsPane() {
val domainTextField = OutlineTextField(255) val domainTextField = OutlineTextField(255)
val uploadConfigButton = JButton(I18n.getString("termora.settings.sync.push"), Icons.upload) val uploadConfigButton = JButton(I18n.getString("termora.settings.sync.push"), Icons.upload)
val exportConfigButton = JButton(I18n.getString("termora.settings.sync.export"), Icons.export) val exportConfigButton = JButton(I18n.getString("termora.settings.sync.export"), Icons.export)
val importConfigButton = JButton(I18n.getString("termora.settings.sync.import"), Icons.import)
val downloadConfigButton = JButton(I18n.getString("termora.settings.sync.pull"), Icons.download) val downloadConfigButton = JButton(I18n.getString("termora.settings.sync.pull"), Icons.download)
val lastSyncTimeLabel = JLabel() val lastSyncTimeLabel = JLabel()
val sync get() = database.sync val sync get() = database.sync
@@ -397,6 +516,7 @@ class SettingsOptionsPane : OptionsPane() {
val keysCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keys")) val keysCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keys"))
val keywordHighlightsCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keyword-highlights")) val keywordHighlightsCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keyword-highlights"))
val macrosCheckBox = JCheckBox(I18n.getString("termora.macro")) val macrosCheckBox = JCheckBox(I18n.getString("termora.macro"))
val keymapCheckBox = JCheckBox(I18n.getString("termora.settings.keymap"))
val visitGistBtn = JButton(Icons.externalLink) val visitGistBtn = JButton(Icons.externalLink)
val getTokenBtn = JButton(Icons.externalLink) val getTokenBtn = JButton(Icons.externalLink)
@@ -489,13 +609,18 @@ class SettingsOptionsPane : OptionsPane() {
getTokenBtn.addActionListener { getTokenBtn.addActionListener {
when (typeComboBox.selectedItem) { when (typeComboBox.selectedItem) {
SyncType.GitLab -> Application.browse(URI.create("https://gitlab.com/-/user_settings/personal_access_tokens")) SyncType.GitLab -> {
val uri = URI.create(domainTextField.text)
Application.browse(URI.create("${uri.scheme}://${uri.host}/-/user_settings/personal_access_tokens?name=Termora%20Sync%20Config&scopes=api"))
}
SyncType.GitHub -> Application.browse(URI.create("https://github.com/settings/tokens")) SyncType.GitHub -> Application.browse(URI.create("https://github.com/settings/tokens"))
SyncType.Gitee -> Application.browse(URI.create("https://gitee.com/profile/personal_access_tokens")) SyncType.Gitee -> Application.browse(URI.create("https://gitee.com/profile/personal_access_tokens"))
} }
} }
exportConfigButton.addActionListener { export() } exportConfigButton.addActionListener { export() }
importConfigButton.addActionListener { import() }
keysCheckBox.addActionListener { refreshButtons() } keysCheckBox.addActionListener { refreshButtons() }
hostsCheckBox.addActionListener { refreshButtons() } hostsCheckBox.addActionListener { refreshButtons() }
@@ -512,6 +637,7 @@ class SettingsOptionsPane : OptionsPane() {
|| keywordHighlightsCheckBox.isSelected || keywordHighlightsCheckBox.isSelected
uploadConfigButton.isEnabled = downloadConfigButton.isEnabled uploadConfigButton.isEnabled = downloadConfigButton.isEnabled
exportConfigButton.isEnabled = downloadConfigButton.isEnabled exportConfigButton.isEnabled = downloadConfigButton.isEnabled
importConfigButton.isEnabled = downloadConfigButton.isEnabled
} }
private fun export() { private fun export() {
@@ -527,6 +653,109 @@ class SettingsOptionsPane : OptionsPane() {
} }
} }
private fun import() {
val fileChooser = FileChooser()
fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY
fileChooser.osxAllowedFileTypes = listOf("json")
fileChooser.win32Filters.add(Pair("JSON files", listOf("json")))
fileChooser.showOpenDialog(owner).thenAccept { files ->
if (files.isNotEmpty()) {
SwingUtilities.invokeLater { importFromFile(files.first()) }
}
}
}
private fun importFromFile(file: File) {
if (!file.exists()) {
return
}
val ranges = getSyncConfig().ranges
if (ranges.isEmpty()) {
return
}
// 最大 100MB
if (file.length() >= 1024 * 1024 * 100) {
OptionPane.showMessageDialog(
owner, I18n.getString("termora.settings.sync.import.file-too-large"),
messageType = JOptionPane.ERROR_MESSAGE
)
return
}
val text = file.readText()
val jsonResult = ohMyJson.runCatching { decodeFromString<JsonObject>(text) }
if (jsonResult.isFailure) {
val e = jsonResult.exceptionOrNull() ?: return
OptionPane.showMessageDialog(
owner, ExceptionUtils.getRootCauseMessage(e),
messageType = JOptionPane.ERROR_MESSAGE
)
return
}
val json = jsonResult.getOrNull() ?: return
if (ranges.contains(SyncRange.Hosts)) {
val hosts = json["hosts"]
if (hosts is JsonArray) {
ohMyJson.runCatching { decodeFromJsonElement<List<Host>>(hosts.jsonArray) }.onSuccess {
for (host in it) {
hostManager.addHost(host)
}
}
}
}
if (ranges.contains(SyncRange.KeyPairs)) {
val keyPairs = json["keyPairs"]
if (keyPairs is JsonArray) {
ohMyJson.runCatching { decodeFromJsonElement<List<OhKeyPair>>(keyPairs.jsonArray) }.onSuccess {
for (keyPair in it) {
keyManager.addOhKeyPair(keyPair)
}
}
}
}
if (ranges.contains(SyncRange.KeywordHighlights)) {
val keywordHighlights = json["keywordHighlights"]
if (keywordHighlights is JsonArray) {
ohMyJson.runCatching { decodeFromJsonElement<List<KeywordHighlight>>(keywordHighlights.jsonArray) }
.onSuccess {
for (keyPair in it) {
keywordHighlightManager.addKeywordHighlight(keyPair)
}
}
}
}
if (ranges.contains(SyncRange.Macros)) {
val macros = json["macros"]
if (macros is JsonArray) {
ohMyJson.runCatching { decodeFromJsonElement<List<Macro>>(macros.jsonArray) }.onSuccess {
for (macro in it) {
macroManager.addMacro(macro)
}
}
}
}
if (ranges.contains(SyncRange.Keymap)) {
val keymaps = json["keymaps"]
if (keymaps is JsonArray) {
for (keymap in keymaps.jsonArray.mapNotNull { Keymap.fromJSON(it.jsonObject) }) {
keymapManager.addKeymap(keymap)
}
}
}
OptionPane.showMessageDialog(
owner, I18n.getString("termora.settings.sync.import.successful"),
messageType = JOptionPane.INFORMATION_MESSAGE
)
}
private fun exportText(file: File) { private fun exportText(file: File) {
val syncConfig = getSyncConfig() val syncConfig = getSyncConfig()
val text = ohMyJson.encodeToString(buildJsonObject { val text = ohMyJson.encodeToString(buildJsonObject {
@@ -537,21 +766,29 @@ class SettingsOptionsPane : OptionsPane() {
put("os", SystemUtils.OS_NAME) put("os", SystemUtils.OS_NAME)
put("exportDateHuman", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(Date(now))) put("exportDateHuman", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(Date(now)))
if (syncConfig.ranges.contains(SyncRange.Hosts)) { if (syncConfig.ranges.contains(SyncRange.Hosts)) {
put("hosts", ohMyJson.encodeToJsonElement(HostManager.instance.hosts())) put("hosts", ohMyJson.encodeToJsonElement(hostManager.hosts()))
} }
if (syncConfig.ranges.contains(SyncRange.KeyPairs)) { if (syncConfig.ranges.contains(SyncRange.KeyPairs)) {
put("keyPairs", ohMyJson.encodeToJsonElement(KeyManager.instance.getOhKeyPairs())) put("keyPairs", ohMyJson.encodeToJsonElement(keyManager.getOhKeyPairs()))
} }
if (syncConfig.ranges.contains(SyncRange.KeywordHighlights)) { if (syncConfig.ranges.contains(SyncRange.KeywordHighlights)) {
put( put(
"keywordHighlights", "keywordHighlights",
ohMyJson.encodeToJsonElement(KeywordHighlightManager.instance.getKeywordHighlights()) ohMyJson.encodeToJsonElement(keywordHighlightManager.getKeywordHighlights())
) )
} }
if (syncConfig.ranges.contains(SyncRange.Macros)) { if (syncConfig.ranges.contains(SyncRange.Macros)) {
put( put(
"macros", "macros",
ohMyJson.encodeToJsonElement(MacroManager.instance.getMacros()) ohMyJson.encodeToJsonElement(macroManager.getMacros())
)
}
if (syncConfig.ranges.contains(SyncRange.Keymap)) {
val keymaps = keymapManager.getKeymaps().filter { !it.isReadonly }
.map { it.toJSONObject() }
put(
"keymaps",
ohMyJson.encodeToJsonElement(keymaps)
) )
} }
put("settings", buildJsonObject { put("settings", buildJsonObject {
@@ -584,6 +821,9 @@ class SettingsOptionsPane : OptionsPane() {
if (macrosCheckBox.isSelected) { if (macrosCheckBox.isSelected) {
range.add(SyncRange.Macros) range.add(SyncRange.Macros)
} }
if (keymapCheckBox.isSelected) {
range.add(SyncRange.Keymap)
}
return SyncConfig( return SyncConfig(
type = typeComboBox.selectedItem as SyncType, type = typeComboBox.selectedItem as SyncType,
token = String(tokenTextField.password), token = String(tokenTextField.password),
@@ -593,6 +833,7 @@ class SettingsOptionsPane : OptionsPane() {
) )
} }
@Suppress("DuplicatedCode")
private suspend fun pushOrPull(push: Boolean) { private suspend fun pushOrPull(push: Boolean) {
if (typeComboBox.selectedItem == SyncType.GitLab) { if (typeComboBox.selectedItem == SyncType.GitLab) {
@@ -648,12 +889,15 @@ class SettingsOptionsPane : OptionsPane() {
withContext(Dispatchers.Swing) { withContext(Dispatchers.Swing) {
exportConfigButton.isEnabled = false exportConfigButton.isEnabled = false
importConfigButton.isEnabled = false
downloadConfigButton.isEnabled = false downloadConfigButton.isEnabled = false
uploadConfigButton.isEnabled = false uploadConfigButton.isEnabled = false
typeComboBox.isEnabled = false typeComboBox.isEnabled = false
gistTextField.isEnabled = false gistTextField.isEnabled = false
tokenTextField.isEnabled = false tokenTextField.isEnabled = false
keysCheckBox.isEnabled = false keysCheckBox.isEnabled = false
macrosCheckBox.isEnabled = false
keymapCheckBox.isEnabled = false
keywordHighlightsCheckBox.isEnabled = false keywordHighlightsCheckBox.isEnabled = false
hostsCheckBox.isEnabled = false hostsCheckBox.isEnabled = false
domainTextField.isEnabled = false domainTextField.isEnabled = false
@@ -669,7 +913,7 @@ class SettingsOptionsPane : OptionsPane() {
// sync // sync
val syncResult = kotlin.runCatching { val syncResult = kotlin.runCatching {
val syncer = SyncerProvider.instance.getSyncer(syncConfig.type) val syncer = SyncerProvider.getInstance().getSyncer(syncConfig.type)
if (push) { if (push) {
syncer.push(syncConfig) syncer.push(syncConfig)
} else { } else {
@@ -681,10 +925,13 @@ class SettingsOptionsPane : OptionsPane() {
withContext(Dispatchers.Swing) { withContext(Dispatchers.Swing) {
downloadConfigButton.isEnabled = true downloadConfigButton.isEnabled = true
exportConfigButton.isEnabled = true exportConfigButton.isEnabled = true
importConfigButton.isEnabled = true
uploadConfigButton.isEnabled = true uploadConfigButton.isEnabled = true
keysCheckBox.isEnabled = true keysCheckBox.isEnabled = true
hostsCheckBox.isEnabled = true hostsCheckBox.isEnabled = true
typeComboBox.isEnabled = true typeComboBox.isEnabled = true
macrosCheckBox.isEnabled = true
keymapCheckBox.isEnabled = true
gistTextField.isEnabled = true gistTextField.isEnabled = true
tokenTextField.isEnabled = true tokenTextField.isEnabled = true
domainTextField.isEnabled = true domainTextField.isEnabled = true
@@ -745,11 +992,13 @@ class SettingsOptionsPane : OptionsPane() {
keysCheckBox.isFocusable = false keysCheckBox.isFocusable = false
keywordHighlightsCheckBox.isFocusable = false keywordHighlightsCheckBox.isFocusable = false
macrosCheckBox.isFocusable = false macrosCheckBox.isFocusable = false
keymapCheckBox.isFocusable = false
hostsCheckBox.isSelected = sync.rangeHosts hostsCheckBox.isSelected = sync.rangeHosts
keysCheckBox.isSelected = sync.rangeKeyPairs keysCheckBox.isSelected = sync.rangeKeyPairs
keywordHighlightsCheckBox.isSelected = sync.rangeKeywordHighlights keywordHighlightsCheckBox.isSelected = sync.rangeKeywordHighlights
macrosCheckBox.isSelected = sync.rangeMacros macrosCheckBox.isSelected = sync.rangeMacros
keymapCheckBox.isSelected = sync.rangeKeymap
typeComboBox.selectedItem = sync.type typeComboBox.selectedItem = sync.type
gistTextField.text = sync.gist gistTextField.text = sync.gist
@@ -812,11 +1061,12 @@ class SettingsOptionsPane : OptionsPane() {
.add(keysCheckBox).xy(3, 1) .add(keysCheckBox).xy(3, 1)
.add(keywordHighlightsCheckBox).xy(5, 1) .add(keywordHighlightsCheckBox).xy(5, 1)
.add(macrosCheckBox).xy(1, 3) .add(macrosCheckBox).xy(1, 3)
.add(keymapCheckBox).xy(3, 3)
.build() .build()
var rows = 1 var rows = 1
val step = 2 val step = 2
val builder = FormBuilder.create().layout(layout).debug(false); val builder = FormBuilder.create().layout(layout).debug(false)
val box = Box.createHorizontalBox() val box = Box.createHorizontalBox()
box.add(typeComboBox) box.add(typeComboBox)
if (typeComboBox.selectedItem == SyncType.GitLab) { if (typeComboBox.selectedItem == SyncType.GitLab) {
@@ -835,10 +1085,11 @@ class SettingsOptionsPane : OptionsPane() {
// Sync buttons // Sync buttons
.add( .add(
FormBuilder.create() FormBuilder.create()
.layout(FormLayout("left:pref, $formMargin, left:pref, $formMargin, left:pref", "pref")) .layout(FormLayout("pref, 2dlu, pref, 2dlu, pref, 2dlu, pref", "pref"))
.add(uploadConfigButton).xy(1, 1) .add(uploadConfigButton).xy(1, 1)
.add(downloadConfigButton).xy(3, 1) .add(downloadConfigButton).xy(3, 1)
.add(exportConfigButton).xy(5, 1) .add(exportConfigButton).xy(5, 1)
.add(importConfigButton).xy(7, 1)
.build() .build()
).xy(3, rows, "center, fill").apply { rows += step } ).xy(3, rows, "center, fill").apply { rows += step }
.add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step } .add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step }
@@ -872,6 +1123,8 @@ class SettingsOptionsPane : OptionsPane() {
var rows = 1 var rows = 1
val step = 2 val step = 2
val branch = if (Application.isUnknownVersion()) "main" else Application.getVersion()
return FormBuilder.create().padding("$formMargin, $formMargin, $formMargin, $formMargin") return FormBuilder.create().padding("$formMargin, $formMargin, $formMargin, $formMargin")
.layout(layout).debug(true) .layout(layout).debug(true)
.add(I18n.getString("termora.settings.about.termora", Application.getVersion())) .add(I18n.getString("termora.settings.about.termora", Application.getVersion()))
@@ -881,7 +1134,7 @@ class SettingsOptionsPane : OptionsPane() {
.add("${I18n.getString("termora.settings.about.source")}:").xy(1, rows) .add("${I18n.getString("termora.settings.about.source")}:").xy(1, rows)
.add( .add(
createHyperlink( createHyperlink(
"https://github.com/TermoraDev/termora/tree/${Application.getVersion()}", "https://github.com/TermoraDev/termora/tree/${branch}",
"https://github.com/TermoraDev/termora", "https://github.com/TermoraDev/termora",
) )
).xy(3, rows).apply { rows += step } ).xy(3, rows).apply { rows += step }
@@ -890,7 +1143,7 @@ class SettingsOptionsPane : OptionsPane() {
.add("${I18n.getString("termora.settings.about.third-party")}:").xy(1, rows) .add("${I18n.getString("termora.settings.about.third-party")}:").xy(1, rows)
.add( .add(
createHyperlink( createHyperlink(
"https://github.com/TermoraDev/termora/blob/${Application.getVersion()}/THIRDPARTY", "https://github.com/TermoraDev/termora/blob/${branch}/THIRDPARTY",
"Open-source software" "Open-source software"
) )
).xy(3, rows).apply { rows += step } ).xy(3, rows).apply { rows += step }
@@ -901,10 +1154,10 @@ class SettingsOptionsPane : OptionsPane() {
private fun createHyperlink(url: String, text: String = url): Hyperlink { private fun createHyperlink(url: String, text: String = url): Hyperlink {
return Hyperlink(object : AnAction(text) { return Hyperlink(object : AnAction(text) {
override fun actionPerformed(evt: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
Application.browse(URI.create(url)) Application.browse(URI.create(url))
} }
}); })
} }
private fun initEvents() {} private fun initEvents() {}
@@ -930,9 +1183,7 @@ class SettingsOptionsPane : OptionsPane() {
private val twoPasswordTextField = OutlinePasswordField(255) private val twoPasswordTextField = OutlinePasswordField(255)
private val tip = FlatLabel() private val tip = FlatLabel()
private val safeBtn = FlatButton() private val safeBtn = FlatButton()
private val doorman get() = Doorman.instance private val doorman get() = Doorman.getInstance()
private val hostManager get() = HostManager.instance
private val keyManager get() = KeyManager.instance
init { init {
initView() initView()
@@ -1155,5 +1406,35 @@ class SettingsOptionsPane : OptionsPane() {
} }
private inner class KeyShortcutsOption : JPanel(BorderLayout()), Option {
private val keymapPanel = KeymapPanel()
init {
initView()
initEvents()
}
private fun initView() {
add(keymapPanel, BorderLayout.CENTER)
}
private fun initEvents() {}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.fitContent
}
override fun getTitle(): String {
return I18n.getString("termora.settings.keymap")
}
override fun getJComponent(): JComponent {
return this
}
}
} }

View File

@@ -5,10 +5,14 @@ import app.termora.terminal.TerminalSize
import org.apache.sshd.client.ClientBuilder import org.apache.sshd.client.ClientBuilder
import org.apache.sshd.client.SshClient import org.apache.sshd.client.SshClient
import org.apache.sshd.client.channel.ChannelShell import org.apache.sshd.client.channel.ChannelShell
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver
import org.apache.sshd.client.kex.DHGClient
import org.apache.sshd.client.session.ClientSession import org.apache.sshd.client.session.ClientSession
import org.apache.sshd.common.SshException import org.apache.sshd.common.SshException
import org.apache.sshd.common.channel.PtyChannelConfiguration import org.apache.sshd.common.channel.PtyChannelConfiguration
import org.apache.sshd.common.global.KeepAliveHandler import org.apache.sshd.common.global.KeepAliveHandler
import org.apache.sshd.common.kex.BuiltinDHFactories
import org.apache.sshd.common.util.net.SshdSocketAddress
import org.apache.sshd.core.CoreModuleProperties import org.apache.sshd.core.CoreModuleProperties
import org.apache.sshd.server.forward.AcceptAllForwardingFilter import org.apache.sshd.server.forward.AcceptAllForwardingFilter
import org.apache.sshd.server.forward.RejectAllForwardingFilter import org.apache.sshd.server.forward.RejectAllForwardingFilter
@@ -16,14 +20,15 @@ import org.eclipse.jgit.internal.transport.sshd.JGitSshClient
import org.eclipse.jgit.transport.CredentialsProvider import org.eclipse.jgit.transport.CredentialsProvider
import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
import org.eclipse.jgit.transport.sshd.ProxyData import org.eclipse.jgit.transport.sshd.ProxyData
import org.slf4j.LoggerFactory
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Proxy import java.net.Proxy
import java.time.Duration import java.time.Duration
import kotlin.math.max import kotlin.math.max
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver
object SshClients { object SshClients {
private val timeout = Duration.ofSeconds(30) private val timeout = Duration.ofSeconds(30)
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
/** /**
* 打开一个 Shell * 打开一个 Shell
@@ -57,6 +62,54 @@ object SshClients {
* 打开一个会话 * 打开一个会话
*/ */
fun openSession(host: Host, client: SshClient): ClientSession { fun openSession(host: Host, client: SshClient): ClientSession {
// 如果没有跳板机直接连接
if (host.options.jumpHosts.isEmpty()) {
return doOpenSession(host, client)
}
val jumpHosts = mutableListOf<Host>()
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
for (jumpHostId in host.options.jumpHosts) {
val e = hosts[jumpHostId]
if (e == null) {
if (log.isWarnEnabled) {
log.warn("Failed to find jump host: $jumpHostId")
}
continue
}
jumpHosts.add(e)
}
// 最后一跳是目标机器
jumpHosts.add(host)
val sessions = mutableListOf<ClientSession>()
for (i in 0 until jumpHosts.size) {
val currentHost = jumpHosts[i]
sessions.add(doOpenSession(currentHost, client))
// 如果有下一跳
if (i < jumpHosts.size - 1) {
val nextHost = jumpHosts[i + 1]
// 通过 currentHost 的 Session 将远程端口映射到本地
val address = sessions.last().startLocalPortForwarding(
SshdSocketAddress.LOCALHOST_ADDRESS,
SshdSocketAddress(nextHost.host, nextHost.port),
)
if (log.isInfoEnabled) {
log.info("jump host: ${currentHost.host}:${currentHost.port} , next host: ${nextHost.host}:${nextHost.port} , local address: ${address.hostName}:${address.port}")
}
// 映射完毕之后修改Host和端口
jumpHosts[i + 1] = nextHost.copy(host = address.hostName, port = address.port)
}
}
return sessions.last()
}
private fun doOpenSession(host: Host, client: SshClient): ClientSession {
val session = client.connect(host.username, host.host, host.port) val session = client.connect(host.username, host.host, host.port)
.verify(timeout).session .verify(timeout).session
if (host.authentication.type == AuthenticationType.Password) { if (host.authentication.type == AuthenticationType.Password) {
@@ -64,12 +117,16 @@ object SshClients {
} else if (host.authentication.type == AuthenticationType.PublicKey) { } else if (host.authentication.type == AuthenticationType.PublicKey) {
session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password) session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password)
} }
if (!session.auth().verify(timeout).await(timeout)) {
val verifyTimeout = Duration.ofSeconds(timeout.seconds * 5)
if (!session.auth().verify(verifyTimeout).await(verifyTimeout)) {
throw SshException("Authentication failed") throw SshException("Authentication failed")
} }
return session return session
} }
/** /**
* 打开一个客户端 * 打开一个客户端
*/ */
@@ -78,7 +135,19 @@ object SshClients {
builder.globalRequestHandlers(listOf(KeepAliveHandler.INSTANCE)) builder.globalRequestHandlers(listOf(KeepAliveHandler.INSTANCE))
.factory { JGitSshClient() } .factory { JGitSshClient() }
if (host.tunnelings.isEmpty()) { val keyExchangeFactories = ClientBuilder.setUpDefaultKeyExchanges(true).toMutableList()
// https://github.com/TermoraDev/termora/issues/123
keyExchangeFactories.addAll(
listOf(
DHGClient.newFactory(BuiltinDHFactories.dhg1),
DHGClient.newFactory(BuiltinDHFactories.dhg14),
DHGClient.newFactory(BuiltinDHFactories.dhgex),
)
)
builder.keyExchangeFactories(keyExchangeFactories)
if (host.tunnelings.isEmpty() && host.options.jumpHosts.isEmpty()) {
builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE) builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE)
} else { } else {
builder.forwardingFilter(AcceptAllForwardingFilter.INSTANCE) builder.forwardingFilter(AcceptAllForwardingFilter.INSTANCE)
@@ -89,6 +158,8 @@ object SshClients {
val sshClient = builder.build() as JGitSshClient val sshClient = builder.build() as JGitSshClient
val heartbeatInterval = max(host.options.heartbeatInterval, 3) val heartbeatInterval = max(host.options.heartbeatInterval, 3)
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong())) CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true)
sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) } sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) }
if (host.proxy.type != ProxyType.No) { if (host.proxy.type != ProxyType.No) {

View File

@@ -1,20 +1,27 @@
package app.termora package app.termora
import app.termora.db.Database
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 java.awt.Color import java.awt.Color
import javax.swing.UIManager import javax.swing.UIManager
class TerminalFactory { class TerminalFactory private constructor() : Disposable {
private val terminals = mutableListOf<Terminal>() private val terminals = mutableListOf<Terminal>()
companion object { companion object {
val instance by lazy { TerminalFactory() } fun getInstance(scope: WindowScope): TerminalFactory {
return scope.getOrCreate(TerminalFactory::class) { TerminalFactory() }
}
} }
fun createTerminal(): Terminal { fun createTerminal(): Terminal {
val terminal = MyVisualTerminal() val terminal = MyVisualTerminal()
// terminal logger listener
terminal.getTerminalModel().addDataListener(TerminalLoggerDataListener(terminal))
terminals.add(terminal) terminals.add(terminal)
return terminal return terminal
} }
@@ -23,7 +30,7 @@ class TerminalFactory {
return terminals return terminals
} }
private inner class MyVisualTerminal : VisualTerminal() { open class MyVisualTerminal : VisualTerminal() {
private val terminalModel by lazy { MyTerminalModel(this) } private val terminalModel by lazy { MyTerminalModel(this) }
override fun getTerminalModel(): TerminalModel { override fun getTerminalModel(): TerminalModel {
@@ -31,13 +38,13 @@ class TerminalFactory {
} }
} }
private inner class MyTerminalModel(terminal: Terminal) : TerminalModelImpl(terminal) { open class MyTerminalModel(terminal: Terminal) : TerminalModelImpl(terminal) {
private val colorPalette by lazy { MyColorPalette(terminal) } private val colorPalette by lazy { MyColorPalette(terminal) }
private val config get() = Database.instance.terminal private val config get() = Database.getDatabase().terminal
init { init {
setData(DataKey.CursorStyle, config.cursor) this.setData(DataKey.CursorStyle, config.cursor)
setData(TerminalPanel.Debug, config.debug) this.setData(TerminalPanel.Debug, config.debug)
} }
override fun getColorPalette(): ColorPalette { override fun getColorPalette(): ColorPalette {
@@ -90,17 +97,19 @@ class TerminalFactory {
TerminalColor.Basic.SELECTION_FOREGROUND TerminalColor.Basic.SELECTION_FOREGROUND
) )
else -> DefaultColorTheme.instance.getColor(color) else -> DefaultColorTheme.getInstance().getColor(color)
} }
} }
} }
private inner class MyColorPalette(terminal: Terminal) : ColorPaletteImpl(terminal) { class MyColorPalette(terminal: Terminal) : ColorPaletteImpl(terminal) {
private val colorTheme by lazy { FlatLafColorTheme() } private val colorTheme by lazy { FlatLafColorTheme() }
override fun getTheme(): ColorTheme { override fun getTheme(): ColorTheme {
return colorTheme return colorTheme
} }
} }
} }

View File

@@ -13,14 +13,16 @@ class TerminalPanelFactory {
private val terminalPanels = mutableListOf<TerminalPanel>() private val terminalPanels = mutableListOf<TerminalPanel>()
companion object { companion object {
val instance by lazy { TerminalPanelFactory() } fun getInstance(scope: Scope): TerminalPanelFactory {
return scope.getOrCreate(TerminalPanelFactory::class) { TerminalPanelFactory() }
}
} }
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel { fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
val terminalPanel = TerminalPanel(terminal, ptyConnector) val terminalPanel = TerminalPanel(terminal, ptyConnector)
terminalPanel.addTerminalPaintListener(MultipleTerminalListener()) terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.instance) terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.instance) terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
terminalPanels.add(terminalPanel) terminalPanels.add(terminalPanel)
return terminalPanel return terminalPanel
} }

View File

@@ -42,5 +42,10 @@ interface TerminalTab : Disposable {
*/ */
fun canClose(): Boolean = true fun canClose(): Boolean = true
/**
* 是否可以克隆
*/
fun canClone(): Boolean = true
} }

View File

@@ -1,5 +1,9 @@
package app.termora package app.termora
import app.termora.actions.DataProvider
import app.termora.actions.DataProviderSupport
import app.termora.actions.DataProviders
import app.termora.terminal.DataKey
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Dimension import java.awt.Dimension
import java.awt.Window import java.awt.Window
@@ -11,7 +15,9 @@ class TerminalTabDialog(
owner: Window, owner: Window,
size: Dimension, size: Dimension,
private val terminalTab: TerminalTab private val terminalTab: TerminalTab
) : DialogWrapper(null), Disposable { ) : DialogWrapper(null), Disposable, DataProvider {
private val dataProviderSupport = DataProviderSupport()
init { init {
title = terminalTab.getTitle() title = terminalTab.getTitle()
@@ -19,6 +25,7 @@ class TerminalTabDialog(
isAlwaysOnTop = false isAlwaysOnTop = false
iconImages = owner.iconImages iconImages = owner.iconImages
escapeDispose = false escapeDispose = false
processGlobalKeymap = true
super.setSize(size) super.setSize(size)
@@ -34,6 +41,15 @@ class TerminalTabDialog(
}) })
setLocationRelativeTo(null) setLocationRelativeTo(null)
if (owner is DataProvider) {
owner.getData(DataProviders.WindowScope)?.let {
dataProviderSupport.addData(DataProviders.WindowScope, it)
}
}
dataProviderSupport.addData(DataProviders.TerminalTab, terminalTab)
} }
override fun createSouthPanel(): JComponent? { override fun createSouthPanel(): JComponent? {
@@ -52,4 +68,8 @@ class TerminalTabDialog(
super<DialogWrapper>.dispose() super<DialogWrapper>.dispose()
} }
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return dataProviderSupport.getData(dataKey)
}
} }

View File

@@ -1,34 +1,41 @@
package app.termora package app.termora
import app.termora.actions.*
import app.termora.findeverywhere.BasicFilterFindEverywhereProvider import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
import app.termora.findeverywhere.FindEverywhere
import app.termora.findeverywhere.FindEverywhereProvider import app.termora.findeverywhere.FindEverywhereProvider
import app.termora.findeverywhere.FindEverywhereResult import app.termora.findeverywhere.FindEverywhereResult
import app.termora.terminal.DataKey
import app.termora.transport.TransportPanel
import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.extras.components.FlatTabbedPane import com.formdev.flatlaf.extras.components.FlatTabbedPane
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionContainerFactory import java.awt.*
import org.jdesktop.swingx.action.ActionManager import java.awt.event.AWTEventListener
import java.awt.BorderLayout
import java.awt.Component
import java.awt.Dimension
import java.awt.event.ActionEvent import java.awt.event.ActionEvent
import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import java.beans.PropertyChangeEvent
import java.beans.PropertyChangeListener import java.beans.PropertyChangeListener
import javax.swing.* import java.util.*
import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
import javax.swing.SwingUtilities
import kotlin.math.min import kotlin.math.min
class TerminalTabbed( class TerminalTabbed(
private val toolbar: JToolBar, private val windowScope: WindowScope,
private val termoraToolBar: TermoraToolBar,
private val tabbedPane: FlatTabbedPane, private val tabbedPane: FlatTabbedPane,
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager { ) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager, DataProvider {
private val tabs = mutableListOf<TerminalTab>() private val tabs = mutableListOf<TerminalTab>()
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
private val toolbar = termoraToolBar.getJToolBar()
private val actionManager = ActionManager.getInstance()
private val dataProviderSupport = DataProviderSupport()
private val titleProperty = UUID.randomUUID().toSimpleString()
private val iconListener = PropertyChangeListener { e -> private val iconListener = PropertyChangeListener { e ->
val source = e.source val source = e.source
if (e.propertyName == "icon" && source is TerminalTab) { if (e.propertyName == "icon" && source is TerminalTab) {
@@ -50,40 +57,14 @@ class TerminalTabbed(
tabbedPane.isTabsClosable = true tabbedPane.isTabsClosable = true
tabbedPane.tabType = FlatTabbedPane.TabType.card tabbedPane.tabType = FlatTabbedPane.TabType.card
tabbedPane.styleMap = mapOf(
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground")
)
val actionManager = ActionManager.getInstance()
val actionContainerFactory = ActionContainerFactory(actionManager)
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
updateBtn.isVisible = updateBtn.isEnabled
updateBtn.addChangeListener { updateBtn.isVisible = updateBtn.isEnabled }
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
override fun actionPerformed(e: ActionEvent?) {
actionManager.getAction(Actions.FIND_EVERYWHERE)?.actionPerformed(e)
}
override fun isEnabled(): Boolean {
return actionManager.getAction(Actions.FIND_EVERYWHERE)?.isEnabled ?: false
}
}))
toolbar.add(Box.createHorizontalStrut(UIManager.getInt("TabbedPane.tabHeight")))
toolbar.add(Box.createHorizontalGlue())
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.MACRO)))
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.KEYWORD_HIGHLIGHT_EVERYWHERE)))
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.KEY_MANAGER)))
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.MULTIPLE)))
toolbar.add(updateBtn)
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.FIND_EVERYWHERE)))
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.SETTING)))
tabbedPane.trailingComponent = toolbar tabbedPane.trailingComponent = toolbar
add(tabbedPane, BorderLayout.CENTER) add(tabbedPane, BorderLayout.CENTER)
windowScope.getOrCreate(TerminalTabbedManager::class) { this }
dataProviderSupport.addData(DataProviders.TerminalTabbed, this)
dataProviderSupport.addData(DataProviders.TerminalTabbedManager, this)
} }
@@ -92,18 +73,16 @@ class TerminalTabbed(
tabbedPane.setTabCloseCallback { _, i -> removeTabAt(i, true) } tabbedPane.setTabCloseCallback { _, i -> removeTabAt(i, true) }
// 选中变动 // 选中变动
tabbedPane.addPropertyChangeListener("selectedIndex", object : PropertyChangeListener { tabbedPane.addPropertyChangeListener("selectedIndex") { evt ->
override fun propertyChange(evt: PropertyChangeEvent) { val oldIndex = evt.oldValue as Int
val oldIndex = evt.oldValue as Int val newIndex = evt.newValue as Int
val newIndex = evt.newValue as Int if (oldIndex >= 0 && tabs.size > newIndex) {
if (oldIndex >= 0 && tabs.size > newIndex) { tabs[oldIndex].onLostFocus()
tabs[oldIndex].onLostFocus()
}
if (newIndex >= 0 && tabs.size > newIndex) {
tabs[newIndex].onGrabFocus()
}
} }
}) if (newIndex >= 0 && tabs.size > newIndex) {
tabs[newIndex].onGrabFocus()
}
}
// 选择变动 // 选择变动
tabbedPane.addChangeListener { tabbedPane.addChangeListener {
@@ -113,35 +92,6 @@ class TerminalTabbed(
} }
} }
// 快捷键
val inputMap = getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
for (i in KeyEvent.VK_1..KeyEvent.VK_9) {
val tabIndex = i - KeyEvent.VK_1 + 1
val actionKey = "select_$tabIndex"
actionMap.put(actionKey, object : AnAction() {
override fun actionPerformed(e: ActionEvent) {
tabbedPane.selectedIndex = if (i == KeyEvent.VK_9 || tabIndex > tabbedPane.tabCount) {
tabbedPane.tabCount - 1
} else {
tabIndex - 1
}
}
})
inputMap.put(KeyStroke.getKeyStroke(i, toolkit.menuShortcutKeyMaskEx), actionKey)
}
// 关闭 tab
actionMap.put("closeTab", object : AnAction() {
override fun actionPerformed(e: ActionEvent) {
if (tabbedPane.selectedIndex >= 0) {
tabbedPane.tabCloseCallback?.accept(tabbedPane, tabbedPane.selectedIndex)
}
}
})
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, toolkit.menuShortcutKeyMaskEx), "closeTab")
// 右键菜单 // 右键菜单
tabbedPane.addMouseListener(object : MouseAdapter() { tabbedPane.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { override fun mouseClicked(e: MouseEvent) {
@@ -170,43 +120,38 @@ class TerminalTabbed(
}) })
// 注册全局搜索 // 注册全局搜索
FindEverywhere.registerProvider(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider { FindEverywhereProvider.getFindEverywhereProviders(windowScope)
override fun find(pattern: String): List<FindEverywhereResult> { .add(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider {
val results = mutableListOf<FindEverywhereResult>() override fun find(pattern: String): List<FindEverywhereResult> {
for (i in 0 until tabbedPane.tabCount) { val results = mutableListOf<FindEverywhereResult>()
if (tabbedPane.getComponentAt(i) is WelcomePanel) { for (i in 0 until tabbedPane.tabCount) {
continue val c = tabbedPane.getComponentAt(i)
} if (c is WelcomePanel || c is TransportPanel) {
results.add( continue
SwitchFindEverywhereResult( }
tabbedPane.getTitleAt(i), results.add(
tabbedPane.getIconAt(i), SwitchFindEverywhereResult(
tabbedPane.getComponentAt(i) tabbedPane.getTitleAt(i),
tabbedPane.getIconAt(i),
tabbedPane.getComponentAt(i)
)
) )
) }
return results
} }
return results
}
override fun group(): String { override fun group(): String {
return I18n.getString("termora.find-everywhere.groups.opened-hosts") return I18n.getString("termora.find-everywhere.groups.opened-hosts")
}
override fun order(): Int {
return Integer.MIN_VALUE + 1
}
}))
// 打开 Host
ActionManager.getInstance().addAction(Actions.OPEN_HOST, object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
if (e !is OpenHostActionEvent) {
return
} }
openHost(e.host)
} override fun order(): Int {
}) return Integer.MIN_VALUE + 1
}
}))
// 监听全局事件
toolkit.addAWTEventListener(customizeToolBarAWTEventListener, AWTEvent.MOUSE_EVENT_MASK)
} }
@@ -238,32 +183,25 @@ class TerminalTabbed(
} }
} }
private fun openHost(host: Host) {
val tab = if (host.protocol == Protocol.SSH) SSHTerminalTab(host) else LocalTerminalTab(host)
addTab(tab)
tab.start()
}
private fun showContextMenu(tabIndex: Int, e: MouseEvent) { private fun showContextMenu(tabIndex: Int, e: MouseEvent) {
val c = tabbedPane.getComponentAt(tabIndex) as JComponent val c = tabbedPane.getComponentAt(tabIndex) as JComponent
val tab = tabs[tabIndex]
val popupMenu = FlatPopupMenu() val popupMenu = FlatPopupMenu()
// 修改名称 // 修改名称
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename")) val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
rename.addActionListener { rename.addActionListener {
val index = tabbedPane.selectedIndex if (tabIndex > 0) {
if (index > 0) {
val dialog = InputDialog( val dialog = InputDialog(
SwingUtilities.getWindowAncestor(this), SwingUtilities.getWindowAncestor(this),
title = rename.text, title = rename.text,
text = tabbedPane.getTitleAt(index), text = tabbedPane.getTitleAt(tabIndex),
) )
val text = dialog.getText() val text = dialog.getText()
if (!text.isNullOrBlank()) { if (!text.isNullOrBlank()) {
tabbedPane.setTitleAt(index, text) tabbedPane.setTitleAt(tabIndex, text)
c.putClientProperty(titleProperty, text)
} }
} }
@@ -271,35 +209,34 @@ class TerminalTabbed(
// 克隆 // 克隆
val clone = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.clone")) val clone = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.clone"))
clone.addActionListener { clone.addActionListener { evt ->
val index = tabbedPane.selectedIndex if (tab is HostTerminalTab) {
if (index > 0) { actionManager
val tab = tabs[index] .getAction(OpenHostAction.OPEN_HOST)
if (tab is HostTerminalTab) { .actionPerformed(OpenHostActionEvent(this, tab.host, evt))
ActionManager.getInstance()
.getAction(Actions.OPEN_HOST)
.actionPerformed(OpenHostActionEvent(this, tab.host))
}
} }
} }
// 在新窗口中打开 // 在新窗口中打开
val openInNewWindow = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.open-in-new-window")) val openInNewWindow = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.open-in-new-window"))
openInNewWindow.addActionListener { openInNewWindow.addActionListener(object : AnAction() {
val index = tabbedPane.selectedIndex override fun actionPerformed(evt: AnActionEvent) {
if (index > 0) { val owner = evt.getData(DataProviders.TermoraFrame) ?: return
val tab = tabs[index] if (tabIndex > 0) {
removeTabAt(index, false) val title = tabbedPane.getTitleAt(tabIndex)
val dialog = TerminalTabDialog( removeTabAt(tabIndex, false)
owner = SwingUtilities.getWindowAncestor(this), val dialog = TerminalTabDialog(
terminalTab = tab, owner = owner,
size = Dimension(min(size.width, 1280), min(size.height, 800)) terminalTab = tab,
) size = Dimension(min(size.width, 1280), min(size.height, 800))
Disposer.register(dialog, tab) )
Disposer.register(this, dialog) dialog.title = title
dialog.isVisible = true Disposer.register(dialog, tab)
Disposer.register(this@TerminalTabbed, dialog)
dialog.isVisible = true
}
} }
} })
popupMenu.addSeparator() popupMenu.addSeparator()
@@ -332,19 +269,17 @@ class TerminalTabbed(
clone.isEnabled = close.isEnabled clone.isEnabled = close.isEnabled
openInNewWindow.isEnabled = close.isEnabled openInNewWindow.isEnabled = close.isEnabled
// SFTP不允许克隆 // 如果不允许克隆
if (clone.isEnabled && getSelectedTerminalTab() is SFTPTerminalTab) { if (clone.isEnabled && !tab.canClone()) {
clone.isEnabled = false clone.isEnabled = false
} }
if (close.isEnabled) { if (close.isEnabled) {
popupMenu.addSeparator() popupMenu.addSeparator()
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect")) val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
reconnect.addActionListener { reconnect.addActionListener {
val index = tabbedPane.selectedIndex if (tabIndex > 0) {
if (index > 0) { tabs[tabIndex].reconnect()
tabs[index].reconnect()
} }
} }
@@ -355,21 +290,85 @@ class TerminalTabbed(
} }
fun addTab(tab: TerminalTab) { private fun addTab(index: Int, tab: TerminalTab) {
tabbedPane.addTab( val c = tab.getJComponent()
tab.getTitle(), val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
tabbedPane.insertTab(
title,
tab.getIcon(), tab.getIcon(),
tab.getJComponent() c,
StringUtils.EMPTY,
index
) )
c.putClientProperty(titleProperty, title)
// 监听 icons 变化 // 监听 icons 变化
tab.addPropertyChangeListener(iconListener) tab.addPropertyChangeListener(iconListener)
tabs.add(tab) tabs.add(index, tab)
tabbedPane.selectedIndex = tabbedPane.tabCount - 1 tabbedPane.selectedIndex = index
Disposer.register(this, tab) Disposer.register(this, tab)
} }
/**
* 对着 ToolBar 右键
*/
private inner class CustomizeToolBarAWTEventListener : AWTEventListener, Disposable {
init {
Disposer.register(this@TerminalTabbed, this)
}
override fun eventDispatched(event: AWTEvent) {
if (event !is MouseEvent || event.id != MouseEvent.MOUSE_CLICKED || !SwingUtilities.isRightMouseButton(event)) return
// 如果 ToolBar 没有显示
if (!toolbar.isShowing) return
// 如果不是作用于在 ToolBar 上面
if (!Rectangle(toolbar.locationOnScreen, toolbar.size).contains(event.locationOnScreen)) return
// 显示右键菜单
showContextMenu(event)
}
private fun showContextMenu(event: MouseEvent) {
val popupMenu = FlatPopupMenu()
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
val dialog = CustomizeToolBarDialog(
SwingUtilities.getWindowAncestor(this@TerminalTabbed),
termoraToolBar
)
if (dialog.open()) {
termoraToolBar.rebuild()
}
}
popupMenu.show(event.component, event.x, event.y)
}
override fun dispose() {
toolkit.removeAWTEventListener(this)
}
}
/*private inner class CustomizeToolBarDialog(owner: Window) : DialogWrapper(owner) {
init {
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
isModal = true
title = I18n.getString("termora.setting")
setLocationRelativeTo(null)
init()
}
override fun createCenterPanel(): JComponent {
val model = DefaultListModel<String>()
val checkBoxList = CheckBoxList(model)
checkBoxList.fixedCellHeight = UIManager.getInt("Tree.rowHeight")
model.addElement("Test")
return checkBoxList
}
}*/
private inner class SwitchFindEverywhereResult( private inner class SwitchFindEverywhereResult(
private val title: String, private val title: String,
private val icon: Icon?, private val icon: Icon?,
@@ -401,7 +400,11 @@ class TerminalTabbed(
} }
override fun addTerminalTab(tab: TerminalTab) { override fun addTerminalTab(tab: TerminalTab) {
addTab(tab) addTab(tabs.size, tab)
}
override fun addTerminalTab(index: Int, tab: TerminalTab) {
addTab(index, tab)
} }
override fun getSelectedTerminalTab(): TerminalTab? { override fun getSelectedTerminalTab(): TerminalTab? {
@@ -426,5 +429,24 @@ class TerminalTabbed(
} }
} }
override fun closeTerminalTab(tab: TerminalTab, disposable: Boolean) {
for (i in 0 until tabs.size) {
if (tabs[i] == tab) {
removeTabAt(i, disposable)
break
}
}
}
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
if (dataKey == DataProviders.TerminalTab) {
dataProviderSupport.removeData(dataKey)
if (tabbedPane.selectedIndex >= 0 && tabs.size > tabbedPane.selectedIndex) {
dataProviderSupport.addData(dataKey, tabs[tabbedPane.selectedIndex])
}
}
return dataProviderSupport.getData(dataKey)
}
} }

View File

@@ -2,7 +2,9 @@ package app.termora
interface TerminalTabbedManager { interface TerminalTabbedManager {
fun addTerminalTab(tab: TerminalTab) fun addTerminalTab(tab: TerminalTab)
fun addTerminalTab(index: Int, tab: TerminalTab)
fun getSelectedTerminalTab(): TerminalTab? fun getSelectedTerminalTab(): TerminalTab?
fun getTerminalTabs(): List<TerminalTab> fun getTerminalTabs(): List<TerminalTab>
fun setSelectedTerminalTab(tab: TerminalTab) fun setSelectedTerminalTab(tab: TerminalTab)
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true)
} }

View File

@@ -1,117 +1,57 @@
package app.termora package app.termora
import app.termora.findeverywhere.FindEverywhere
import app.termora.highlight.KeywordHighlightDialog import app.termora.actions.ActionManager
import app.termora.keymgr.KeyManagerDialog import app.termora.actions.DataProvider
import app.termora.macro.MacroAction import app.termora.actions.DataProviderSupport
import app.termora.actions.DataProviders
import app.termora.terminal.DataKey
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.FlatDesktop
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import com.jetbrains.JBR import com.jetbrains.JBR
import io.github.g00fy2.versioncompare.Version
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.JXEditorPane
import org.jdesktop.swingx.action.ActionManager
import org.slf4j.LoggerFactory
import java.awt.Dimension import java.awt.Dimension
import java.awt.Insets import java.awt.Insets
import java.awt.KeyEventDispatcher
import java.awt.KeyboardFocusManager import java.awt.KeyboardFocusManager
import java.awt.event.* import java.awt.event.MouseAdapter
import java.net.URI import java.awt.event.MouseEvent
import java.util.*
import javax.imageio.ImageIO import javax.imageio.ImageIO
import javax.swing.* import javax.swing.Box
import javax.swing.JFrame
import javax.swing.SwingUtilities
import javax.swing.SwingUtilities.isEventDispatchThread import javax.swing.SwingUtilities.isEventDispatchThread
import javax.swing.event.HyperlinkEvent import javax.swing.UIManager
import kotlin.concurrent.fixedRateTimer
import kotlin.math.max import kotlin.math.max
import kotlin.system.exitProcess
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
fun assertEventDispatchThread() { fun assertEventDispatchThread() {
if (!isEventDispatchThread()) throw WrongThreadException("AWT EventQueue") if (!isEventDispatchThread()) throw WrongThreadException("AWT EventQueue")
} }
class TermoraFrame : JFrame() { class TermoraFrame : JFrame(), DataProvider {
companion object {
private val log = LoggerFactory.getLogger(TermoraFrame::class.java)
}
private val toolbar = JToolBar() private val actionManager get() = ActionManager.getInstance()
private val tabbedPane = MyTabbedPane() private val id = UUID.randomUUID().toString()
private lateinit var terminalTabbed: TerminalTabbed private val windowScope = ApplicationScope.forWindowScope(this)
private val disposable = Disposer.newDisposable()
private val isWindowDecorationsSupported by lazy { JBR.isWindowDecorationsSupported() }
private val titleBar = LogicCustomTitleBar.createCustomTitleBar(this) private val titleBar = LogicCustomTitleBar.createCustomTitleBar(this)
private val updaterManager get() = UpdaterManager.instance private val tabbedPane = MyTabbedPane()
private val toolbar = TermoraToolBar(titleBar, tabbedPane)
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane)
private val isWindowDecorationsSupported by lazy { JBR.isWindowDecorationsSupported() }
private val dataProviderSupport = DataProviderSupport()
private val welcomePanel = WelcomePanel(windowScope)
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
private val preferencesHandler = object : Runnable {
override fun run() {
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow ?: this@TermoraFrame
if (owner != this@TermoraFrame) {
return
}
val that = this
FlatDesktop.setPreferencesHandler {}
val dialog = SettingsDialog(owner)
dialog.addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
FlatDesktop.setPreferencesHandler(that)
}
})
dialog.isVisible = true
}
}
init { init {
initActions()
initView() initView()
initEvents() initEvents()
initDesktopHandler()
scheduleUpdate()
} }
private fun initEvents() { private fun initEvents() {
// 监听窗口大小变动,然后修改边距避开控制按钮
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
if (SystemInfo.isMacOS) {
val left = titleBar.leftInset.toInt()
if (tabbedPane.tabAreaInsets.left != left) {
tabbedPane.tabAreaInsets = Insets(0, left, 0, 0)
}
} else if (SystemInfo.isWindows || SystemInfo.isLinux) {
val right = titleBar.rightInset.toInt()
for (i in 0 until toolbar.componentCount) {
val c = toolbar.getComponent(i)
if (c.name == "spacing") {
if (c.width == right) {
return
}
toolbar.remove(i)
break
}
}
if (right > 0) {
val spacing = Box.createHorizontalStrut(right)
spacing.name = "spacing"
toolbar.add(spacing)
}
}
}
})
forceHitTest() forceHitTest()
// macos 需要判断是否全部删除 // macos 需要判断是否全部删除
@@ -126,146 +66,19 @@ class TermoraFrame : JFrame() {
} }
} }
// global shortcuts
rootPane.actionMap.put(Actions.FIND_EVERYWHERE, ActionManager.getInstance().getAction(Actions.FIND_EVERYWHERE))
rootPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_T, toolkit.menuShortcutKeyMaskEx), Actions.FIND_EVERYWHERE)
// double shift
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(object : KeyEventDispatcher {
private var lastTime = -1L
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
if (e.keyCode == KeyEvent.VK_SHIFT && e.id == KeyEvent.KEY_PRESSED) {
val now = System.currentTimeMillis()
if (now - 250 < lastTime) {
ActionManager.getInstance().getAction(Actions.FIND_EVERYWHERE)
.actionPerformed(ActionEvent(rootPane, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
}
lastTime = now
} else if (e.keyCode != KeyEvent.VK_SHIFT) { // 如果不是 Shift 键,那么就阻断了连续性,重置时间
lastTime = -1
}
return false
}
})
// 监听主题变化 需要动态修改控制栏颜色 // 监听主题变化 需要动态修改控制栏颜色
if (SystemInfo.isWindows && isWindowDecorationsSupported) { if (SystemInfo.isWindows && isWindowDecorationsSupported) {
ThemeManager.instance.addThemeChangeListener(object : ThemeChangeListener { ThemeManager.getInstance().addThemeChangeListener(object : ThemeChangeListener {
override fun onChanged() { override fun onChanged() {
titleBar.putProperty("controls.dark", FlatLaf.isLafDark()) titleBar.putProperty("controls.dark", FlatLaf.isLafDark())
} }
}) })
} }
// dispose
addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
Disposer.dispose(disposable)
Disposer.dispose(ApplicationDisposable.instance)
try {
Disposer.getTree().assertIsEmpty(true)
} catch (e: Exception) {
log.error(e.message)
}
exitProcess(0)
}
})
} }
private fun initActions() {
// SETTING
ActionManager.getInstance().addAction(Actions.SETTING, object : AnAction(
I18n.getString("termora.setting"),
Icons.settings
) {
override fun actionPerformed(e: ActionEvent) {
preferencesHandler.run()
}
})
// MULTIPLE
ActionManager.getInstance().addAction(Actions.MULTIPLE, object : AnAction(
I18n.getString("termora.tools.multiple"),
Icons.vcs
) {
init {
setStateAction()
}
override fun actionPerformed(evt: ActionEvent) {
TerminalPanelFactory.instance.repaintAll()
}
})
// Keyword Highlight
ActionManager.getInstance().addAction(Actions.KEYWORD_HIGHLIGHT_EVERYWHERE, object : AnAction(
I18n.getString("termora.highlight"),
Icons.edit
) {
override fun actionPerformed(evt: ActionEvent) {
KeywordHighlightDialog(this@TermoraFrame).isVisible = true
}
})
// app update
ActionManager.getInstance().addAction(Actions.APP_UPDATE, object :
AnAction(
StringUtils.EMPTY,
Icons.ideUpdate
) {
init {
isEnabled = false
}
override fun actionPerformed(evt: ActionEvent) {
showUpdateDialog()
}
})
// macro
ActionManager.getInstance().addAction(Actions.MACRO, MacroAction())
// FIND_EVERYWHERE
ActionManager.getInstance().addAction(Actions.FIND_EVERYWHERE, object : AnAction(
I18n.getString("termora.find-everywhere"),
Icons.find
) {
override fun actionPerformed(evt: ActionEvent) {
if (this.isEnabled) {
val focusWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
val frame = this@TermoraFrame
if (focusWindow == frame) {
FindEverywhere(frame).isVisible = true
}
}
}
})
// Key manager
ActionManager.getInstance().addAction(Actions.KEY_MANAGER, object : AnAction(
I18n.getString("termora.keymgr.title"),
Icons.greyKey
) {
override fun actionPerformed(evt: ActionEvent) {
if (this.isEnabled) {
KeyManagerDialog(this@TermoraFrame).isVisible = true
}
}
})
}
private fun initView() { private fun initView() {
if (isWindowDecorationsSupported) { if (isWindowDecorationsSupported) {
titleBar.height = UIManager.getInt("TabbedPane.tabHeight").toFloat() titleBar.height = UIManager.getInt("TabbedPane.tabHeight").toFloat()
@@ -288,10 +101,7 @@ class TermoraFrame : JFrame() {
} }
minimumSize = Dimension(640, 400) minimumSize = Dimension(640, 400)
terminalTabbed = TerminalTabbed(toolbar, tabbedPane).apply { terminalTabbed.addTerminalTab(welcomePanel)
Application.registerService(TerminalTabbedManager::class, this)
}
terminalTabbed.addTab(WelcomePanel())
// macOS 要避开左边的控制栏 // macOS 要避开左边的控制栏
if (SystemInfo.isMacOS) { if (SystemInfo.isMacOS) {
@@ -303,89 +113,13 @@ class TermoraFrame : JFrame() {
} }
} }
Disposer.register(disposable, terminalTabbed) Disposer.register(windowScope, terminalTabbed)
add(terminalTabbed) add(terminalTabbed)
dataProviderSupport.addData(DataProviders.TermoraFrame, this)
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
} }
private fun showUpdateDialog() {
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(
this,
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) {
ActionManager.getInstance().setEnabled(Actions.APP_UPDATE, false)
updaterManager.ignore(updaterManager.lastVersion.version)
} else if (option == JOptionPane.YES_OPTION) {
ActionManager.getInstance()
.setEnabled(Actions.APP_UPDATE, false)
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${lastVersion.version}"))
}
}
@OptIn(DelicateCoroutinesApi::class)
private fun scheduleUpdate() {
fixedRateTimer(
name = "check-update-timer",
initialDelay = 3.minutes.inWholeMilliseconds,
period = 5.hours.inWholeMilliseconds, daemon = true
) {
GlobalScope.launch(Dispatchers.IO) { supervisorScope { launch { checkUpdate() } } }
}
}
private suspend fun checkUpdate() {
val latestVersion = updaterManager.fetchLatestVersion()
if (latestVersion.isSelf) {
return
}
val newVersion = Version(latestVersion.version)
val version = Version(Application.getVersion())
if (newVersion <= version) {
return
}
if (updaterManager.isIgnored(latestVersion.version)) {
return
}
withContext(Dispatchers.Swing) {
ActionManager.getInstance()
.setEnabled(Actions.APP_UPDATE, true)
}
}
private fun forceHitTest() { private fun forceHitTest() {
val mouseAdapter = object : MouseAdapter() { val mouseAdapter = object : MouseAdapter() {
@@ -405,7 +139,7 @@ class TermoraFrame : JFrame() {
} }
override fun mousePressed(e: MouseEvent) { override fun mousePressed(e: MouseEvent) {
if (e.source == toolbar) { if (e.source == toolbar.getJToolBar()) {
if (!isWindowDecorationsSupported && SwingUtilities.isLeftMouseButton(e)) { if (!isWindowDecorationsSupported && SwingUtilities.isLeftMouseButton(e)) {
if (JBR.isWindowMoveSupported()) { if (JBR.isWindowMoveSupported()) {
JBR.getWindowMove().startMovingTogetherWithMouse(this@TermoraFrame, e.button) JBR.getWindowMove().startMovingTogetherWithMouse(this@TermoraFrame, e.button)
@@ -440,15 +174,29 @@ class TermoraFrame : JFrame() {
tabbedPane.addMouseListener(mouseAdapter) tabbedPane.addMouseListener(mouseAdapter)
tabbedPane.addMouseMotionListener(mouseAdapter) tabbedPane.addMouseMotionListener(mouseAdapter)
toolbar.addMouseListener(mouseAdapter) toolbar.getJToolBar().addMouseListener(mouseAdapter)
toolbar.addMouseMotionListener(mouseAdapter) toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
} }
private fun initDesktopHandler() { override fun <T : Any> getData(dataKey: DataKey<T>): T? {
if (SystemInfo.isMacOS) { return dataProviderSupport.getData(dataKey)
FlatDesktop.setPreferencesHandler { ?: terminalTabbed.getData(dataKey)
preferencesHandler.run() ?: welcomePanel.getData(dataKey)
}
}
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TermoraFrame
return id == other.id
}
override fun hashCode(): Int {
return id.hashCode()
}
} }

View File

@@ -0,0 +1,60 @@
package app.termora
import com.formdev.flatlaf.util.SystemInfo
import org.slf4j.LoggerFactory
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.swing.WindowConstants.DISPOSE_ON_CLOSE
import kotlin.system.exitProcess
class TermoraFrameManager {
companion object {
private val log = LoggerFactory.getLogger(TermoraFrameManager::class.java)
fun getInstance(): TermoraFrameManager {
return ApplicationScope.forApplicationScope()
.getOrCreate(TermoraFrameManager::class) { TermoraFrameManager() }
}
}
fun createWindow(): TermoraFrame {
val frame = TermoraFrame()
registerCloseCallback(frame)
frame.title = if (SystemInfo.isLinux) null else Application.getName()
frame.defaultCloseOperation = DISPOSE_ON_CLOSE
frame.setSize(1280, 800)
frame.setLocationRelativeTo(null)
return frame
}
private fun registerCloseCallback(window: TermoraFrame) {
window.addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
// dispose windowScope
Disposer.dispose(ApplicationScope.forWindowScope(e.window))
val windowScopes = ApplicationScope.windowScopes()
// 如果已经没有 Window 域了,那么就可以退出程序了
if (windowScopes.isEmpty()) {
this@TermoraFrameManager.dispose()
}
}
})
}
private fun dispose() {
Disposer.dispose(ApplicationScope.forApplicationScope())
try {
Disposer.getTree().assertIsEmpty(true)
} catch (e: Exception) {
log.error(e.message)
}
exitProcess(0)
}
}

View File

@@ -0,0 +1,177 @@
package app.termora
import app.termora.Application.ohMyJson
import app.termora.actions.ActionManager
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.actions.SettingsAction
import app.termora.findeverywhere.FindEverywhereAction
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.util.SystemInfo
import com.jetbrains.WindowDecorations
import kotlinx.serialization.Serializable
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionContainerFactory
import java.awt.Insets
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.Box
import javax.swing.JToolBar
@Serializable
data class ToolBarAction(
val id: String,
val visible: Boolean,
)
class TermoraToolBar(
private val titleBar: WindowDecorations.CustomTitleBar,
private val tabbedPane: FlatTabbedPane
) {
private val properties by lazy { Database.getDatabase().properties }
private val toolbar by lazy { MyToolBar().apply { rebuild(this) } }
fun getJToolBar(): JToolBar {
return toolbar
}
/**
* 获取到所有的 Action
*/
fun getAllActions(): List<ToolBarAction> {
return listOf(
ToolBarAction(Actions.SFTP, true),
ToolBarAction(Actions.TERMINAL_LOGGER, true),
ToolBarAction(Actions.MACRO, true),
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
ToolBarAction(Actions.KEY_MANAGER, true),
ToolBarAction(Actions.MULTIPLE, true),
ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
ToolBarAction(SettingsAction.SETTING, true),
)
}
/**
* 获取到所有 Action会根据用户个性化排序/显示
*/
fun getActions(): List<ToolBarAction> {
val text = properties.getString(
"Termora.ToolBar.Actions",
StringUtils.EMPTY
)
val actions = getAllActions()
if (text.isBlank()) {
return actions
}
// 存储的 action
val storageActions = (ohMyJson.runCatching {
ohMyJson.decodeFromString<List<ToolBarAction>>(text)
}.getOrNull() ?: return actions).toMutableList()
for (action in actions) {
// 如果存储的 action 不包含这个,那么这个可能是新增的,新增的默认显示出来
if (storageActions.none { it.id == action.id }) {
storageActions.addFirst(ToolBarAction(action.id, true))
}
}
// 如果存储的 Action 在所有 Action 里没有,那么移除
storageActions.removeIf { e -> actions.none { e.id == it.id } }
return storageActions
}
fun rebuild() {
rebuild(this.toolbar)
}
private fun rebuild(toolbar: JToolBar) {
val actionManager = ActionManager.getInstance()
val actionContainerFactory = ActionContainerFactory(actionManager)
toolbar.removeAll()
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
override fun actionPerformed(evt: AnActionEvent) {
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
}
override fun isEnabled(): Boolean {
return actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.isEnabled ?: false
}
}))
toolbar.add(Box.createHorizontalGlue())
// update btn
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
updateBtn.isVisible = updateBtn.isEnabled
updateBtn.addChangeListener { updateBtn.isVisible = updateBtn.isEnabled }
toolbar.add(updateBtn)
// 获取显示的Action如果不是 false 那么就是显示出来
for (action in getActions()) {
if (action.visible) {
actionManager.getAction(action.id)?.let {
toolbar.add(actionContainerFactory.createButton(it))
}
}
}
if (toolbar is MyToolBar) {
toolbar.adjust()
}
toolbar.revalidate()
toolbar.repaint()
}
private inner class MyToolBar : JToolBar() {
init {
// 监听窗口大小变动,然后修改边距避开控制按钮
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
adjust()
}
})
}
fun adjust() {
if (SystemInfo.isMacOS) {
val left = titleBar.leftInset.toInt()
if (tabbedPane.tabAreaInsets.left != left) {
tabbedPane.tabAreaInsets = Insets(0, left, 0, 0)
}
} else if (SystemInfo.isWindows || SystemInfo.isLinux) {
val right = titleBar.rightInset.toInt()
val toolbar = this@MyToolBar
for (i in 0 until toolbar.componentCount) {
val c = toolbar.getComponent(i)
if (c.name == "spacing") {
if (c.width == right) {
return
}
toolbar.remove(i)
break
}
}
if (right > 0) {
val spacing = Box.createHorizontalStrut(right)
spacing.name = "spacing"
toolbar.add(spacing)
}
}
}
}
}

View File

@@ -1,13 +1,11 @@
package app.termora package app.termora
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.* import com.formdev.flatlaf.extras.components.*
import com.formdev.flatlaf.ui.FlatTextBorder import com.formdev.flatlaf.ui.FlatTextBorder
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import java.awt.Component import java.awt.Component
import java.awt.event.FocusAdapter import java.awt.event.*
import java.awt.event.FocusEvent
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.text.ParseException import java.text.ParseException
import javax.swing.DefaultListCellRenderer import javax.swing.DefaultListCellRenderer
import javax.swing.JComboBox import javax.swing.JComboBox
@@ -53,6 +51,15 @@ class OutlineTextArea : FlatTextArea() {
} }
} }
class OutlineComboBox<T> : JComboBox<T>() {
init {
addItemListener {
if (it.stateChange == ItemEvent.SELECTED) {
putClientProperty(FlatClientProperties.OUTLINE, null)
}
}
}
}
class FixedLengthTextArea(var maxLength: Int = Int.MAX_VALUE) : FlatTextArea() { class FixedLengthTextArea(var maxLength: Int = Int.MAX_VALUE) : FlatTextArea() {
init { init {

View File

@@ -1,6 +1,5 @@
package app.termora package app.termora
import app.termora.db.Database
import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.FlatAnimatedLafChange import com.formdev.flatlaf.extras.FlatAnimatedLafChange
import com.jthemedetecor.OsThemeDetector import com.jthemedetecor.OsThemeDetector
@@ -24,12 +23,16 @@ class ThemeManager private constructor() {
companion object { companion object {
private val log = LoggerFactory.getLogger(ThemeManager::class.java) private val log = LoggerFactory.getLogger(ThemeManager::class.java)
val instance by lazy { ThemeManager() } fun getInstance(): ThemeManager {
return ApplicationScope.forApplicationScope().getOrCreate(ThemeManager::class) { ThemeManager() }
}
} }
val appearance by lazy { Database.getDatabase().appearance }
val themes = mapOf( val themes = mapOf(
"Light" to LightLaf::class.java.name, "Light" to LightLaf::class.java.name,
"Dark" to DarkLaf::class.java.name, "Dark" to DarkLaf::class.java.name,
"Dracula" to DraculaLaf::class.java.name,
"iTerm2 Dark" to iTerm2DarkLaf::class.java.name, "iTerm2 Dark" to iTerm2DarkLaf::class.java.name,
"Termius Dark" to TermiusDarkLaf::class.java.name, "Termius Dark" to TermiusDarkLaf::class.java.name,
"Termius Light" to TermiusLightLaf::class.java.name, "Termius Light" to TermiusLightLaf::class.java.name,
@@ -77,18 +80,16 @@ class ThemeManager private constructor() {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
OsThemeDetector.getDetector().registerListener(object : Consumer<Boolean> { OsThemeDetector.getDetector().registerListener(object : Consumer<Boolean> {
override fun accept(isDark: Boolean) { override fun accept(isDark: Boolean) {
if (!Database.instance.appearance.followSystem) { if (!appearance.followSystem) {
return return
} }
if (FlatLaf.isLafDark() && isDark) { SwingUtilities.invokeLater {
return if (isDark) {
} change(appearance.darkTheme)
} else {
if (isDark) { change(appearance.lightTheme)
SwingUtilities.invokeLater { change("Dark") } }
} else {
SwingUtilities.invokeLater { change("Light") }
} }
} }
}) })

View File

@@ -1,7 +1,6 @@
package app.termora package app.termora
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.db.Database
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import okhttp3.Request import okhttp3.Request
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
@@ -19,7 +18,9 @@ import java.util.*
class UpdaterManager private constructor() { class UpdaterManager private constructor() {
companion object { companion object {
private val log = LoggerFactory.getLogger(UpdaterManager::class.java) private val log = LoggerFactory.getLogger(UpdaterManager::class.java)
val instance by lazy { UpdaterManager() } fun getInstance(): UpdaterManager {
return ApplicationScope.forApplicationScope().getOrCreate(UpdaterManager::class) { UpdaterManager() }
}
} }
data class Asset( data class Asset(
@@ -58,7 +59,7 @@ class UpdaterManager private constructor() {
val isSelf get() = this == self val isSelf get() = this == self
} }
private val properties get() = Database.instance.properties private val properties get() = Database.getDatabase().properties
var lastVersion = LatestVersion.self var lastVersion = LatestVersion.self
fun fetchLatestVersion(): LatestVersion { fun fetchLatestVersion(): LatestVersion {

View File

@@ -1,10 +1,11 @@
package app.termora package app.termora
import app.termora.db.Database
import app.termora.actions.*
import app.termora.findeverywhere.BasicFilterFindEverywhereProvider import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
import app.termora.findeverywhere.FindEverywhere
import app.termora.findeverywhere.FindEverywhereProvider import app.termora.findeverywhere.FindEverywhereProvider
import app.termora.findeverywhere.FindEverywhereResult import app.termora.findeverywhere.FindEverywhereResult
import app.termora.terminal.DataKey
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.FlatSVGIcon import com.formdev.flatlaf.extras.FlatSVGIcon
@@ -19,17 +20,18 @@ import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent import java.awt.event.ComponentEvent
import javax.swing.* import javax.swing.*
import javax.swing.event.DocumentEvent import javax.swing.event.DocumentEvent
import javax.swing.tree.TreePath
import kotlin.math.max import kotlin.math.max
class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab { class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()), Disposable, TerminalTab,
private val properties get() = Database.instance.properties DataProvider {
private val properties get() = Database.getDatabase().properties
private val rootPanel = JPanel(BorderLayout()) private val rootPanel = JPanel(BorderLayout())
private val searchTextField = FlatTextField() private val searchTextField = FlatTextField()
private val hostTree = HostTree() private val hostTree = HostTree()
private val bannerPanel = BannerPanel() private val bannerPanel = BannerPanel()
private val toggle = FlatButton() private val toggle = FlatButton()
private var fullContent = properties.getString("WelcomeFullContent", "false").toBoolean() private var fullContent = properties.getString("WelcomeFullContent", "false").toBoolean()
private val dataProviderSupport = DataProviderSupport()
init { init {
initView() initView()
@@ -51,6 +53,7 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
rootPanel.add(panel, BorderLayout.CENTER) rootPanel.add(panel, BorderLayout.CENTER)
add(rootPanel, BorderLayout.CENTER) add(rootPanel, BorderLayout.CENTER)
dataProviderSupport.addData(DataProviders.Welcome.HostTree, hostTree)
} }
@@ -73,7 +76,7 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
newHost.isFocusable = false newHost.isFocusable = false
newHost.buttonType = FlatButton.ButtonType.toolBarButton newHost.buttonType = FlatButton.ButtonType.toolBarButton
newHost.addActionListener { e -> newHost.addActionListener { e ->
ActionManager.getInstance().getAction(Actions.ADD_HOST)?.actionPerformed(e) ActionManager.getInstance().getAction(NewHostAction.NEW_HOST)?.actionPerformed(e)
} }
@@ -117,7 +120,7 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
private fun createHostPanel(): JComponent { private fun createHostPanel(): JComponent {
val panel = JPanel(BorderLayout()) val panel = JPanel(BorderLayout())
hostTree.actionMap.put("find", object : AnAction() { hostTree.actionMap.put("find", object : AnAction() {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
searchTextField.requestFocusInWindow() searchTextField.requestFocusInWindow()
} }
}) })
@@ -160,31 +163,23 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
}) })
ActionManager.getInstance().addAction(Actions.ADD_HOST, object : AnAction() { FindEverywhereProvider.getFindEverywhereProviders(windowScope)
override fun actionPerformed(e: ActionEvent) { .add(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider {
if (hostTree.selectionCount < 1) { override fun find(pattern: String): List<FindEverywhereResult> {
hostTree.selectionPath = TreePath(hostTree.model.root) return TreeUtils.children(hostTree.model, hostTree.model.root)
.filterIsInstance<Host>()
.filter { it.protocol != Protocol.Folder }
.map { HostFindEverywhereResult(it) }
} }
hostTree.showAddHostDialog()
}
})
FindEverywhere.registerProvider(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider { override fun group(): String {
override fun find(pattern: String): List<FindEverywhereResult> { return I18n.getString("termora.find-everywhere.groups.open-new-hosts")
return TreeUtils.children(hostTree.model, hostTree.model.root) }
.filterIsInstance<Host>()
.filter { it.protocol != Protocol.Folder }
.map { HostFindEverywhereResult(it) }
}
override fun group(): String { override fun order(): Int {
return I18n.getString("termora.find-everywhere.groups.open-new-hosts") return Integer.MIN_VALUE + 2
} }
}))
override fun order(): Int {
return Integer.MIN_VALUE + 2
}
}))
searchTextField.document.addDocumentListener(object : DocumentAdaptor() { searchTextField.document.addDocumentListener(object : DocumentAdaptor() {
private var state = StringUtils.EMPTY private var state = StringUtils.EMPTY
@@ -233,15 +228,28 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
return this return this
} }
override fun canReconnect(): Boolean {
return false
}
override fun canClose(): Boolean {
return false
}
override fun canClone(): Boolean {
return false
}
override fun dispose() { override fun dispose() {
hostTree.setModel(null)
properties.putString("WelcomeFullContent", fullContent.toString()) properties.putString("WelcomeFullContent", fullContent.toString())
} }
private class HostFindEverywhereResult(val host: Host) : FindEverywhereResult { private class HostFindEverywhereResult(val host: Host) : FindEverywhereResult {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(e: ActionEvent) {
ActionManager.getInstance() ActionManager.getInstance()
.getAction(Actions.OPEN_HOST) .getAction(OpenHostAction.OPEN_HOST)
?.actionPerformed(OpenHostActionEvent(this, host)) ?.actionPerformed(OpenHostActionEvent(e.source, host, e))
} }
override fun getIcon(isSelected: Boolean): Icon { override fun getIcon(isSelected: Boolean): Icon {
@@ -258,5 +266,9 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
} }
} }
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return dataProviderSupport.getData(dataKey)
}
} }

View File

@@ -0,0 +1,72 @@
package app.termora.actions
import app.termora.Actions
import app.termora.ApplicationScope
import app.termora.findeverywhere.FindEverywhereAction
import app.termora.highlight.KeywordHighlightAction
import app.termora.keymgr.KeyManagerAction
import app.termora.macro.MacroAction
import app.termora.tlog.TerminalLoggerAction
import app.termora.transport.SFTPAction
import javax.swing.Action
class ActionManager : org.jdesktop.swingx.action.ActionManager() {
companion object {
fun getInstance(): ActionManager {
return ApplicationScope.forApplicationScope().getOrCreate(ActionManager::class) { ActionManager() }
}
}
init {
setInstance(this)
registerActions()
}
private fun registerActions() {
addAction(NewWindowAction.NEW_WINDOW, NewWindowAction())
addAction(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
addAction(Actions.MULTIPLE, MultipleAction())
addAction(Actions.APP_UPDATE, AppUpdateAction())
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
addAction(Actions.SFTP, SFTPAction())
addAction(Actions.MACRO, MacroAction())
addAction(Actions.KEY_MANAGER, KeyManagerAction())
addAction(SwitchTabAction.SWITCH_TAB, SwitchTabAction())
addAction(TabReconnectAction.RECONNECT_TAB, TabReconnectAction())
addAction(SettingsAction.SETTING, SettingsAction())
addAction(NewHostAction.NEW_HOST, NewHostAction())
addAction(OpenHostAction.OPEN_HOST, OpenHostAction())
addAction(TerminalCopyAction.COPY, TerminalCopyAction())
addAction(TerminalPasteAction.PASTE, TerminalPasteAction())
addAction(TerminalFindAction.FIND, TerminalFindAction())
addAction(TerminalCloseAction.CLOSE, TerminalCloseAction())
addAction(TerminalClearScreenAction.CLEAR_SCREEN, TerminalClearScreenAction())
addAction(OpenLocalTerminalAction.LOCAL_TERMINAL, OpenLocalTerminalAction())
addAction(TerminalSelectAllAction.SELECT_ALL, TerminalSelectAllAction())
addAction(TerminalZoomInAction.ZOOM_IN, TerminalZoomInAction())
addAction(TerminalZoomOutAction.ZOOM_OUT, TerminalZoomOutAction())
addAction(TerminalZoomResetAction.ZOOM_RESET, TerminalZoomResetAction())
}
override fun addAction(action: Action): Action {
val actionId = action.getValue(Action.ACTION_COMMAND_KEY) ?: throw IllegalArgumentException("Invalid action ID")
return addAction(actionId, action)
}
override fun addAction(id: Any, action: Action): Action {
if (getAction(id) != null) {
throw IllegalArgumentException("Action already exists")
}
return super.addAction(id, action)
}
}

View File

@@ -0,0 +1,30 @@
package app.termora.actions
import org.jdesktop.swingx.action.BoundAction
import java.awt.event.ActionEvent
import javax.swing.Icon
abstract class AnAction : BoundAction {
constructor() : super()
constructor(icon: Icon) : super() {
super.putValue(SMALL_ICON, icon)
}
constructor(name: String?) : super(name)
constructor(name: String?, icon: Icon?) : super(name, icon)
final override fun actionPerformed(evt: ActionEvent) {
if (evt is AnActionEvent) {
actionPerformed(evt)
} else {
actionPerformed(AnActionEvent(evt.source, evt.actionCommand, evt))
}
}
protected abstract fun actionPerformed(evt: AnActionEvent)
}

View File

@@ -0,0 +1,61 @@
package app.termora.actions
import app.termora.terminal.DataKey
import java.awt.Component
import java.awt.KeyboardFocusManager
import java.awt.Window
import java.awt.event.ActionEvent
import java.util.*
import javax.swing.JPopupMenu
open class AnActionEvent(
source: Any, command: String,
val event: EventObject
) : ActionEvent(source, AN_ACTION_PERFORMED, command), DataProvider {
companion object {
const val AN_ACTION_PERFORMED = ACTION_PERFORMED + 1
}
val window: Window
get() = getData(DataProviders.TermoraFrame)
?: KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
public override fun consume() {
super.consumed = true
}
public override fun isConsumed(): Boolean {
return super.isConsumed()
}
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
val source = getSource()
if (source !is Component) {
if (source is DataProvider) {
return source.getData(dataKey)
}
return null
} else {
var c = source as Component?
while (c != null) {
if (c is DataProvider) {
val data = c.getData(dataKey)
if (data != null) {
return data
}
}
val p = c.parent
c = if (p == null && c is JPopupMenu) {
c.invoker
} else {
p
}
}
return null
}
}
}

View File

@@ -0,0 +1,117 @@
package app.termora.actions
import app.termora.*
import io.github.g00fy2.versioncompare.Version
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.JXEditorPane
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
import kotlin.concurrent.fixedRateTimer
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
class AppUpdateAction : AnAction(
StringUtils.EMPTY,
Icons.ideUpdate
) {
private val updaterManager get() = UpdaterManager.getInstance()
init {
isEnabled = false
scheduleUpdate()
}
override fun actionPerformed(evt: AnActionEvent) {
showUpdateDialog()
}
@OptIn(DelicateCoroutinesApi::class)
private fun scheduleUpdate() {
fixedRateTimer(
name = "check-update-timer",
initialDelay = 3.minutes.inWholeMilliseconds,
period = 5.hours.inWholeMilliseconds, daemon = true
) {
GlobalScope.launch(Dispatchers.IO) { supervisorScope { launch { checkUpdate() } } }
}
}
private suspend fun checkUpdate() {
val latestVersion = updaterManager.fetchLatestVersion()
if (latestVersion.isSelf) {
return
}
val newVersion = Version(latestVersion.version)
val version = Version(Application.getVersion())
if (newVersion <= version) {
return
}
if (updaterManager.isIgnored(latestVersion.version)) {
return
}
withContext(Dispatchers.Swing) {
ActionManager.getInstance()
.setEnabled(Actions.APP_UPDATE, true)
}
}
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) {
ActionManager.getInstance().setEnabled(Actions.APP_UPDATE, false)
updaterManager.ignore(updaterManager.lastVersion.version)
} else if (option == JOptionPane.YES_OPTION) {
ActionManager.getInstance()
.setEnabled(Actions.APP_UPDATE, false)
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${lastVersion.version}"))
}
}
}

View File

@@ -0,0 +1,21 @@
package app.termora.actions
import app.termora.terminal.DataKey
/**
* 数据提供者,从 [AnActionEvent.source] 开始搜索然后依次 [getData] 获取数据
*/
interface DataProvider {
companion object {
val EMPTY = object : DataProvider {
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return null
}
}
}
/**
* 数据提供
*/
fun <T : Any> getData(dataKey: DataKey<T>): T?
}

View File

@@ -0,0 +1,24 @@
package app.termora.actions
import app.termora.terminal.DataKey
class DataProviderSupport : DataProvider {
private val map = mutableMapOf<DataKey<*>, Any>()
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
if (map.containsKey(dataKey)) {
@Suppress("UNCHECKED_CAST")
return map[dataKey] as T
}
return null
}
fun <T : Any> addData(key: DataKey<T>, data: T) {
map[key] = data
}
fun removeData(key: DataKey<*>) {
map.remove(key)
}
}

View File

@@ -0,0 +1,21 @@
package app.termora.actions
import app.termora.terminal.DataKey
object DataProviders {
val TerminalPanel = DataKey(app.termora.terminal.panel.TerminalPanel::class)
val Terminal = DataKey(app.termora.terminal.Terminal::class)
val PtyConnector = DataKey(app.termora.terminal.PtyConnector::class)
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
val TerminalTab = DataKey(app.termora.TerminalTab::class)
val TerminalTabbedManager = DataKey(app.termora.TerminalTabbedManager::class)
val TermoraFrame = DataKey(app.termora.TermoraFrame::class)
val WindowScope = DataKey(app.termora.WindowScope::class)
object Welcome {
val HostTree = DataKey(app.termora.HostTree::class)
}
}

View File

@@ -0,0 +1,17 @@
package app.termora.actions
import app.termora.*
class MultipleAction : AnAction(
I18n.getString("termora.tools.multiple"),
Icons.vcs
) {
init {
setStateAction()
}
override fun actionPerformed(evt: AnActionEvent) {
ApplicationScope.windowScopes().map { TerminalPanelFactory.getInstance(it) }
.forEach { it.repaintAll() }
}
}

View File

@@ -0,0 +1,46 @@
package app.termora.actions
import app.termora.Host
import app.termora.HostDialog
import app.termora.HostManager
import app.termora.Protocol
import javax.swing.tree.TreePath
class NewHostAction : AnAction() {
companion object {
/**
* 添加主机对话框
*/
const val NEW_HOST = "NewHostAction"
}
private val hostManager get() = HostManager.getInstance()
override fun actionPerformed(evt: AnActionEvent) {
val tree = evt.getData(DataProviders.Welcome.HostTree) ?: return
val model = tree.model
var lastHost = tree.lastSelectedPathComponent ?: model.root
if (lastHost !is Host) {
return
}
if (lastHost.protocol != Protocol.Folder) {
val p = model.getParent(lastHost) ?: return
lastHost = p
}
val dialog = HostDialog(evt.window)
dialog.setLocationRelativeTo(evt.window)
dialog.isVisible = true
val host = (dialog.host ?: return).copy(parentId = lastHost.id)
hostManager.addHost(host)
tree.expandNode(lastHost)
tree.selectionPath = TreePath(model.getPathToRoot(host))
}
}

View File

@@ -0,0 +1,27 @@
package app.termora.actions
import app.termora.I18n
import app.termora.TermoraFrameManager
import java.awt.KeyboardFocusManager
class NewWindowAction : AnAction() {
companion object {
/**
* 打开一个新的窗口
*/
const val NEW_WINDOW = "NewWindowAction"
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.open-new-window"))
putValue(ACTION_COMMAND_KEY, NEW_WINDOW)
}
override fun actionPerformed(evt: AnActionEvent) {
val focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
if (focusedWindow == evt.window) {
TermoraFrameManager.getInstance().createWindow().isVisible = true
}
}
}

View File

@@ -0,0 +1,28 @@
package app.termora.actions
import app.termora.LocalTerminalTab
import app.termora.OpenHostActionEvent
import app.termora.Protocol
import app.termora.SSHTerminalTab
class OpenHostAction : AnAction() {
companion object {
/**
* 打开一个主机
*/
const val OPEN_HOST = "OpenHostAction"
}
override fun actionPerformed(evt: AnActionEvent) {
if (evt !is OpenHostActionEvent) return
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
val tab = if (evt.host.protocol == Protocol.SSH)
SSHTerminalTab(windowScope, evt.host)
else LocalTerminalTab(windowScope, evt.host)
terminalTabbedManager.addTerminalTab(tab)
tab.start()
}
}

View File

@@ -0,0 +1,35 @@
package app.termora.actions
import app.termora.*
class OpenLocalTerminalAction : AnAction(
I18n.getString("termora.find-everywhere.quick-command.local-terminal"),
Icons.terminal
) {
companion object {
const val LOCAL_TERMINAL = "OpenLocalTerminal"
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.open-local-terminal"))
putValue(ACTION_COMMAND_KEY, LOCAL_TERMINAL)
}
override fun actionPerformed(evt: AnActionEvent) {
ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)?.actionPerformed(
OpenHostActionEvent(
evt.source,
Host(
name = name,
protocol = Protocol.Local
),
evt
)
)
evt.consume()
}
}

View File

@@ -0,0 +1,55 @@
package app.termora.actions
import app.termora.ApplicationScope
import app.termora.I18n
import app.termora.Icons
import app.termora.SettingsDialog
import com.formdev.flatlaf.extras.FlatDesktop
import org.apache.commons.lang3.StringUtils
import java.awt.KeyboardFocusManager
import java.awt.event.ActionEvent
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
class SettingsAction : AnAction(
I18n.getString("termora.setting"),
Icons.settings
) {
companion object {
/**
* 打开设置
*/
const val SETTING = "SettingAction"
}
private var isShowing = false
init {
FlatDesktop.setPreferencesHandler {
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
// Doorman 的情况下不允许打开
if (owner != null && ApplicationScope.windowScopes().isNotEmpty()) {
actionPerformed(ActionEvent(owner, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
}
}
}
override fun actionPerformed(evt: AnActionEvent) {
if (isShowing) {
return
}
isShowing = true
val owner = evt.window
val dialog = SettingsDialog(owner)
dialog.addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
this@SettingsAction.isShowing = false
}
})
dialog.setLocationRelativeTo(owner)
dialog.isVisible = true
}
}

View File

@@ -0,0 +1,36 @@
package app.termora.actions
import app.termora.I18n
import java.awt.event.KeyEvent
class SwitchTabAction : AnAction() {
companion object {
const val SWITCH_TAB = "SwitchTabAction"
}
init {
putValue(ACTION_COMMAND_KEY, SWITCH_TAB)
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.switch-tab"))
}
override fun actionPerformed(evt: AnActionEvent) {
val original = evt.event
if (original !is KeyEvent) return
if (original.keyCode !in KeyEvent.VK_1..KeyEvent.VK_9) return
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
val tabs = terminalTabbedManager.getTerminalTabs()
if (tabs.isEmpty()) return
val tabIndex = original.keyCode - KeyEvent.VK_1
if (tabIndex >= tabs.size) {
terminalTabbedManager.setSelectedTerminalTab(tabs.last())
} else {
terminalTabbedManager.setSelectedTerminalTab(tabs[tabIndex])
}
evt.consume()
}
}

View File

@@ -0,0 +1,21 @@
package app.termora.actions
import app.termora.I18n
class TabReconnectAction : AnAction() {
companion object {
const val RECONNECT_TAB = "TabReconnectAction"
}
init {
putValue(ACTION_COMMAND_KEY, RECONNECT_TAB)
putValue(SHORT_DESCRIPTION, I18n.getString("termora.tabbed.contextmenu.reconnect"))
}
override fun actionPerformed(evt: AnActionEvent) {
val tab = evt.getData(DataProviders.TerminalTab) ?: return
if (tab.canReconnect()) {
tab.reconnect()
}
}
}

View File

@@ -0,0 +1,20 @@
package app.termora.actions
class TerminalClearScreenAction : AnAction() {
companion object {
const val CLEAR_SCREEN = "ClearScreen"
}
init {
putValue(SHORT_DESCRIPTION, "Clear Terminal Buffer")
putValue(ACTION_COMMAND_KEY, CLEAR_SCREEN)
}
override fun actionPerformed(evt: AnActionEvent) {
val terminal = evt.getData(DataProviders.Terminal) ?: return
terminal.getDocument().eraseInDisplay(3)
evt.consume()
}
}

View File

@@ -0,0 +1,24 @@
package app.termora.actions
import app.termora.I18n
class TerminalCloseAction : AnAction() {
companion object {
const val CLOSE = "Close"
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.close-tab"))
putValue(ACTION_COMMAND_KEY, CLOSE)
}
override fun actionPerformed(evt: AnActionEvent) {
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
terminalTabbedManager.getSelectedTerminalTab()?.let {
terminalTabbedManager.closeTerminalTab(it)
evt.consume()
}
}
}

View File

@@ -1,26 +1,29 @@
package app.termora.terminal.panel package app.termora.actions
import app.termora.I18n import app.termora.I18n
import com.formdev.flatlaf.util.SystemInfo
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.awt.datatransfer.DataFlavor import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection import java.awt.datatransfer.StringSelection
import java.awt.datatransfer.Transferable import java.awt.datatransfer.Transferable
import java.awt.datatransfer.UnsupportedFlavorException import java.awt.datatransfer.UnsupportedFlavorException
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
class TerminalCopyAction(private val terminalPanel: TerminalPanel) : TerminalAction( class TerminalCopyAction : AnAction() {
KeyStroke.getKeyStroke(KeyEvent.VK_C, terminalPanel.toolkit.menuShortcutKeyMaskEx)
) {
companion object { companion object {
const val COPY = "TerminalCopy"
private val log = LoggerFactory.getLogger(TerminalCopyAction::class.java) private val log = LoggerFactory.getLogger(TerminalCopyAction::class.java)
} }
private val systemClipboard get() = terminalPanel.toolkit.systemClipboard init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.copy-from-terminal"))
putValue(ACTION_COMMAND_KEY, COPY)
}
override fun actionPerformed(e: KeyEvent) { override fun actionPerformed(evt: AnActionEvent) {
val terminalPanel = evt.getData(DataProviders.TerminalPanel) ?: return
val text = terminalPanel.copy() val text = terminalPanel.copy()
val systemClipboard = terminalPanel.toolkit.systemClipboard
evt.consume()
// 如果文本为空,那么清空剪切板 // 如果文本为空,那么清空剪切板
if (text.isEmpty()) { if (text.isEmpty()) {
@@ -31,16 +34,10 @@ class TerminalCopyAction(private val terminalPanel: TerminalPanel) : TerminalAct
systemClipboard.setContents(StringSelection(text), null) systemClipboard.setContents(StringSelection(text), null)
terminalPanel.toast(I18n.getString("termora.terminal.copied")) terminalPanel.toast(I18n.getString("termora.terminal.copied"))
if (log.isTraceEnabled) { if (log.isTraceEnabled) {
log.info("Copy to clipboard. {}", text) log.trace("Copy to clipboard. {}", text)
} }
} }
override fun test(keyStroke: KeyStroke, e: KeyEvent): Boolean {
if (!SystemInfo.isMacOS) {
return false
}
return super.test(keyStroke, e)
}
private class EmptyTransferable : Transferable { private class EmptyTransferable : Transferable {
override fun getTransferDataFlavors(): Array<DataFlavor> { override fun getTransferDataFlavors(): Array<DataFlavor> {
@@ -56,4 +53,5 @@ class TerminalCopyAction(private val terminalPanel: TerminalPanel) : TerminalAct
} }
} }
} }

View File

@@ -0,0 +1,23 @@
package app.termora.actions
import app.termora.I18n
class TerminalFindAction : AnAction() {
companion object {
const val FIND = "TerminalFind"
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.open-terminal-find"))
putValue(ACTION_COMMAND_KEY, FIND)
}
override fun actionPerformed(evt: AnActionEvent) {
val terminalPanel = evt.getData(DataProviders.TerminalPanel) ?: return
terminalPanel.showFind()
evt.consume()
}
}

View File

@@ -0,0 +1,35 @@
package app.termora.actions
import app.termora.I18n
import org.slf4j.LoggerFactory
import java.awt.datatransfer.DataFlavor
class TerminalPasteAction : AnAction() {
companion object {
const val PASTE = "TerminalPaste"
private val log = LoggerFactory.getLogger(TerminalPasteAction::class.java)
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.paste-to-terminal"))
putValue(ACTION_COMMAND_KEY, PASTE)
}
override fun actionPerformed(evt: AnActionEvent) {
val terminalPanel = evt.getData(DataProviders.TerminalPanel) ?: return
val systemClipboard = terminalPanel.toolkit.systemClipboard
if (systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
val text = systemClipboard.getData(DataFlavor.stringFlavor)
if (text is String) {
terminalPanel.paste(text)
if (log.isTraceEnabled) {
log.info("Paste {}", text)
}
}
}
evt.consume()
}
}

View File

@@ -0,0 +1,25 @@
package app.termora.actions
import app.termora.I18n
import app.termora.terminal.Position
class TerminalSelectAllAction : AnAction() {
companion object {
const val SELECT_ALL = "TerminalSelectAll"
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.select-all-in-terminal"))
putValue(ACTION_COMMAND_KEY, SELECT_ALL)
}
override fun actionPerformed(evt: AnActionEvent) {
val terminal = evt.getData(DataProviders.Terminal) ?: return
terminal.getSelectionModel().setSelection(
Position(y = 1, x = 1),
Position(y = terminal.getDocument().getLineCount(), x = terminal.getTerminalModel().getCols())
)
evt.consume()
}
}

View File

@@ -0,0 +1,23 @@
package app.termora.actions
import app.termora.ApplicationScope
import app.termora.Database
import app.termora.TerminalPanelFactory
abstract class TerminalZoomAction : AnAction() {
protected val fontSize get() = Database.getDatabase().terminal.fontSize
abstract fun zoom(): Boolean
override fun actionPerformed(evt: AnActionEvent) {
evt.getData(DataProviders.TerminalPanel) ?: return
if (zoom()) {
ApplicationScope.windowScopes().forEach {
TerminalPanelFactory.getInstance(it)
.fireResize()
}
evt.consume()
}
}
}

View File

@@ -0,0 +1,20 @@
package app.termora.actions
import app.termora.Database
import app.termora.I18n
class TerminalZoomInAction : TerminalZoomAction() {
companion object {
const val ZOOM_IN = "TerminalZoomInAction"
}
init {
putValue(ACTION_COMMAND_KEY, ZOOM_IN)
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.zoom-in-terminal"))
}
override fun zoom(): Boolean {
Database.getDatabase().terminal.fontSize += 2
return true
}
}

View File

@@ -0,0 +1,22 @@
package app.termora.actions
import app.termora.Database
import app.termora.I18n
import kotlin.math.max
class TerminalZoomOutAction : TerminalZoomAction() {
companion object {
const val ZOOM_OUT = "TerminalZoomOutAction"
}
init {
putValue(ACTION_COMMAND_KEY, ZOOM_OUT)
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.zoom-out-terminal"))
}
override fun zoom(): Boolean {
val oldFontSize = fontSize
Database.getDatabase().terminal.fontSize = max(fontSize - 2, 9)
return oldFontSize != fontSize
}
}

View File

@@ -0,0 +1,25 @@
package app.termora.actions
import app.termora.Database
import app.termora.I18n
class TerminalZoomResetAction : TerminalZoomAction() {
companion object {
const val ZOOM_RESET = "TerminalZoomResetAction"
}
init {
putValue(ACTION_COMMAND_KEY, ZOOM_RESET)
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.zoom-reset-terminal"))
}
private val defaultFontSize = 14
override fun zoom(): Boolean {
if (fontSize == defaultFontSize) {
return false
}
Database.getDatabase().terminal.fontSize = defaultFontSize
return true
}
}

View File

@@ -1,11 +1,14 @@
package app.termora.findeverywhere package app.termora.findeverywhere
import app.termora.* import app.termora.DialogWrapper
import app.termora.DynamicColor
import app.termora.I18n
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.macro.MacroFindEverywhereProvider import app.termora.macro.MacroFindEverywhereProvider
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatTextField import com.formdev.flatlaf.extras.components.FlatTextField
import com.jetbrains.JBR import com.jetbrains.JBR
import org.jdesktop.swingx.action.ActionManager
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Dimension import java.awt.Dimension
import java.awt.Insets import java.awt.Insets
@@ -20,24 +23,13 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
private val model = DefaultListModel<FindEverywhereResult>() private val model = DefaultListModel<FindEverywhereResult>()
private val resultList = FindEverywhereXList(model) private val resultList = FindEverywhereXList(model)
private val centerPanel = JPanel(BorderLayout()) private val centerPanel = JPanel(BorderLayout())
private val providers = mutableListOf<FindEverywhereProvider>(
BasicFilterFindEverywhereProvider(QuickCommandFindEverywhereProvider()),
BasicFilterFindEverywhereProvider(SettingsFindEverywhereProvider()),
BasicFilterFindEverywhereProvider(QuickActionsFindEverywhereProvider()),
BasicFilterFindEverywhereProvider(MacroFindEverywhereProvider()),
)
companion object {
private val providers = mutableListOf<FindEverywhereProvider>(
BasicFilterFindEverywhereProvider(QuickCommandFindEverywhereProvider()),
BasicFilterFindEverywhereProvider(SettingsFindEverywhereProvider()),
BasicFilterFindEverywhereProvider(QuickActionsFindEverywhereProvider()),
BasicFilterFindEverywhereProvider(MacroFindEverywhereProvider()),
)
fun registerProvider(provider: FindEverywhereProvider) {
providers.add(provider)
providers.sortBy { it.order() }
}
fun unregisterProvider(provider: FindEverywhereProvider) {
providers.remove(provider)
}
}
init { init {
initView() initView()
@@ -154,7 +146,7 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
action = action =
if (resultList.selectedIndex + 1 == resultList.elementCount) { if (resultList.selectedIndex + 1 == resultList.elementCount) {
object : AnAction() { object : AnAction() {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
resultList.selectedIndex = 1 resultList.selectedIndex = 1
} }
} }
@@ -175,12 +167,12 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
resultList.actionMap.put("action", object : AnAction() { resultList.actionMap.put("action", object : AnAction() {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
if (resultList.selectedIndex < 0) { if (resultList.selectedIndex < 0) {
return return
} }
val event = ActionEvent(e.source, ActionEvent.ACTION_PERFORMED, String()) val event = ActionEvent(evt.source, ActionEvent.ACTION_PERFORMED, String())
// fire // fire
SwingUtilities.invokeLater { model.get(resultList.selectedIndex).actionPerformed(event) } SwingUtilities.invokeLater { model.get(resultList.selectedIndex).actionPerformed(event) }
@@ -203,22 +195,15 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
} }
}) })
}
fun registerProvider(provider: FindEverywhereProvider) {
providers.add(provider)
providers.sortBy { it.order() }
}
addWindowListener(object : WindowAdapter() { fun unregisterProvider(provider: FindEverywhereProvider) {
override fun windowClosed(e: WindowEvent) { providers.remove(provider)
ActionManager.getInstance()
.getAction(Actions.FIND_EVERYWHERE)
.isEnabled = true
}
override fun windowOpened(e: WindowEvent) {
ActionManager.getInstance()
.getAction(Actions.FIND_EVERYWHERE)
.isEnabled = false
}
})
} }
override fun createCenterPanel(): JComponent { override fun createCenterPanel(): JComponent {

View File

@@ -0,0 +1,63 @@
package app.termora.findeverywhere
import app.termora.I18n
import app.termora.Icons
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
import org.apache.commons.lang3.StringUtils
import java.awt.Component
import java.awt.KeyboardFocusManager
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
class FindEverywhereAction : AnAction(StringUtils.EMPTY, Icons.find) {
companion object {
/**
* 查找
*/
const val FIND_EVERYWHERE = "FindEverywhereAction"
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.open-find-everywhere"))
putValue(NAME, I18n.getString("termora.find-everywhere"))
putValue(ACTION_COMMAND_KEY, FIND_EVERYWHERE)
}
override fun actionPerformed(evt: AnActionEvent) {
val scope = evt.getData(DataProviders.WindowScope) ?: return
if (scope.getBoolean("FindEverywhereShown", false)) {
return
}
val source = evt.source
if (source !is Component) {
return
}
val owner = evt.window
val focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
if (owner != focusedWindow) {
return
}
val dialog = FindEverywhere(owner)
for (provider in FindEverywhereProvider.getFindEverywhereProviders(scope)) {
dialog.registerProvider(provider)
}
dialog.setLocationRelativeTo(owner)
dialog.addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
scope.putBoolean("FindEverywhereShown", false)
}
})
dialog.isVisible = true
scope.putBoolean("FindEverywhereShown", true)
}
}

View File

@@ -1,7 +1,21 @@
package app.termora.findeverywhere package app.termora.findeverywhere
import app.termora.Scope
interface FindEverywhereProvider { interface FindEverywhereProvider {
companion object {
@Suppress("UNCHECKED_CAST")
fun getFindEverywhereProviders(scope: Scope): MutableList<FindEverywhereProvider> {
var list = scope.getAnyOrNull("FindEverywhereProviders")
if (list == null) {
list = mutableListOf<FindEverywhereProvider>()
scope.putAny("FindEverywhereProviders", list)
}
return list as MutableList<FindEverywhereProvider>
}
}
/** /**
* 搜索 * 搜索
*/ */

View File

@@ -2,10 +2,16 @@ package app.termora.findeverywhere
import app.termora.Actions import app.termora.Actions
import app.termora.I18n import app.termora.I18n
import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.action.ActionManager
class QuickActionsFindEverywhereProvider : FindEverywhereProvider { class QuickActionsFindEverywhereProvider : FindEverywhereProvider {
private val actions = listOf(Actions.KEY_MANAGER, Actions.KEYWORD_HIGHLIGHT_EVERYWHERE, Actions.MULTIPLE) private val actions = listOf(
Actions.KEY_MANAGER,
Actions.KEYWORD_HIGHLIGHT,
Actions.MULTIPLE,
)
override fun find(pattern: String): List<FindEverywhereResult> { override fun find(pattern: String): List<FindEverywhereResult> {
val actionManager = ActionManager.getInstance() val actionManager = ActionManager.getInstance()
return actions return actions

View File

@@ -1,54 +1,30 @@
package app.termora.findeverywhere package app.termora.findeverywhere
import app.termora.* import app.termora.Actions
import app.termora.I18n
import app.termora.Icons
import app.termora.actions.NewHostAction
import app.termora.actions.OpenLocalTerminalAction
import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLaf
import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.action.ActionManager
import java.awt.event.ActionEvent
import javax.swing.Icon import javax.swing.Icon
class QuickCommandFindEverywhereProvider : FindEverywhereProvider { class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
private val actionManager get() = ActionManager.getInstance()
override fun find(pattern: String): List<FindEverywhereResult> { override fun find(pattern: String): List<FindEverywhereResult> {
val list = mutableListOf<FindEverywhereResult>() val list = mutableListOf<FindEverywhereResult>()
ActionManager.getInstance().getAction(Actions.ADD_HOST)?.let { actionManager.let { list.add(CreateHostFindEverywhereResult()) }
list.add(CreateHostFindEverywhereResult())
}
// Local terminal // Local terminal
list.add(ActionFindEverywhereResult(object : AnAction( actionManager.getAction(OpenLocalTerminalAction.LOCAL_TERMINAL)?.let {
I18n.getString("termora.find-everywhere.quick-command.local-terminal"), list.add(ActionFindEverywhereResult(it))
Icons.terminal }
) {
override fun actionPerformed(evt: ActionEvent) {
ActionManager.getInstance().getAction(Actions.OPEN_HOST)
?.actionPerformed(
OpenHostActionEvent(
this, Host(
name = name,
protocol = Protocol.Local
)
)
)
}
}))
// SFTP // SFTP
list.add(ActionFindEverywhereResult(object : AnAction("SFTP", Icons.fileTransfer) { actionManager.getAction(Actions.SFTP)?.let {
override fun actionPerformed(evt: ActionEvent) { list.add(ActionFindEverywhereResult(it))
val terminalTabbedManager = Application.getService(TerminalTabbedManager::class) }
val tabs = terminalTabbedManager.getTerminalTabs()
for (i in tabs.indices) {
val tab = tabs[i]
if (tab is SFTPTerminalTab) {
terminalTabbedManager.setSelectedTerminalTab(tab)
return
}
}
// 创建一个新的
terminalTabbedManager.addTerminalTab(SFTPTerminalTab())
}
}))
return list return list
} }
@@ -63,7 +39,7 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
} }
private class CreateHostFindEverywhereResult : ActionFindEverywhereResult( private class CreateHostFindEverywhereResult : ActionFindEverywhereResult(
ActionManager.getInstance().getAction(Actions.ADD_HOST) ActionManager.getInstance().getAction(NewHostAction.NEW_HOST)
) { ) {
override fun getIcon(isSelected: Boolean): Icon { override fun getIcon(isSelected: Boolean): Icon {
if (isSelected) { if (isSelected) {

View File

@@ -1,5 +1,6 @@
package app.termora.highlight package app.termora.highlight
import app.termora.ApplicationScope
import app.termora.DialogWrapper import app.termora.DialogWrapper
import app.termora.TerminalFactory import app.termora.TerminalFactory
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
@@ -30,7 +31,8 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
override fun createCenterPanel(): JComponent { override fun createCenterPanel(): JComponent {
val panel = JPanel(GridLayout(2, 8, 4, 4)) val panel = JPanel(GridLayout(2, 8, 4, 4))
val colorPalette = TerminalFactory.instance.createTerminal().getTerminalModel().getColorPalette() val colorPalette = TerminalFactory.getInstance(ApplicationScope.forWindowScope(this))
.createTerminal().getTerminalModel().getColorPalette()
for (i in 1..16) { for (i in 1..16) {
val c = JPanel() val c = JPanel()
c.preferredSize = Dimension(24, 24) c.preferredSize = Dimension(24, 24)

View File

@@ -0,0 +1,18 @@
package app.termora.highlight
import app.termora.I18n
import app.termora.Icons
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
class KeywordHighlightAction : AnAction(
I18n.getString("termora.highlight"),
Icons.edit
) {
override fun actionPerformed(evt: AnActionEvent) {
val owner = evt.window
val dialog = KeywordHighlightDialog(owner)
dialog.setLocationRelativeTo(owner)
dialog.isVisible = true
}
}

View File

@@ -19,8 +19,11 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
private val model = KeywordHighlightTableModel() private val model = KeywordHighlightTableModel()
private val table = FlatTable() private val table = FlatTable()
private val keywordHighlightManager by lazy { KeywordHighlightManager.instance } private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
private val colorPalette by lazy { TerminalFactory.instance.createTerminal().getTerminalModel().getColorPalette() } private val colorPalette by lazy {
TerminalFactory.getInstance(ApplicationScope.forWindowScope(this)).createTerminal().getTerminalModel()
.getColorPalette()
}
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add")) private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
private val editBtn = JButton(I18n.getString("termora.keymgr.edit")) private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))

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