Compare commits

..

36 Commits

Author SHA1 Message Date
hstyi
1c90fb4e18 release: 2.0.0-beta.4 2025-07-04 14:35:50 +08:00
dependabot[bot]
c1f1d5185e chore(deps): bump com.fazecast:jSerialComm from 2.11.0 to 2.11.2
Bumps [com.fazecast:jSerialComm](https://github.com/Fazecast/jSerialComm) from 2.11.0 to 2.11.2.
- [Release notes](https://github.com/Fazecast/jSerialComm/releases)
- [Commits](https://github.com/Fazecast/jSerialComm/compare/v2.11.0...v2.11.2)

---
updated-dependencies:
- dependency-name: com.fazecast:jSerialComm
  dependency-version: 2.11.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-04 12:20:45 +08:00
dependabot[bot]
aa4863712d chore(deps): bump org.jetbrains.kotlinx:kotlinx-serialization-json
Bumps [org.jetbrains.kotlinx:kotlinx-serialization-json](https://github.com/Kotlin/kotlinx.serialization) from 1.8.1 to 1.9.0.
- [Release notes](https://github.com/Kotlin/kotlinx.serialization/releases)
- [Changelog](https://github.com/Kotlin/kotlinx.serialization/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Kotlin/kotlinx.serialization/compare/v1.8.1...v1.9.0)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlinx:kotlinx-serialization-json
  dependency-version: 1.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-04 12:20:34 +08:00
dependabot[bot]
247640f2e5 chore(deps): bump org.dom4j:dom4j from 2.1.4 to 2.2.0
Bumps [org.dom4j:dom4j](https://github.com/dom4j/dom4j) from 2.1.4 to 2.2.0.
- [Release notes](https://github.com/dom4j/dom4j/releases)
- [Commits](https://github.com/dom4j/dom4j/compare/version-2.1.4...version/2.2.0)

---
updated-dependencies:
- dependency-name: org.dom4j:dom4j
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-04 12:20:24 +08:00
dependabot[bot]
5b1f803fa8 chore(deps): bump com.aliyun.oss:aliyun-sdk-oss from 3.18.2 to 3.18.3
Bumps [com.aliyun.oss:aliyun-sdk-oss](https://github.com/aliyun/aliyun-oss-java-sdk) from 3.18.2 to 3.18.3.
- [Release notes](https://github.com/aliyun/aliyun-oss-java-sdk/releases)
- [Commits](https://github.com/aliyun/aliyun-oss-java-sdk/commits)

---
updated-dependencies:
- dependency-name: com.aliyun.oss:aliyun-sdk-oss
  dependency-version: 3.18.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-04 12:20:15 +08:00
hstyi
accf590c17 feat: support fallback font 2025-07-04 12:19:56 +08:00
hstyi
19fbeab817 fix: shortcut keys cannot be saved 2025-07-04 12:13:01 +08:00
hstyi
a785ab4680 chore: disable opengl 2025-07-04 09:58:08 +08:00
hstyi
5ee23cb379 fix: cursor style not working 2025-07-04 09:14:58 +08:00
hstyi
145d2de001 chore: tiny images 2025-07-03 15:54:38 +08:00
hstyi
8d3f5fe622 chore: README 2025-07-03 15:47:19 +08:00
hstyi
9ce4a88041 feat: use extension for floating toolbar 2025-07-03 12:00:02 +08:00
dependabot[bot]
c0ecc9fa7d chore(deps): bump exposed from 1.0.0-beta-2 to 1.0.0-beta-3
Bumps `exposed` from 1.0.0-beta-2 to 1.0.0-beta-3.

Updates `org.jetbrains.exposed:exposed-core` from 1.0.0-beta-2 to 1.0.0-beta-3
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/compare/1.0.0-beta-2...1.0.0-beta-3)

Updates `org.jetbrains.exposed:exposed-crypt` from 1.0.0-beta-2 to 1.0.0-beta-3
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/compare/1.0.0-beta-2...1.0.0-beta-3)

Updates `org.jetbrains.exposed:exposed-jdbc` from 1.0.0-beta-2 to 1.0.0-beta-3
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/compare/1.0.0-beta-2...1.0.0-beta-3)

Updates `org.jetbrains.exposed:exposed-migration` from 1.0.0-beta-2 to 1.0.0-beta-3
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/compare/1.0.0-beta-2...1.0.0-beta-3)

---
updated-dependencies:
- dependency-name: org.jetbrains.exposed:exposed-core
  dependency-version: 1.0.0-beta-3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.exposed:exposed-crypt
  dependency-version: 1.0.0-beta-3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.exposed:exposed-jdbc
  dependency-version: 1.0.0-beta-3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.exposed:exposed-migration
  dependency-version: 1.0.0-beta-3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 11:38:43 +08:00
hstyi
cb33a4468a chore: show close button 2025-07-03 10:48:15 +08:00
hstyi
168c4c5c64 chore: improve team sync 2025-07-03 08:49:00 +08:00
hstyi
9916edbd13 feat: support custom layout 2025-07-02 12:15:28 +08:00
hstyi
ab6b6a2127 chore: improve Windows task icon 2025-07-01 13:42:50 +08:00
hstyi
c45f5f4c92 fix: plugins compatibility 2025-07-01 13:21:53 +08:00
hstyi
92ee2d72f2 chore: only show flags on macOS 2025-07-01 12:03:27 +08:00
hstyi
a4364bcd6a release: 2.0.0-beta.3 2025-07-01 11:00:55 +08:00
hstyi
d0827c3b0c chore: improve connect-with 2025-07-01 10:52:45 +08:00
hstyi
036a04b0b3 feat: support SMB 2025-07-01 10:49:27 +08:00
hstyi
eee016c643 chore: supports retaining file modification date 2025-07-01 10:46:17 +08:00
hstyi
472bf6e81f feat: support login scripts 2025-07-01 10:40:06 +08:00
hstyi
21229e352f chore: do not refresh during installation 2025-06-30 17:34:02 +08:00
hstyi
1138f48a6e fix: host deletion query error 2025-06-30 17:18:05 +08:00
hstyi
f044e0480e chore: password show caps lock 2025-06-30 17:16:52 +08:00
hstyi
7047f17783 fix: quick open transfer failure 2025-06-30 14:33:55 +08:00
dependabot[bot]
9308f15abb chore(deps): bump com.qcloud:cos_api from 5.6.245 to 5.6.247
Bumps [com.qcloud:cos_api](https://github.com/tencentyun/cos-java-sdk-v5) from 5.6.245 to 5.6.247.
- [Release notes](https://github.com/tencentyun/cos-java-sdk-v5/releases)
- [Changelog](https://github.com/tencentyun/cos-java-sdk-v5/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tencentyun/cos-java-sdk-v5/commits)

---
updated-dependencies:
- dependency-name: com.qcloud:cos_api
  dependency-version: 5.6.247
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 14:29:41 +08:00
dependabot[bot]
b2672f11fc chore(deps): bump org.testcontainers:testcontainers-bom
Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 1.21.2 to 1.21.3.
- [Release notes](https://github.com/testcontainers/testcontainers-java/releases)
- [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.21.2...1.21.3)

---
updated-dependencies:
- dependency-name: org.testcontainers:testcontainers-bom
  dependency-version: 1.21.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 14:29:29 +08:00
dependabot[bot]
f92c6586b2 chore(deps): bump org.semver4j:semver4j from 5.8.0 to 6.0.0
Bumps [org.semver4j:semver4j](https://github.com/semver4j/semver4j) from 5.8.0 to 6.0.0.
- [Release notes](https://github.com/semver4j/semver4j/releases)
- [Commits](https://github.com/semver4j/semver4j/compare/v5.8.0...v6.0.0)

---
updated-dependencies:
- dependency-name: org.semver4j:semver4j
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 14:29:20 +08:00
dependabot[bot]
69e07a9bd9 chore(deps): bump com.huaweicloud:esdk-obs-java-bundle
Bumps [com.huaweicloud:esdk-obs-java-bundle](https://github.com/huaweicloud/huaweicloud-sdk-java-obs) from 3.25.4 to 3.25.5.
- [Release notes](https://github.com/huaweicloud/huaweicloud-sdk-java-obs/releases)
- [Commits](https://github.com/huaweicloud/huaweicloud-sdk-java-obs/compare/v3.25.4...v3.25.5)

---
updated-dependencies:
- dependency-name: com.huaweicloud:esdk-obs-java-bundle
  dependency-version: 3.25.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 14:29:07 +08:00
dependabot[bot]
cdec60fd25 chore(deps): bump org.xerial:sqlite-jdbc from 3.50.1.0 to 3.50.2.0
Bumps [org.xerial:sqlite-jdbc](https://github.com/xerial/sqlite-jdbc) from 3.50.1.0 to 3.50.2.0.
- [Release notes](https://github.com/xerial/sqlite-jdbc/releases)
- [Changelog](https://github.com/xerial/sqlite-jdbc/blob/master/CHANGELOG)
- [Commits](https://github.com/xerial/sqlite-jdbc/compare/3.50.1.0...3.50.2.0)

---
updated-dependencies:
- dependency-name: org.xerial:sqlite-jdbc
  dependency-version: 3.50.2.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 14:28:47 +08:00
hstyi
7c30933794 fix: wsl reg exception 2025-06-30 13:36:35 +08:00
hstyi
b892d2fe13 release: 2.0.0-beta.2 2025-06-30 12:50:10 +08:00
hstyi
91ee463d41 fix: data migration not working 2025-06-30 12:43:31 +08:00
141 changed files with 2763 additions and 772 deletions

127
README.md
View File

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

View File

@@ -1,48 +1,100 @@
# Termora # Termora
**Termora** 是一终端模拟器和 SSH 客户端,支持 WindowsmacOSLinux。 **Termora** 是一款跨平台终端模拟器和 SSH 客户端,支持 **WindowsmacOSLinux**
<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)
- **专有许可**:如需闭源或商业用途,请联系作者获取授权

View File

@@ -1 +1 @@
2.0.0-beta.1 2.0.0-beta.4

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

BIN
docs/host-zh_CN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
docs/host.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
docs/plugins-zh_CN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
docs/plugins.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/tags-zh_CN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
docs/tags.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
docs/transfer-edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
docs/transfer-zh_CN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

BIN
docs/transfer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -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"
@@ -35,19 +35,19 @@ bip39 = "1.0.9"
colorpicker = "2.0.1" colorpicker = "2.0.1"
rhino = "1.8.0" rhino = "1.8.0"
delight-rhino-sandbox = "0.0.17" delight-rhino-sandbox = "0.0.17"
testcontainers = "1.21.2" 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.1.0" sqlite = "3.50.2.0"
jug = "5.1.0" jug = "5.1.0"
semver4j = "5.8.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" }

View File

@@ -72,4 +72,8 @@ https://www.apache.org/licenses/LICENSE-2.0.html
GeoLite2 (https://www.maxmind.com) GeoLite2 (https://www.maxmind.com)
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
https://creativecommons.org/licenses/by-sa/4.0/ https://creativecommons.org/licenses/by-sa/4.0/
smbj
Apache License, Version 2.0
https://github.com/hierynomus/smbj/blob/master/LICENSE_HEADER

View File

@@ -2,13 +2,13 @@ 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.qcloud:cos_api:5.6.245") implementation("com.qcloud:cos_api:5.6.247")
compileOnly(project(":")) compileOnly(project(":"))
} }

View File

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

View File

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

View File

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

View File

@@ -3,12 +3,12 @@ plugins {
} }
project.version = "0.0.1" project.version = "0.0.2"
dependencies { dependencies {
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.4") implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.5")
compileOnly(project(":")) compileOnly(project(":"))
} }

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ plugins {
} }
project.version = "0.0.5" project.version = "0.0.6"
dependencies { dependencies {

View File

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

View File

@@ -0,0 +1,14 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.2"
dependencies {
testImplementation(kotlin("test"))
implementation("com.hierynomus:smbj:0.14.0")
compileOnly(project(":"))
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -0,0 +1,15 @@
package app.termora.plugins.smb
import app.termora.transfer.s3.S3FileSystem
import com.hierynomus.smbj.session.Session
import com.hierynomus.smbj.share.DiskShare
class SMBFileSystem(private val share: DiskShare, session: Session) :
S3FileSystem(SMBFileSystemProvider(share, session)) {
override fun close() {
share.close()
super.close()
}
}

View File

@@ -0,0 +1,111 @@
package app.termora.plugins.smb
import app.termora.transfer.s3.S3FileSystemProvider
import app.termora.transfer.s3.S3Path
import com.hierynomus.msdtyp.AccessMask
import com.hierynomus.msfscc.FileAttributes
import com.hierynomus.mssmb2.SMB2CreateDisposition
import com.hierynomus.mssmb2.SMB2CreateOptions
import com.hierynomus.mssmb2.SMB2ShareAccess
import com.hierynomus.smbj.session.Session
import com.hierynomus.smbj.share.DiskShare
import org.apache.commons.io.FilenameUtils
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.AccessMode
import java.nio.file.NoSuchFileException
import java.nio.file.Path
import java.nio.file.attribute.FileAttribute
import kotlin.io.path.absolutePathString
class SMBFileSystemProvider(private val share: DiskShare, private val session: Session) : S3FileSystemProvider() {
override fun getScheme(): String? {
return "smb"
}
override fun getOutputStream(path: S3Path): OutputStream {
val file = share.openFile(
path.absolutePathString(),
setOf(AccessMask.GENERIC_WRITE),
setOf(FileAttributes.FILE_ATTRIBUTE_NORMAL),
setOf(SMB2ShareAccess.FILE_SHARE_READ),
SMB2CreateDisposition.FILE_OVERWRITE_IF,
setOf(SMB2CreateOptions.FILE_NON_DIRECTORY_FILE)
)
val os = file.outputStream
return object : OutputStream() {
override fun write(b: Int) {
os.write(b)
}
override fun close() {
IOUtils.closeQuietly(os)
file.closeNoWait()
}
}
}
override fun getInputStream(path: S3Path): InputStream {
val file = share.openFile(
path.absolutePathString(),
setOf(AccessMask.GENERIC_READ),
setOf(FileAttributes.FILE_ATTRIBUTE_NORMAL),
setOf(SMB2ShareAccess.FILE_SHARE_READ),
SMB2CreateDisposition.FILE_OPEN,
setOf(SMB2CreateOptions.FILE_NON_DIRECTORY_FILE)
)
val input = file.inputStream
return object : InputStream() {
override fun read(): Int = input.read()
override fun close() {
IOUtils.closeQuietly(input)
file.closeNoWait()
}
}
}
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
val paths = mutableListOf<S3Path>()
val absolutePath = FilenameUtils.separatorsToUnix(path.absolutePathString())
for (information in share.list(if (absolutePath == path.fileSystem.separator) StringUtils.EMPTY else absolutePath)) {
if (information.fileName == "." || information.fileName == "..") continue
val isDir = information.fileAttributes and FileAttributes.FILE_ATTRIBUTE_DIRECTORY.value != 0L
val path = path.resolve(information.fileName)
path.attributes = path.attributes.copy(
directory = isDir, regularFile = isDir.not(),
size = information.endOfFile,
lastModifiedTime = information.lastWriteTime.toDate().time,
lastAccessTime = information.lastAccessTime.toDate().time,
)
paths.add(path)
}
return paths
}
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) {
share.mkdir(dir.absolutePathString())
}
override fun delete(path: S3Path, isDirectory: Boolean) {
if (isDirectory) {
share.rmdir(path.absolutePathString(), false)
} else {
share.rm(path.absolutePathString())
}
}
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
if (share.fileExists(path.absolutePathString()) || share.folderExists(path.absolutePathString())) {
return
}
throw NoSuchFileException(path.absolutePathString())
}
}

View File

@@ -0,0 +1,261 @@
package app.termora.plugins.smb
import app.termora.*
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.ui.FlatTextBorder
import com.hierynomus.smbj.SMBClient
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.KeyboardFocusManager
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.*
class SMBHostOptionsPane : OptionsPane() {
private val generalOption = GeneralOption()
private val sftpOption = SFTPOption()
init {
addOption(generalOption)
addOption(sftpOption)
}
fun getHost(): Host {
val name = generalOption.nameTextField.text
val protocol = SMBProtocolProvider.PROTOCOL
val host = generalOption.hostTextField.text
val port = generalOption.portTextField.value as Int
var authentication = Authentication.Companion.No
val authenticationType = AuthenticationType.Password
authentication = authentication.copy(
type = authenticationType,
password = String(generalOption.passwordTextField.password)
)
val options = Options.Default.copy(
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
extras = mutableMapOf(
"smb.share" to generalOption.shareTextField.text,
)
)
return Host(
name = name,
protocol = protocol,
host = host,
port = port,
username = generalOption.usernameTextField.selectedItem as String,
authentication = authentication,
sort = System.currentTimeMillis(),
remark = generalOption.remarkTextArea.text,
options = options,
)
}
fun setHost(host: Host) {
generalOption.nameTextField.text = host.name
generalOption.usernameTextField.selectedItem = host.username
generalOption.hostTextField.text = host.host
generalOption.portTextField.value = host.port
generalOption.remarkTextArea.text = host.remark
generalOption.passwordTextField.text = host.authentication.password
generalOption.shareTextField.text = host.options.extras["smb.share"] ?: StringUtils.EMPTY
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
}
fun validateFields(): Boolean {
// general
if (validateField(generalOption.nameTextField)
|| validateField(generalOption.hostTextField)
|| validateField(generalOption.shareTextField)
) {
return false
}
val username = generalOption.usernameTextField.selectedItem as String?
if (username.isNullOrBlank()) {
setOutlineError(generalOption.usernameTextField)
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: JComponent) {
selectOptionJComponent(textField)
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
textField.requestFocusInWindow()
}
private inner class GeneralOption : JPanel(BorderLayout()), Option {
val portTextField = PortSpinner(SMBClient.DEFAULT_PORT)
val nameTextField = OutlineTextField(128)
val shareTextField = OutlineTextField(256)
val usernameTextField = OutlineComboBox<String>()
val hostTextField = OutlineTextField(255)
val passwordTextField = OutlinePasswordField(255)
val remarkTextArea = FixedLengthTextArea(512)
init {
initView()
initEvents()
}
private fun initView() {
usernameTextField.isEditable = true
usernameTextField.addItem("Guest")
usernameTextField.addItem("Anonymous")
add(getCenterComponent(), BorderLayout.CENTER)
}
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.password")}:").xy(1, rows)
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
.add("${SMBI18n.getString("termora.plugins.smb.share")}:").xy(1, rows)
.add(shareTextField).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 SFTPOption : JPanel(BorderLayout()), Option {
val defaultDirectoryField = OutlineTextField(255)
init {
initView()
initEvents()
}
private fun initView() {
add(getCenterComponent(), BorderLayout.CENTER)
}
private fun initEvents() {
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.folder
}
override fun getTitle(): String {
return I18n.getString("termora.transport.sftp")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
var rows = 1
val step = 2
val panel = FormBuilder.create().layout(layout)
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
.build()
return panel
}
}
}

View File

@@ -0,0 +1,24 @@
package app.termora.plugins.smb
import app.termora.I18n
import app.termora.NamedI18n
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.*
object SMBI18n : NamedI18n("i18n/messages") {
private val log = LoggerFactory.getLogger(SMBI18n::class.java)
override fun getLogger(): Logger {
return log
}
override fun getString(key: String): String {
return try {
substitutor.replace(getBundle().getString(key))
} catch (_: MissingResourceException) {
I18n.getString(key)
}
}
}

View File

@@ -0,0 +1,20 @@
package app.termora.plugins.smb
import app.termora.protocol.PathHandler
import com.hierynomus.smbj.SMBClient
import com.hierynomus.smbj.session.Session
import org.apache.commons.io.IOUtils
import java.nio.file.FileSystem
import java.nio.file.Path
class SMBPathHandler(
private val client: SMBClient,
private val session: Session,
fileSystem: FileSystem, path: Path
) : PathHandler(fileSystem, path) {
override fun dispose() {
super.dispose()
session.close()
IOUtils.closeQuietly(client)
}
}

View File

@@ -0,0 +1,31 @@
package app.termora.plugins.smb
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.PaidPlugin
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProviderExtension
class SMBPlugin : PaidPlugin {
private val support = ExtensionSupport()
init {
support.addExtension(ProtocolProviderExtension::class.java) { SMBProtocolProviderExtension.instance }
support.addExtension(ProtocolHostPanelExtension::class.java) { SMBProtocolHostPanelExtension.instance }
}
override fun getAuthor(): String {
return "TermoraDev"
}
override fun getName(): String {
return "SMB"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,36 @@
package app.termora.plugins.smb
import app.termora.Disposer
import app.termora.Host
import app.termora.protocol.ProtocolHostPanel
import java.awt.BorderLayout
class SMBProtocolHostPanel : ProtocolHostPanel() {
private val pane = SMBHostOptionsPane()
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()
}
}

View File

@@ -0,0 +1,20 @@
package app.termora.plugins.smb
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
class SMBProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
companion object {
val instance by lazy { SMBProtocolHostPanelExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return SMBProtocolProvider.instance
}
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return SMBProtocolHostPanel()
}
}

View File

@@ -0,0 +1,57 @@
package app.termora.plugins.smb
import app.termora.DynamicIcon
import app.termora.Icons
import app.termora.protocol.PathHandler
import app.termora.protocol.PathHandlerRequest
import app.termora.protocol.TransferProtocolProvider
import com.hierynomus.smbj.SMBClient
import com.hierynomus.smbj.auth.AuthenticationContext
import com.hierynomus.smbj.share.DiskShare
import org.apache.commons.io.FilenameUtils
import org.apache.commons.lang3.StringUtils
class SMBProtocolProvider private constructor() : TransferProtocolProvider {
companion object {
val instance by lazy { SMBProtocolProvider() }
const val PROTOCOL = "SMB"
}
override fun getProtocol(): String {
return PROTOCOL
}
override fun getIcon(width: Int, height: Int): DynamicIcon {
return Icons.windows7
}
override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
val client = SMBClient()
val host = requester.host
val connection = client.connect(host.host, host.port)
val session = when (host.username) {
"Guest" -> connection.authenticate(AuthenticationContext.guest())
"Anonymous" -> connection.authenticate(AuthenticationContext.anonymous())
else -> connection.authenticate(
AuthenticationContext(
host.username,
host.authentication.password.toCharArray(),
null
)
)
}
val share = session.connectShare(host.options.extras["smb.share"] ?: StringUtils.EMPTY) as DiskShare
var sftpDefaultDirectory = StringUtils.defaultString(host.options.sftpDefaultDirectory)
sftpDefaultDirectory = if (sftpDefaultDirectory.isNotBlank()) {
FilenameUtils.separatorsToUnix(sftpDefaultDirectory)
} else {
"/"
}
val fs = SMBFileSystem(share, session)
return SMBPathHandler(client, session, fs, fs.getPath(sftpDefaultDirectory))
}
}

View File

@@ -0,0 +1,14 @@
package app.termora.plugins.smb
import app.termora.protocol.ProtocolProvider
import app.termora.protocol.ProtocolProviderExtension
class SMBProtocolProviderExtension private constructor() : ProtocolProviderExtension {
companion object {
val instance by lazy { SMBProtocolProviderExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return SMBProtocolProvider.instance
}
}

View File

@@ -0,0 +1,24 @@
<termora-plugin>
<id>smb</id>
<name>SMB</name>
<paid/>
<version>${projectVersion}</version>
<termora-version since=">=${rootProjectVersion}" until=""/>
<entry>app.termora.plugins.smb.SMBPlugin</entry>
<descriptions>
<description>Connecting to SMB</description>
<description language="zh_CN">支持连接到 SMB</description>
<description language="zh_TW">支援連接到 SMB</description>
</descriptions>
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
</termora-plugin>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@@ -0,0 +1 @@
termora.plugins.smb.share=Share name

View File

@@ -0,0 +1 @@
termora.plugins.smb.share=共享名称

View File

@@ -0,0 +1 @@
termora.plugins.smb.share=共享名稱

View File

@@ -3,7 +3,7 @@ plugins {
} }
project.version = "0.0.2" project.version = "0.0.3"
dependencies { dependencies {

View File

@@ -2,7 +2,7 @@ plugins {
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.jvm)
} }
project.version = "0.0.1" project.version = "0.0.2"
dependencies { dependencies {
testImplementation(kotlin("test")) testImplementation(kotlin("test"))

View File

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

View File

@@ -14,3 +14,4 @@ include("plugins:migration")
include("plugins:editor") include("plugins:editor")
include("plugins:geo") include("plugins:geo")
include("plugins:webdav") include("plugins:webdav")
include("plugins:smb")

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ package app.termora
import com.formdev.flatlaf.extras.FlatSVGIcon import com.formdev.flatlaf.extras.FlatSVGIcon
open class DynamicIcon(name: String, private val darkName: String, val allowColorFilter: Boolean = true) : open class DynamicIcon(name: String, private val darkName: String = name, val allowColorFilter: Boolean = true) :
FlatSVGIcon(name) { FlatSVGIcon(name) {
constructor(name: String) : this(name, name) constructor(name: String) : this(name, name)

View 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 = "&lt;None&gt;"
}
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
}
}

View File

@@ -88,7 +88,27 @@ data class SerialComm(
) )
@Serializable @Serializable
data class HostTag(val text: String) data class LoginScript(
/**
* 等待字符串
*/
val expect: String,
/**
* 等待之后发送
*/
val send: String,
/**
* [expect] 是否是正则
*/
val regex: Boolean = false,
/**
* [expect] 是否大小写匹配,如果为 true 表示不忽略大小写,也就是:'A != a';如果为 false 那么 'A == a'
*/
val matchCase: Boolean = false,
)
@Serializable @Serializable
@@ -97,6 +117,10 @@ data class Options(
* 跳板机 * 跳板机
*/ */
val jumpHosts: List<String> = mutableListOf(), val jumpHosts: List<String> = mutableListOf(),
/**
* 登录脚本
*/
val loginScripts: List<LoginScript> = emptyList(),
/** /**
* 编码 * 编码
*/ */

View File

@@ -55,6 +55,7 @@ class HostManager private constructor() : Disposable {
fun getHost(id: String): Host? { fun getHost(id: String): Host? {
val data = databaseManager.data(id) ?: return null val data = databaseManager.data(id) ?: return null
if (data.type != DataType.Host.name) return null if (data.type != DataType.Host.name) return null
if (data.deleted) return null
return ohMyJson.decodeFromString(data.data) return ohMyJson.decodeFromString(data.data)
} }

View File

@@ -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,7 +49,10 @@ abstract class HostTerminalTab(
if (hasFocus || unread) { if (hasFocus || unread) {
return return
} }
unread = true // 如果当前选中的不是这个 Tab那么设置成未读
if (terminalTabbedManager?.getSelectedTerminalTab() != this@HostTerminalTab) {
unread = true
}
} }
} }
}) })

View File

@@ -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") }
@@ -80,6 +81,7 @@ object Icons {
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") }
val windows7 by lazy { DynamicIcon("icons/windows7.svg", allowColorFilter = false) }
val powershell by lazy { DynamicIcon("icons/powershell.svg", "icons/powershell_dark.svg") } val powershell by lazy { DynamicIcon("icons/powershell.svg", "icons/powershell_dark.svg") }
val serial by lazy { DynamicIcon("icons/serial.svg", "icons/serial_dark.svg") } val serial by lazy { DynamicIcon("icons/serial.svg", "icons/serial_dark.svg") }
val fileFormat by lazy { DynamicIcon("icons/fileFormat.svg", "icons/fileFormat_dark.svg") } val fileFormat by lazy { DynamicIcon("icons/fileFormat.svg", "icons/fileFormat_dark.svg") }

View File

@@ -0,0 +1,123 @@
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.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)
g.color = UIManager.getColor("controlShadow")
g.fillRect(splitPane.dividerLocation, 0, 1, topOffset.get())
}
}
override fun doLayout() {
super.doLayout()
layeredPane.doLayout()
}
}

View File

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

View File

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

View File

@@ -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() {
@@ -37,7 +40,7 @@ abstract class PtyHostTerminalTab(
} }
// 开启 PTY // 开启 PTY
val ptyConnector = openPtyConnector() val ptyConnector = loginScriptsPtyConnector(host, openPtyConnector())
ptyConnectorDelegate.ptyConnector = ptyConnector ptyConnectorDelegate.ptyConnector = ptyConnector
// 开启 reader // 开启 reader
@@ -81,6 +84,73 @@ abstract class PtyHostTerminalTab(
} }
} }
/**
* 登录脚本
*/
open fun loginScriptsPtyConnector(host: Host, ptyConnector: PtyConnector): PtyConnector {
val loginScripts = host.options.loginScripts.toMutableList()
if (loginScripts.isEmpty()) {
return ptyConnector
}
return object : PtyConnectorDelegate(ptyConnector) {
override fun read(buffer: CharArray): Int {
val len = super.read(buffer)
// 获取一个匹配的登录脚本
val scripts = runCatching { popLoginScript(buffer, len) }.getOrNull() ?: return len
if (scripts.isEmpty()) return len
for (script in scripts) {
// send
write(script.send.toByteArray(getCharset()))
// send \r or \n
val enter = terminal.getKeyEncoder().encode(TerminalKeyEvent(KeyEvent.VK_ENTER))
.toByteArray(getCharset())
write(enter)
}
return len
}
private fun popLoginScript(buffer: CharArray, len: Int): List<LoginScript> {
if (loginScripts.isEmpty()) return emptyList()
if (len < 1) return emptyList()
val scripts = mutableListOf<LoginScript>()
val text = String(buffer, 0, len)
val iterator = loginScripts.iterator()
while (iterator.hasNext()) {
val script = iterator.next()
if (script.expect.isEmpty()) {
scripts.add(script)
iterator.remove()
continue
} else if (script.regex) {
val regex = if (script.matchCase) script.expect.toRegex()
else script.expect.toRegex(RegexOption.IGNORE_CASE)
if (regex.matches(text)) {
scripts.add(script)
iterator.remove()
continue
}
} else {
if (text.contains(script.expect, script.matchCase.not())) {
scripts.add(script)
iterator.remove()
continue
}
}
break
}
return scripts
}
}
}
open fun sendStartupCommand(ptyConnector: PtyConnector, bytes: ByteArray) { open fun sendStartupCommand(ptyConnector: PtyConnector, bytes: ByteArray) {
ptyConnector.write(bytes) ptyConnector.write(bytes)
} }

View File

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

View File

@@ -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")) {
if (items.contains(family).not()) fontComboBox.addItem(family)
}
fontComboBox.addPopupMenuListener(object : PopupMenuListener { if (terminalSetting.fallbackFont.isNotBlank()) {
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) { fallbackFontComboBox.addItem(StringUtils.EMPTY)
if (!fontsLoaded) { }
val selectedItem = fontComboBox.selectedItem fallbackFontComboBox.addItem(terminalSetting.fallbackFont)
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) {}
override fun popupMenuCanceled(e: PopupMenuEvent) {}
})
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)

View File

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

View File

@@ -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
@@ -32,6 +33,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 +112,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 +194,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)
@@ -255,10 +245,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
@@ -497,6 +492,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() {
} }

View File

@@ -0,0 +1,106 @@
package app.termora
import app.termora.tree.NewHostTree
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.util.SystemInfo
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.Font
import java.awt.event.MouseAdapter
import javax.swing.*
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()
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
add(mySplitPane, BorderLayout.CENTER)
}
private fun initEvents() {
Disposer.register(this, leftTreePanel)
splitPane.addPropertyChangeListener("dividerLocation") { mySplitPane.doLayout() }
}
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(Box.createHorizontalGlue())
if (SystemInfo.isMacOS.not()) box.add(label)
box.add(Box.createHorizontalGlue())
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)
}
}
override fun dispose() {
enableManager.setFlag("Termora.Fence.dividerLocation", splitPane.dividerLocation)
}
fun getHostTree(): NewHostTree {
return leftTreePanel.hostTree
}
}

