Compare commits
32 Commits
2.0.0-beta
...
2.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5050aa37f5 | ||
|
|
53d3d96a06 | ||
|
|
d40b8a4c9c | ||
|
|
728671509c | ||
|
|
b7178a30fb | ||
|
|
939d6a1fd7 | ||
|
|
2986a9cc46 | ||
|
|
f36afaf5d3 | ||
|
|
8cec835583 | ||
|
|
a32838dad6 | ||
|
|
d54671757e | ||
|
|
d1dba56bcd | ||
|
|
919c06779d | ||
|
|
1c90fb4e18 | ||
|
|
c1f1d5185e | ||
|
|
aa4863712d | ||
|
|
247640f2e5 | ||
|
|
5b1f803fa8 | ||
|
|
accf590c17 | ||
|
|
19fbeab817 | ||
|
|
a785ab4680 | ||
|
|
5ee23cb379 | ||
|
|
145d2de001 | ||
|
|
8d3f5fe622 | ||
|
|
9ce4a88041 | ||
|
|
c0ecc9fa7d | ||
|
|
cb33a4468a | ||
|
|
168c4c5c64 | ||
|
|
9916edbd13 | ||
|
|
ab6b6a2127 | ||
|
|
c45f5f4c92 | ||
|
|
92ee2d72f2 |
127
README.md
@@ -1,53 +1,102 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="./README.zh_CN.md">🇨🇳 简体中文</a>
|
<a href="./README.zh_CN.md">简体中文</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
# Termora
|
# Termora
|
||||||
|
|
||||||
**Termora** is a terminal emulator and SSH client for Windows, macOS and Linux.
|
**Termora** is a cross-platform terminal emulator and SSH client, available on **Windows, macOS, and Linux**.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./docs/readme.png" alt="termora" />
|
<img src="docs/readme.png" alt="Readme" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
**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).
|
Termora is developed using [**Kotlin/JVM**](https://kotlinlang.org/) and partially implements the [**XTerm control sequence protocol**](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html). Its long-term goal is to achieve **full platform support** (including Android, iOS, and iPadOS) via [**Kotlin Multiplatform**](https://kotlinlang.org/docs/multiplatform.html).
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- SSH and local terminal support
|
|
||||||
- Serial port protocol support
|
|
||||||
- [SFTP](./docs/sftp.png?raw=1) & [Command](./docs/sftp-command.png?raw=1) file transfer support
|
|
||||||
- Compatible with Windows, macOS, and Linux
|
|
||||||
- Zmodem protocol support
|
|
||||||
- SSH port forwarding & Jump hosts
|
|
||||||
- Support for X11 and SSH-Agent
|
|
||||||
- Terminal log
|
|
||||||
- Configuration synchronization via [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
|
||||||
- Macro support (record and replay scripts)
|
|
||||||
- Keyword highlighting
|
|
||||||
- Key management
|
|
||||||
- Broadcast commands to multiple sessions
|
|
||||||
- [Find Everywhere](./docs/findeverywhere.png?raw=1) quick navigation
|
|
||||||
- Data encryption
|
|
||||||
- Support [plugins](https://www.termora.app/plugins)
|
|
||||||
- ...
|
|
||||||
|
|
||||||
## Download
|
|
||||||
|
|
||||||
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
|
||||||
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
|
||||||
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
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`.
|
|
||||||
|
|
||||||
|
|
||||||
## LICENSE
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- 🧬 Cross-platform support
|
||||||
|
- 🔐 Built-in key manager
|
||||||
|
- 🖼️ X11 forwarding
|
||||||
|
- 🧑💻 SSH-Agent integration
|
||||||
|
- 💻 System information display
|
||||||
|
- 📁 GUI-based SFTP file management
|
||||||
|
- 📊 Nvidia GPU usage monitoring
|
||||||
|
- ⚡ Quick command shortcuts
|
||||||
|
|
||||||
|
|
||||||
|
## 🚀 File Transfer
|
||||||
|
|
||||||
|
- Direct transfers between server A ↔ B
|
||||||
|
- Recursive folder support
|
||||||
|
- Up to **6 concurrent transfer tasks**
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="docs/transfer.png" alt="Transfer" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 📝 File Editing
|
||||||
|
|
||||||
|
- Auto-upload after editing and saving
|
||||||
|
- Rename files and folders
|
||||||
|
- Quick deletion of large folders (`rm -rf` supported)
|
||||||
|
- Visual permission editing
|
||||||
|
- Create new files and folders
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="docs/transfer-edit.png" alt="Transfer Edit" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 💻 Hosts
|
||||||
|
|
||||||
|
- Tree-like hierarchical structure, similar to folders
|
||||||
|
- Assign tags to individual hosts
|
||||||
|
- Import hosts from other tools
|
||||||
|
- Open with the transfer tool
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="docs/host.png" alt="Transfer Edit" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 🧩 Plugins
|
||||||
|
|
||||||
|
- 🌍 Geo: Display geolocation of hosts
|
||||||
|
- 🔄 Sync: Sync settings to Gist or WebDAV
|
||||||
|
- 🗂️ WebDAV: Connect to WebDAV storage
|
||||||
|
- 📝 Editor: Built-in SFTP file editor
|
||||||
|
- 📡 SMB: Connect to [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
|
||||||
|
- ☁️ S3: Connect to S3 object storage
|
||||||
|
- ☁️ Huawei OBS: Connect to Huawei Cloud OBS
|
||||||
|
- ☁️ Tencent COS: Connect to Tencent Cloud COS
|
||||||
|
- ☁️ Alibaba OSS: Connect to Alibaba Cloud OSS
|
||||||
|
- 👉 [View all plugins...](https://www.termora.app/plugins)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 📦 Download
|
||||||
|
|
||||||
|
- 🧾 [Latest Release](https://github.com/TermoraDev/termora/releases/latest)
|
||||||
|
- 🍺 **Homebrew**: `brew install --cask termora`
|
||||||
|
- 🔨 **WinGet**: `winget install termora`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 🛠️ Development
|
||||||
|
|
||||||
|
We recommend using the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK for development.
|
||||||
|
|
||||||
|
- Run locally: `./gradlew :run`
|
||||||
|
- Build for current OS: `./gradlew :dist`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
This software is distributed under a dual-license model. You may choose one of the following options:
|
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).
|
- **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.
|
- **Proprietary License**: For closed-source or proprietary use, please contact the author to obtain a commercial license.
|
||||||
|
|||||||
116
README.zh_CN.md
@@ -1,48 +1,100 @@
|
|||||||
# Termora
|
# Termora
|
||||||
|
|
||||||
**Termora** 是一个终端模拟器和 SSH 客户端,支持 Windows,macOS 和 Linux。
|
**Termora** 是一款跨平台终端模拟器和 SSH 客户端,支持 **Windows、macOS、Linux**。
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./docs/readme-zh_CN.png" alt="termora" />
|
<img src="docs/readme-zh_CN.png" alt="Readme" />
|
||||||
</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 使用 [**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) & [命令行](./docs/sftp-command.png?raw=1) 文件传输
|
|
||||||
- 支持 Windows、macOS、Linux 平台
|
|
||||||
- 支持 Zmodem 协议
|
|
||||||
- 支持 SSH 端口转发和跳板机
|
|
||||||
- 支持 X11 和 SSH-Agent
|
|
||||||
- 终端日志记录
|
|
||||||
- 支持配置同步到 [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
|
||||||
- 支持宏(录制脚本并回放)
|
|
||||||
- 支持关键词高亮
|
|
||||||
- 支持密钥管理器
|
|
||||||
- 支持将命令发送到多个会话
|
|
||||||
- 支持 [Find Everywhere](./docs/findeverywhere-zh_CN.png?raw=1) 快速跳转
|
|
||||||
- 支持数据加密
|
|
||||||
- 支持[插件](https://www.termora.app/plugins)
|
|
||||||
- ...
|
|
||||||
|
|
||||||
## 下载
|
## ✨ 功能特性
|
||||||
|
|
||||||
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
- 🧬 跨平台运行
|
||||||
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
- 🔐 内建密钥管理器
|
||||||
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
- 🖼️ 支持 X11 转发
|
||||||
|
- 🧑💻 SSH-Agent 集成
|
||||||
|
- 💻 系统信息展示
|
||||||
|
- 📁 图形化 SFTP 文件管理
|
||||||
|
- 📊 Nvidia 显卡使用率查看
|
||||||
|
- ⚡ 快捷指令支持
|
||||||
|
|
||||||
## 开发
|
|
||||||
|
|
||||||
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) 的 JDK 版本,通过 `./gradlew :run` 即可运行程序。
|
## 🚀 文件传输
|
||||||
|
|
||||||
通过 `./gradlew dist` 可以自动构建适用于本机的版本。在 macOS 上是:`dmg`,在 Windows 上是:`zip`,在 Linux 上是:`tar.gz`。
|
- 支持 A ↔ B 服务器间直接传输
|
||||||
|
- 文件夹递归复制支持
|
||||||
|
- 最多可同时运行 **6 个传输任务**
|
||||||
|
|
||||||
## 协议
|
<div align="center">
|
||||||
|
<img src="docs/transfer-zh_CN.png" alt="Transfer" />
|
||||||
|
</div>
|
||||||
|
|
||||||
本软件采用双重许可模式,您可以选择以下任意一种许可方式:
|
|
||||||
|
|
||||||
- AGPL-3.0:根据 [AGPL-3.0](https://opensource.org/license/agpl-v3) 的条款,您可以自由使用、分发和修改本软件。
|
## 📝 文件编辑功能
|
||||||
- 专有许可:如果希望在闭源或专有环境中使用,请联系作者获取许可。
|
|
||||||
|
- 保存后自动上传修改内容
|
||||||
|
- 文件 / 文件夹 重命名
|
||||||
|
- 快速删除大文件夹:`rm -rf` 支持
|
||||||
|
- 可视化更改权限
|
||||||
|
- 支持新建文件 / 文件夹
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="docs/transfer-edit-zh_CN.png" alt="Transfer Edit" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 💻 主机
|
||||||
|
|
||||||
|
- 类似文件夹树形结构
|
||||||
|
- 给主机添加标签
|
||||||
|
- 从其它软件导入
|
||||||
|
- 使用传输工具打开
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="docs/host-zh_CN.png" alt="Transfer Edit" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
## 🧩 插件
|
||||||
|
|
||||||
|
- 🌍 Geo:显示主机位置信息
|
||||||
|
- 🔄 Sync:将配置同步至 Gist 或 WebDAV
|
||||||
|
- 🗂️ WebDAV:连接 WebDAV 对象存储
|
||||||
|
- 📝 Editor:内置 SFTP 文件编辑器
|
||||||
|
- 📡 SMB: 连接 [SMB](https://baike.baidu.com/item/smb/4750512) 文件共享协议
|
||||||
|
- ☁️ S3:连接 S3 对象存储
|
||||||
|
- ☁️ Huawei OBS:连接华为云对象存储
|
||||||
|
- ☁️ Tencent COS:连接腾讯云 COS
|
||||||
|
- ☁️ Alibaba OSS:连接阿里云 OSS
|
||||||
|
- 👉 [查看所有插件...](https://www.termora.cn/plugins)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 📦 下载
|
||||||
|
|
||||||
|
- 🧾 [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||||
|
- 🍺 **Homebrew**:`brew install --cask termora`
|
||||||
|
- 🪟 **WinGet**:`winget install termora`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 🛠️ 开发指南
|
||||||
|
|
||||||
|
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK 运行环境。
|
||||||
|
|
||||||
|
- 本地运行:`./gradlew :run`
|
||||||
|
- 构建当前系统安装包:`./gradlew :dist`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 📄 授权协议
|
||||||
|
|
||||||
|
Termora 采用双重许可方式,您可以选择:
|
||||||
|
|
||||||
|
- **AGPL-3.0**:自由使用、修改、分发(遵循 [AGPL 条款](https://opensource.org/license/agpl-v3))
|
||||||
|
- **专有许可**:如需闭源或商业用途,请联系作者获取授权
|
||||||
|
|||||||
@@ -137,10 +137,6 @@ application {
|
|||||||
|
|
||||||
args.add("-DTERMORA_PLUGIN_DIRECTORY=${layout.buildDirectory.get().asFile.absolutePath}${File.separator}plugins")
|
args.add("-DTERMORA_PLUGIN_DIRECTORY=${layout.buildDirectory.get().asFile.absolutePath}${File.separator}plugins")
|
||||||
|
|
||||||
if (os.isLinux) {
|
|
||||||
args.add("-Dsun.java2d.opengl=true")
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationDefaultJvmArgs = args
|
applicationDefaultJvmArgs = args
|
||||||
mainClass = "app.termora.MainKt"
|
mainClass = "app.termora.MainKt"
|
||||||
}
|
}
|
||||||
@@ -441,6 +437,7 @@ tasks.register<Exec>("jpackage") {
|
|||||||
// NSWindow
|
// NSWindow
|
||||||
options.add("-Dapple.awt.application.appearance=system")
|
options.add("-Dapple.awt.application.appearance=system")
|
||||||
options.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
options.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||||
|
options.add("--add-opens java.desktop/sun.font=ALL-UNNAMED")
|
||||||
options.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
options.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||||
options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||||
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||||
@@ -448,7 +445,6 @@ tasks.register<Exec>("jpackage") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (os.isLinux) {
|
if (os.isLinux) {
|
||||||
options.add("-Dsun.java2d.opengl=true")
|
|
||||||
if (isDeb) {
|
if (isDeb) {
|
||||||
options.add("-Djpackage.app-layout=deb")
|
options.add("-Djpackage.app-layout=deb")
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 61 KiB |
BIN
docs/host-zh_CN.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/host.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
docs/plugins-zh_CN.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/plugins.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 166 KiB |
BIN
docs/readme.png
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 55 KiB |
BIN
docs/sftp.png
|
Before Width: | Height: | Size: 49 KiB |
BIN
docs/tags-zh_CN.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/tags.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
docs/transfer-edit-zh_CN.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
docs/transfer-edit.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
docs/transfer-zh_CN.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
docs/transfer.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
@@ -5,7 +5,7 @@ pty4j = "0.13.6"
|
|||||||
tinylog = "2.7.0"
|
tinylog = "2.7.0"
|
||||||
kotlinx-coroutines = "1.10.2"
|
kotlinx-coroutines = "1.10.2"
|
||||||
flatlaf = "3.6"
|
flatlaf = "3.6"
|
||||||
kotlinx-serialization-json = "1.8.1"
|
kotlinx-serialization-json = "1.9.0"
|
||||||
commons-codec = "1.18.0"
|
commons-codec = "1.18.0"
|
||||||
commons-lang3 = "3.17.0"
|
commons-lang3 = "3.17.0"
|
||||||
commons-csv = "1.14.0"
|
commons-csv = "1.14.0"
|
||||||
@@ -37,17 +37,17 @@ rhino = "1.8.0"
|
|||||||
delight-rhino-sandbox = "0.0.17"
|
delight-rhino-sandbox = "0.0.17"
|
||||||
testcontainers = "1.21.3"
|
testcontainers = "1.21.3"
|
||||||
mixpanel = "1.5.3"
|
mixpanel = "1.5.3"
|
||||||
jSerialComm = "2.11.0"
|
jSerialComm = "2.11.2"
|
||||||
ini4j = "0.5.5-2"
|
ini4j = "0.5.5-2"
|
||||||
restart4j = "0.0.1"
|
restart4j = "0.0.1"
|
||||||
eddsa = "0.3.0"
|
eddsa = "0.3.0"
|
||||||
exposed = "1.0.0-beta-2"
|
exposed = "1.0.0-beta-3"
|
||||||
h2 = "2.3.232"
|
h2 = "2.3.232"
|
||||||
sqlite = "3.50.2.0"
|
sqlite = "3.50.2.0"
|
||||||
jug = "5.1.0"
|
jug = "5.1.0"
|
||||||
semver4j = "6.0.0"
|
semver4j = "6.0.0"
|
||||||
jsvg = "1.4.0"
|
jsvg = "1.4.0"
|
||||||
dom4j = "2.1.4"
|
dom4j = "2.2.0"
|
||||||
|
|
||||||
[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" }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
}
|
}
|
||||||
|
|
||||||
project.version = "0.0.2"
|
project.version = "0.0.3"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugins.cos
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -13,7 +14,7 @@ class COSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
|
|||||||
return COSProtocolProvider.instance
|
return COSProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return COSProtocolHostPanel()
|
return COSProtocolHostPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
}
|
}
|
||||||
|
|
||||||
project.version = "0.0.5"
|
project.version = "0.0.6"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import app.termora.tree.HostTreeNode
|
|||||||
import app.termora.tree.MarkerSimpleTreeCellAnnotation
|
import app.termora.tree.MarkerSimpleTreeCellAnnotation
|
||||||
import app.termora.tree.SimpleTreeCellAnnotation
|
import app.termora.tree.SimpleTreeCellAnnotation
|
||||||
import app.termora.tree.SimpleTreeCellRendererExtension
|
import app.termora.tree.SimpleTreeCellRendererExtension
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import java.awt.Color
|
import java.awt.Color
|
||||||
import javax.swing.JTree
|
import javax.swing.JTree
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ class GeoSimpleTreeCellRendererExtension private constructor() : SimpleTreeCellR
|
|||||||
if (GeoHostTreeShowMoreEnableExtension.instance.isShowMore().not()) return emptyList()
|
if (GeoHostTreeShowMoreEnableExtension.instance.isShowMore().not()) return emptyList()
|
||||||
val country = geo.country(node.data.host) ?: return emptyList()
|
val country = geo.country(node.data.host) ?: return emptyList()
|
||||||
|
|
||||||
val text = "${countryCodeToFlagEmoji(country.isoCode)}${country.name}"
|
val text = if (SystemInfo.isMacOS) "${countryCodeToFlagEmoji(country.isoCode)}${country.name}" else country.name
|
||||||
return listOf(
|
return listOf(
|
||||||
MarkerSimpleTreeCellAnnotation(
|
MarkerSimpleTreeCellAnnotation(
|
||||||
text,
|
text,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
project.version = "0.0.1"
|
project.version = "0.0.2"
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugins.obs
|
package app.termora.plugins.obs
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -13,7 +14,7 @@ class OBSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
|
|||||||
return OBSProtocolProvider.instance
|
return OBSProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return OBSProtocolHostPanel()
|
return OBSProtocolHostPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,11 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
}
|
}
|
||||||
|
|
||||||
project.version = "0.0.2"
|
project.version = "0.0.3"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
implementation("com.aliyun.oss:aliyun-sdk-oss:3.18.2")
|
implementation("com.aliyun.oss:aliyun-sdk-oss:3.18.3")
|
||||||
implementation("javax.xml.bind:jaxb-api:2.3.1")
|
implementation("javax.xml.bind:jaxb-api:2.3.1")
|
||||||
implementation("javax.activation:activation:1.1.1")
|
implementation("javax.activation:activation:1.1.1")
|
||||||
implementation("org.glassfish.jaxb:jaxb-runtime:2.3.3")
|
implementation("org.glassfish.jaxb:jaxb-runtime:2.3.3")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugins.oss
|
package app.termora.plugins.oss
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -13,7 +14,7 @@ class OSSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
|
|||||||
return OSSProtocolProvider.instance
|
return OSSProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return OSSProtocolHostPanel()
|
return OSSProtocolHostPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
project.version = "0.0.5"
|
project.version = "0.0.6"
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugins.s3
|
package app.termora.plugins.s3
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -13,7 +14,7 @@ class S3ProtocolHostPanelExtension private constructor() : ProtocolHostPanelExte
|
|||||||
return S3ProtocolProvider.instance
|
return S3ProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return S3ProtocolHostPanel()
|
return S3ProtocolHostPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
}
|
}
|
||||||
|
|
||||||
project.version = "0.0.1"
|
project.version = "0.0.2"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugins.smb
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -13,7 +14,7 @@ class SMBProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
|
|||||||
return SMBProtocolProvider.instance
|
return SMBProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return SMBProtocolHostPanel()
|
return SMBProtocolHostPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
project.version = "0.0.2"
|
project.version = "0.0.3"
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
}
|
}
|
||||||
|
|
||||||
project.version = "0.0.1"
|
project.version = "0.0.2"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugins.webdav
|
package app.termora.plugins.webdav
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -13,7 +14,7 @@ class WebDAVProtocolHostPanelExtension private constructor() : ProtocolHostPanel
|
|||||||
return WebDAVProtocolProvider.instance
|
return WebDAVProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return WebDAVProtocolHostPanel()
|
return WebDAVProtocolHostPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ record ExtensionProxy(Plugin plugin, Extension extension) implements InvocationH
|
|||||||
}
|
}
|
||||||
throw new IllegalCallerException(target.getMessage(), target);
|
throw new IllegalCallerException(target.getMessage(), target);
|
||||||
}
|
}
|
||||||
throw e;
|
throw target;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,7 @@ import org.apache.commons.lang3.LocaleUtils
|
|||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.MenuItem
|
import java.awt.*
|
||||||
import java.awt.PopupMenu
|
|
||||||
import java.awt.SystemTray
|
|
||||||
import java.awt.TrayIcon
|
|
||||||
import java.awt.desktop.AppReopenedEvent
|
import java.awt.desktop.AppReopenedEvent
|
||||||
import java.awt.desktop.AppReopenedListener
|
import java.awt.desktop.AppReopenedListener
|
||||||
import java.awt.desktop.SystemEventListener
|
import java.awt.desktop.SystemEventListener
|
||||||
@@ -173,7 +170,6 @@ class ApplicationRunner {
|
|||||||
private fun setupLaf() {
|
private fun setupLaf() {
|
||||||
|
|
||||||
System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux || SystemInfo.isWindows}")
|
System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux || SystemInfo.isWindows}")
|
||||||
System.setProperty(FlatSystemProperties.USE_ROUNDED_POPUP_BORDER, "false")
|
|
||||||
|
|
||||||
if (SystemInfo.isLinux) {
|
if (SystemInfo.isLinux) {
|
||||||
JFrame.setDefaultLookAndFeelDecorated(true)
|
JFrame.setDefaultLookAndFeelDecorated(true)
|
||||||
@@ -197,12 +193,13 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
themeManager.change(theme, true)
|
themeManager.change(theme, true)
|
||||||
|
|
||||||
|
if (Application.isBetaVersion()) {
|
||||||
FlatInspector.install("ctrl shift X")
|
FlatInspector.install("ctrl shift 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)
|
||||||
UIManager.put("TitlePane.useWindowDecorations", false)
|
UIManager.put(FlatClientProperties.POPUP_FORCE_HEAVY_WEIGHT, true)
|
||||||
|
|
||||||
UIManager.put("Component.arc", 5)
|
UIManager.put("Component.arc", 5)
|
||||||
UIManager.put("TextComponent.arc", UIManager.getInt("Component.arc"))
|
UIManager.put("TextComponent.arc", UIManager.getInt("Component.arc"))
|
||||||
@@ -213,7 +210,6 @@ class ApplicationRunner {
|
|||||||
UIManager.put("Dialog.width", 650)
|
UIManager.put("Dialog.width", 650)
|
||||||
UIManager.put("Dialog.height", 550)
|
UIManager.put("Dialog.height", 550)
|
||||||
|
|
||||||
|
|
||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isMacOS) {
|
||||||
UIManager.put("TabbedPane.tabHeight", UIManager.getInt("TitleBar.height"))
|
UIManager.put("TabbedPane.tabHeight", UIManager.getInt("TitleBar.height"))
|
||||||
} else if (SystemInfo.isLinux) {
|
} else if (SystemInfo.isLinux) {
|
||||||
@@ -231,15 +227,33 @@ class ApplicationRunner {
|
|||||||
UIManager.put("Table.rowHeight", 24)
|
UIManager.put("Table.rowHeight", 24)
|
||||||
UIManager.put("Table.focusCellHighlightBorder", FlatTableCellBorder.Default())
|
UIManager.put("Table.focusCellHighlightBorder", FlatTableCellBorder.Default())
|
||||||
UIManager.put("Table.focusSelectedCellHighlightBorder", FlatTableCellBorder.Default())
|
UIManager.put("Table.focusSelectedCellHighlightBorder", FlatTableCellBorder.Default())
|
||||||
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
|
||||||
|
|
||||||
UIManager.put("Tree.rowHeight", 24)
|
UIManager.put("Tree.rowHeight", 24)
|
||||||
UIManager.put("Tree.background", DynamicColor("window"))
|
UIManager.put("Tree.background", DynamicColor("window"))
|
||||||
UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc"))
|
|
||||||
UIManager.put("Tree.showCellFocusIndicator", false)
|
UIManager.put("Tree.showCellFocusIndicator", false)
|
||||||
UIManager.put("Tree.repaintWholeRow", true)
|
UIManager.put("Tree.repaintWholeRow", true)
|
||||||
|
|
||||||
|
// Linux 更多的是尖锐风格
|
||||||
|
if (SystemInfo.isMacOS || SystemInfo.isWindows) {
|
||||||
|
val selectionInsets = Insets(0, 2, 0, 2)
|
||||||
|
UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("Tree.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("List.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
|
UIManager.put("ComboBox.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("ComboBox.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
|
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("Table.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
|
UIManager.put("MenuBar.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("MenuBar.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
|
UIManager.put("MenuItem.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("MenuItem.selectionInsets", selectionInsets)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class BannerPanel(fontSize: Int = 11, val beautiful: Boolean = false) : JCompone
|
|||||||
size = preferredSize
|
size = preferredSize
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun paintComponent(g: Graphics) {
|
public override fun paintComponent(g: Graphics) {
|
||||||
if (g is Graphics2D) {
|
if (g is Graphics2D) {
|
||||||
g.setRenderingHints(
|
g.setRenderingHints(
|
||||||
RenderingHints(
|
RenderingHints(
|
||||||
@@ -33,7 +33,7 @@ class BannerPanel(fontSize: Int = 11, val beautiful: Boolean = false) : JCompone
|
|||||||
}
|
}
|
||||||
|
|
||||||
g.font = font
|
g.font = font
|
||||||
g.color = UIManager.getColor("TextField.placeholderForeground")
|
g.color = foreground ?: UIManager.getColor("TextField.placeholderForeground")
|
||||||
|
|
||||||
val height = g.fontMetrics.height
|
val height = g.fontMetrics.height
|
||||||
val descent = g.fontMetrics.descent
|
val descent = g.fontMetrics.descent
|
||||||
|
|||||||
71
src/main/kotlin/app/termora/FontComboBox.kt
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
|
import com.formdev.flatlaf.util.FontUtils
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.Dimension
|
||||||
|
import javax.swing.DefaultListCellRenderer
|
||||||
|
import javax.swing.JList
|
||||||
|
import javax.swing.event.PopupMenuEvent
|
||||||
|
import javax.swing.event.PopupMenuListener
|
||||||
|
|
||||||
|
class FontComboBox : FlatComboBox<String>() {
|
||||||
|
private var fontsLoaded = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
val fontComboBox = this
|
||||||
|
fontComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
init {
|
||||||
|
preferredSize = Dimension(preferredSize.width, fontComboBox.preferredSize.height - 2)
|
||||||
|
maximumSize = Dimension(preferredSize.width, preferredSize.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = value
|
||||||
|
if (text is String) {
|
||||||
|
if (text.isBlank()) {
|
||||||
|
text = "<None>"
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
"<html><font face='$text'>$text</font></html>",
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fontComboBox.maximumSize = fontComboBox.preferredSize
|
||||||
|
|
||||||
|
fontComboBox.addPopupMenuListener(object : PopupMenuListener {
|
||||||
|
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
||||||
|
if (fontsLoaded) return
|
||||||
|
val selectedItem = fontComboBox.selectedItem
|
||||||
|
val families = getItems()
|
||||||
|
for (family in FontUtils.getAvailableFontFamilyNames()) {
|
||||||
|
if (families.contains(family).not()) fontComboBox.addItem(family)
|
||||||
|
}
|
||||||
|
fontComboBox.selectedItem = selectedItem
|
||||||
|
fontsLoaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {}
|
||||||
|
override fun popupMenuCanceled(e: PopupMenuEvent) {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getItems(): Set<String> {
|
||||||
|
val families = mutableSetOf<String>()
|
||||||
|
for (i in 0 until itemCount) families.add(getItemAt(i))
|
||||||
|
return families
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
@@ -8,7 +9,9 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.beans.PropertyChangeEvent
|
import java.beans.PropertyChangeEvent
|
||||||
|
import java.util.*
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
|
|
||||||
abstract class HostTerminalTab(
|
abstract class HostTerminalTab(
|
||||||
@@ -20,6 +23,10 @@ abstract class HostTerminalTab(
|
|||||||
val Host = DataKey(app.termora.Host::class)
|
val Host = DataKey(app.termora.Host::class)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected val terminalTabbedManager
|
||||||
|
get() = AnActionEvent(getJComponent(), StringUtils.EMPTY, EventObject(getJComponent()))
|
||||||
|
.getData(DataProviders.TerminalTabbedManager)
|
||||||
protected val coroutineScope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.Swing) }
|
protected val coroutineScope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.Swing) }
|
||||||
protected val terminalModel get() = terminal.getTerminalModel()
|
protected val terminalModel get() = terminal.getTerminalModel()
|
||||||
protected var unread = false
|
protected var unread = false
|
||||||
@@ -42,9 +49,12 @@ abstract class HostTerminalTab(
|
|||||||
if (hasFocus || unread) {
|
if (hasFocus || unread) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 如果当前选中的不是这个 Tab,那么设置成未读
|
||||||
|
if (terminalTabbedManager?.getSelectedTerminalTab() != this@HostTerminalTab) {
|
||||||
unread = true
|
unread = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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 dataColumn by lazy { DynamicIcon("icons/dataColumn.svg", "icons/dataColumn_dark.svg") }
|
||||||
val dbms by lazy { DynamicIcon("icons/dbms.svg", "icons/dbms_dark.svg") }
|
val dbms by lazy { DynamicIcon("icons/dbms.svg", "icons/dbms_dark.svg") }
|
||||||
val newUI by lazy { DynamicIcon("icons/newUI.svg", "icons/newUI.svg") }
|
val newUI by lazy { DynamicIcon("icons/newUI.svg", "icons/newUI.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") }
|
||||||
@@ -33,6 +34,7 @@ object Icons {
|
|||||||
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 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 breakpoint by lazy { DynamicIcon("icons/breakpoint.svg", "icons/breakpoint_dark.svg") }
|
||||||
val softWrap by lazy { DynamicIcon("icons/softWrap.svg", "icons/softWrap_dark.svg") }
|
val softWrap by lazy { DynamicIcon("icons/softWrap.svg", "icons/softWrap_dark.svg") }
|
||||||
val scrollUp by lazy { DynamicIcon("icons/scrollUp.svg", "icons/scrollUp_dark.svg") }
|
val scrollUp by lazy { DynamicIcon("icons/scrollUp.svg", "icons/scrollUp_dark.svg") }
|
||||||
val reformatCode by lazy { DynamicIcon("icons/reformatCode.svg", "icons/reformatCode_dark.svg") }
|
val reformatCode by lazy { DynamicIcon("icons/reformatCode.svg", "icons/reformatCode_dark.svg") }
|
||||||
@@ -77,6 +79,7 @@ object Icons {
|
|||||||
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") }
|
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") }
|
||||||
val export by lazy { DynamicIcon("icons/export.svg", "icons/export_dark.svg") }
|
val export by lazy { DynamicIcon("icons/export.svg", "icons/export_dark.svg") }
|
||||||
val terminal by lazy { DynamicIcon("icons/terminal.svg", "icons/terminal_dark.svg") }
|
val terminal by lazy { DynamicIcon("icons/terminal.svg", "icons/terminal_dark.svg") }
|
||||||
|
val telnet by lazy { DynamicIcon("icons/telnet.svg", "icons/telnet_dark.svg") }
|
||||||
val ssh by lazy { DynamicIcon("icons/ssh.svg", "icons/ssh_dark.svg") }
|
val ssh by lazy { DynamicIcon("icons/ssh.svg", "icons/ssh_dark.svg") }
|
||||||
val ftp by lazy { DynamicIcon("icons/ftp.svg", "icons/ftp_dark.svg") }
|
val ftp by lazy { DynamicIcon("icons/ftp.svg", "icons/ftp_dark.svg") }
|
||||||
val minio by lazy { DynamicIcon("icons/minio.svg", "icons/minio_dark.svg") }
|
val minio by lazy { DynamicIcon("icons/minio.svg", "icons/minio_dark.svg") }
|
||||||
|
|||||||
126
src/main/kotlin/app/termora/JSplitPaneWithZeroSizeDivider.kt
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.ui.FlatSplitPaneUI
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Cursor
|
||||||
|
import java.awt.Graphics
|
||||||
|
import java.awt.event.MouseAdapter
|
||||||
|
import java.awt.event.MouseEvent
|
||||||
|
import java.awt.event.MouseMotionAdapter
|
||||||
|
import java.util.function.Supplier
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
class SplitPaneUI : FlatSplitPaneUI() {
|
||||||
|
public override fun startDragging() {
|
||||||
|
super.startDragging()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun dragDividerTo(location: Int) {
|
||||||
|
super.dragDividerTo(location)
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun finishDraggingTo(location: Int) {
|
||||||
|
super.finishDraggingTo(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JSplitPaneWithZeroSizeDivider(
|
||||||
|
private val splitPane: JSplitPane,
|
||||||
|
private val topOffset: Supplier<Int>,
|
||||||
|
) : JPanel(BorderLayout()) {
|
||||||
|
|
||||||
|
private val dividerDragSize = 7
|
||||||
|
private val layeredPane = LayeredPane()
|
||||||
|
private val divider = Divider()
|
||||||
|
|
||||||
|
init {
|
||||||
|
layeredPane.add(splitPane, JLayeredPane.DEFAULT_LAYER as Any)
|
||||||
|
layeredPane.add(divider, JLayeredPane.PALETTE_LAYER as Any)
|
||||||
|
add(layeredPane, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class Divider : JComponent() {
|
||||||
|
private var dragging = false
|
||||||
|
private var dragStartX = 0
|
||||||
|
private var initialDividerLocation = 0
|
||||||
|
private val splitPaneUI get() = splitPane.ui as SplitPaneUI
|
||||||
|
|
||||||
|
init {
|
||||||
|
cursor = Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR)
|
||||||
|
addMouseListener(object : MouseAdapter() {
|
||||||
|
override fun mousePressed(e: MouseEvent) {
|
||||||
|
if (SwingUtilities.isLeftMouseButton(e)) {
|
||||||
|
dragging = true
|
||||||
|
dragStartX = e.xOnScreen
|
||||||
|
initialDividerLocation = splitPane.dividerLocation
|
||||||
|
splitPaneUI.startDragging()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseReleased(e: MouseEvent) {
|
||||||
|
if (SwingUtilities.isLeftMouseButton(e)) {
|
||||||
|
if (dragging) {
|
||||||
|
val deltaX = e.xOnScreen - dragStartX
|
||||||
|
val newLocation = initialDividerLocation + deltaX
|
||||||
|
splitPaneUI.finishDraggingTo(newLocation)
|
||||||
|
}
|
||||||
|
dragging = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
addMouseMotionListener(object : MouseMotionAdapter() {
|
||||||
|
override fun mouseDragged(e: MouseEvent) {
|
||||||
|
if (dragging) {
|
||||||
|
val deltaX = e.xOnScreen - dragStartX
|
||||||
|
val newLocation = initialDividerLocation + deltaX
|
||||||
|
splitPaneUI.dragDividerTo(newLocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun paint(g: Graphics) {
|
||||||
|
g.color = UIManager.getColor("controlShadow")
|
||||||
|
g.fillRect(width / 2, 0, 1, height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class LayeredPane : JLayeredPane() {
|
||||||
|
private val w get() = (dividerDragSize - 1) / 2
|
||||||
|
|
||||||
|
override fun doLayout() {
|
||||||
|
synchronized(treeLock) {
|
||||||
|
for (c in components) {
|
||||||
|
if (c == divider) {
|
||||||
|
c.isVisible = splitPane.leftComponent.isVisible
|
||||||
|
c.setBounds(
|
||||||
|
splitPane.dividerLocation - w,
|
||||||
|
topOffset.get(),
|
||||||
|
dividerDragSize,
|
||||||
|
height - topOffset.get()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
c.setBounds(0, 0, width, height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun paint(g: Graphics) {
|
||||||
|
super.paint(g)
|
||||||
|
if (divider.isVisible) {
|
||||||
|
g.color = UIManager.getColor("controlShadow")
|
||||||
|
g.fillRect(splitPane.dividerLocation, 0, 1, topOffset.get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doLayout() {
|
||||||
|
super.doLayout()
|
||||||
|
layeredPane.doLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -9,6 +9,12 @@ import java.awt.Window
|
|||||||
object NativeMacLibrary {
|
object NativeMacLibrary {
|
||||||
private val log = LoggerFactory.getLogger(NativeMacLibrary::class.java)
|
private val log = LoggerFactory.getLogger(NativeMacLibrary::class.java)
|
||||||
|
|
||||||
|
enum class NSWindowButton {
|
||||||
|
NSWindowCloseButton,
|
||||||
|
NSWindowMiniaturizeButton,
|
||||||
|
NSWindowZoomButton,
|
||||||
|
}
|
||||||
|
|
||||||
fun getNSWindow(window: Window): Long? {
|
fun getNSWindow(window: Window): Long? {
|
||||||
try {
|
try {
|
||||||
val peerField = Component::class.java.getDeclaredField("peer") ?: return null
|
val peerField = Component::class.java.getDeclaredField("peer") ?: return null
|
||||||
@@ -31,13 +37,17 @@ object NativeMacLibrary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setControlsVisible(window: Window, visible: Boolean) {
|
fun setControlsVisible(
|
||||||
|
window: Window,
|
||||||
|
visible: Boolean,
|
||||||
|
buttons: Array<NSWindowButton> = NSWindowButton.entries.toTypedArray()
|
||||||
|
) {
|
||||||
val nsWindow = ID(getNSWindow(window) ?: return)
|
val nsWindow = ID(getNSWindow(window) ?: return)
|
||||||
try {
|
try {
|
||||||
Foundation.executeOnMainThread(true, true) {
|
Foundation.executeOnMainThread(true, true) {
|
||||||
for (i in 0..2) {
|
for (button in buttons) {
|
||||||
val button = Foundation.invoke(nsWindow, "standardWindowButton:", i)
|
val button = Foundation.invoke(nsWindow, "standardWindowButton:", button.ordinal)
|
||||||
Foundation.invoke(button, "setHidden:", !visible)
|
Foundation.invoke(button, "setHidden:", visible.not())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.protocol.*
|
import app.termora.protocol.*
|
||||||
@@ -18,7 +19,11 @@ import java.awt.Dimension
|
|||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
class NewHostDialogV2(owner: Window, private val editHost: Host? = null) : DialogWrapper(owner) {
|
class NewHostDialogV2(
|
||||||
|
owner: Window,
|
||||||
|
private val editHost: Host? = null,
|
||||||
|
private val accountOwner: AccountOwner,
|
||||||
|
) : DialogWrapper(owner) {
|
||||||
|
|
||||||
private object Current {
|
private object Current {
|
||||||
var card: ProtocolHostPanel? = null
|
var card: ProtocolHostPanel? = null
|
||||||
@@ -65,11 +70,11 @@ class NewHostDialogV2(owner: Window, private val editHost: Host? = null) : Dialo
|
|||||||
toolbar.add(Box.createHorizontalGlue())
|
toolbar.add(Box.createHorizontalGlue())
|
||||||
|
|
||||||
val extensions = ProtocolHostPanelExtension.extensions
|
val extensions = ProtocolHostPanelExtension.extensions
|
||||||
.filter { it.canCreateProtocolHostPanel() }
|
.filter { it.canCreateProtocolHostPanel(accountOwner) }
|
||||||
for ((index, extension) in extensions.withIndex()) {
|
for ((index, extension) in extensions.withIndex()) {
|
||||||
val protocol = extension.getProtocolProvider().getProtocol()
|
val protocol = extension.getProtocolProvider().getProtocol()
|
||||||
val icon = ScaleIcon(extension.getProtocolProvider().getIcon(), 22)
|
val icon = ScaleIcon(extension.getProtocolProvider().getIcon(), 22)
|
||||||
val hostPanel = extension.createProtocolHostPanel()
|
val hostPanel = extension.createProtocolHostPanel(accountOwner)
|
||||||
val button = JToggleButton(protocol, icon).apply { buttonGroup.add(this) }
|
val button = JToggleButton(protocol, icon).apply { buttonGroup.add(this) }
|
||||||
button.setVerticalTextPosition(SwingConstants.BOTTOM)
|
button.setVerticalTextPosition(SwingConstants.BOTTOM)
|
||||||
button.setHorizontalTextPosition(SwingConstants.CENTER)
|
button.setHorizontalTextPosition(SwingConstants.CENTER)
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ abstract class PtyHostTerminalTab(
|
|||||||
|
|
||||||
private var readerJob: Job? = null
|
private var readerJob: Job? = null
|
||||||
private val ptyConnectorDelegate = PtyConnectorDelegate()
|
private val ptyConnectorDelegate = PtyConnectorDelegate()
|
||||||
protected val terminalPanel = TerminalPanelFactory.getInstance().createTerminalPanel(terminal, ptyConnectorDelegate)
|
protected val terminalPanel = TerminalPanelFactory.getInstance().createTerminalPanel(
|
||||||
|
this,
|
||||||
|
terminal, ptyConnectorDelegate
|
||||||
|
)
|
||||||
protected val ptyConnectorFactory get() = PtyConnectorFactory.getInstance()
|
protected val ptyConnectorFactory get() = PtyConnectorFactory.getInstance()
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ open class Scope(
|
|||||||
return get(clazz)
|
return get(clazz)
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized(clazz) {
|
synchronized(this) {
|
||||||
if (beans.containsKey(clazz)) {
|
if (beans.containsKey(clazz)) {
|
||||||
return get(clazz)
|
return get(clazz)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import com.formdev.flatlaf.FlatClientProperties
|
|||||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
import com.formdev.flatlaf.util.FontUtils
|
|
||||||
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
|
||||||
@@ -29,7 +28,6 @@ import org.apache.commons.lang3.StringUtils
|
|||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.Dimension
|
|
||||||
import java.awt.Toolkit
|
import java.awt.Toolkit
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
import java.awt.event.ItemEvent
|
import java.awt.event.ItemEvent
|
||||||
@@ -112,6 +110,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private inner class AppearanceOption : JPanel(BorderLayout()), Option {
|
private inner class AppearanceOption : JPanel(BorderLayout()), Option {
|
||||||
val themeManager = ThemeManager.getInstance()
|
val themeManager = ThemeManager.getInstance()
|
||||||
val themeComboBox = FlatComboBox<String>()
|
val themeComboBox = FlatComboBox<String>()
|
||||||
|
val layoutComboBox = FlatComboBox<TermoraLayout>()
|
||||||
val languageComboBox = FlatComboBox<String>()
|
val languageComboBox = FlatComboBox<String>()
|
||||||
val backgroundComBoBox = YesOrNoComboBox()
|
val backgroundComBoBox = YesOrNoComboBox()
|
||||||
val confirmTabCloseComBoBox = YesOrNoComboBox()
|
val confirmTabCloseComBoBox = YesOrNoComboBox()
|
||||||
@@ -129,6 +128,38 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
|
|
||||||
|
layoutComboBox.addItem(TermoraLayout.Screen)
|
||||||
|
layoutComboBox.addItem(TermoraLayout.Fence)
|
||||||
|
layoutComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = value
|
||||||
|
if (value == TermoraLayout.Screen) {
|
||||||
|
text = I18n.getString("termora.settings.appearance.layout.screen")
|
||||||
|
} else if (value == TermoraLayout.Fence) {
|
||||||
|
text = I18n.getString("termora.settings.appearance.layout.fence")
|
||||||
|
}
|
||||||
|
|
||||||
|
val c = super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
||||||
|
icon = null
|
||||||
|
|
||||||
|
if (value == TermoraLayout.Screen) {
|
||||||
|
icon = if (isSelected) Icons.uiForm.dark else Icons.uiForm
|
||||||
|
} else if (value == TermoraLayout.Fence) {
|
||||||
|
icon = if (isSelected) Icons.dataColumn.dark else Icons.dataColumn
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layoutComboBox.selectedItem = runCatching { TermoraLayout.valueOf(appearance.layout) }
|
||||||
|
.getOrNull() ?: TermoraLayout.Layout
|
||||||
|
|
||||||
backgroundComBoBox.isEnabled = SystemInfo.isWindows || SystemInfo.isMacOS
|
backgroundComBoBox.isEnabled = SystemInfo.isWindows || SystemInfo.isMacOS
|
||||||
|
|
||||||
opacitySpinner.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows
|
opacitySpinner.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows
|
||||||
@@ -184,6 +215,17 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layoutComboBox.addItemListener(object : ItemListener {
|
||||||
|
override fun itemStateChanged(e: ItemEvent) {
|
||||||
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
|
appearance.layout = layoutComboBox.selectedItem?.toString() ?: return
|
||||||
|
if (TermoraLayout.Layout.name != appearance.layout) {
|
||||||
|
SwingUtilities.invokeLater { TermoraRestarter.getInstance().scheduleRestart(owner) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
opacitySpinner.addChangeListener {
|
opacitySpinner.addChangeListener {
|
||||||
val opacity = opacitySpinner.value
|
val opacity = opacitySpinner.value
|
||||||
if (opacity is Double) {
|
if (opacity is Double) {
|
||||||
@@ -307,7 +349,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private fun getFormPanel(): JPanel {
|
private fun getFormPanel(): JPanel {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default, default:grow",
|
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default, default:grow",
|
||||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
)
|
)
|
||||||
val box = FlatToolBar()
|
val box = FlatToolBar()
|
||||||
box.add(followSystemCheckBox)
|
box.add(followSystemCheckBox)
|
||||||
@@ -329,6 +371,9 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
})).xy(5, rows).apply { rows += step }
|
})).xy(5, rows).apply { rows += step }
|
||||||
|
|
||||||
|
|
||||||
|
builder.add("${I18n.getString("termora.settings.appearance.layout")}:").xy(1, rows)
|
||||||
|
.add(layoutComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
|
||||||
builder.add("${I18n.getString("termora.settings.appearance.opacity")}:").xy(1, rows)
|
builder.add("${I18n.getString("termora.settings.appearance.opacity")}:").xy(1, rows)
|
||||||
.add(opacitySpinner).xy(3, rows).apply { rows += step }
|
.add(opacitySpinner).xy(3, rows).apply { rows += step }
|
||||||
|
|
||||||
@@ -352,7 +397,8 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private val debugComboBox = YesOrNoComboBox()
|
private val debugComboBox = YesOrNoComboBox()
|
||||||
private val beepComboBox = YesOrNoComboBox()
|
private val beepComboBox = YesOrNoComboBox()
|
||||||
private val cursorBlinkComboBox = YesOrNoComboBox()
|
private val cursorBlinkComboBox = YesOrNoComboBox()
|
||||||
private val fontComboBox = FlatComboBox<String>()
|
private val fontComboBox = FontComboBox()
|
||||||
|
private val fallbackFontComboBox = FontComboBox()
|
||||||
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)
|
||||||
@@ -371,6 +417,13 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fallbackFontComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
terminalSetting.fallbackFont = fallbackFontComboBox.selectedItem as String
|
||||||
|
fireFontChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
autoCloseTabComboBox.addItemListener { e ->
|
autoCloseTabComboBox.addItemListener { e ->
|
||||||
if (e.stateChange == ItemEvent.SELECTED) {
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
terminalSetting.autoCloseTabWhenDisconnected = autoCloseTabComboBox.selectedItem as Boolean
|
terminalSetting.autoCloseTabWhenDisconnected = autoCloseTabComboBox.selectedItem as Boolean
|
||||||
@@ -479,33 +532,6 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fontComboBox.renderer = object : DefaultListCellRenderer() {
|
|
||||||
init {
|
|
||||||
preferredSize = Dimension(preferredSize.width, fontComboBox.preferredSize.height - 2)
|
|
||||||
maximumSize = Dimension(preferredSize.width, preferredSize.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -519,29 +545,18 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
shellComboBox.selectedItem = terminalSetting.localShell
|
shellComboBox.selectedItem = terminalSetting.localShell
|
||||||
|
|
||||||
fontComboBox.addItem(terminalSetting.font)
|
fontComboBox.addItem(terminalSetting.font)
|
||||||
var fontsLoaded = false
|
val items = fontComboBox.getItems()
|
||||||
|
for (family in listOf("JetBrains Mono", "Source Code Pro", "Monospaced")) {
|
||||||
fontComboBox.addPopupMenuListener(object : PopupMenuListener {
|
if (items.contains(family).not()) fontComboBox.addItem(family)
|
||||||
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
|
||||||
if (!fontsLoaded) {
|
|
||||||
val selectedItem = fontComboBox.selectedItem
|
|
||||||
fontComboBox.removeAllItems();
|
|
||||||
fontComboBox.addItem("JetBrains Mono")
|
|
||||||
fontComboBox.addItem("Source Code Pro")
|
|
||||||
fontComboBox.addItem("Monospaced")
|
|
||||||
FontUtils.getAvailableFontFamilyNames().forEach {
|
|
||||||
fontComboBox.addItem(it)
|
|
||||||
}
|
|
||||||
fontComboBox.selectedItem = selectedItem
|
|
||||||
fontsLoaded = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {}
|
if (terminalSetting.fallbackFont.isNotBlank()) {
|
||||||
override fun popupMenuCanceled(e: PopupMenuEvent) {}
|
fallbackFontComboBox.addItem(StringUtils.EMPTY)
|
||||||
})
|
}
|
||||||
|
fallbackFontComboBox.addItem(terminalSetting.fallbackFont)
|
||||||
|
|
||||||
fontComboBox.selectedItem = terminalSetting.font
|
fontComboBox.selectedItem = terminalSetting.font
|
||||||
|
fallbackFontComboBox.selectedItem = terminalSetting.fallbackFont
|
||||||
debugComboBox.selectedItem = terminalSetting.debug
|
debugComboBox.selectedItem = terminalSetting.debug
|
||||||
beepComboBox.selectedItem = terminalSetting.beep
|
beepComboBox.selectedItem = terminalSetting.beep
|
||||||
hyperlinkComboBox.selectedItem = terminalSetting.hyperlink
|
hyperlinkComboBox.selectedItem = terminalSetting.hyperlink
|
||||||
@@ -580,7 +595,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private fun getCenterComponent(): JComponent {
|
private fun getCenterComponent(): JComponent {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, left:pref, $FORM_MARGIN, pref, default:grow",
|
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, left:pref, $FORM_MARGIN, pref, default:grow",
|
||||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
)
|
)
|
||||||
|
|
||||||
val beepBtn = JButton(Icons.run)
|
val beepBtn = JButton(Icons.run)
|
||||||
@@ -596,6 +611,8 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
.add(fontComboBox).xy(3, rows)
|
.add(fontComboBox).xy(3, rows)
|
||||||
.add("${I18n.getString("termora.settings.terminal.size")}:").xy(5, rows)
|
.add("${I18n.getString("termora.settings.terminal.size")}:").xy(5, rows)
|
||||||
.add(fontSizeTextField).xy(7, rows).apply { rows += step }
|
.add(fontSizeTextField).xy(7, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.settings.terminal.fallback-font")}:").xy(1, rows)
|
||||||
|
.add(fallbackFontComboBox).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.max-rows")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.max-rows")}:").xy(1, rows)
|
||||||
.add(maxRowsTextField).xy(3, rows).apply { rows += step }
|
.add(maxRowsTextField).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.debug")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.debug")}:").xy(1, rows)
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ class TerminalPanelFactory : Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
fun createTerminalPanel(tab: TerminalTab?, terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
||||||
val writer = MyTerminalWriter(ptyConnector)
|
val writer = MyTerminalWriter(ptyConnector)
|
||||||
val terminalPanel = TerminalPanel(terminal, writer)
|
val terminalPanel = TerminalPanel(tab, terminal, writer)
|
||||||
|
|
||||||
// processDeviceStatusReport
|
// processDeviceStatusReport
|
||||||
terminal.getTerminalModel().setData(DataKey.TerminalWriter, writer)
|
terminal.getTerminalModel().setData(DataKey.TerminalWriter, writer)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
|
||||||
|
import app.termora.account.AccountManager
|
||||||
import app.termora.actions.*
|
import app.termora.actions.*
|
||||||
import app.termora.database.DatabaseChangedExtension
|
import app.termora.database.DatabaseChangedExtension
|
||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
@@ -8,10 +9,8 @@ import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
|
|||||||
import app.termora.findeverywhere.FindEverywhereProvider
|
import app.termora.findeverywhere.FindEverywhereProvider
|
||||||
import app.termora.findeverywhere.FindEverywhereProviderExtension
|
import app.termora.findeverywhere.FindEverywhereProviderExtension
|
||||||
import app.termora.findeverywhere.FindEverywhereResult
|
import app.termora.findeverywhere.FindEverywhereResult
|
||||||
|
import app.termora.plugin.ExtensionManager
|
||||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||||
import app.termora.plugin.internal.sftppty.SFTPPtyProtocolProvider
|
|
||||||
import app.termora.plugin.internal.sftppty.SFTPPtyTerminalTab
|
|
||||||
import app.termora.plugin.internal.ssh.SSHProtocolProvider
|
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
@@ -23,7 +22,6 @@ import java.awt.event.ActionEvent
|
|||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import java.util.*
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -32,6 +30,7 @@ class TerminalTabbed(
|
|||||||
private val windowScope: WindowScope,
|
private val windowScope: WindowScope,
|
||||||
private val termoraToolBar: TermoraToolBar,
|
private val termoraToolBar: TermoraToolBar,
|
||||||
private val tabbedPane: FlatTabbedPane,
|
private val tabbedPane: FlatTabbedPane,
|
||||||
|
private val layout: TermoraLayout,
|
||||||
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager, DataProvider {
|
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager, DataProvider {
|
||||||
private val tabs = mutableListOf<TerminalTab>()
|
private val tabs = mutableListOf<TerminalTab>()
|
||||||
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
|
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
|
||||||
@@ -110,20 +109,6 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// 点击
|
|
||||||
tabbedPane.addMouseListener(object : MouseAdapter() {
|
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
|
||||||
if (SwingUtilities.isLeftMouseButton(e)) {
|
|
||||||
val index = tabbedPane.indexAtLocation(e.x, e.y)
|
|
||||||
if (index > 0) {
|
|
||||||
tabbedPane.getComponentAt(index).requestFocusInWindow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
// 注册全局搜索
|
// 注册全局搜索
|
||||||
DynamicExtensionHandler.getInstance()
|
DynamicExtensionHandler.getInstance()
|
||||||
.register(FindEverywhereProviderExtension::class.java, object : FindEverywhereProviderExtension {
|
.register(FindEverywhereProviderExtension::class.java, object : FindEverywhereProviderExtension {
|
||||||
@@ -206,11 +191,13 @@ class TerminalTabbed(
|
|||||||
// remove ele
|
// remove ele
|
||||||
tabs.removeAt(index)
|
tabs.removeAt(index)
|
||||||
|
|
||||||
|
if (tabbedPane.tabCount > 0) {
|
||||||
// 新的获取到焦点
|
// 新的获取到焦点
|
||||||
tabs[tabbedPane.selectedIndex].onGrabFocus()
|
tabs[tabbedPane.selectedIndex].onGrabFocus()
|
||||||
|
|
||||||
// 新的真正获取焦点
|
// 新的真正获取焦点
|
||||||
tabbedPane.getComponentAt(tabbedPane.selectedIndex).requestFocusInWindow()
|
tabbedPane.getComponentAt(tabbedPane.selectedIndex).requestFocusInWindow()
|
||||||
|
}
|
||||||
|
|
||||||
if (disposable) {
|
if (disposable) {
|
||||||
Disposer.dispose(tab)
|
Disposer.dispose(tab)
|
||||||
@@ -221,6 +208,15 @@ class TerminalTabbed(
|
|||||||
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 tab = tabs[tabIndex]
|
||||||
|
val extensions = ExtensionManager.getInstance().getExtensions(TerminalTabbedContextMenuExtension::class.java)
|
||||||
|
val menuItems = mutableListOf<JMenuItem>()
|
||||||
|
for (extension in extensions) {
|
||||||
|
try {
|
||||||
|
menuItems.add(extension.createJMenuItem(windowScope, tab))
|
||||||
|
} catch (_: UnsupportedOperationException) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val popupMenu = FlatPopupMenu()
|
val popupMenu = FlatPopupMenu()
|
||||||
|
|
||||||
@@ -242,7 +238,7 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 克隆
|
// 克隆
|
||||||
val clone = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.clone"))
|
val clone = popupMenu.add(I18n.getString("termora.copy"))
|
||||||
clone.addActionListener { evt ->
|
clone.addActionListener { evt ->
|
||||||
if (tab is HostTerminalTab) {
|
if (tab is HostTerminalTab) {
|
||||||
actionManager
|
actionManager
|
||||||
@@ -255,10 +251,15 @@ class TerminalTabbed(
|
|||||||
val edit = popupMenu.add(I18n.getString("termora.keymgr.edit"))
|
val edit = popupMenu.add(I18n.getString("termora.keymgr.edit"))
|
||||||
edit.addActionListener(object : AnAction() {
|
edit.addActionListener(object : AnAction() {
|
||||||
private val hostManager get() = HostManager.getInstance()
|
private val hostManager get() = HostManager.getInstance()
|
||||||
|
private val accountManager get() = AccountManager.getInstance()
|
||||||
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
if (tab is HostTerminalTab) {
|
if (tab is HostTerminalTab) {
|
||||||
val host = hostManager.getHost(tab.host.id) ?: return
|
val host = hostManager.getHost(tab.host.id) ?: return
|
||||||
val dialog = NewHostDialogV2(evt.window, host)
|
val dialog = NewHostDialogV2(
|
||||||
|
evt.window, host,
|
||||||
|
accountManager.getOwners().first { it.id == host.ownerId },
|
||||||
|
)
|
||||||
dialog.setLocationRelativeTo(evt.window)
|
dialog.setLocationRelativeTo(evt.window)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
|
|
||||||
@@ -289,14 +290,10 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (tab is HostTerminalTab) {
|
if (menuItems.isNotEmpty()) {
|
||||||
val openHostAction = actionManager.getAction(OpenHostAction.OPEN_HOST)
|
|
||||||
if (openHostAction != null) {
|
|
||||||
if (tab.host.protocol == SSHProtocolProvider.PROTOCOL || tab.host.protocol == SFTPPtyProtocolProvider.PROTOCOL) {
|
|
||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
val sftpCommand = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
|
for (item in menuItems) {
|
||||||
sftpCommand.addActionListener { openSFTPPtyTab(tab, openHostAction, it) }
|
popupMenu.add(item)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,36 +385,6 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun openSFTPPtyTab(tab: HostTerminalTab, openHostAction: Action, evt: EventObject) {
|
|
||||||
if (!SFTPPtyTerminalTab.canSupports) {
|
|
||||||
OptionPane.showMessageDialog(
|
|
||||||
SwingUtilities.getWindowAncestor(this),
|
|
||||||
I18n.getString("termora.tabbed.contextmenu.sftp-not-install"),
|
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var host = tab.host
|
|
||||||
|
|
||||||
if (host.protocol == SSHProtocolProvider.PROTOCOL) {
|
|
||||||
val envs = tab.host.options.envs().toMutableMap()
|
|
||||||
val currentDir = tab.getData(DataProviders.Terminal)?.getTerminalModel()
|
|
||||||
?.getData(DataKey.CurrentDir, StringUtils.EMPTY) ?: StringUtils.EMPTY
|
|
||||||
|
|
||||||
if (currentDir.isNotBlank()) {
|
|
||||||
envs["CurrentDir"] = currentDir
|
|
||||||
}
|
|
||||||
|
|
||||||
host = host.copy(
|
|
||||||
protocol = SFTPPtyProtocolProvider.PROTOCOL,
|
|
||||||
options = host.options.copy(env = envs.toPropertiesString())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
openHostAction.actionPerformed(OpenHostActionEvent(this, host, evt))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对着 ToolBar 右键
|
* 对着 ToolBar 右键
|
||||||
*/
|
*/
|
||||||
@@ -497,6 +464,32 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun paint(g: Graphics) {
|
||||||
|
super.paint(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||||
|
private val banner = BannerPanel(fontSize = 13).apply {
|
||||||
|
foreground = UIManager.getColor("textInactiveText")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun paintComponent(g: Graphics) {
|
||||||
|
super.paintComponent(g)
|
||||||
|
|
||||||
|
if (layout == TermoraLayout.Fence) {
|
||||||
|
if (g is Graphics2D) {
|
||||||
|
if (tabbedPane.tabCount < 1) {
|
||||||
|
border.paintBorder(this, g, 0, tabbedPane.tabHeight, width, tabbedPane.tabHeight)
|
||||||
|
banner.setBounds(0, 0, width, height)
|
||||||
|
g.save()
|
||||||
|
g.translate(0, 180)
|
||||||
|
banner.paintComponent(g)
|
||||||
|
g.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import javax.swing.JMenuItem
|
||||||
|
|
||||||
|
interface TerminalTabbedContextMenuExtension : Extension {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抛出 [UnsupportedOperationException] 表示不支持
|
||||||
|
*/
|
||||||
|
fun createJMenuItem(windowScope: WindowScope, tab: TerminalTab): JMenuItem
|
||||||
|
}
|
||||||
163
src/main/kotlin/app/termora/TermoraFencePanel.kt
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnAction
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.tree.NewHostTree
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Font
|
||||||
|
import java.awt.event.ComponentAdapter
|
||||||
|
import java.awt.event.ComponentEvent
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
|
import java.awt.event.MouseAdapter
|
||||||
|
import javax.swing.*
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
|
||||||
|
class TermoraFencePanel(
|
||||||
|
private val terminalTabbed: TerminalTabbed,
|
||||||
|
private val tabbed: FlatTabbedPane,
|
||||||
|
private val moveMouseAdapter: MouseAdapter,
|
||||||
|
) : JPanel(BorderLayout()), Disposable {
|
||||||
|
private val splitPane = object : JSplitPane() {
|
||||||
|
override fun updateUI() {
|
||||||
|
setUI(SplitPaneUI())
|
||||||
|
revalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val leftTreePanel = LeftTreePanel()
|
||||||
|
private val mySplitPane = JSplitPaneWithZeroSizeDivider(splitPane) { tabbed.tabHeight }
|
||||||
|
private val enableManager get() = EnableManager.getInstance()
|
||||||
|
private val toolbar = FlatToolBar().apply { isFloatable = false }
|
||||||
|
private var dividerLocation = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
|
||||||
|
splitPane.border = null
|
||||||
|
|
||||||
|
splitPane.leftComponent = leftTreePanel
|
||||||
|
splitPane.rightComponent = terminalTabbed
|
||||||
|
splitPane.dividerSize = 0
|
||||||
|
splitPane.dividerLocation = enableManager.getFlag("Termora.Fence.dividerLocation", 220)
|
||||||
|
|
||||||
|
leftTreePanel.preferredSize = Dimension(180, -1)
|
||||||
|
|
||||||
|
tabbed.tabType = FlatTabbedPane.TabType.underlined
|
||||||
|
tabbed.tabAreaInsets = null
|
||||||
|
|
||||||
|
// macOS 避开控制栏
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
toolbar.add(Box.createHorizontalStrut(76))
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbar.add(createColspanAction())
|
||||||
|
tabbed.leadingComponent = toolbar
|
||||||
|
toolbar.isVisible = false
|
||||||
|
|
||||||
|
add(mySplitPane, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
Disposer.register(this, leftTreePanel)
|
||||||
|
splitPane.addPropertyChangeListener("dividerLocation") { mySplitPane.doLayout() }
|
||||||
|
|
||||||
|
leftTreePanel.addComponentListener(object : ComponentAdapter() {
|
||||||
|
override fun componentHidden(e: ComponentEvent) {
|
||||||
|
toolbar.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun componentShown(e: ComponentEvent) {
|
||||||
|
toolbar.isVisible = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
actionMap.put("toggle", createColspanAction())
|
||||||
|
getInputMap(WHEN_IN_FOCUSED_WINDOW).put(
|
||||||
|
KeyStroke.getKeyStroke(
|
||||||
|
KeyEvent.VK_B,
|
||||||
|
toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK
|
||||||
|
), "toggle"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class LeftTreePanel : JPanel(BorderLayout()), Disposable {
|
||||||
|
val hostTree = NewHostTree()
|
||||||
|
private val box = JToolBar().apply { isFloatable = false }
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
val scrollPane = JScrollPane(hostTree)
|
||||||
|
hostTree.name = "FenceHostTree"
|
||||||
|
hostTree.restoreExpansions()
|
||||||
|
box.preferredSize = Dimension(-1, tabbed.tabHeight)
|
||||||
|
|
||||||
|
val label = JLabel(Application.getName())
|
||||||
|
label.foreground = UIManager.getColor("textInactiveText")
|
||||||
|
label.font = label.font.deriveFont(Font.BOLD)
|
||||||
|
// 与最后一个按钮对冲,使其宽度和谐
|
||||||
|
box.add(JButton(Icons.empty))
|
||||||
|
box.add(Box.createHorizontalGlue())
|
||||||
|
if (SystemInfo.isMacOS.not()) {
|
||||||
|
box.add(label)
|
||||||
|
}
|
||||||
|
box.add(Box.createHorizontalGlue())
|
||||||
|
box.add(createColspanAction())
|
||||||
|
|
||||||
|
if (SystemInfo.isMacOS || SystemInfo.isLinux) {
|
||||||
|
box.addMouseListener(moveMouseAdapter)
|
||||||
|
box.addMouseMotionListener(moveMouseAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollPane.verticalScrollBar.unitIncrement = 16
|
||||||
|
scrollPane.horizontalScrollBar.unitIncrement = 16
|
||||||
|
scrollPane.border = BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor),
|
||||||
|
BorderFactory.createEmptyBorder(4, 4, 0, 4)
|
||||||
|
)
|
||||||
|
|
||||||
|
add(box, BorderLayout.NORTH)
|
||||||
|
add(scrollPane, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
Disposer.register(this, hostTree)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createColspanAction(): Action {
|
||||||
|
return object : AnAction(Icons.dataColumn) {
|
||||||
|
init {
|
||||||
|
val text = I18n.getString("termora.welcome.toggle-sidebar")
|
||||||
|
putValue(SHORT_DESCRIPTION, "$text (${if (SystemInfo.isMacOS) '⌘' else "Ctrl"} + Shift + B)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
if (leftTreePanel.isVisible) dividerLocation = splitPane.dividerLocation
|
||||||
|
leftTreePanel.isVisible = leftTreePanel.isVisible.not()
|
||||||
|
if (leftTreePanel.isVisible) splitPane.dividerLocation = dividerLocation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHostTree(): NewHostTree {
|
||||||
|
return leftTreePanel.hostTree
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,21 +4,29 @@ package app.termora
|
|||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.actions.DataProviderSupport
|
import app.termora.actions.DataProviderSupport
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
|
import app.termora.actions.OpenHostAction
|
||||||
|
import app.termora.findeverywhere.FindEverywhereProvider
|
||||||
|
import app.termora.findeverywhere.FindEverywhereProviderExtension
|
||||||
|
import app.termora.findeverywhere.FindEverywhereResult
|
||||||
import app.termora.plugin.ExtensionManager
|
import app.termora.plugin.ExtensionManager
|
||||||
|
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||||
|
import app.termora.plugin.internal.ssh.SSHProtocolProvider
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
|
import app.termora.tree.NewHostTreeModel
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import com.formdev.flatlaf.ui.FlatRootPaneUI
|
import com.formdev.flatlaf.ui.FlatRootPaneUI
|
||||||
import com.formdev.flatlaf.ui.FlatTitlePane
|
import com.formdev.flatlaf.ui.FlatTitlePane
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jetbrains.JBR
|
import com.jetbrains.JBR
|
||||||
import org.apache.commons.lang3.ArrayUtils
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.*
|
||||||
import java.awt.event.MouseEvent
|
|
||||||
import java.awt.event.MouseListener
|
|
||||||
import java.awt.event.MouseMotionListener
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.imageio.ImageIO
|
import javax.imageio.ImageIO
|
||||||
|
import javax.swing.Icon
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.JFrame
|
import javax.swing.JFrame
|
||||||
import javax.swing.SwingUtilities.isEventDispatchThread
|
import javax.swing.SwingUtilities.isEventDispatchThread
|
||||||
@@ -32,15 +40,16 @@ fun assertEventDispatchThread() {
|
|||||||
|
|
||||||
class TermoraFrame : JFrame(), DataProvider {
|
class TermoraFrame : JFrame(), DataProvider {
|
||||||
|
|
||||||
|
private val layout get() = TermoraLayout.Layout
|
||||||
|
private val titleBarHeight = computedTitleBarHeight()
|
||||||
private val id = UUID.randomUUID().toString()
|
private val id = UUID.randomUUID().toString()
|
||||||
private val windowScope = ApplicationScope.forWindowScope(this)
|
private val windowScope = ApplicationScope.forWindowScope(this)
|
||||||
private val tabbedPane = MyTabbedPane()
|
private val tabbedPane = MyTabbedPane().apply { tabHeight = titleBarHeight }
|
||||||
private val toolbar = TermoraToolBar(windowScope, this)
|
private val toolbar = TermoraToolBar(windowScope, this)
|
||||||
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane)
|
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane, layout)
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
private val welcomePanel = WelcomePanel(windowScope)
|
|
||||||
private var notifyListeners = emptyArray<NotifyListener>()
|
private var notifyListeners = emptyArray<NotifyListener>()
|
||||||
|
private val moveMouseAdapter = createMoveMouseAdaptor()
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -50,7 +59,201 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
if (SystemInfo.isLinux) {
|
if (SystemInfo.isLinux) {
|
||||||
val mouseAdapter = object : MouseAdapter() {
|
toolbar.getJToolBar().addMouseListener(moveMouseAdapter)
|
||||||
|
toolbar.getJToolBar().addMouseMotionListener(moveMouseAdapter)
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
terminalTabbed.addMouseListener(moveMouseAdapter)
|
||||||
|
terminalTabbed.addMouseMotionListener(moveMouseAdapter)
|
||||||
|
|
||||||
|
tabbedPane.addMouseListener(moveMouseAdapter)
|
||||||
|
tabbedPane.addMouseMotionListener(moveMouseAdapter)
|
||||||
|
|
||||||
|
toolbar.getJToolBar().addMouseListener(moveMouseAdapter)
|
||||||
|
toolbar.getJToolBar().addMouseMotionListener(moveMouseAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindEverywhere
|
||||||
|
DynamicExtensionHandler.getInstance()
|
||||||
|
.register(FindEverywhereProviderExtension::class.java, object : FindEverywhereProviderExtension {
|
||||||
|
private val hostTreeModel get() = NewHostTreeModel.getInstance()
|
||||||
|
|
||||||
|
private val provider = object : FindEverywhereProvider {
|
||||||
|
override fun find(pattern: String, scope: Scope): List<FindEverywhereResult> {
|
||||||
|
if (scope != windowScope) return emptyList()
|
||||||
|
|
||||||
|
var filter = hostTreeModel.root.getAllChildren()
|
||||||
|
.filter { it.isFolder.not() }
|
||||||
|
.map { it.host }
|
||||||
|
|
||||||
|
if (pattern.isNotBlank()) {
|
||||||
|
filter = filter.filter {
|
||||||
|
if (it.protocol == SSHProtocolProvider.PROTOCOL) {
|
||||||
|
it.name.contains(pattern, true) || it.host.contains(pattern, true)
|
||||||
|
} else {
|
||||||
|
it.name.contains(pattern, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.map { HostFindEverywhereResult(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun group(): String {
|
||||||
|
return I18n.getString("termora.find-everywhere.groups.open-new-hosts")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun order(): Int {
|
||||||
|
return Integer.MIN_VALUE + 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFindEverywhereProvider(): FindEverywhereProvider {
|
||||||
|
return provider
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class HostFindEverywhereResult(val host: Host) : FindEverywhereResult {
|
||||||
|
private val showMoreInfo get() = EnableManager.getInstance().isShowMoreInfo()
|
||||||
|
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
ActionManager.getInstance()
|
||||||
|
.getAction(OpenHostAction.OPEN_HOST)
|
||||||
|
?.actionPerformed(OpenHostActionEvent(e.source, host, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
if (isSelected) {
|
||||||
|
if (!FlatLaf.isLafDark()) {
|
||||||
|
return Icons.terminal.dark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Icons.terminal
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getText(isSelected: Boolean): String {
|
||||||
|
if (showMoreInfo) {
|
||||||
|
val color = UIManager.getColor(if (isSelected) "textHighlightText" else "textInactiveText")
|
||||||
|
val moreInfo = when (host.protocol) {
|
||||||
|
SSHProtocolProvider.PROTOCOL -> "${host.username}@${host.host}"
|
||||||
|
"Serial" -> host.options.serialComm.port
|
||||||
|
else -> StringUtils.EMPTY
|
||||||
|
}
|
||||||
|
if (moreInfo.isNotBlank()) {
|
||||||
|
return "<html>${host.name} <font color=rgb(${color.red},${color.green},${color.blue})>${moreInfo}</font></html>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return host.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}).let { Disposer.register(windowScope, it) }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
|
||||||
|
// macOS 要避开左边的控制栏
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
tabbedPane.tabAreaInsets = Insets(0, 76, 0, 0)
|
||||||
|
} else if (SystemInfo.isWindows) {
|
||||||
|
// Windows 10 会有1像素误差
|
||||||
|
tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0)
|
||||||
|
} else if (SystemInfo.isLinux) {
|
||||||
|
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
val height = UIManager.getInt("TabbedPane.tabHeight") + tabbedPane.tabAreaInsets.top
|
||||||
|
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICON, false)
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
val sizes = listOf(16, 20, 24, 28, 32, 48, 64, 128)
|
||||||
|
val loader = TermoraFrame::class.java.classLoader
|
||||||
|
val images = sizes.mapNotNull { e ->
|
||||||
|
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
|
||||||
|
}
|
||||||
|
iconImages = images
|
||||||
|
}
|
||||||
|
|
||||||
|
minimumSize = Dimension(640, 400)
|
||||||
|
|
||||||
|
val glassPane = GlassPane()
|
||||||
|
rootPane.glassPane = glassPane
|
||||||
|
glassPane.isOpaque = false
|
||||||
|
glassPane.isVisible = true
|
||||||
|
|
||||||
|
for (extension in ExtensionManager.getInstance().getExtensions(GlassPaneAwareExtension::class.java)) {
|
||||||
|
extension.setGlassPane(this, glassPane)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout == TermoraLayout.Fence) {
|
||||||
|
val fencePanel = TermoraFencePanel(terminalTabbed, tabbedPane, moveMouseAdapter)
|
||||||
|
add(fencePanel, BorderLayout.CENTER)
|
||||||
|
dataProviderSupport.addData(DataProviders.Welcome.HostTree, fencePanel.getHostTree())
|
||||||
|
Disposer.register(windowScope, fencePanel)
|
||||||
|
} else {
|
||||||
|
val screenPanel = TermoraScreenPanel(windowScope, terminalTabbed)
|
||||||
|
add(screenPanel, BorderLayout.CENTER)
|
||||||
|
dataProviderSupport.addData(DataProviders.Welcome.HostTree, screenPanel.getHostTree())
|
||||||
|
}
|
||||||
|
|
||||||
|
Disposer.register(windowScope, terminalTabbed)
|
||||||
|
|
||||||
|
dataProviderSupport.addData(DataProviders.TabbedPane, tabbedPane)
|
||||||
|
dataProviderSupport.addData(DataProviders.TermoraFrame, this)
|
||||||
|
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
|
return dataProviderSupport.getData(dataKey) ?: terminalTabbed.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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addNotifyListener(listener: NotifyListener) {
|
||||||
|
notifyListeners += listener
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeNotifyListener(listener: NotifyListener) {
|
||||||
|
notifyListeners = ArrayUtils.removeElements(notifyListeners, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addNotify() {
|
||||||
|
super.addNotify()
|
||||||
|
notifyListeners.forEach { it.addNotify() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun computedTitleBarHeight(): Int {
|
||||||
|
val tabHeight = UIManager.getInt("TabbedPane.tabHeight")
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
// Windows 10 会有1像素误差
|
||||||
|
return tabHeight + if (SystemInfo.isWindows_11_orLater) 1 else 2
|
||||||
|
} else if (SystemInfo.isLinux) {
|
||||||
|
return tabHeight + 1
|
||||||
|
}
|
||||||
|
return tabHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMoveMouseAdaptor(): MouseAdapter {
|
||||||
|
if (SystemInfo.isLinux) {
|
||||||
|
return object : MouseAdapter() {
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
getMouseHandler()?.mouseClicked(e)
|
getMouseHandler()?.mouseClicked(e)
|
||||||
}
|
}
|
||||||
@@ -97,8 +300,6 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
return titlePaneField.get(ui) as? FlatTitlePane
|
return titlePaneField.get(ui) as? FlatTitlePane
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toolbar.getJToolBar().addMouseListener(mouseAdapter)
|
|
||||||
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// force hit
|
/// force hit
|
||||||
@@ -145,111 +346,13 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
terminalTabbed.addMouseListener(mouseAdapter)
|
|
||||||
terminalTabbed.addMouseMotionListener(mouseAdapter)
|
|
||||||
|
|
||||||
tabbedPane.addMouseListener(mouseAdapter)
|
|
||||||
tabbedPane.addMouseMotionListener(mouseAdapter)
|
|
||||||
|
|
||||||
toolbar.getJToolBar().addMouseListener(mouseAdapter)
|
|
||||||
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
|
||||||
|
|
||||||
JBR.getWindowDecorations().setCustomTitleBar(this, customTitleBar)
|
JBR.getWindowDecorations().setCustomTitleBar(this, customTitleBar)
|
||||||
}
|
|
||||||
|
return mouseAdapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return object : MouseAdapter() {}
|
||||||
private fun initView() {
|
|
||||||
|
|
||||||
// macOS 要避开左边的控制栏
|
|
||||||
if (SystemInfo.isMacOS) {
|
|
||||||
tabbedPane.tabAreaInsets = Insets(0, 76, 0, 0)
|
|
||||||
} else if (SystemInfo.isWindows) {
|
|
||||||
// Windows 10 会有1像素误差
|
|
||||||
tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0)
|
|
||||||
} else if (SystemInfo.isLinux) {
|
|
||||||
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
val height = UIManager.getInt("TabbedPane.tabHeight") + tabbedPane.tabAreaInsets.top
|
|
||||||
|
|
||||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
|
||||||
rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
|
||||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICON, false)
|
|
||||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
|
||||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, height)
|
|
||||||
} else if (SystemInfo.isMacOS) {
|
|
||||||
rootPane.putClientProperty("apple.awt.windowTitleVisible", false)
|
|
||||||
rootPane.putClientProperty("apple.awt.fullWindowContent", true)
|
|
||||||
rootPane.putClientProperty("apple.awt.transparentTitleBar", true)
|
|
||||||
rootPane.putClientProperty(
|
|
||||||
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING,
|
|
||||||
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING_MEDIUM
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
|
||||||
val sizes = listOf(16, 20, 24, 28, 32, 48, 64)
|
|
||||||
val loader = TermoraFrame::class.java.classLoader
|
|
||||||
val images = sizes.mapNotNull { e ->
|
|
||||||
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
|
|
||||||
}
|
|
||||||
iconImages = images
|
|
||||||
}
|
|
||||||
|
|
||||||
minimumSize = Dimension(640, 400)
|
|
||||||
terminalTabbed.addTerminalTab(welcomePanel)
|
|
||||||
|
|
||||||
val glassPane = GlassPane()
|
|
||||||
rootPane.glassPane = glassPane
|
|
||||||
glassPane.isOpaque = false
|
|
||||||
glassPane.isVisible = true
|
|
||||||
|
|
||||||
for (extension in ExtensionManager.getInstance().getExtensions(GlassPaneAwareExtension::class.java)) {
|
|
||||||
extension.setGlassPane(this, glassPane)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Disposer.register(windowScope, terminalTabbed)
|
|
||||||
add(terminalTabbed, BorderLayout.CENTER)
|
|
||||||
|
|
||||||
dataProviderSupport.addData(DataProviders.TabbedPane, tabbedPane)
|
|
||||||
dataProviderSupport.addData(DataProviders.TermoraFrame, this)
|
|
||||||
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
|
||||||
return dataProviderSupport.getData(dataKey)
|
|
||||||
?: terminalTabbed.getData(dataKey)
|
|
||||||
?: 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addNotifyListener(listener: NotifyListener) {
|
|
||||||
notifyListeners += listener
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeNotifyListener(listener: NotifyListener) {
|
|
||||||
notifyListeners = ArrayUtils.removeElements(notifyListeners, listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addNotify() {
|
|
||||||
super.addNotify()
|
|
||||||
notifyListeners.forEach { it.addNotify() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
18
src/main/kotlin/app/termora/TermoraLayout.kt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.database.DatabaseManager
|
||||||
|
|
||||||
|
enum class TermoraLayout {
|
||||||
|
/**
|
||||||
|
* Split
|
||||||
|
*/
|
||||||
|
Fence,
|
||||||
|
|
||||||
|
Screen, ;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val Layout by lazy {
|
||||||
|
runCatching { TermoraLayout.valueOf(DatabaseManager.getInstance().appearance.layout) }.getOrNull() ?: Screen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/main/kotlin/app/termora/TermoraScreenPanel.kt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
|
import app.termora.tree.NewHostTree
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.JPanel
|
||||||
|
|
||||||
|
class TermoraScreenPanel(private val windowScope: WindowScope, private val terminalTabbed: TerminalTabbed) :
|
||||||
|
JPanel(BorderLayout()) {
|
||||||
|
private val welcomePanel = WelcomePanel()
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(terminalTabbed, BorderLayout.CENTER)
|
||||||
|
terminalTabbed.addTerminalTab(welcomePanel, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
Disposer.register(windowScope, welcomePanel)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHostTree(): NewHostTree {
|
||||||
|
return Objects.requireNonNull<NewHostTree>(welcomePanel.getData(DataProviders.Welcome.HostTree))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
import javax.swing.PopupFactory
|
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
|
|
||||||
@@ -118,11 +117,7 @@ internal class ThemeManager private constructor() {
|
|||||||
|
|
||||||
private fun immediateChange(classname: String) {
|
private fun immediateChange(classname: String) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
val oldPopupFactory = PopupFactory.getSharedInstance()
|
|
||||||
UIManager.setLookAndFeel(classname)
|
UIManager.setLookAndFeel(classname)
|
||||||
PopupFactory.setSharedInstance(oldPopupFactory)
|
|
||||||
|
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
log.error(ex.message, ex)
|
log.error(ex.message, ex)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,9 @@ package app.termora
|
|||||||
import app.termora.actions.*
|
import app.termora.actions.*
|
||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
import app.termora.findeverywhere.FindEverywhereProvider
|
import app.termora.findeverywhere.FindEverywhereProvider
|
||||||
import app.termora.findeverywhere.FindEverywhereProviderExtension
|
|
||||||
import app.termora.findeverywhere.FindEverywhereResult
|
|
||||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
|
||||||
import app.termora.plugin.internal.ssh.SSHProtocolProvider
|
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.tree.*
|
import app.termora.tree.*
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
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.FlatButton
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
@@ -24,8 +19,7 @@ import java.awt.event.*
|
|||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()), Disposable, TerminalTab,
|
class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProvider {
|
||||||
DataProvider {
|
|
||||||
|
|
||||||
private val properties get() = DatabaseManager.getInstance().properties
|
private val properties get() = DatabaseManager.getInstance().properties
|
||||||
private val rootPanel = JPanel(BorderLayout())
|
private val rootPanel = JPanel(BorderLayout())
|
||||||
@@ -52,6 +46,7 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
val panel = JPanel(BorderLayout())
|
val panel = JPanel(BorderLayout())
|
||||||
panel.add(createSearchPanel(), BorderLayout.NORTH)
|
panel.add(createSearchPanel(), BorderLayout.NORTH)
|
||||||
panel.add(createHostPanel(), BorderLayout.CENTER)
|
panel.add(createHostPanel(), BorderLayout.CENTER)
|
||||||
|
bannerPanel.foreground = UIManager.getColor("TextField.placeholderForeground")
|
||||||
|
|
||||||
if (!fullContent) {
|
if (!fullContent) {
|
||||||
rootPanel.add(bannerPanel, BorderLayout.NORTH)
|
rootPanel.add(bannerPanel, BorderLayout.NORTH)
|
||||||
@@ -209,44 +204,6 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
DynamicExtensionHandler.getInstance()
|
|
||||||
.register(FindEverywhereProviderExtension::class.java, object : FindEverywhereProviderExtension {
|
|
||||||
private val provider = object : FindEverywhereProvider {
|
|
||||||
override fun find(pattern: String, scope: Scope): List<FindEverywhereResult> {
|
|
||||||
if (scope != windowScope) return emptyList()
|
|
||||||
|
|
||||||
var filter = hostTreeModel.root.getAllChildren()
|
|
||||||
.map { it.host }
|
|
||||||
.filter { it.isFolder.not() }
|
|
||||||
|
|
||||||
if (pattern.isNotBlank()) {
|
|
||||||
filter = filter.filter {
|
|
||||||
if (it.protocol == SSHProtocolProvider.PROTOCOL) {
|
|
||||||
it.name.contains(pattern, true) || it.host.contains(pattern, true)
|
|
||||||
} else {
|
|
||||||
it.name.contains(pattern, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filter.map { HostFindEverywhereResult(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun group(): String {
|
|
||||||
return I18n.getString("termora.find-everywhere.groups.open-new-hosts")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun order(): Int {
|
|
||||||
return Integer.MIN_VALUE + 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFindEverywhereProvider(): FindEverywhereProvider {
|
|
||||||
return provider
|
|
||||||
}
|
|
||||||
|
|
||||||
}).let { Disposer.register(this, it) }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun perform() {
|
private fun perform() {
|
||||||
@@ -302,40 +259,6 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
properties.putString("WelcomeFullContent", fullContent.toString())
|
properties.putString("WelcomeFullContent", fullContent.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class HostFindEverywhereResult(val host: Host) : FindEverywhereResult {
|
|
||||||
private val showMoreInfo get() = EnableManager.getInstance().isShowMoreInfo()
|
|
||||||
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
|
||||||
ActionManager.getInstance()
|
|
||||||
.getAction(OpenHostAction.OPEN_HOST)
|
|
||||||
?.actionPerformed(OpenHostActionEvent(e.source, host, e))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getIcon(isSelected: Boolean): Icon {
|
|
||||||
if (isSelected) {
|
|
||||||
if (!FlatLaf.isLafDark()) {
|
|
||||||
return Icons.terminal.dark
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Icons.terminal
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getText(isSelected: Boolean): String {
|
|
||||||
if (showMoreInfo) {
|
|
||||||
val color = UIManager.getColor(if (isSelected) "textHighlightText" else "textInactiveText")
|
|
||||||
val moreInfo = when (host.protocol) {
|
|
||||||
SSHProtocolProvider.PROTOCOL -> "${host.username}@${host.host}"
|
|
||||||
"Serial" -> host.options.serialComm.port
|
|
||||||
else -> StringUtils.EMPTY
|
|
||||||
}
|
|
||||||
if (moreInfo.isNotBlank()) {
|
|
||||||
return "<html>${host.name} <font color=rgb(${color.red},${color.green},${color.blue})>${moreInfo}</font></html>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return host.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
return dataProviderSupport.getData(dataKey)
|
return dataProviderSupport.getData(dataKey)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,8 +183,8 @@ object AccountHttp {
|
|||||||
|
|
||||||
if (isRefreshing.compareAndSet(false, true)) {
|
if (isRefreshing.compareAndSet(false, true)) {
|
||||||
try {
|
try {
|
||||||
// 刷新 token
|
// 刷新 token 和用户
|
||||||
accountManager.refreshToken()
|
accountManager.refresh()
|
||||||
} finally {
|
} finally {
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
isRefreshing.set(false)
|
isRefreshing.set(false)
|
||||||
|
|||||||
@@ -14,12 +14,15 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||||||
import org.apache.commons.codec.binary.Base64
|
import org.apache.commons.codec.binary.Base64
|
||||||
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.slf4j.LoggerFactory
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
class AccountManager private constructor() : ApplicationRunnerExtension {
|
class AccountManager private constructor() : ApplicationRunnerExtension {
|
||||||
companion object {
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(AccountManager::class.java)
|
||||||
|
|
||||||
fun getInstance(): AccountManager {
|
fun getInstance(): AccountManager {
|
||||||
return ApplicationScope.forApplicationScope()
|
return ApplicationScope.forApplicationScope()
|
||||||
.getOrCreate(AccountManager::class) { AccountManager() }
|
.getOrCreate(AccountManager::class) { AccountManager() }
|
||||||
@@ -30,6 +33,7 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val serverManager get() = ServerManager.getInstance()
|
||||||
private var account = locally()
|
private var account = locally()
|
||||||
private val accountProperties get() = AccountProperties.getInstance()
|
private val accountProperties get() = AccountProperties.getInstance()
|
||||||
|
|
||||||
@@ -48,10 +52,14 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
|
|||||||
fun getAccessToken() = account.accessToken
|
fun getAccessToken() = account.accessToken
|
||||||
fun getRefreshToken() = account.refreshToken
|
fun getRefreshToken() = account.refreshToken
|
||||||
fun getOwnerIds() = account.teams.map { it.id }.toMutableList().apply { add(getAccountId()) }.toSet()
|
fun getOwnerIds() = account.teams.map { it.id }.toMutableList().apply { add(getAccountId()) }.toSet()
|
||||||
fun getOwners() =
|
fun getOwners(): Set<AccountOwner> {
|
||||||
account.teams.map { AccountOwner(it.id, it.name, OwnerType.Team) }
|
val owners = mutableSetOf<AccountOwner>()
|
||||||
.toMutableList().apply { AccountOwner(getAccountId(), getEmail(), OwnerType.User) }
|
owners.add(AccountOwner(getAccountId(), getEmail(), OwnerType.User))
|
||||||
.toSet()
|
for (team in getTeams()) {
|
||||||
|
owners.add(AccountOwner(team.id, team.name, OwnerType.Team))
|
||||||
|
}
|
||||||
|
return owners
|
||||||
|
}
|
||||||
|
|
||||||
fun isFreePlan(): Boolean {
|
fun isFreePlan(): Boolean {
|
||||||
return isLocally() || getSubscription().plan == SubscriptionPlan.Free
|
return isLocally() || getSubscription().plan == SubscriptionPlan.Free
|
||||||
@@ -126,6 +134,7 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
|
|||||||
* 设置账户信息,可以多次调用,每次修改用户信息都要通过这个方法
|
* 设置账户信息,可以多次调用,每次修改用户信息都要通过这个方法
|
||||||
*/
|
*/
|
||||||
internal fun login(account: Account) {
|
internal fun login(account: Account) {
|
||||||
|
synchronized(this) {
|
||||||
|
|
||||||
val oldAccount = this.account
|
val oldAccount = this.account
|
||||||
|
|
||||||
@@ -158,6 +167,7 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
|
|||||||
// 通知变化
|
// 通知变化
|
||||||
notifyAccountChanged(oldAccount, account)
|
notifyAccountChanged(oldAccount, account)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun notifyAccountChanged(oldAccount: Account, newAccount: Account) {
|
private fun notifyAccountChanged(oldAccount: Account, newAccount: Account) {
|
||||||
if (SwingUtilities.isEventDispatchThread()) {
|
if (SwingUtilities.isEventDispatchThread()) {
|
||||||
@@ -220,7 +230,7 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
|
|||||||
|
|
||||||
override fun ready() {
|
override fun ready() {
|
||||||
if (isLocally().not()) {
|
if (isLocally().not()) {
|
||||||
swingCoroutineScope.launch(Dispatchers.IO) { refreshToken() }
|
swingCoroutineScope.launch(Dispatchers.IO) { refresh() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,8 +238,34 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
|
|||||||
/**
|
/**
|
||||||
* 刷新用户
|
* 刷新用户
|
||||||
*/
|
*/
|
||||||
fun refresh(accessToken: String = getAccessToken()) {
|
fun refresh() {
|
||||||
|
runCatching { refreshToken() }.onSuccess {
|
||||||
|
refreshAccount()
|
||||||
|
}.onFailure {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(it.message, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshAccount() {
|
||||||
|
try {
|
||||||
|
val me = serverManager.callMe(account.server, getAccessToken())
|
||||||
|
val teams = me.teams.map {
|
||||||
|
Team(
|
||||||
|
id = it.id,
|
||||||
|
name = it.name,
|
||||||
|
secretKey = RSA.decrypt(getPrivateKey(), Base64.decodeBase64(it.secretKey)),
|
||||||
|
role = it.role
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 重新登录
|
||||||
|
login(account.copy(teams = teams, subscriptions = me.subscriptions))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
|
class AccountApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
|
||||||
|
|||||||
@@ -75,8 +75,10 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
|
|||||||
val subscription = accountManager.getSubscription()
|
val subscription = accountManager.getSubscription()
|
||||||
val isFreePlan = accountManager.isFreePlan()
|
val isFreePlan = accountManager.isFreePlan()
|
||||||
val isLocally = accountManager.isLocally()
|
val isLocally = accountManager.isLocally()
|
||||||
val validTo = if (isFreePlan) "-" else if (subscription.endAt >= Long.MAX_VALUE)
|
val validTo = if (isFreePlan) "-"
|
||||||
I18n.getString("termora.settings.account.lifetime") else
|
else if (subscription.endAt >= Long.MAX_VALUE)
|
||||||
|
I18n.getString("termora.settings.account.lifetime")
|
||||||
|
else
|
||||||
DateFormatUtils.format(Date(subscription.endAt), I18n.getString("termora.date-format"))
|
DateFormatUtils.format(Date(subscription.endAt), I18n.getString("termora.date-format"))
|
||||||
val lastSynchronizationOn = if (isFreePlan) "-" else
|
val lastSynchronizationOn = if (isFreePlan) "-" else
|
||||||
DateFormatUtils.format(
|
DateFormatUtils.format(
|
||||||
@@ -158,9 +160,19 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
|
|||||||
if (isFreePlan.not()) {
|
if (isFreePlan.not()) {
|
||||||
actions.add(JXHyperlink(object : AnAction(I18n.getString("termora.settings.account.sync-now")) {
|
actions.add(JXHyperlink(object : AnAction(I18n.getString("termora.settings.account.sync-now")) {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
// 全量同步
|
||||||
|
accountProperties.nextSynchronizationSince = 0
|
||||||
|
|
||||||
|
// 拉取
|
||||||
PullService.getInstance().trigger()
|
PullService.getInstance().trigger()
|
||||||
|
|
||||||
|
// 推送
|
||||||
PushService.getInstance().trigger()
|
PushService.getInstance().trigger()
|
||||||
|
|
||||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
|
// 刷新账户
|
||||||
|
accountManager.refreshAccount()
|
||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
isEnabled = false
|
isEnabled = false
|
||||||
lastSynchronizationOnLabel.text = DateFormatUtils.format(
|
lastSynchronizationOnLabel.text = DateFormatUtils.format(
|
||||||
|
|||||||
@@ -2,5 +2,4 @@ package app.termora.account
|
|||||||
|
|
||||||
import app.termora.database.OwnerType
|
import app.termora.database.OwnerType
|
||||||
|
|
||||||
data class AccountOwner(val id: String, val name: String, val type: OwnerType) {
|
data class AccountOwner(val id: String, val name: String, val type: OwnerType)
|
||||||
}
|
|
||||||
@@ -48,6 +48,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
Server(I18n.getString("termora.settings.account.server-singapore"), "https://account.termora.app")
|
Server(I18n.getString("termora.settings.account.server-singapore"), "https://account.termora.app")
|
||||||
private val chinaServer =
|
private val chinaServer =
|
||||||
Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn")
|
Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn")
|
||||||
|
private val serverManager get() = ServerManager.getInstance()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
isModal = true
|
isModal = true
|
||||||
@@ -359,7 +360,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
|
|
||||||
val loginJob = swingCoroutineScope.launch(Dispatchers.IO) {
|
val loginJob = swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
ServerManager.getInstance().login(
|
serverManager.login(
|
||||||
server, usernameTextField.text,
|
server, usernameTextField.text,
|
||||||
String(passwordField.password), mfaTextField.text.trim()
|
String(passwordField.password), mfaTextField.text.trim()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,6 +45,15 @@ class PullService private constructor() : SyncService(), Disposable, Application
|
|||||||
lastChangeHash = StringUtils.EMPTY
|
lastChangeHash = StringUtils.EMPTY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 团队变了,全量同步
|
||||||
|
if (oldAccount.id == newAccount.id) {
|
||||||
|
if (oldAccount.teams != newAccount.teams) {
|
||||||
|
accountProperties.nextSynchronizationSince = 0
|
||||||
|
trigger()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (oldAccount.isLocally && newAccount.isLocally.not()) {
|
if (oldAccount.isLocally && newAccount.isLocally.not()) {
|
||||||
trigger()
|
trigger()
|
||||||
}
|
}
|
||||||
@@ -213,7 +222,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
|
|||||||
log.debug("拉取数据: {} 成功, 响应码: {}", id, response.code)
|
log.debug("拉取数据: {} 成功, 响应码: {}", id, response.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(response.isSuccessful.not()){
|
if (response.isSuccessful.not()) {
|
||||||
IOUtils.closeQuietly(response)
|
IOUtils.closeQuietly(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +290,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
|
|||||||
ownerId = ownerId,
|
ownerId = ownerId,
|
||||||
ownerType = ownerType,
|
ownerType = ownerType,
|
||||||
type = type,
|
type = type,
|
||||||
data = decryptData(id, data),
|
data = decryptData(id, data, ownerId),
|
||||||
version = version,
|
version = version,
|
||||||
// 因为已经是拉取最新版本了,所以这里无需再同步了
|
// 因为已经是拉取最新版本了,所以这里无需再同步了
|
||||||
synced = true,
|
synced = true,
|
||||||
@@ -298,11 +307,8 @@ class PullService private constructor() : SyncService(), Disposable, Application
|
|||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("数据: {}, 类型: {} 云端已经删除,本地即将删除", id, type)
|
log.debug("数据: {}, 类型: {} 云端已经删除,本地即将删除", id, type)
|
||||||
}
|
}
|
||||||
databaseManager.delete(
|
|
||||||
id, type,
|
|
||||||
DatabaseChangedExtension.Source.Sync
|
|
||||||
)
|
|
||||||
|
|
||||||
|
databaseManager.delete(id, type, DatabaseChangedExtension.Source.Sync)
|
||||||
if (log.isInfoEnabled) {
|
if (log.isInfoEnabled) {
|
||||||
log.info("数据: {}, 类型: {} 已从本地删除", id, type)
|
log.info("数据: {}, 类型: {} 已从本地删除", id, type)
|
||||||
|
|
||||||
@@ -340,7 +346,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
|
|||||||
ownerId = ownerId,
|
ownerId = ownerId,
|
||||||
ownerType = ownerType,
|
ownerType = ownerType,
|
||||||
type = type,
|
type = type,
|
||||||
data = decryptData(id, data),
|
data = decryptData(id, data, ownerId),
|
||||||
version = version,
|
version = version,
|
||||||
// 因为已经是拉取最新版本了,所以这里无需再同步了
|
// 因为已经是拉取最新版本了,所以这里无需再同步了
|
||||||
synced = true,
|
synced = true,
|
||||||
@@ -377,7 +383,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
|
|||||||
pullChanges()
|
pullChanges()
|
||||||
|
|
||||||
// N 秒拉一次
|
// N 秒拉一次
|
||||||
val result = withTimeoutOrNull(Random.nextInt(5, 15).seconds) {
|
val result = withTimeoutOrNull(Random.nextInt(3, 10).seconds) {
|
||||||
channel.receiveCatching()
|
channel.receiveCatching()
|
||||||
} ?: continue
|
} ?: continue
|
||||||
if (result.isFailure) break
|
if (result.isFailure) break
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import app.termora.*
|
|||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.database.Data
|
import app.termora.database.Data
|
||||||
import app.termora.database.DatabaseChangedExtension
|
import app.termora.database.DatabaseChangedExtension
|
||||||
|
import app.termora.database.OwnerType
|
||||||
import app.termora.plugin.DispatchThread
|
import app.termora.plugin.DispatchThread
|
||||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -106,7 +107,20 @@ class PushService private constructor() : SyncService(), Disposable, Application
|
|||||||
.delete()
|
.delete()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
try {
|
||||||
AccountHttp.execute(request = request)
|
AccountHttp.execute(request = request)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is ResponseException) {
|
||||||
|
if (e.code == 403) {
|
||||||
|
// 如果是 Team 发现没有权限,那么很有可能是被提出团队
|
||||||
|
if (data.ownerType == OwnerType.Team.name) {
|
||||||
|
// 刷新用户
|
||||||
|
accountManager.refreshAccount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
// 修改为已经同步
|
// 修改为已经同步
|
||||||
updateData(data.id, synced = true)
|
updateData(data.id, synced = true)
|
||||||
@@ -153,6 +167,12 @@ class PushService private constructor() : SyncService(), Disposable, Application
|
|||||||
}
|
}
|
||||||
// 标记为已经同步
|
// 标记为已经同步
|
||||||
updateData(data.id, synced = true, version = data.version)
|
updateData(data.id, synced = true, version = data.version)
|
||||||
|
|
||||||
|
// 如果是 Team 发现没有权限,那么很有可能是被提出团队
|
||||||
|
if (data.ownerType == OwnerType.Team.name) {
|
||||||
|
// 刷新用户
|
||||||
|
accountManager.refreshAccount()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
} else if (response.code == 409) { // 版本冲突,一般来说是云端版本大于本地版本
|
} else if (response.code == 409) { // 版本冲突,一般来说是云端版本大于本地版本
|
||||||
val json = ohMyJson.decodeFromString<JsonObject>(text)
|
val json = ohMyJson.decodeFromString<JsonObject>(text)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class ServerManager private constructor() {
|
|||||||
val loginResponse = callLogin(serverInfo, server, username, password, mfa)
|
val loginResponse = callLogin(serverInfo, server, username, password, mfa)
|
||||||
|
|
||||||
// call me
|
// call me
|
||||||
val meResponse = callMe(server, loginResponse.accessToken)
|
val meResponse = callMe(server.server, loginResponse.accessToken)
|
||||||
|
|
||||||
// 解密
|
// 解密
|
||||||
val salt = "${serverInfo.salt}:${username}".toByteArray()
|
val salt = "${serverInfo.salt}:${username}".toByteArray()
|
||||||
@@ -139,9 +139,9 @@ class ServerManager private constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun callMe(server: Server, accessToken: String): MeResponse {
|
fun callMe(server: String, accessToken: String): MeResponse {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url("${server.server}/v1/users/me")
|
.url("${server}/v1/users/me")
|
||||||
.header("Authorization", "Bearer $accessToken")
|
.header("Authorization", "Bearer $accessToken")
|
||||||
.build()
|
.build()
|
||||||
val text = AccountHttp.execute(request = request)
|
val text = AccountHttp.execute(request = request)
|
||||||
@@ -149,13 +149,13 @@ class ServerManager private constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class ServerInfo(val salt: String)
|
data class ServerInfo(val salt: String)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class LoginResponse(val accessToken: String, val refreshToken: String)
|
data class LoginResponse(val accessToken: String, val refreshToken: String)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class MeResponse(
|
data class MeResponse(
|
||||||
val id: String,
|
val id: String,
|
||||||
val email: String,
|
val email: String,
|
||||||
val publicKey: String,
|
val publicKey: String,
|
||||||
@@ -167,5 +167,5 @@ class ServerManager private constructor() {
|
|||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class MeTeam(val id: String, val name: String, val role: TeamRole, val secretKey: String)
|
data class MeTeam(val id: String, val name: String, val role: TeamRole, val secretKey: String)
|
||||||
}
|
}
|
||||||
@@ -78,28 +78,23 @@ abstract class SyncService {
|
|||||||
|
|
||||||
protected fun encryptData(id: String, data: String, ownerId: String): String {
|
protected fun encryptData(id: String, data: String, ownerId: String): String {
|
||||||
val iv = DigestUtils.sha256(id).copyOf(12)
|
val iv = DigestUtils.sha256(id).copyOf(12)
|
||||||
var secretKey = EMPTY_BYTE_ARRAY
|
val secretKey = getSecretKey(ownerId)
|
||||||
if (ownerId != accountManager.getAccountId()) {
|
|
||||||
val team = accountManager.getTeams().firstOrNull { it.id == ownerId }
|
|
||||||
if (team == null) {
|
|
||||||
return StringUtils.EMPTY
|
|
||||||
} else {
|
|
||||||
secretKey = team.secretKey
|
|
||||||
}
|
|
||||||
} else if (ownerId == accountManager.getAccountId()) {
|
|
||||||
secretKey = accountManager.getSecretKey()
|
|
||||||
}
|
|
||||||
if (secretKey.isEmpty()) return StringUtils.EMPTY
|
if (secretKey.isEmpty()) return StringUtils.EMPTY
|
||||||
return Base64.encodeBase64String(AES.GCM.encrypt(secretKey, iv, data.toByteArray()))
|
return Base64.encodeBase64String(AES.GCM.encrypt(secretKey, iv, data.toByteArray()))
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun decryptData(id: String, data: String): String {
|
protected fun getSecretKey(ownerId: String): ByteArray {
|
||||||
|
if (ownerId == accountManager.getAccountId()) {
|
||||||
|
return accountManager.getSecretKey()
|
||||||
|
}
|
||||||
|
val team = accountManager.getTeams().firstOrNull { it.id == ownerId }
|
||||||
|
return team?.secretKey ?: EMPTY_BYTE_ARRAY
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun decryptData(id: String, data: String, ownerId: String): String {
|
||||||
val iv = DigestUtils.sha256(id).copyOf(12)
|
val iv = DigestUtils.sha256(id).copyOf(12)
|
||||||
return String(
|
val secretKey = getSecretKey(ownerId)
|
||||||
AES.GCM.decrypt(
|
if (secretKey.isEmpty()) throw IllegalStateException("根据 ownerId 无法获取对应密钥")
|
||||||
accountManager.getSecretKey(), iv,
|
return String(AES.GCM.decrypt(secretKey, iv, Base64.decodeBase64(data)))
|
||||||
Base64.decodeBase64(data)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,4 +26,22 @@ class Team(
|
|||||||
* 所属角色
|
* 所属角色
|
||||||
*/
|
*/
|
||||||
val role: TeamRole,
|
val role: TeamRole,
|
||||||
)
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Team
|
||||||
|
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (name != other.name) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id.hashCode()
|
||||||
|
result = 31 * result + name.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.jdesktop.swingx.action.BoundAction
|
import org.jdesktop.swingx.action.BoundAction
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
@@ -20,7 +21,7 @@ abstract class AnAction : BoundAction {
|
|||||||
if (evt is AnActionEvent) {
|
if (evt is AnActionEvent) {
|
||||||
actionPerformed(evt)
|
actionPerformed(evt)
|
||||||
} else {
|
} else {
|
||||||
actionPerformed(AnActionEvent(evt.source, evt.actionCommand, evt))
|
actionPerformed(AnActionEvent(evt.source, StringUtils.defaultString(evt.actionCommand), evt))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
import app.termora.NewHostDialogV2
|
import app.termora.NewHostDialogV2
|
||||||
|
import app.termora.account.AccountManager
|
||||||
import app.termora.tree.HostTreeNode
|
import app.termora.tree.HostTreeNode
|
||||||
import javax.swing.tree.TreePath
|
import javax.swing.tree.TreePath
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ class NewHostAction : AnAction() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val accountManager get() = AccountManager.getInstance()
|
||||||
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val tree = evt.getData(DataProviders.Welcome.HostTree) ?: return
|
val tree = evt.getData(DataProviders.Welcome.HostTree) ?: return
|
||||||
var lastNode = (tree.lastSelectedPathComponent ?: tree.model.root) as? HostTreeNode ?: return
|
var lastNode = (tree.lastSelectedPathComponent ?: tree.model.root) as? HostTreeNode ?: return
|
||||||
@@ -27,7 +30,7 @@ class NewHostAction : AnAction() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val lastHost = lastNode.host
|
val lastHost = lastNode.host
|
||||||
val dialog = NewHostDialogV2(evt.window)
|
val dialog = NewHostDialogV2(evt.window, accountOwner = accountManager.getOwners().first { it.id == lastHost.ownerId })
|
||||||
dialog.setLocationRelativeTo(evt.window)
|
dialog.setLocationRelativeTo(evt.window)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
val host = (dialog.host ?: return).copy(
|
val host = (dialog.host ?: return).copy(
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import app.termora.highlight.KeywordHighlightManager
|
|||||||
import app.termora.keymap.KeymapManager
|
import app.termora.keymap.KeymapManager
|
||||||
import app.termora.keymgr.KeyManager
|
import app.termora.keymgr.KeyManager
|
||||||
import app.termora.macro.MacroManager
|
import app.termora.macro.MacroManager
|
||||||
import app.termora.plugin.ExtensionManager
|
|
||||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||||
import app.termora.snippet.SnippetManager
|
import app.termora.snippet.SnippetManager
|
||||||
import app.termora.terminal.CursorStyle
|
import app.termora.terminal.CursorStyle
|
||||||
@@ -23,6 +22,7 @@ import org.jetbrains.exposed.v1.core.statements.StatementType
|
|||||||
import org.jetbrains.exposed.v1.jdbc.*
|
import org.jetbrains.exposed.v1.jdbc.*
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
@@ -32,11 +32,8 @@ import kotlin.reflect.KProperty
|
|||||||
|
|
||||||
class DatabaseManager private constructor() : Disposable {
|
class DatabaseManager private constructor() : Disposable {
|
||||||
companion object {
|
companion object {
|
||||||
|
val log: Logger = LoggerFactory.getLogger(DatabaseManager::class.java)
|
||||||
|
|
||||||
private const val DB_PASSWORD = "DB_PASSWORD"
|
|
||||||
private const val DB_SALT = "DB_SALT"
|
|
||||||
|
|
||||||
val log = LoggerFactory.getLogger(DatabaseManager::class.java)!!
|
|
||||||
fun getInstance(): DatabaseManager {
|
fun getInstance(): DatabaseManager {
|
||||||
return ApplicationScope.forApplicationScope()
|
return ApplicationScope.forApplicationScope()
|
||||||
.getOrCreate(DatabaseManager::class) { DatabaseManager() }
|
.getOrCreate(DatabaseManager::class) { DatabaseManager() }
|
||||||
@@ -52,14 +49,6 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
val sftp by lazy { SFTP(this) }
|
val sftp by lazy { SFTP(this) }
|
||||||
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
internal var dbPassword = StringUtils.EMPTY
|
|
||||||
private set
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
internal var dbSalt = StringUtils.EMPTY
|
|
||||||
private set
|
|
||||||
|
|
||||||
private val map = Collections.synchronizedMap<String, String?>(mutableMapOf())
|
private val map = Collections.synchronizedMap<String, String?>(mutableMapOf())
|
||||||
private val accountManager get() = AccountManager.getInstance()
|
private val accountManager get() = AccountManager.getInstance()
|
||||||
|
|
||||||
@@ -101,11 +90,6 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
// 注册动态扩展
|
// 注册动态扩展
|
||||||
registerDynamicExtensions()
|
registerDynamicExtensions()
|
||||||
|
|
||||||
for (extension in ExtensionManager.getInstance().getExtensions(DatabaseReadyExtension::class.java)) {
|
|
||||||
extension.ready(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun registerDynamicExtensions() {
|
private fun registerDynamicExtensions() {
|
||||||
@@ -308,6 +292,7 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
DataEntity.update({ DataEntity.id eq id }) {
|
DataEntity.update({ DataEntity.id eq id }) {
|
||||||
it[DataEntity.deleted] = true
|
it[DataEntity.deleted] = true
|
||||||
// 如果是本地用户,那么删除是不需要同步的,云端用户才需要同步
|
// 如果是本地用户,那么删除是不需要同步的,云端用户才需要同步
|
||||||
|
// 云端用户也会判断,如果来源 Sync 那么默认同步了
|
||||||
it[DataEntity.synced] = accountManager.isLocally()
|
it[DataEntity.synced] = accountManager.isLocally()
|
||||||
it[DataEntity.data] = StringUtils.EMPTY
|
it[DataEntity.data] = StringUtils.EMPTY
|
||||||
}
|
}
|
||||||
@@ -367,7 +352,6 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
|
|
||||||
|
|
||||||
private inner class AccountDataTransferExtension : AccountExtension {
|
private inner class AccountDataTransferExtension : AccountExtension {
|
||||||
private val hostManager get() = HostManager.getInstance()
|
|
||||||
override fun onAccountChanged(oldAccount: Account, newAccount: Account) {
|
override fun onAccountChanged(oldAccount: Account, newAccount: Account) {
|
||||||
if (oldAccount.isLocally && newAccount.isLocally) {
|
if (oldAccount.isLocally && newAccount.isLocally) {
|
||||||
return
|
return
|
||||||
@@ -481,13 +465,10 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果团队变更,那么删除所有旧的团队数据,静默删除
|
||||||
if (oldAccount.id == newAccount.id) {
|
if (oldAccount.id == newAccount.id) {
|
||||||
return
|
if (oldAccount.teams != newAccount.teams) {
|
||||||
}
|
|
||||||
|
|
||||||
for (team in oldAccount.teams) {
|
for (team in oldAccount.teams) {
|
||||||
// 如果被踢出团队,那么移除该团队的所有资产
|
|
||||||
if (newAccount.teams.none { it.id == team.id }) {
|
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
transaction(database) {
|
transaction(database) {
|
||||||
DataEntity.deleteWhere {
|
DataEntity.deleteWhere {
|
||||||
@@ -499,6 +480,7 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,6 +619,11 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
*/
|
*/
|
||||||
var font by StringPropertyDelegate("JetBrains Mono")
|
var font by StringPropertyDelegate("JetBrains Mono")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回退字体
|
||||||
|
*/
|
||||||
|
var fallbackFont by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认终端
|
* 默认终端
|
||||||
*/
|
*/
|
||||||
@@ -722,6 +709,11 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
*/
|
*/
|
||||||
var theme by StringPropertyDelegate("Light")
|
var theme by StringPropertyDelegate("Light")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 布局
|
||||||
|
*/
|
||||||
|
var layout by StringPropertyDelegate(TermoraLayout.Screen.name)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 跟随系统
|
* 跟随系统
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
package app.termora.database
|
|
||||||
|
|
||||||
import app.termora.database.DatabaseManager.Companion.log
|
|
||||||
import app.termora.plugin.DispatchThread
|
|
||||||
import app.termora.plugin.Extension
|
|
||||||
import app.termora.plugin.ExtensionManager
|
|
||||||
import javax.swing.SwingUtilities
|
|
||||||
|
|
||||||
interface DatabaseReadyExtension : Extension {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fireReady(databaseManager: DatabaseManager) {
|
|
||||||
if (SwingUtilities.isEventDispatchThread()) {
|
|
||||||
for (extension in ExtensionManager.getInstance().getExtensions(DatabaseReadyExtension::class.java)) {
|
|
||||||
try {
|
|
||||||
extension.ready(databaseManager)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (log.isErrorEnabled) {
|
|
||||||
log.error(e.message, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SwingUtilities.invokeLater { fireReady(databaseManager) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 数据库初始化完成
|
|
||||||
*/
|
|
||||||
fun ready(databaseManager: DatabaseManager) {}
|
|
||||||
|
|
||||||
override fun getDispatchThread(): DispatchThread {
|
|
||||||
return DispatchThread.BGT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -107,7 +107,7 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
id = keymap.id,
|
id = keymap.id,
|
||||||
ownerId = accountId,
|
ownerId = accountId,
|
||||||
ownerType = OwnerType.User.name,
|
ownerType = OwnerType.User.name,
|
||||||
type = DataType.KeywordHighlight.name,
|
type = DataType.Keymap.name,
|
||||||
data = keymap.toJSON(),
|
data = keymap.toJSON(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ class KeymapPanel : JPanel(BorderLayout()) {
|
|||||||
)
|
)
|
||||||
if (!text.isNullOrBlank()) {
|
if (!text.isNullOrBlank()) {
|
||||||
if (text != keymap.name) {
|
if (text != keymap.name) {
|
||||||
keymapManager.removeKeymap(keymap.name)
|
keymapManager.removeKeymap(keymap.id)
|
||||||
val newKeymap = cloneKeymap(text, keymap)
|
val newKeymap = cloneKeymap(text, keymap)
|
||||||
keymapManager.addKeymap(newKeymap)
|
keymapManager.addKeymap(newKeymap)
|
||||||
keymapModel.removeElementAt(index)
|
keymapModel.removeElementAt(index)
|
||||||
@@ -152,7 +152,7 @@ class KeymapPanel : JPanel(BorderLayout()) {
|
|||||||
messageType = JOptionPane.WARNING_MESSAGE
|
messageType = JOptionPane.WARNING_MESSAGE
|
||||||
) == JOptionPane.YES_OPTION
|
) == JOptionPane.YES_OPTION
|
||||||
) {
|
) {
|
||||||
keymapManager.removeKeymap(keymap.name)
|
keymapManager.removeKeymap(keymap.id)
|
||||||
keymapModel.removeElementAt(index)
|
keymapModel.removeElementAt(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class KeyManagerDialog(
|
|||||||
owner: Window,
|
owner: Window,
|
||||||
private val selectMode: Boolean = false,
|
private val selectMode: Boolean = false,
|
||||||
size: Dimension = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height")),
|
size: Dimension = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height")),
|
||||||
|
private val accountOwner: AccountOwner? = null,
|
||||||
) : DialogWrapper(owner) {
|
) : DialogWrapper(owner) {
|
||||||
var ok: Boolean = false
|
var ok: Boolean = false
|
||||||
|
|
||||||
@@ -56,12 +57,40 @@ class KeyManagerDialog(
|
|||||||
tabbed.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
tabbed.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||||
tabbed.tabPlacement = JTabbedPane.TOP
|
tabbed.tabPlacement = JTabbedPane.TOP
|
||||||
|
|
||||||
|
if (accountOwner == null || accountOwner.type == OwnerType.User) {
|
||||||
tabbed.addTab(
|
tabbed.addTab(
|
||||||
I18n.getString("termora.keymgr.my-keys"),
|
I18n.getString("termora.keymgr.my-keys"),
|
||||||
Icons.user,
|
Icons.user,
|
||||||
KeyManagerPanel(AccountOwner(accountManager.getAccountId(), accountManager.getEmail(), OwnerType.User))
|
KeyManagerPanel(
|
||||||
|
AccountOwner(
|
||||||
|
accountManager.getAccountId(),
|
||||||
|
accountManager.getEmail(),
|
||||||
|
OwnerType.User
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accountOwner != null && accountManager.hasTeamFeature()) {
|
||||||
|
for (team in accountManager.getTeams()) {
|
||||||
|
if (team.id == accountOwner.id) {
|
||||||
|
tabbed.addTab(
|
||||||
|
team.name,
|
||||||
|
Icons.cwmUsers,
|
||||||
|
KeyManagerPanel(
|
||||||
|
AccountOwner(
|
||||||
|
team.id,
|
||||||
|
team.name,
|
||||||
|
OwnerType.Team
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return tabbed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (accountManager.hasTeamFeature()) {
|
if (accountManager.hasTeamFeature()) {
|
||||||
for (team in accountManager.getTeams()) {
|
for (team in accountManager.getTeams()) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app.termora.keymgr
|
|||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
|
import app.termora.plugin.internal.ssh.SshClients
|
||||||
import app.termora.terminal.ControlCharacters
|
import app.termora.terminal.ControlCharacters
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.terminal.PtyConnectorDelegate
|
import app.termora.terminal.PtyConnectorDelegate
|
||||||
@@ -40,7 +41,7 @@ class SSHCopyIdDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val terminalPanel by lazy {
|
private val terminalPanel by lazy {
|
||||||
terminalPanelFactory.createTerminalPanel(terminal, PtyConnectorDelegate())
|
terminalPanelFactory.createTerminalPanel(null, terminal, PtyConnectorDelegate())
|
||||||
.apply { enableFloatingToolbar = false }
|
.apply { enableFloatingToolbar = false }
|
||||||
}
|
}
|
||||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import app.termora.plugin.internal.rdp.RDPInternalPlugin
|
|||||||
import app.termora.plugin.internal.serial.SerialInternalPlugin
|
import app.termora.plugin.internal.serial.SerialInternalPlugin
|
||||||
import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin
|
import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin
|
||||||
import app.termora.plugin.internal.ssh.SSHInternalPlugin
|
import app.termora.plugin.internal.ssh.SSHInternalPlugin
|
||||||
|
import app.termora.plugin.internal.telnet.TelnetInternalPlugin
|
||||||
import app.termora.plugin.internal.wsl.WSLInternalPlugin
|
import app.termora.plugin.internal.wsl.WSLInternalPlugin
|
||||||
import app.termora.swingCoroutineScope
|
import app.termora.swingCoroutineScope
|
||||||
|
import app.termora.terminal.panel.vw.FloatingToolbarPlugin
|
||||||
import app.termora.transfer.internal.local.LocalPlugin
|
import app.termora.transfer.internal.local.LocalPlugin
|
||||||
import app.termora.transfer.internal.sftp.SFTPPlugin
|
import app.termora.transfer.internal.sftp.SFTPPlugin
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
@@ -110,12 +112,14 @@ internal class PluginManager private constructor() {
|
|||||||
|
|
||||||
// ssh plugin
|
// ssh plugin
|
||||||
plugins.add(PluginDescriptor(SSHInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
plugins.add(PluginDescriptor(SSHInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
// serial plugin
|
|
||||||
plugins.add(PluginDescriptor(SerialInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
|
||||||
// local plugin
|
// local plugin
|
||||||
plugins.add(PluginDescriptor(LocalInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
plugins.add(PluginDescriptor(LocalInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
// rdp plugin
|
// rdp plugin
|
||||||
plugins.add(PluginDescriptor(RDPInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
plugins.add(PluginDescriptor(RDPInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
|
// telnet plugin
|
||||||
|
plugins.add(PluginDescriptor(TelnetInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
|
// serial plugin
|
||||||
|
plugins.add(PluginDescriptor(SerialInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
// wsl plugin
|
// wsl plugin
|
||||||
if (SystemUtils.IS_OS_WINDOWS) {
|
if (SystemUtils.IS_OS_WINDOWS) {
|
||||||
plugins.add(PluginDescriptor(WSLInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
plugins.add(PluginDescriptor(WSLInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
@@ -127,6 +131,9 @@ internal class PluginManager private constructor() {
|
|||||||
plugins.add(PluginDescriptor(LocalPlugin(), origin = PluginOrigin.Internal, version = version))
|
plugins.add(PluginDescriptor(LocalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
// sftp transfer plugin
|
// sftp transfer plugin
|
||||||
plugins.add(PluginDescriptor(SFTPPlugin(), origin = PluginOrigin.Internal, version = version))
|
plugins.add(PluginDescriptor(SFTPPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
|
|
||||||
|
// floating
|
||||||
|
plugins.add(PluginDescriptor(FloatingToolbarPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadSystemPlugins() {
|
private fun loadSystemPlugins() {
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import java.awt.Component
|
|||||||
import java.awt.event.ItemEvent
|
import java.awt.event.ItemEvent
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
class BasicProxyOption(private val proxyTypes: List<ProxyType> = listOf(ProxyType.HTTP, ProxyType.SOCKS5)) :
|
class BasicProxyOption(
|
||||||
|
private val proxyTypes: List<ProxyType> = listOf(ProxyType.HTTP, ProxyType.SOCKS5),
|
||||||
|
private val authenticationTypes: List<AuthenticationType> = listOf(AuthenticationType.Password),
|
||||||
|
) :
|
||||||
JPanel(BorderLayout()), Option {
|
JPanel(BorderLayout()), Option {
|
||||||
private val formMargin = "7dlu"
|
private val formMargin = "7dlu"
|
||||||
|
|
||||||
@@ -21,6 +24,10 @@ class BasicProxyOption(private val proxyTypes: List<ProxyType> = listOf(ProxyTyp
|
|||||||
val proxyPortTextField = PortSpinner(1080)
|
val proxyPortTextField = PortSpinner(1080)
|
||||||
val proxyAuthenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
val proxyAuthenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||||
|
|
||||||
|
constructor(proxyTypes: List<ProxyType> = listOf(ProxyType.HTTP, ProxyType.SOCKS5)) : this(
|
||||||
|
proxyTypes,
|
||||||
|
listOf(AuthenticationType.Password)
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -67,7 +74,9 @@ class BasicProxyOption(private val proxyTypes: List<ProxyType> = listOf(ProxyTyp
|
|||||||
}
|
}
|
||||||
|
|
||||||
proxyAuthenticationTypeComboBox.addItem(AuthenticationType.No)
|
proxyAuthenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||||
proxyAuthenticationTypeComboBox.addItem(AuthenticationType.Password)
|
for (type in authenticationTypes) {
|
||||||
|
proxyAuthenticationTypeComboBox.addItem(type)
|
||||||
|
}
|
||||||
|
|
||||||
proxyUsernameTextField.text = "root"
|
proxyUsernameTextField.text = "root"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugin.internal.local
|
package app.termora.plugin.internal.local
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -14,7 +15,11 @@ internal class LocalProtocolHostPanelExtension private constructor() : ProtocolH
|
|||||||
return LocalProtocolProvider.instance
|
return LocalProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return LocalProtocolHostPanel()
|
return LocalProtocolHostPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,8 @@ package app.termora.plugin.internal.local
|
|||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.protocol.GenericProtocolProvider
|
import app.termora.protocol.GenericProtocolProvider
|
||||||
import app.termora.protocol.ProtocolTestRequest
|
|
||||||
import app.termora.protocol.ProtocolTester
|
|
||||||
|
|
||||||
internal class LocalProtocolProvider private constructor() : GenericProtocolProvider, ProtocolTester {
|
internal class LocalProtocolProvider private constructor() : GenericProtocolProvider {
|
||||||
companion object {
|
companion object {
|
||||||
val instance by lazy { LocalProtocolProvider() }
|
val instance by lazy { LocalProtocolProvider() }
|
||||||
const val PROTOCOL = "local"
|
const val PROTOCOL = "local"
|
||||||
@@ -20,9 +18,6 @@ internal class LocalProtocolProvider private constructor() : GenericProtocolProv
|
|||||||
return Icons.powershell
|
return Icons.powershell
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun canTestConnection(requester: ProtocolTestRequest): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createTerminalTab(dataProvider: DataProvider, windowScope: WindowScope, host: Host): TerminalTab {
|
override fun createTerminalTab(dataProvider: DataProvider, windowScope: WindowScope, host: Host): TerminalTab {
|
||||||
return LocalTerminalTab(windowScope, host)
|
return LocalTerminalTab(windowScope, host)
|
||||||
|
|||||||
@@ -232,7 +232,8 @@ class PluginPanel(val descriptor: PluginPluginDescriptor) : JPanel(), Disposable
|
|||||||
|
|
||||||
// 当有多个插件正在安装时,那么最后一个安装成功的询问是否重启
|
// 当有多个插件正在安装时,那么最后一个安装成功的询问是否重启
|
||||||
if (installing.get() <= 1) {
|
if (installing.get() <= 1) {
|
||||||
restarter.scheduleRestart(owner)
|
// 不阻塞按钮状态变更
|
||||||
|
SwingUtilities.invokeLater { restarter.scheduleRestart(owner) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是更新,那么也需要刷新 InstalledPanel 下的按钮状态
|
// 如果是更新,那么也需要刷新 InstalledPanel 下的按钮状态
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app.termora.plugin.internal.plugin
|
|||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
@@ -22,7 +23,6 @@ internal class PluginRepositoryDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
||||||
isModal = true
|
isModal = true
|
||||||
isResizable = false
|
isResizable = false
|
||||||
controlsVisible = false
|
|
||||||
title = "Custom Plugin Repository"
|
title = "Custom Plugin Repository"
|
||||||
list.fixedCellHeight = UIManager.getInt("Tree.rowHeight")
|
list.fixedCellHeight = UIManager.getInt("Tree.rowHeight")
|
||||||
for (url in PluginRepositoryManager.getInstance().getRepositories()) {
|
for (url in PluginRepositoryManager.getInstance().getRepositories()) {
|
||||||
@@ -98,6 +98,21 @@ internal class PluginRepositoryDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
return panel
|
return panel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun addNotify() {
|
||||||
|
super.addNotify()
|
||||||
|
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
NativeMacLibrary.setControlsVisible(
|
||||||
|
this,
|
||||||
|
false,
|
||||||
|
arrayOf(
|
||||||
|
NativeMacLibrary.NSWindowButton.NSWindowZoomButton,
|
||||||
|
NativeMacLibrary.NSWindowButton.NSWindowMiniaturizeButton
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun createSouthPanel(): JComponent? {
|
override fun createSouthPanel(): JComponent? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugin.internal.rdp
|
package app.termora.plugin.internal.rdp
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -14,7 +15,11 @@ internal class RDPProtocolHostPanelExtension private constructor() : ProtocolHos
|
|||||||
return RDPProtocolProvider.instance
|
return RDPProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return RDPProtocolHostPanel()
|
return RDPProtocolHostPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugin.internal.serial
|
package app.termora.plugin.internal.serial
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -14,8 +15,11 @@ internal class SerialProtocolHostPanelExtension private constructor() : Protocol
|
|||||||
return SerialProtocolProvider.instance
|
return SerialProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return SerialProtocolHostPanel()
|
return SerialProtocolHostPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 5
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import app.termora.*
|
|||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
import app.termora.keymgr.KeyManager
|
import app.termora.keymgr.KeyManager
|
||||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||||
|
import app.termora.plugin.internal.ssh.SshClients
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.apache.commons.io.Charsets
|
import org.apache.commons.io.Charsets
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
import app.termora.TerminalTab
|
||||||
|
import app.termora.TerminalTabbedContextMenuExtension
|
||||||
|
import app.termora.WindowScope
|
||||||
|
import app.termora.actions.AnAction
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
|
import javax.swing.JMenuItem
|
||||||
|
|
||||||
|
class CloneSessionTerminalTabbedContextMenuExtension private constructor() : TerminalTabbedContextMenuExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = CloneSessionTerminalTabbedContextMenuExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createJMenuItem(
|
||||||
|
windowScope: WindowScope,
|
||||||
|
tab: TerminalTab
|
||||||
|
): JMenuItem {
|
||||||
|
if (tab is SSHTerminalTab) {
|
||||||
|
if (tab.host.protocol == SSHProtocolProvider.PROTOCOL) {
|
||||||
|
val cloneSession = JMenuItem(I18n.getString("termora.tabbed.contextmenu.clone-session"))
|
||||||
|
val c = tab.getData(SSHTerminalTab.MySshHandler)
|
||||||
|
cloneSession.isEnabled = c?.channel?.isOpen == true
|
||||||
|
if (c != null) {
|
||||||
|
cloneSession.addActionListener(object : AnAction() {
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||||
|
val handler = c.copy(channel = null)
|
||||||
|
val newTab = SSHTerminalTab(windowScope, tab.host, handler)
|
||||||
|
terminalTabbedManager.addTerminalTab(newTab)
|
||||||
|
newTab.start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return cloneSession
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.termora.plugin.internal.ssh
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.keymgr.KeyManager
|
import app.termora.keymgr.KeyManager
|
||||||
import app.termora.keymgr.KeyManagerDialog
|
import app.termora.keymgr.KeyManagerDialog
|
||||||
import app.termora.plugin.internal.BasicProxyOption
|
import app.termora.plugin.internal.BasicProxyOption
|
||||||
@@ -29,7 +30,7 @@ import javax.swing.table.DefaultTableModel
|
|||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
@Suppress("CascadeIf")
|
@Suppress("CascadeIf")
|
||||||
open class SSHHostOptionsPane : OptionsPane() {
|
open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() {
|
||||||
protected val tunnelingOption = TunnelingOption()
|
protected val tunnelingOption = TunnelingOption()
|
||||||
protected val generalOption = GeneralOption()
|
protected val generalOption = GeneralOption()
|
||||||
protected val proxyOption = BasicProxyOption()
|
protected val proxyOption = BasicProxyOption()
|
||||||
@@ -375,6 +376,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
val dialog = KeyManagerDialog(
|
val dialog = KeyManagerDialog(
|
||||||
owner,
|
owner,
|
||||||
selectMode = true,
|
selectMode = true,
|
||||||
|
accountOwner = accountOwner,
|
||||||
)
|
)
|
||||||
dialog.pack()
|
dialog.pack()
|
||||||
dialog.setLocationRelativeTo(owner)
|
dialog.setLocationRelativeTo(owner)
|
||||||
@@ -383,7 +385,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
val selectedItem = publicKeyComboBox.selectedItem
|
val selectedItem = publicKeyComboBox.selectedItem
|
||||||
|
|
||||||
publicKeyComboBox.removeAllItems()
|
publicKeyComboBox.removeAllItems()
|
||||||
for (keyPair in KeyManager.getInstance().getOhKeyPairs()) {
|
for (keyPair in KeyManager.getInstance().getOhKeyPairs(accountOwner.id)) {
|
||||||
publicKeyComboBox.addItem(keyPair.id)
|
publicKeyComboBox.addItem(keyPair.id)
|
||||||
}
|
}
|
||||||
publicKeyComboBox.selectedItem = selectedItem
|
publicKeyComboBox.selectedItem = selectedItem
|
||||||
@@ -465,7 +467,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
if (authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
|
if (authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
|
||||||
val selectedItem = publicKeyComboBox.selectedItem
|
val selectedItem = publicKeyComboBox.selectedItem
|
||||||
publicKeyComboBox.removeAllItems()
|
publicKeyComboBox.removeAllItems()
|
||||||
for (pair in KeyManager.getInstance().getOhKeyPairs()) {
|
for (pair in KeyManager.getInstance().getOhKeyPairs(accountOwner.id)) {
|
||||||
publicKeyComboBox.addItem(pair.id)
|
publicKeyComboBox.addItem(pair.id)
|
||||||
}
|
}
|
||||||
publicKeyComboBox.selectedItem = selectedItem
|
publicKeyComboBox.selectedItem = selectedItem
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugin.internal.ssh
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import app.termora.TerminalTabbedContextMenuExtension
|
||||||
import app.termora.plugin.Extension
|
import app.termora.plugin.Extension
|
||||||
import app.termora.plugin.InternalPlugin
|
import app.termora.plugin.InternalPlugin
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
@@ -9,6 +10,8 @@ internal class SSHInternalPlugin : InternalPlugin() {
|
|||||||
init {
|
init {
|
||||||
support.addExtension(ProtocolProviderExtension::class.java) { SSHProtocolProviderExtension.instance }
|
support.addExtension(ProtocolProviderExtension::class.java) { SSHProtocolProviderExtension.instance }
|
||||||
support.addExtension(ProtocolHostPanelExtension::class.java) { SSHProtocolHostPanelExtension.instance }
|
support.addExtension(ProtocolHostPanelExtension::class.java) { SSHProtocolHostPanelExtension.instance }
|
||||||
|
support.addExtension(TerminalTabbedContextMenuExtension::class.java) { SftpCommandTerminalTabbedContextMenuExtension.instance }
|
||||||
|
support.addExtension(TerminalTabbedContextMenuExtension::class.java) { CloneSessionTerminalTabbedContextMenuExtension.instance }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String {
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ package app.termora.plugin.internal.ssh
|
|||||||
|
|
||||||
import app.termora.Disposer
|
import app.termora.Disposer
|
||||||
import app.termora.Host
|
import app.termora.Host
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
|
|
||||||
class SSHProtocolHostPanel : ProtocolHostPanel() {
|
class SSHProtocolHostPanel(accountOwner: AccountOwner) : ProtocolHostPanel() {
|
||||||
|
|
||||||
private val pane = SSHHostOptionsPane()
|
private val pane = SSHHostOptionsPane(accountOwner)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugin.internal.ssh
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -14,8 +15,12 @@ internal class SSHProtocolHostPanelExtension private constructor() : ProtocolHos
|
|||||||
return SSHProtocolProvider.instance
|
return SSHProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return SSHProtocolHostPanel()
|
return SSHProtocolHostPanel(accountOwner)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.termora.plugin.internal.ssh
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.actions.AnActionEvent
|
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.actions.TabReconnectAction
|
import app.termora.actions.TabReconnectAction
|
||||||
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
||||||
@@ -11,45 +10,36 @@ 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
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.apache.commons.io.Charsets
|
import org.apache.commons.io.Charsets
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
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.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
import org.apache.sshd.common.SshConstants
|
import org.apache.sshd.common.future.CloseFuture
|
||||||
import org.apache.sshd.common.channel.Channel
|
import org.apache.sshd.common.future.SshFutureListener
|
||||||
import org.apache.sshd.common.channel.ChannelListener
|
|
||||||
import org.apache.sshd.common.session.Session
|
|
||||||
import org.apache.sshd.common.session.SessionListener
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.*
|
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
class SSHTerminalTab(
|
||||||
|
windowScope: WindowScope, host: Host,
|
||||||
|
private val handler: SshHandler = SshHandler()
|
||||||
|
) : PtyHostTerminalTab(windowScope, host) {
|
||||||
|
|
||||||
class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|
||||||
PtyHostTerminalTab(windowScope, host) {
|
|
||||||
companion object {
|
companion object {
|
||||||
val SSHSession = DataKey(ClientSession::class)
|
val SSHSession = DataKey(ClientSession::class)
|
||||||
|
internal val MySshHandler = DataKey(SshHandler::class)
|
||||||
private val log = LoggerFactory.getLogger(SSHTerminalTab::class.java)
|
private val log = LoggerFactory.getLogger(SSHTerminalTab::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
private val tab = this
|
private val owner get() = SwingUtilities.getWindowAncestor(terminalPanel)
|
||||||
|
private val tab get() = this
|
||||||
private var sshClient: SshClient? = null
|
|
||||||
private var sshSession: ClientSession? = null
|
|
||||||
private var sshChannelShell: ChannelShell? = null
|
|
||||||
private val terminalTabbedManager
|
|
||||||
get() = AnActionEvent(getJComponent(), StringUtils.EMPTY, EventObject(getJComponent()))
|
|
||||||
.getData(DataProviders.TerminalTabbedManager)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
terminalPanel.dropFiles = false
|
terminalPanel.dropFiles = false
|
||||||
@@ -60,12 +50,10 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
return terminalPanel
|
return terminalPanel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun canReconnect(): Boolean {
|
override fun canReconnect(): Boolean {
|
||||||
return !mutex.isLocked
|
return mutex.isLocked.not()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override suspend fun openPtyConnector(): PtyConnector {
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
if (mutex.tryLock()) {
|
if (mutex.tryLock()) {
|
||||||
try {
|
try {
|
||||||
@@ -87,74 +75,32 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
// hide cursor
|
// hide cursor
|
||||||
terminalModel.setData(DataKey.Companion.ShowCursor, false)
|
terminalModel.setData(DataKey.Companion.ShowCursor, false)
|
||||||
// print
|
// print
|
||||||
terminal.write("SSH client is opening...\r\n")
|
terminal.write("Connecting to remote server ")
|
||||||
}
|
}
|
||||||
|
|
||||||
val owner = SwingUtilities.getWindowAncestor(terminalPanel)
|
val loading = coroutineScope.launch(Dispatchers.Swing) {
|
||||||
val client = SshClients.openClient(host, owner).also { sshClient = it }
|
var c = 0
|
||||||
val sessionListener = MySessionListener()
|
while (isActive) {
|
||||||
val channelListener = MyChannelListener()
|
if (++c > 6) c = 1
|
||||||
|
terminal.write("${ControlCharacters.ESC}[1;32m")
|
||||||
withContext(Dispatchers.Swing) { terminal.write("SSH client opened successfully.\r\n\r\n") }
|
terminal.write(".".repeat(c))
|
||||||
|
terminal.write(" ".repeat(6 - c))
|
||||||
client.addSessionListener(sessionListener)
|
terminal.write("${ControlCharacters.ESC}[0m")
|
||||||
client.addChannelListener(channelListener)
|
delay(350.milliseconds)
|
||||||
|
terminal.write("${ControlCharacters.BS}".repeat(6))
|
||||||
val (session, channel) = try {
|
|
||||||
val session = SshClients.openSession(host, client).also { sshSession = it }
|
|
||||||
val channel = SshClients.openShell(
|
|
||||||
host,
|
|
||||||
terminalPanel.winSize(),
|
|
||||||
session
|
|
||||||
).also { sshChannelShell = it }
|
|
||||||
Pair(session, channel)
|
|
||||||
} finally {
|
|
||||||
client.removeSessionListener(sessionListener)
|
|
||||||
client.removeChannelListener(channelListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newline
|
|
||||||
withContext(Dispatchers.Swing) {
|
|
||||||
terminal.write("\r\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
channel.addChannelListener(object : ChannelListener {
|
|
||||||
private val reconnectShortcut
|
|
||||||
get() = KeymapManager.Companion.getInstance().getActiveKeymap()
|
|
||||||
.getShortcut(TabReconnectAction.Companion.RECONNECT_TAB).firstOrNull()
|
|
||||||
|
|
||||||
override fun channelClosed(channel: Channel, reason: Throwable?) {
|
|
||||||
coroutineScope.launch(Dispatchers.Swing) {
|
|
||||||
terminal.write("\r\n\r\n${ControlCharacters.Companion.ESC}[31m")
|
|
||||||
terminal.write(I18n.getString("termora.terminal.channel-disconnected"))
|
|
||||||
if (reconnectShortcut is KeyShortcut) {
|
|
||||||
terminal.write(
|
|
||||||
I18n.getString(
|
|
||||||
"termora.terminal.channel-reconnect",
|
|
||||||
reconnectShortcut.toString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
terminal.write("\r\n")
|
|
||||||
terminal.write("${ControlCharacters.Companion.ESC}[0m")
|
|
||||||
terminalModel.setData(DataKey.Companion.ShowCursor, false)
|
|
||||||
if (DatabaseManager.getInstance().terminal.autoCloseTabWhenDisconnected) {
|
|
||||||
terminalTabbedManager?.let { manager ->
|
|
||||||
SwingUtilities.invokeLater {
|
|
||||||
manager.closeTerminalTab(tab, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop
|
val channel: ChannelShell
|
||||||
stop()
|
try {
|
||||||
}
|
val client = openClient()
|
||||||
}
|
val session = openSession(client)
|
||||||
})
|
channel = openChannel(session)
|
||||||
|
|
||||||
// 打开隧道
|
// 打开隧道
|
||||||
openTunnelings(session, host)
|
openTunnelings(session, host)
|
||||||
|
} finally {
|
||||||
|
loading.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
// 隐藏提示
|
// 隐藏提示
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
@@ -199,10 +145,68 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openClient(): SshClient {
|
||||||
|
val client = handler.client
|
||||||
|
if (client != null) return client
|
||||||
|
return SshClients.openClient(host, owner).also { handler.client = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openSession(client: SshClient): ClientSession {
|
||||||
|
val session = handler.session
|
||||||
|
if (session != null) return SshSessionPool.register(session, client)
|
||||||
|
return SshClients.openSession(host, client).also { handler.session = SshSessionPool.register(it, client) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openChannel(session: ClientSession): ChannelShell {
|
||||||
|
val channel = SshClients.openShell(host, terminalPanel.winSize(), session)
|
||||||
|
handler.channel = channel
|
||||||
|
|
||||||
|
channel.addCloseFutureListener(object : SshFutureListener<CloseFuture> {
|
||||||
|
private val reconnectShortcut
|
||||||
|
get() = KeymapManager.Companion.getInstance().getActiveKeymap()
|
||||||
|
.getShortcut(TabReconnectAction.Companion.RECONNECT_TAB).firstOrNull()
|
||||||
|
private val autoCloseTabWhenDisconnected get() = DatabaseManager.getInstance().terminal.autoCloseTabWhenDisconnected
|
||||||
|
|
||||||
|
override fun operationComplete(future: CloseFuture) {
|
||||||
|
coroutineScope.launch(Dispatchers.Swing) {
|
||||||
|
terminal.write("\r\n\r\n${ControlCharacters.Companion.ESC}[31m")
|
||||||
|
terminal.write(I18n.getString("termora.terminal.channel-disconnected"))
|
||||||
|
if (reconnectShortcut is KeyShortcut) {
|
||||||
|
terminal.write(
|
||||||
|
I18n.getString(
|
||||||
|
"termora.terminal.channel-reconnect",
|
||||||
|
reconnectShortcut.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
terminal.write("\r\n")
|
||||||
|
terminal.write("${ControlCharacters.Companion.ESC}[0m")
|
||||||
|
terminalModel.setData(DataKey.Companion.ShowCursor, false)
|
||||||
|
|
||||||
|
if (autoCloseTabWhenDisconnected) {
|
||||||
|
terminalTabbedManager?.let { manager ->
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
manager.closeTerminalTab(tab, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
if (dataKey == SSHSession) {
|
if (dataKey == SSHSession) {
|
||||||
return sshSession as T?
|
return handler.session as T?
|
||||||
|
}
|
||||||
|
if (dataKey == MySshHandler) {
|
||||||
|
return handler as T?
|
||||||
}
|
}
|
||||||
return super.getData(dataKey)
|
return super.getData(dataKey)
|
||||||
}
|
}
|
||||||
@@ -211,16 +215,7 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
if (mutex.tryLock()) {
|
if (mutex.tryLock()) {
|
||||||
try {
|
try {
|
||||||
super.stop()
|
super.stop()
|
||||||
|
handler.close()
|
||||||
sshChannelShell?.close(true)
|
|
||||||
sshSession?.disableSessionHeartbeat()
|
|
||||||
sshSession?.disconnect(SshConstants.SSH2_DISCONNECT_BY_APPLICATION, StringUtils.EMPTY)
|
|
||||||
sshSession?.close(true)
|
|
||||||
sshClient?.close(true)
|
|
||||||
|
|
||||||
sshChannelShell = null
|
|
||||||
sshSession = null
|
|
||||||
sshClient = null
|
|
||||||
} finally {
|
} finally {
|
||||||
mutex.unlock()
|
mutex.unlock()
|
||||||
}
|
}
|
||||||
@@ -236,36 +231,4 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
terminalPanel.storeVisualWindows(host.id)
|
terminalPanel.storeVisualWindows(host.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class MySessionListener : SessionListener, Disposable {
|
|
||||||
override fun sessionEvent(session: Session, event: SessionListener.Event) {
|
|
||||||
coroutineScope.launch {
|
|
||||||
when (event) {
|
|
||||||
SessionListener.Event.KeyEstablished -> terminal.write("Session Key exchange successful.\r\n")
|
|
||||||
SessionListener.Event.Authenticated -> terminal.write("Session authentication successful.\r\n\r\n")
|
|
||||||
SessionListener.Event.KexCompleted -> terminal.write("Session KEX negotiation successful.\r\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun sessionEstablished(session: Session) {
|
|
||||||
coroutineScope.launch { terminal.write("Session established.\r\n") }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun sessionCreated(session: Session?) {
|
|
||||||
coroutineScope.launch { terminal.write("Session created.\r\n") }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class MyChannelListener : ChannelListener, Disposable {
|
|
||||||
override fun channelOpenSuccess(channel: Channel) {
|
|
||||||
coroutineScope.launch { terminal.write("Channel shell opened successfully.\r\n") }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun channelInitialized(channel: Channel) {
|
|
||||||
coroutineScope.launch { terminal.write("Channel shell initialization successful.\r\n") }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.actions.*
|
||||||
|
import app.termora.plugin.internal.sftppty.SFTPPtyProtocolProvider
|
||||||
|
import app.termora.plugin.internal.sftppty.SFTPPtyTerminalTab
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.Action
|
||||||
|
import javax.swing.JMenuItem
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
|
||||||
|
class SftpCommandTerminalTabbedContextMenuExtension private constructor() : TerminalTabbedContextMenuExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = SftpCommandTerminalTabbedContextMenuExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val actionManager = ActionManager.getInstance()
|
||||||
|
|
||||||
|
override fun createJMenuItem(
|
||||||
|
windowScope: WindowScope,
|
||||||
|
tab: TerminalTab
|
||||||
|
): JMenuItem {
|
||||||
|
if (tab is HostTerminalTab) {
|
||||||
|
val openHostAction = actionManager.getAction(OpenHostAction.OPEN_HOST)
|
||||||
|
if (openHostAction != null) {
|
||||||
|
if (tab.host.protocol == SSHProtocolProvider.PROTOCOL || tab.host.protocol == SFTPPtyProtocolProvider.PROTOCOL) {
|
||||||
|
val sftpCommand = JMenuItem(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
|
||||||
|
sftpCommand.addActionListener(object : AnAction() {
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
openSFTPPtyTab(tab, openHostAction, evt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return sftpCommand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openSFTPPtyTab(tab: HostTerminalTab, openHostAction: Action, evt: EventObject) {
|
||||||
|
if (SFTPPtyTerminalTab.canSupports.not()) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
tab.windowScope.window,
|
||||||
|
I18n.getString("termora.tabbed.contextmenu.sftp-not-install"),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var host = tab.host
|
||||||
|
|
||||||
|
if (host.protocol == SSHProtocolProvider.PROTOCOL) {
|
||||||
|
val envs = tab.host.options.envs().toMutableMap()
|
||||||
|
val currentDir = tab.getData(DataProviders.Terminal)?.getTerminalModel()
|
||||||
|
?.getData(DataKey.CurrentDir, StringUtils.EMPTY) ?: StringUtils.EMPTY
|
||||||
|
|
||||||
|
if (currentDir.isNotBlank()) {
|
||||||
|
envs["CurrentDir"] = currentDir
|
||||||
|
}
|
||||||
|
|
||||||
|
host = host.copy(
|
||||||
|
protocol = SFTPPtyProtocolProvider.PROTOCOL,
|
||||||
|
options = host.options.copy(env = envs.toPropertiesString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
openHostAction.actionPerformed(OpenHostActionEvent(evt.source, host, evt))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||||
import app.termora.terminal.TerminalSize
|
import app.termora.terminal.TerminalSize
|
||||||
@@ -29,7 +30,6 @@ import org.apache.sshd.client.session.ClientSession
|
|||||||
import org.apache.sshd.client.session.ClientSessionImpl
|
import org.apache.sshd.client.session.ClientSessionImpl
|
||||||
import org.apache.sshd.client.session.SessionFactory
|
import org.apache.sshd.client.session.SessionFactory
|
||||||
import org.apache.sshd.common.AttributeRepository
|
import org.apache.sshd.common.AttributeRepository
|
||||||
import org.apache.sshd.common.SshConstants
|
|
||||||
import org.apache.sshd.common.SshException
|
import org.apache.sshd.common.SshException
|
||||||
import org.apache.sshd.common.channel.ChannelFactory
|
import org.apache.sshd.common.channel.ChannelFactory
|
||||||
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
||||||
@@ -63,7 +63,7 @@ import org.eclipse.jgit.internal.transport.sshd.proxy.AbstractClientProxyConnect
|
|||||||
import org.eclipse.jgit.internal.transport.sshd.proxy.HttpClientConnector
|
import org.eclipse.jgit.internal.transport.sshd.proxy.HttpClientConnector
|
||||||
import org.eclipse.jgit.internal.transport.sshd.proxy.Socks5ClientConnector
|
import org.eclipse.jgit.internal.transport.sshd.proxy.Socks5ClientConnector
|
||||||
import org.eclipse.jgit.transport.CredentialsProvider
|
import org.eclipse.jgit.transport.CredentialsProvider
|
||||||
import org.eclipse.jgit.transport.SshConstants.IDENTITY_AGENT
|
import org.eclipse.jgit.transport.SshConstants
|
||||||
import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
|
import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
|
||||||
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory
|
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -89,7 +89,7 @@ object SshClients {
|
|||||||
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
|
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
|
||||||
|
|
||||||
private val timeout = Duration.ofSeconds(30)
|
private val timeout = Duration.ofSeconds(30)
|
||||||
private val hostManager get() = HostManager.getInstance()
|
private val hostManager get() = HostManager.Companion.getInstance()
|
||||||
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
|
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,7 +166,7 @@ object SshClients {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val jumpHosts = mutableListOf<Host>()
|
val jumpHosts = mutableListOf<Host>()
|
||||||
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
val hosts = HostManager.Companion.getInstance().hosts().associateBy { it.id }
|
||||||
for (jumpHostId in h.options.jumpHosts) {
|
for (jumpHostId in h.options.jumpHosts) {
|
||||||
val e = hosts[jumpHostId]
|
val e = hosts[jumpHostId]
|
||||||
if (e == null) {
|
if (e == null) {
|
||||||
@@ -235,16 +235,16 @@ object SshClients {
|
|||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isMacOS) {
|
||||||
val file = FileUtils.getFile(Application.getBaseDataDir(), "config", "ssh-agent.sock")
|
val file = FileUtils.getFile(Application.getBaseDataDir(), "config", "ssh-agent.sock")
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
entry.setProperty(IDENTITY_AGENT, file.absolutePath)
|
entry.setProperty(SshConstants.IDENTITY_AGENT, file.absolutePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (entry.getProperty(IDENTITY_AGENT).isNullOrBlank()) {
|
if (entry.getProperty(SshConstants.IDENTITY_AGENT).isNullOrBlank()) {
|
||||||
if (host.authentication.password.isNotBlank())
|
if (host.authentication.password.isNotBlank())
|
||||||
entry.setProperty(IDENTITY_AGENT, host.authentication.password)
|
entry.setProperty(SshConstants.IDENTITY_AGENT, host.authentication.password)
|
||||||
else if (SystemInfo.isWindows)
|
else if (SystemInfo.isWindows)
|
||||||
entry.setProperty(IDENTITY_AGENT, PageantConnector.DESCRIPTOR.identityAgent)
|
entry.setProperty(SshConstants.IDENTITY_AGENT, PageantConnector.DESCRIPTOR.identityAgent)
|
||||||
else
|
else
|
||||||
entry.setProperty(IDENTITY_AGENT, UnixDomainSocketConnector.DESCRIPTOR.identityAgent)
|
entry.setProperty(SshConstants.IDENTITY_AGENT, UnixDomainSocketConnector.DESCRIPTOR.identityAgent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ object SshClients {
|
|||||||
throw SshException("Authentication failed")
|
throw SshException("Authentication failed")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e !is SshException || e.disconnectCode != SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) throw e
|
if (e !is SshException || e.disconnectCode != org.apache.sshd.common.SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) throw e
|
||||||
val owner = client.properties["owner"] as Window? ?: throw e
|
val owner = client.properties["owner"] as Window? ?: throw e
|
||||||
val askUserInfo = ask(host, entry, owner) ?: throw e
|
val askUserInfo = ask(host, entry, owner) ?: throw e
|
||||||
if (askUserInfo.authentication.type == AuthenticationType.No) throw e
|
if (askUserInfo.authentication.type == AuthenticationType.No) throw e
|
||||||
@@ -383,7 +383,7 @@ object SshClients {
|
|||||||
|
|
||||||
val channelFactories = mutableListOf<ChannelFactory>()
|
val channelFactories = mutableListOf<ChannelFactory>()
|
||||||
channelFactories.addAll(ClientBuilder.DEFAULT_CHANNEL_FACTORIES)
|
channelFactories.addAll(ClientBuilder.DEFAULT_CHANNEL_FACTORIES)
|
||||||
channelFactories.add(X11ChannelFactory.INSTANCE)
|
channelFactories.add(X11ChannelFactory.Companion.INSTANCE)
|
||||||
builder.channelFactories(channelFactories)
|
builder.channelFactories(channelFactories)
|
||||||
|
|
||||||
val sshClient = builder.build() as JGitSshClient
|
val sshClient = builder.build() as JGitSshClient
|
||||||
@@ -726,4 +726,3 @@ object SshClients {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import org.apache.sshd.client.SshClient
|
||||||
|
import org.apache.sshd.client.session.ClientSession
|
||||||
|
import org.apache.sshd.common.channel.Channel
|
||||||
|
|
||||||
|
data class SshHandler(
|
||||||
|
var client: SshClient? = null,
|
||||||
|
var session: ClientSession? = null,
|
||||||
|
var channel: Channel? = null
|
||||||
|
) : AutoCloseable {
|
||||||
|
override fun close() {
|
||||||
|
|
||||||
|
channel?.close(true)?.await()
|
||||||
|
session?.close(true)?.await()
|
||||||
|
|
||||||
|
channel = null
|
||||||
|
session = null
|
||||||
|
|
||||||
|
|
||||||
|
// client 由 SshSessionPool 负责关闭
|
||||||
|
if (client?.isClosing == true || client?.isClosed == true) {
|
||||||
|
client = null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import org.apache.sshd.client.SshClient
|
||||||
|
import org.apache.sshd.client.channel.ChannelExec
|
||||||
|
import org.apache.sshd.client.channel.ChannelShell
|
||||||
|
import org.apache.sshd.client.session.ClientSession
|
||||||
|
import org.apache.sshd.client.session.forward.DynamicPortForwardingTracker
|
||||||
|
import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker
|
||||||
|
import org.apache.sshd.common.AttributeRepository
|
||||||
|
import org.apache.sshd.common.channel.Channel
|
||||||
|
import org.apache.sshd.common.channel.PtyChannelConfigurationHolder
|
||||||
|
import org.apache.sshd.common.channel.throttle.ChannelStreamWriter
|
||||||
|
import org.apache.sshd.common.channel.throttle.ChannelStreamWriterResolver
|
||||||
|
import org.apache.sshd.common.future.CloseFuture
|
||||||
|
import org.apache.sshd.common.future.DefaultCloseFuture
|
||||||
|
import org.apache.sshd.common.io.IoWriteFuture
|
||||||
|
import org.apache.sshd.common.session.SessionHeartbeatController
|
||||||
|
import org.apache.sshd.common.util.buffer.Buffer
|
||||||
|
import org.apache.sshd.common.util.net.SshdSocketAddress
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.net.SocketAddress
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import java.util.function.Function
|
||||||
|
|
||||||
|
internal object SshSessionPool {
|
||||||
|
private val map = WeakHashMap<ClientSession, MyClientSession>()
|
||||||
|
|
||||||
|
fun register(session: ClientSession, client: SshClient): ClientSession {
|
||||||
|
if (session is MyClientSession) {
|
||||||
|
session.refCount.incrementAndGet()
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(session) {
|
||||||
|
val delegate = map[session] ?: MyClientSession(client, session)
|
||||||
|
map[session] = delegate
|
||||||
|
delegate.refCount.incrementAndGet()
|
||||||
|
return delegate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class MyClientSession(
|
||||||
|
private val client: SshClient,
|
||||||
|
private val delegate: ClientSession
|
||||||
|
) : ClientSession by delegate {
|
||||||
|
val refCount = AtomicInteger(0)
|
||||||
|
|
||||||
|
override fun createShellChannel(): ChannelShell? {
|
||||||
|
return delegate.createShellChannel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createExecChannel(command: String?): ChannelExec? {
|
||||||
|
return delegate.createExecChannel(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createExecChannel(
|
||||||
|
command: String?,
|
||||||
|
ptyConfig: PtyChannelConfigurationHolder?,
|
||||||
|
env: Map<String?, *>?
|
||||||
|
): ChannelExec? {
|
||||||
|
return delegate.createExecChannel(command, ptyConfig, env)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun executeRemoteCommand(command: String?): String? {
|
||||||
|
return delegate.executeRemoteCommand(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun executeRemoteCommand(
|
||||||
|
command: String?,
|
||||||
|
stderr: OutputStream?,
|
||||||
|
charset: Charset?
|
||||||
|
): String? {
|
||||||
|
return delegate.executeRemoteCommand(command, stderr, charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun executeRemoteCommand(
|
||||||
|
command: String?,
|
||||||
|
stdout: OutputStream?,
|
||||||
|
stderr: OutputStream?,
|
||||||
|
charset: Charset?
|
||||||
|
) {
|
||||||
|
delegate.executeRemoteCommand(command, stdout, stderr, charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createLocalPortForwardingTracker(
|
||||||
|
localPort: Int,
|
||||||
|
remote: SshdSocketAddress?
|
||||||
|
): ExplicitPortForwardingTracker? {
|
||||||
|
return delegate.createLocalPortForwardingTracker(localPort, remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createLocalPortForwardingTracker(
|
||||||
|
local: SshdSocketAddress?,
|
||||||
|
remote: SshdSocketAddress?
|
||||||
|
): ExplicitPortForwardingTracker? {
|
||||||
|
return delegate.createLocalPortForwardingTracker(local, remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createRemotePortForwardingTracker(
|
||||||
|
remote: SshdSocketAddress?,
|
||||||
|
local: SshdSocketAddress?
|
||||||
|
): ExplicitPortForwardingTracker? {
|
||||||
|
return delegate.createRemotePortForwardingTracker(remote, local)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createDynamicPortForwardingTracker(local: SshdSocketAddress?): DynamicPortForwardingTracker? {
|
||||||
|
return delegate.createDynamicPortForwardingTracker(local)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun waitFor(
|
||||||
|
mask: Collection<ClientSession.ClientSessionEvent?>?,
|
||||||
|
timeout: Duration?
|
||||||
|
): Set<ClientSession.ClientSessionEvent?>? {
|
||||||
|
return delegate.waitFor(mask, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createBuffer(cmd: Byte): Buffer? {
|
||||||
|
return delegate.createBuffer(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writePacket(
|
||||||
|
buffer: Buffer?,
|
||||||
|
timeout: Duration?
|
||||||
|
): IoWriteFuture? {
|
||||||
|
return delegate.writePacket(buffer, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writePacket(
|
||||||
|
buffer: Buffer?,
|
||||||
|
maxWaitMillis: Long
|
||||||
|
): IoWriteFuture? {
|
||||||
|
return delegate.writePacket(buffer, maxWaitMillis)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun request(
|
||||||
|
request: String?,
|
||||||
|
buffer: Buffer?,
|
||||||
|
timeout: Long,
|
||||||
|
unit: TimeUnit?
|
||||||
|
): Buffer? {
|
||||||
|
return delegate.request(request, buffer, timeout, unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun request(
|
||||||
|
request: String?,
|
||||||
|
buffer: Buffer?,
|
||||||
|
timeout: Duration?
|
||||||
|
): Buffer? {
|
||||||
|
return delegate.request(request, buffer, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLocalAddress(): SocketAddress? {
|
||||||
|
return delegate.getLocalAddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRemoteAddress(): SocketAddress? {
|
||||||
|
return delegate.getRemoteAddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : Any?> resolveAttribute(key: AttributeRepository.AttributeKey<T?>?): T? {
|
||||||
|
return delegate.resolveAttribute(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSessionHeartbeatType(): SessionHeartbeatController.HeartbeatType? {
|
||||||
|
return delegate.getSessionHeartbeatType()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSessionHeartbeatInterval(): Duration? {
|
||||||
|
return delegate.getSessionHeartbeatInterval()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disableSessionHeartbeat() {
|
||||||
|
delegate.disableSessionHeartbeat()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSessionHeartbeat(
|
||||||
|
type: SessionHeartbeatController.HeartbeatType?,
|
||||||
|
unit: TimeUnit?,
|
||||||
|
count: Long
|
||||||
|
) {
|
||||||
|
delegate.setSessionHeartbeat(type, unit, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSessionHeartbeat(
|
||||||
|
type: SessionHeartbeatController.HeartbeatType?,
|
||||||
|
interval: Duration?
|
||||||
|
) {
|
||||||
|
delegate.setSessionHeartbeat(type, interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isEmpty(): Boolean {
|
||||||
|
return delegate.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLongProperty(name: String?, def: Long): Long {
|
||||||
|
return delegate.getLongProperty(name, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLong(name: String?): Long? {
|
||||||
|
return delegate.getLong(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIntProperty(name: String?, def: Int): Int {
|
||||||
|
return delegate.getIntProperty(name, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInteger(name: String?): Int? {
|
||||||
|
return delegate.getInteger(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBooleanProperty(name: String?, def: Boolean): Boolean {
|
||||||
|
return delegate.getBooleanProperty(name, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBoolean(name: String?): Boolean? {
|
||||||
|
return delegate.getBoolean(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStringProperty(name: String?, def: String?): String? {
|
||||||
|
return delegate.getStringProperty(name, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(name: String?): String? {
|
||||||
|
return delegate.getString(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getObject(name: String?): Any? {
|
||||||
|
return delegate.getObject(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCharset(
|
||||||
|
name: String?,
|
||||||
|
defaultValue: Charset?
|
||||||
|
): Charset? {
|
||||||
|
return delegate.getCharset(name, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : Any?> computeAttributeIfAbsent(
|
||||||
|
key: AttributeRepository.AttributeKey<T?>?,
|
||||||
|
resolver: Function<in AttributeRepository.AttributeKey<T>, out T?>?
|
||||||
|
): T? {
|
||||||
|
return delegate.computeAttributeIfAbsent(key, resolver)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
close(true)?.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close(immediately: Boolean): CloseFuture? {
|
||||||
|
synchronized(delegate) {
|
||||||
|
if (refCount.decrementAndGet() < 1) {
|
||||||
|
delegate.close(immediately).await()
|
||||||
|
return client.close(immediately)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DefaultCloseFuture(this, this).apply { setClosed() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isOpen(): Boolean {
|
||||||
|
return delegate.isOpen()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCipherFactoriesNameList(): String? {
|
||||||
|
return delegate.getCipherFactoriesNameList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCipherFactoriesNames(): List<String?>? {
|
||||||
|
return delegate.getCipherFactoriesNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCipherFactoriesNameList(names: String?) {
|
||||||
|
delegate.setCipherFactoriesNameList(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCipherFactoriesNames(vararg names: String?) {
|
||||||
|
delegate.setCipherFactoriesNames(*names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCipherFactoriesNames(names: Collection<String?>?) {
|
||||||
|
delegate.setCipherFactoriesNames(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCompressionFactoriesNameList(): String? {
|
||||||
|
return delegate.getCompressionFactoriesNameList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCompressionFactoriesNames(): List<String?>? {
|
||||||
|
return delegate.getCompressionFactoriesNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCompressionFactoriesNameList(names: String?) {
|
||||||
|
delegate.setCompressionFactoriesNameList(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCompressionFactoriesNames(vararg names: String?) {
|
||||||
|
delegate.setCompressionFactoriesNames(*names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCompressionFactoriesNames(names: Collection<String?>?) {
|
||||||
|
delegate.setCompressionFactoriesNames(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMacFactoriesNameList(): String? {
|
||||||
|
return delegate.getMacFactoriesNameList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMacFactoriesNames(): List<String?>? {
|
||||||
|
return delegate.getMacFactoriesNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMacFactoriesNameList(names: String?) {
|
||||||
|
delegate.setMacFactoriesNameList(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMacFactoriesNames(vararg names: String?) {
|
||||||
|
delegate.setMacFactoriesNames(*names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMacFactoriesNames(names: Collection<String?>?) {
|
||||||
|
delegate.setMacFactoriesNames(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSignatureFactoriesNameList(names: String?) {
|
||||||
|
delegate.setSignatureFactoriesNameList(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSignatureFactoriesNames(vararg names: String?) {
|
||||||
|
delegate.setSignatureFactoriesNames(*names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSignatureFactoriesNames(names: Collection<String?>?) {
|
||||||
|
delegate.setSignatureFactoriesNames(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSignatureFactoriesNameList(): String? {
|
||||||
|
return delegate.getSignatureFactoriesNameList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSignatureFactoriesNames(): List<String?>? {
|
||||||
|
return delegate.getSignatureFactoriesNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolveChannelStreamWriterResolver(): ChannelStreamWriterResolver? {
|
||||||
|
return delegate.resolveChannelStreamWriterResolver()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolveChannelStreamWriter(
|
||||||
|
channel: Channel?,
|
||||||
|
cmd: Byte
|
||||||
|
): ChannelStreamWriter? {
|
||||||
|
return delegate.resolveChannelStreamWriter(channel, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isLocalPortForwardingStartedForPort(port: Int): Boolean {
|
||||||
|
return delegate.isLocalPortForwardingStartedForPort(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isRemotePortForwardingStartedForPort(port: Int): Boolean {
|
||||||
|
return delegate.isRemotePortForwardingStartedForPort(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setUserAuthFactoriesNames(names: Collection<String?>?) {
|
||||||
|
delegate.setUserAuthFactoriesNames(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setUserAuthFactoriesNames(vararg names: String?) {
|
||||||
|
delegate.setUserAuthFactoriesNames(*names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getUserAuthFactoriesNameList(): String? {
|
||||||
|
return delegate.getUserAuthFactoriesNameList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getUserAuthFactoriesNames(): List<String?>? {
|
||||||
|
return delegate.getUserAuthFactoriesNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setUserAuthFactoriesNameList(names: String?) {
|
||||||
|
delegate.setUserAuthFactoriesNameList(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startLocalPortForwarding(
|
||||||
|
localPort: Int,
|
||||||
|
remote: SshdSocketAddress?
|
||||||
|
): SshdSocketAddress? {
|
||||||
|
return delegate.startLocalPortForwarding(localPort, remote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,448 @@
|
|||||||
|
package app.termora.plugin.internal.telnet
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.plugin.internal.BasicProxyOption
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.KeyboardFocusManager
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.event.ComponentAdapter
|
||||||
|
import java.awt.event.ComponentEvent
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() {
|
||||||
|
protected val generalOption = GeneralOption()
|
||||||
|
|
||||||
|
// telnet 不支持代理密码
|
||||||
|
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
|
||||||
|
private val terminalOption = TerminalOption()
|
||||||
|
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
|
init {
|
||||||
|
addOption(generalOption)
|
||||||
|
addOption(proxyOption)
|
||||||
|
addOption(terminalOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getHost(): Host {
|
||||||
|
val name = generalOption.nameTextField.text
|
||||||
|
val protocol = TelnetProtocolProvider.PROTOCOL
|
||||||
|
val host = generalOption.hostTextField.text
|
||||||
|
val port = (generalOption.portTextField.value ?: 23) as Int
|
||||||
|
var authentication = Authentication.No
|
||||||
|
var proxy = Proxy.Companion.No
|
||||||
|
val authenticationType = generalOption.authenticationTypeComboBox.selectedItem as AuthenticationType
|
||||||
|
|
||||||
|
if (authenticationType == AuthenticationType.Password) {
|
||||||
|
authentication = authentication.copy(
|
||||||
|
type = authenticationType,
|
||||||
|
password = String(generalOption.passwordTextField.password)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||||
|
proxy = proxy.copy(
|
||||||
|
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
|
||||||
|
host = proxyOption.proxyHostTextField.text,
|
||||||
|
username = proxyOption.proxyUsernameTextField.text,
|
||||||
|
password = String(proxyOption.proxyPasswordTextField.password),
|
||||||
|
port = proxyOption.proxyPortTextField.value as Int,
|
||||||
|
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val serialComm = SerialComm()
|
||||||
|
|
||||||
|
val options = Options.Companion.Default.copy(
|
||||||
|
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
||||||
|
env = terminalOption.environmentTextArea.text,
|
||||||
|
startupCommand = terminalOption.startupCommandTextField.text,
|
||||||
|
serialComm = serialComm,
|
||||||
|
extras = mutableMapOf("backspace" to (terminalOption.backspaceComboBox.selectedItem as Backspace).name)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Host(
|
||||||
|
name = name,
|
||||||
|
protocol = protocol,
|
||||||
|
host = host,
|
||||||
|
port = port,
|
||||||
|
username = generalOption.usernameTextField.text,
|
||||||
|
authentication = authentication,
|
||||||
|
proxy = proxy,
|
||||||
|
sort = System.currentTimeMillis(),
|
||||||
|
remark = generalOption.remarkTextArea.text,
|
||||||
|
options = options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHost(host: Host) {
|
||||||
|
generalOption.portTextField.value = host.port
|
||||||
|
generalOption.nameTextField.text = host.name
|
||||||
|
generalOption.usernameTextField.text = host.username
|
||||||
|
generalOption.hostTextField.text = host.host
|
||||||
|
generalOption.remarkTextArea.text = host.remark
|
||||||
|
generalOption.authenticationTypeComboBox.selectedItem = host.authentication.type
|
||||||
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
|
generalOption.passwordTextField.text = host.authentication.password
|
||||||
|
}
|
||||||
|
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||||
|
proxyOption.proxyHostTextField.text = host.proxy.host
|
||||||
|
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
||||||
|
proxyOption.proxyUsernameTextField.text = host.proxy.username
|
||||||
|
proxyOption.proxyPortTextField.value = host.proxy.port
|
||||||
|
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
|
||||||
|
|
||||||
|
terminalOption.charsetComboBox.selectedItem = host.options.encoding
|
||||||
|
terminalOption.environmentTextArea.text = host.options.env
|
||||||
|
terminalOption.startupCommandTextField.text = host.options.startupCommand
|
||||||
|
terminalOption.backspaceComboBox.selectedItem =
|
||||||
|
Backspace.valueOf(host.options.extras["backspace"] ?: Backspace.Delete.name)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateFields(): Boolean {
|
||||||
|
val host = getHost()
|
||||||
|
|
||||||
|
// general
|
||||||
|
if (validateField(generalOption.nameTextField)
|
||||||
|
|| validateField(generalOption.hostTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
|
if (validateField(generalOption.usernameTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (validateField(generalOption.passwordTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxy
|
||||||
|
if (host.proxy.type != ProxyType.No) {
|
||||||
|
if (validateField(proxyOption.proxyHostTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.proxy.authenticationType != AuthenticationType.No) {
|
||||||
|
if (validateField(proxyOption.proxyUsernameTextField)
|
||||||
|
|| validateField(proxyOption.proxyPasswordTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示有错误
|
||||||
|
*/
|
||||||
|
private fun validateField(textField: JTextField): Boolean {
|
||||||
|
if (textField.isEnabled && textField.text.isBlank()) {
|
||||||
|
setOutlineError(textField)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setOutlineError(textField: JTextField) {
|
||||||
|
selectOptionJComponent(textField)
|
||||||
|
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
|
textField.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示有错误
|
||||||
|
*/
|
||||||
|
private fun validateField(comboBox: JComboBox<*>): Boolean {
|
||||||
|
val selectedItem = comboBox.selectedItem
|
||||||
|
if (comboBox.isEnabled && (selectedItem == null || (selectedItem is String && selectedItem.isBlank()))) {
|
||||||
|
selectOptionJComponent(comboBox)
|
||||||
|
comboBox.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
|
comboBox.requestFocusInWindow()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
protected inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||||
|
val portTextField = PortSpinner(23)
|
||||||
|
val nameTextField = OutlineTextField(128)
|
||||||
|
val usernameTextField = OutlineTextField(128)
|
||||||
|
val hostTextField = OutlineTextField(255)
|
||||||
|
val passwordTextField = OutlinePasswordField(255)
|
||||||
|
val publicKeyComboBox = OutlineComboBox<String>()
|
||||||
|
val remarkTextArea = FixedLengthTextArea(512)
|
||||||
|
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
|
||||||
|
publicKeyComboBox.isEditable = false
|
||||||
|
|
||||||
|
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = StringUtils.EMPTY
|
||||||
|
if (value is String) {
|
||||||
|
text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
text,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = value?.toString() ?: ""
|
||||||
|
when (value) {
|
||||||
|
AuthenticationType.Password -> {
|
||||||
|
text = "Password"
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationType.PublicKey -> {
|
||||||
|
text = "Public Key"
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationType.KeyboardInteractive -> {
|
||||||
|
text = "Keyboard Interactive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
text,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||||
|
|
||||||
|
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
addComponentListener(object : ComponentAdapter() {
|
||||||
|
override fun componentResized(e: ComponentEvent) {
|
||||||
|
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
|
||||||
|
removeComponentListener(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.settings
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.new-host.general")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
|
||||||
|
remarkTextArea.rows = 8
|
||||||
|
remarkTextArea.lineWrap = true
|
||||||
|
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
|
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
|
||||||
|
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
||||||
|
.add(hostTextField).xy(3, rows)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||||
|
.add(portTextField).xy(7, rows).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
||||||
|
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
|
||||||
|
.add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
||||||
|
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
||||||
|
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
||||||
|
.xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class TerminalOption : JPanel(BorderLayout()), Option {
|
||||||
|
val charsetComboBox = JComboBox<String>()
|
||||||
|
val backspaceComboBox = JComboBox<Backspace>()
|
||||||
|
val startupCommandTextField = OutlineTextField()
|
||||||
|
val environmentTextArea = FixedLengthTextArea(2048)
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
|
||||||
|
backspaceComboBox.addItem(Backspace.Delete)
|
||||||
|
backspaceComboBox.addItem(Backspace.Backspace)
|
||||||
|
backspaceComboBox.addItem(Backspace.VT220)
|
||||||
|
|
||||||
|
|
||||||
|
environmentTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
environmentTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
|
||||||
|
environmentTextArea.rows = 8
|
||||||
|
environmentTextArea.lineWrap = true
|
||||||
|
environmentTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
|
|
||||||
|
for (e in Charset.availableCharsets()) {
|
||||||
|
charsetComboBox.addItem(e.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
charsetComboBox.selectedItem = "UTF-8"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.terminal
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.new-host.terminal")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
|
||||||
|
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.backspace")}:").xy(1, rows)
|
||||||
|
.add(backspaceComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows)
|
||||||
|
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
|
||||||
|
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
|
||||||
|
.apply { rows += step }
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Backspace {
|
||||||
|
/**
|
||||||
|
* 0x08
|
||||||
|
*/
|
||||||
|
Backspace,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 0x7F 默认
|
||||||
|
*/
|
||||||
|
Delete,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ESC[3~
|
||||||
|
*/
|
||||||
|
VT220, ;
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return when (this) {
|
||||||
|
Backspace -> "ASCII Backspace (0x08)"
|
||||||
|
Delete -> "ASCII Delete (0x7F)"
|
||||||
|
VT220 -> "VT220 Delete (ESC[3~)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package app.termora.plugin.internal.telnet
|
||||||
|
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.InternalPlugin
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
internal class TelnetInternalPlugin : InternalPlugin() {
|
||||||
|
init {
|
||||||
|
support.addExtension(ProtocolProviderExtension::class.java) { TelnetProtocolProviderExtension.instance }
|
||||||
|
support.addExtension(ProtocolHostPanelExtension::class.java) { TelnetProtocolHostPanelExtension.instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "Telnet Protocol"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package app.termora.plugin.internal.telnet
|
||||||
|
|
||||||
|
import app.termora.Disposer
|
||||||
|
import app.termora.Host
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
|
||||||
|
class TelnetProtocolHostPanel(accountOwner: AccountOwner) : ProtocolHostPanel() {
|
||||||
|
|
||||||
|
private val pane = TelnetHostOptionsPane(accountOwner)
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(pane, BorderLayout.CENTER)
|
||||||
|
Disposer.register(this, pane)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getHost(): Host {
|
||||||
|
return pane.getHost()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setHost(host: Host) {
|
||||||
|
pane.setHost(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun validateFields(): Boolean {
|
||||||
|
return pane.validateFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package app.termora.plugin.internal.telnet
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
|
||||||
|
internal class TelnetProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = TelnetProtocolHostPanelExtension()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return TelnetProtocolProvider.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
|
return TelnetProtocolHostPanel(accountOwner)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
}
|
||||||