View File

@@ -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,209 @@ 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}&nbsp;&nbsp;&nbsp;&nbsp;<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)
} 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, 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 +308,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 +354,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() }
} }

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

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

View File

@@ -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}&nbsp;&nbsp;&nbsp;&nbsp;<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)
} }

View File

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

View File

@@ -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,37 +134,39 @@ 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
this.account = account this.account = account
// 立即保存到数据库 // 立即保存到数据库
val accountProperties = AccountProperties.getInstance() val accountProperties = AccountProperties.getInstance()
accountProperties.id = account.id accountProperties.id = account.id
accountProperties.server = account.server accountProperties.server = account.server
accountProperties.email = account.email accountProperties.email = account.email
accountProperties.teams = ohMyJson.encodeToString(account.teams) accountProperties.teams = ohMyJson.encodeToString(account.teams)
accountProperties.subscriptions = ohMyJson.encodeToString(account.subscriptions) accountProperties.subscriptions = ohMyJson.encodeToString(account.subscriptions)
accountProperties.accessToken = account.accessToken accountProperties.accessToken = account.accessToken
accountProperties.refreshToken = account.refreshToken accountProperties.refreshToken = account.refreshToken
accountProperties.secretKey = ohMyJson.encodeToString(account.secretKey) accountProperties.secretKey = ohMyJson.encodeToString(account.secretKey)
// 如果变更账户了那么同步时间从0开始 // 如果变更账户了那么同步时间从0开始
if (oldAccount.id != account.id) { if (oldAccount.id != account.id) {
accountProperties.nextSynchronizationSince = 0 accountProperties.nextSynchronizationSince = 0
}
if (isLocally().not()) {
accountProperties.publicKey = Base64.encodeBase64String(account.publicKey.encoded)
accountProperties.privateKey = Base64.encodeBase64String(account.privateKey.encoded)
} else {
accountProperties.publicKey = StringUtils.EMPTY
accountProperties.privateKey = StringUtils.EMPTY
}
// 通知变化
notifyAccountChanged(oldAccount, account)
} }
if (isLocally().not()) {
accountProperties.publicKey = Base64.encodeBase64String(account.publicKey.encoded)
accountProperties.privateKey = Base64.encodeBase64String(account.privateKey.encoded)
} else {
accountProperties.publicKey = StringUtils.EMPTY
accountProperties.privateKey = StringUtils.EMPTY
}
// 通知变化
notifyAccountChanged(oldAccount, account)
} }
private fun notifyAccountChanged(oldAccount: Account, newAccount: Account) { private fun notifyAccountChanged(oldAccount: Account, newAccount: Account) {
@@ -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 {

View File

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

View File

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

View File

@@ -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
@@ -59,6 +60,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
size = Dimension(max(preferredSize.width, UIManager.getInt("Dialog.width") - 250), preferredSize.height) size = Dimension(max(preferredSize.width, UIManager.getInt("Dialog.width") - 250), preferredSize.height)
setLocationRelativeTo(owner) setLocationRelativeTo(owner)
passwordField.putClientProperty(FlatClientProperties.STYLE, mapOf("showCapsLock" to true))
addWindowListener(object : WindowAdapter() { addWindowListener(object : WindowAdapter() {
override fun windowOpened(e: WindowEvent) { override fun windowOpened(e: WindowEvent) {
@@ -358,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()
) )

View File

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

View File

@@ -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()
AccountHttp.execute(request = request) try {
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,19 +5,24 @@ import app.termora.Application.ohMyJson
import app.termora.account.Account import app.termora.account.Account
import app.termora.account.AccountExtension import app.termora.account.AccountExtension
import app.termora.account.AccountManager import app.termora.account.AccountManager
import app.termora.account.AccountOwner
import app.termora.database.Data.Companion.toData import app.termora.database.Data.Companion.toData
import app.termora.plugin.ExtensionManager import app.termora.highlight.KeywordHighlightManager
import app.termora.keymap.KeymapManager
import app.termora.keymgr.KeyManager
import app.termora.macro.MacroManager
import app.termora.plugin.internal.extension.DynamicExtensionHandler import app.termora.plugin.internal.extension.DynamicExtensionHandler
import app.termora.snippet.SnippetManager
import app.termora.terminal.CursorStyle import app.termora.terminal.CursorStyle
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.jetbrains.exposed.v1.core.SqlExpressionBuilder.eq import org.jetbrains.exposed.v1.core.SqlExpressionBuilder.eq
import org.jetbrains.exposed.v1.core.SqlExpressionBuilder.inList
import org.jetbrains.exposed.v1.core.and import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.statements.StatementType 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
@@ -27,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() }
@@ -47,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()
@@ -96,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() {
@@ -303,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
} }
@@ -362,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
@@ -402,41 +391,65 @@ class DatabaseManager private constructor() : Disposable {
} }
} }
private fun silentDelete(id: String) {
lock.withLock {
transaction(database) {
DataEntity.deleteWhere { DataEntity.id.eq(id) }
}
}
}
private fun transferData(account: Account) { private fun transferData(account: Account) {
val deleteIds = mutableSetOf<String>() val hostManager = HostManager.getInstance()
val snippetManager = SnippetManager.getInstance()
val macroManager = MacroManager.getInstance()
val keymapManager = KeymapManager.getInstance()
val keyManager = KeyManager.getInstance()
val highlightManager = KeywordHighlightManager.getInstance()
val accountOwner = AccountOwner(
id = account.id,
name = account.email,
type = OwnerType.User
)
for (host in hostManager.hosts()) { for (host in hostManager.hosts()) {
// 已经删除,则忽略
if (host.deleted) continue
// 不是用户数据,那么忽略 // 不是用户数据,那么忽略
if (host.ownerType.isNotBlank() && host.ownerType != OwnerType.User.name) continue if (host.ownerType.isNotBlank() && host.ownerType != OwnerType.User.name) continue
// 不是本地用户数据,那么忽略 // 不是本地用户数据,那么忽略
if (AccountManager.isLocally(host.ownerId).not()) continue if (AccountManager.isLocally(host.ownerId).not()) continue
// 转移资产 // 先删除,因为 ID 没有改变,改变的只是 owner 信息
val newHost = host.copy( silentDelete(host.id)
id = randomUUID(), hostManager.addHost(host.copy(ownerId = accountOwner.id, ownerType = accountOwner.type.name))
ownerId = account.id,
ownerType = OwnerType.User.name,
)
// 保存数据
save(
Data(
id = newHost.id,
ownerId = newHost.ownerId,
ownerType = newHost.ownerType,
type = DataType.Host.name,
data = ohMyJson.encodeToString(newHost),
)
)
deleteIds.add(host.id)
} }
if (deleteIds.isNotEmpty()) { for (snippet in snippetManager.snippets()) {
lock.withLock { if (snippet.deleted) continue
transaction(database) { silentDelete(snippet.id)
DataEntity.deleteWhere { DataEntity.id.inList(deleteIds) } snippetManager.addSnippet(snippet)
}
}
} }
for (macro in macroManager.getMacros()) {
silentDelete(macro.id)
macroManager.addMacro(macro)
}
for (keymap in keymapManager.getKeymaps()) {
silentDelete(keymap.id)
keymapManager.addKeymap(keymap)
}
for (keypair in keyManager.getOhKeyPairs()) {
silentDelete(keypair.id)
keyManager.addOhKeyPair(keypair, accountOwner)
}
for (e in highlightManager.getKeywordHighlights()) {
silentDelete(e.id)
highlightManager.addKeywordHighlight(e, accountOwner)
}
} }
override fun ordered(): Long { override fun ordered(): Long {
@@ -452,19 +465,17 @@ 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) {
lock.withLock {
for (team in oldAccount.teams) { transaction(database) {
// 如果被踢出团队,那么移除该团队的所有资产 DataEntity.deleteWhere {
if (newAccount.teams.none { it.id == team.id }) { DataEntity.ownerId.eq(team.id) and (DataEntity.ownerType.eq(
lock.withLock { OwnerType.Team.name
transaction(database) { ))
DataEntity.deleteWhere { }
DataEntity.ownerId.eq(team.id) and (DataEntity.ownerType.eq(
OwnerType.Team.name
))
} }
} }
} }
@@ -608,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)
/** /**
* 默认终端 * 默认终端
*/ */
@@ -693,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)
/** /**
* 跟随系统 * 跟随系统
*/ */

View File

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

View File

@@ -45,8 +45,8 @@ class NewKeywordHighlightDialog(
Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND)), Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND)),
I18n.getString("termora.highlight.background-color") I18n.getString("termora.highlight.background-color")
) )
val matchCaseBtn = JToggleButton(Icons.matchCase) val matchCaseBtn = JToggleButton(Icons.matchCase).apply { toolTipText = I18n.getString("termora.match-case") }
val regexBtn = JToggleButton(Icons.regex) val regexBtn = JToggleButton(Icons.regex).apply { toolTipText = I18n.getString("termora.regex") }
private val textColorRevert = JButton(Icons.revert) private val textColorRevert = JButton(Icons.revert)

View File

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

View File

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

View File

@@ -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(
I18n.getString("termora.keymgr.my-keys"),
Icons.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
}
}
}
tabbed.addTab(
I18n.getString("termora.keymgr.my-keys"),
Icons.user,
KeyManagerPanel(AccountOwner(accountManager.getAccountId(), accountManager.getEmail(), OwnerType.User))
)
if (accountManager.hasTeamFeature()) { if (accountManager.hasTeamFeature()) {
for (team in accountManager.getTeams()) { for (team in accountManager.getTeams()) {

View File

@@ -40,7 +40,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)

View File

@@ -13,6 +13,7 @@ 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.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
@@ -127,6 +128,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() {

View File

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

View File

@@ -15,10 +15,17 @@ import java.awt.BorderLayout
import java.awt.CardLayout import java.awt.CardLayout
import java.awt.event.ActionEvent import java.awt.event.ActionEvent
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import javax.swing.* import javax.swing.*
class MarketplacePanel : JPanel(BorderLayout()), Disposable { class MarketplacePanel : JPanel(BorderLayout()), Disposable {
companion object {
/**
* 正在安装的数量
*/
val installing = AtomicInteger(0)
}
private val pluginsPanel = JPanel(VerticalFlowLayout(VerticalFlowLayout.TOP, 0, 8)) private val pluginsPanel = JPanel(VerticalFlowLayout(VerticalFlowLayout.TOP, 0, 8))
private val cardLayout = CardLayout() private val cardLayout = CardLayout()
@@ -93,6 +100,7 @@ class MarketplacePanel : JPanel(BorderLayout()), Disposable {
} }
fun reload() { fun reload() {
if (installing.get() > 0) return
if (isLoading.compareAndSet(false, true)) { if (isLoading.compareAndSet(false, true)) {
coroutineScope.launch { coroutineScope.launch {
withContext(Dispatchers.Swing) { withContext(Dispatchers.Swing) {

View File

@@ -20,7 +20,6 @@ import org.apache.commons.net.io.Util
import org.jdesktop.swingx.JXLabel import org.jdesktop.swingx.JXLabel
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.awt.Dimension import java.awt.Dimension
import java.util.concurrent.atomic.AtomicInteger
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import javax.swing.* import javax.swing.*
@@ -33,11 +32,6 @@ class PluginPanel(val descriptor: PluginPluginDescriptor) : JPanel(), Disposable
private val log = LoggerFactory.getLogger(PluginPanel::class.java) private val log = LoggerFactory.getLogger(PluginPanel::class.java)
private val installed = mutableSetOf<String>() private val installed = mutableSetOf<String>()
private val uninstalled = mutableSetOf<String>() private val uninstalled = mutableSetOf<String>()
/**
* 正在安装的数量
*/
private val installing = AtomicInteger(0)
private val publicKey = Ed25519.generatePublic( private val publicKey = Ed25519.generatePublic(
Base64.decodeBase64("MCowBQYDK2VwAyEAHPyJ5kt2UHWYUPnWU84DOEhCCUE5FEpzdAbeTCNV31A") Base64.decodeBase64("MCowBQYDK2VwAyEAHPyJ5kt2UHWYUPnWU84DOEhCCUE5FEpzdAbeTCNV31A")
) )
@@ -47,7 +41,7 @@ class PluginPanel(val descriptor: PluginPluginDescriptor) : JPanel(), Disposable
private val updateButton = InstallButton().apply { update = true } private val updateButton = InstallButton().apply { update = true }
private val installButton = InstallButton() private val installButton = InstallButton()
private val uninstallButton = JButton(I18n.getString("termora.settings.plugin.uninstall")) private val uninstallButton = JButton(I18n.getString("termora.settings.plugin.uninstall"))
private val installing get() = MarketplacePanel.installing
private val restarter get() = TermoraRestarter.getInstance() private val restarter get() = TermoraRestarter.getInstance()
private val pluginManager get() = PluginManager.getInstance() private val pluginManager get() = PluginManager.getInstance()
private val owner get() = SwingUtilities.getWindowAncestor(this) private val owner get() = SwingUtilities.getWindowAncestor(this)

View File

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

View File

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

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