Compare commits

..

49 Commits

Author SHA1 Message Date
hstyi
5050aa37f5 release: 2.0.0-beta.5 2025-07-07 09:47:35 +08:00
hstyi
53d3d96a06 chore: dynamically modify icons 2025-07-07 09:35:55 +08:00
hstyi
d40b8a4c9c fix: windows drive list failure 2025-07-06 16:48:50 +08:00
hstyi
728671509c feat: transfer support disconnection and reconnection 2025-07-06 16:16:43 +08:00
hstyi
b7178a30fb chore: telnet supports backspace key setting 2025-07-06 11:01:44 +08:00
hstyi
939d6a1fd7 chore: improve flatlaf 2025-07-05 16:02:56 +08:00
hstyi
2986a9cc46 fix: binary compatibility 2025-07-05 14:35:42 +08:00
hstyi
f36afaf5d3 chore: telnet default port 23 2025-07-05 14:07:10 +08:00
hstyi
8cec835583 feat: support telnet 2025-07-05 14:07:10 +08:00
hstyi
a32838dad6 feat: support clone session 2025-07-05 12:07:33 +08:00
hstyi
d54671757e fix: tab drag and drop 2025-07-05 10:17:57 +08:00
hstyi
d1dba56bcd chore: improve sidebar 2025-07-04 16:36:37 +08:00
hstyi
919c06779d chore: improve rm -rf 2025-07-04 15:32:51 +08:00
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
185 changed files with 4819 additions and 1252 deletions

127
README.md
View File

@@ -1,53 +1,102 @@
<div align="center">
<a href="./README.zh_CN.md">🇨🇳 简体中文</a>
<a href="./README.zh_CN.md">简体中文</a>
</div>
# 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">
<img src="./docs/readme.png" alt="termora" />
<img src="docs/readme.png" alt="Readme" />
</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).
## 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`.
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).
## 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:
- AGPL-3.0: Use, distribute, and modify the software under the terms of the [AGPL-3.0](https://opensource.org/license/agpl-v3).
- Proprietary License: For closed-source or proprietary use, please contact the author to obtain a commercial license.
- **AGPL-3.0**: 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.

View File

@@ -1,48 +1,100 @@
# Termora
**Termora** 是一终端模拟器和 SSH 客户端,支持 WindowsmacOSLinux。
**Termora** 是一款跨平台终端模拟器和 SSH 客户端,支持 **WindowsmacOSLinux**
<div align="center">
<img src="./docs/readme-zh_CN.png" alt="termora" />
<img src="docs/readme-zh_CN.png" alt="Readme" />
</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.5

View File

@@ -137,10 +137,6 @@ application {
args.add("-DTERMORA_PLUGIN_DIRECTORY=${layout.buildDirectory.get().asFile.absolutePath}${File.separator}plugins")
if (os.isLinux) {
args.add("-Dsun.java2d.opengl=true")
}
applicationDefaultJvmArgs = args
mainClass = "app.termora.MainKt"
}
@@ -441,6 +437,7 @@ tasks.register<Exec>("jpackage") {
// NSWindow
options.add("-Dapple.awt.application.appearance=system")
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.macosx=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) {
options.add("-Dsun.java2d.opengl=true")
if (isDeb) {
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"
kotlinx-coroutines = "1.10.2"
flatlaf = "3.6"
kotlinx-serialization-json = "1.8.1"
kotlinx-serialization-json = "1.9.0"
commons-codec = "1.18.0"
commons-lang3 = "3.17.0"
commons-csv = "1.14.0"
@@ -35,19 +35,19 @@ bip39 = "1.0.9"
colorpicker = "2.0.1"
rhino = "1.8.0"
delight-rhino-sandbox = "0.0.17"
testcontainers = "1.21.2"
testcontainers = "1.21.3"
mixpanel = "1.5.3"
jSerialComm = "2.11.0"
jSerialComm = "2.11.2"
ini4j = "0.5.5-2"
restart4j = "0.0.1"
eddsa = "0.3.0"
exposed = "1.0.0-beta-2"
exposed = "1.0.0-beta-3"
h2 = "2.3.232"
sqlite = "3.50.1.0"
sqlite = "3.50.2.0"
jug = "5.1.0"
semver4j = "5.8.0"
semver4j = "6.0.0"
jsvg = "1.4.0"
dom4j = "2.1.4"
dom4j = "2.2.0"
[libraries]
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }

View File

@@ -73,3 +73,7 @@ https://www.apache.org/licenses/LICENSE-2.0.html
GeoLite2 (https://www.maxmind.com)
Creative Commons Attribution-ShareAlike 4.0 International (CC 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)
}
project.version = "0.0.2"
project.version = "0.0.3"
dependencies {
testImplementation(kotlin("test"))
implementation("com.qcloud:cos_api:5.6.245")
implementation("com.qcloud:cos_api:5.6.247")
compileOnly(project(":"))
}

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.cos
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class COSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
return COSProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return COSProtocolHostPanel()
}
}

View File

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

View File

@@ -5,6 +5,7 @@ import app.termora.tree.HostTreeNode
import app.termora.tree.MarkerSimpleTreeCellAnnotation
import app.termora.tree.SimpleTreeCellAnnotation
import app.termora.tree.SimpleTreeCellRendererExtension
import com.formdev.flatlaf.util.SystemInfo
import java.awt.Color
import javax.swing.JTree
@@ -33,7 +34,7 @@ class GeoSimpleTreeCellRendererExtension private constructor() : SimpleTreeCellR
if (GeoHostTreeShowMoreEnableExtension.instance.isShowMore().not()) 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(
MarkerSimpleTreeCellAnnotation(
text,

View File

@@ -3,12 +3,12 @@ plugins {
}
project.version = "0.0.1"
project.version = "0.0.2"
dependencies {
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(":"))
}

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.obs
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class OBSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
return OBSProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return OBSProtocolHostPanel()
}
}

View File

@@ -2,11 +2,11 @@ plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.2"
project.version = "0.0.3"
dependencies {
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.activation:activation:1.1.1")
implementation("org.glassfish.jaxb:jaxb-runtime:2.3.3")

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.oss
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class OSSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
return OSSProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return OSSProtocolHostPanel()
}
}

View File

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

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.s3
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class S3ProtocolHostPanelExtension private constructor() : ProtocolHostPanelExte
return S3ProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
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 {

View File

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

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.webdav
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class WebDAVProtocolHostPanelExtension private constructor() : ProtocolHostPanel
return WebDAVProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return WebDAVProtocolHostPanel()
}
}

View File

@@ -14,3 +14,4 @@ include("plugins:migration")
include("plugins:editor")
include("plugins:geo")
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 e;
throw target;
}
}
}

View File

@@ -22,10 +22,7 @@ import org.apache.commons.lang3.LocaleUtils
import org.apache.commons.lang3.SystemUtils
import org.json.JSONObject
import org.slf4j.LoggerFactory
import java.awt.MenuItem
import java.awt.PopupMenu
import java.awt.SystemTray
import java.awt.TrayIcon
import java.awt.*
import java.awt.desktop.AppReopenedEvent
import java.awt.desktop.AppReopenedListener
import java.awt.desktop.SystemEventListener
@@ -173,7 +170,6 @@ class ApplicationRunner {
private fun setupLaf() {
System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux || SystemInfo.isWindows}")
System.setProperty(FlatSystemProperties.USE_ROUNDED_POPUP_BORDER, "false")
if (SystemInfo.isLinux) {
JFrame.setDefaultLookAndFeelDecorated(true)
@@ -197,12 +193,13 @@ class ApplicationRunner {
themeManager.change(theme, true)
FlatInspector.install("ctrl shift X")
if (Application.isBetaVersion()) {
FlatInspector.install("ctrl shift X")
}
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false)
UIManager.put("TitlePane.useWindowDecorations", false)
UIManager.put(FlatClientProperties.POPUP_FORCE_HEAVY_WEIGHT, true)
UIManager.put("Component.arc", 5)
UIManager.put("TextComponent.arc", UIManager.getInt("Component.arc"))
@@ -213,7 +210,6 @@ class ApplicationRunner {
UIManager.put("Dialog.width", 650)
UIManager.put("Dialog.height", 550)
if (SystemInfo.isMacOS) {
UIManager.put("TabbedPane.tabHeight", UIManager.getInt("TitleBar.height"))
} else if (SystemInfo.isLinux) {
@@ -231,15 +227,33 @@ class ApplicationRunner {
UIManager.put("Table.rowHeight", 24)
UIManager.put("Table.focusCellHighlightBorder", FlatTableCellBorder.Default())
UIManager.put("Table.focusSelectedCellHighlightBorder", FlatTableCellBorder.Default())
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
UIManager.put("Tree.rowHeight", 24)
UIManager.put("Tree.background", DynamicColor("window"))
UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc"))
UIManager.put("Tree.showCellFocusIndicator", false)
UIManager.put("Tree.repaintWholeRow", true)
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
// Linux 更多的是尖锐风格
if (SystemInfo.isMacOS || SystemInfo.isWindows) {
val selectionInsets = Insets(0, 2, 0, 2)
UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc"))
UIManager.put("Tree.selectionInsets", selectionInsets)
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
UIManager.put("List.selectionInsets", selectionInsets)
UIManager.put("ComboBox.selectionArc", UIManager.getInt("Component.arc"))
UIManager.put("ComboBox.selectionInsets", selectionInsets)
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
UIManager.put("Table.selectionInsets", selectionInsets)
UIManager.put("MenuBar.selectionArc", UIManager.getInt("Component.arc"))
UIManager.put("MenuBar.selectionInsets", selectionInsets)
UIManager.put("MenuItem.selectionArc", UIManager.getInt("Component.arc"))
UIManager.put("MenuItem.selectionInsets", selectionInsets)
}
}

View File

@@ -23,7 +23,7 @@ class BannerPanel(fontSize: Int = 11, val beautiful: Boolean = false) : JCompone
size = preferredSize
}
override fun paintComponent(g: Graphics) {
public override fun paintComponent(g: Graphics) {
if (g is Graphics2D) {
g.setRenderingHints(
RenderingHints(
@@ -33,7 +33,7 @@ class BannerPanel(fontSize: Int = 11, val beautiful: Boolean = false) : JCompone
}
g.font = font
g.color = UIManager.getColor("TextField.placeholderForeground")
g.color = foreground ?: UIManager.getColor("TextField.placeholderForeground")
val height = g.fontMetrics.height
val descent = g.fontMetrics.descent

View File

@@ -2,7 +2,7 @@ package app.termora
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) {
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
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
@@ -97,6 +117,10 @@ data class Options(
* 跳板机
*/
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? {
val data = databaseManager.data(id) ?: return null
if (data.type != DataType.Host.name) return null
if (data.deleted) return null
return ohMyJson.decodeFromString(data.data)
}

View File

@@ -1,5 +1,6 @@
package app.termora
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProvider
import app.termora.actions.DataProviders
import app.termora.terminal.*
@@ -8,7 +9,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.swing.Swing
import org.apache.commons.lang3.StringUtils
import java.beans.PropertyChangeEvent
import java.util.*
import javax.swing.Icon
abstract class HostTerminalTab(
@@ -20,6 +23,10 @@ abstract class HostTerminalTab(
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 terminalModel get() = terminal.getTerminalModel()
protected var unread = false
@@ -42,7 +49,10 @@ abstract class HostTerminalTab(
if (hasFocus || unread) {
return
}
unread = true
// 如果当前选中的不是这个 Tab那么设置成未读
if (terminalTabbedManager?.getSelectedTerminalTab() != this@HostTerminalTab) {
unread = true
}
}
}
})

View File

@@ -2,6 +2,7 @@ package app.termora
object Icons {
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 newUI by lazy { DynamicIcon("icons/newUI.svg", "icons/newUI.svg") }
val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") }
@@ -33,6 +34,7 @@ object Icons {
val empty by lazy { DynamicIcon("icons/empty.svg") }
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") }
val add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") }
val breakpoint by lazy { DynamicIcon("icons/breakpoint.svg", "icons/breakpoint_dark.svg") }
val softWrap by lazy { DynamicIcon("icons/softWrap.svg", "icons/softWrap_dark.svg") }
val scrollUp by lazy { DynamicIcon("icons/scrollUp.svg", "icons/scrollUp_dark.svg") }
val reformatCode by lazy { DynamicIcon("icons/reformatCode.svg", "icons/reformatCode_dark.svg") }
@@ -77,9 +79,11 @@ object Icons {
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") }
val export by lazy { DynamicIcon("icons/export.svg", "icons/export_dark.svg") }
val terminal by lazy { DynamicIcon("icons/terminal.svg", "icons/terminal_dark.svg") }
val telnet by lazy { DynamicIcon("icons/telnet.svg", "icons/telnet_dark.svg") }
val ssh by lazy { DynamicIcon("icons/ssh.svg", "icons/ssh_dark.svg") }
val ftp by lazy { DynamicIcon("icons/ftp.svg", "icons/ftp_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 serial by lazy { DynamicIcon("icons/serial.svg", "icons/serial_dark.svg") }
val fileFormat by lazy { DynamicIcon("icons/fileFormat.svg", "icons/fileFormat_dark.svg") }

View File

@@ -0,0 +1,126 @@
package app.termora
import com.formdev.flatlaf.ui.FlatSplitPaneUI
import java.awt.BorderLayout
import java.awt.Cursor
import java.awt.Graphics
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.MouseMotionAdapter
import java.util.function.Supplier
import javax.swing.*
class SplitPaneUI : FlatSplitPaneUI() {
public override fun startDragging() {
super.startDragging()
}
public override fun dragDividerTo(location: Int) {
super.dragDividerTo(location)
}
public override fun finishDraggingTo(location: Int) {
super.finishDraggingTo(location)
}
}
class JSplitPaneWithZeroSizeDivider(
private val splitPane: JSplitPane,
private val topOffset: Supplier<Int>,
) : JPanel(BorderLayout()) {
private val dividerDragSize = 7
private val layeredPane = LayeredPane()
private val divider = Divider()
init {
layeredPane.add(splitPane, JLayeredPane.DEFAULT_LAYER as Any)
layeredPane.add(divider, JLayeredPane.PALETTE_LAYER as Any)
add(layeredPane, BorderLayout.CENTER)
}
private inner class Divider : JComponent() {
private var dragging = false
private var dragStartX = 0
private var initialDividerLocation = 0
private val splitPaneUI get() = splitPane.ui as SplitPaneUI
init {
cursor = Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR)
addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent) {
if (SwingUtilities.isLeftMouseButton(e)) {
dragging = true
dragStartX = e.xOnScreen
initialDividerLocation = splitPane.dividerLocation
splitPaneUI.startDragging()
}
}
override fun mouseReleased(e: MouseEvent) {
if (SwingUtilities.isLeftMouseButton(e)) {
if (dragging) {
val deltaX = e.xOnScreen - dragStartX
val newLocation = initialDividerLocation + deltaX
splitPaneUI.finishDraggingTo(newLocation)
}
dragging = false
}
}
})
addMouseMotionListener(object : MouseMotionAdapter() {
override fun mouseDragged(e: MouseEvent) {
if (dragging) {
val deltaX = e.xOnScreen - dragStartX
val newLocation = initialDividerLocation + deltaX
splitPaneUI.dragDividerTo(newLocation)
}
}
})
}
override fun paint(g: Graphics) {
g.color = UIManager.getColor("controlShadow")
g.fillRect(width / 2, 0, 1, height)
}
}
private inner class LayeredPane : JLayeredPane() {
private val w get() = (dividerDragSize - 1) / 2
override fun doLayout() {
synchronized(treeLock) {
for (c in components) {
if (c == divider) {
c.isVisible = splitPane.leftComponent.isVisible
c.setBounds(
splitPane.dividerLocation - w,
topOffset.get(),
dividerDragSize,
height - topOffset.get()
)
} else {
c.setBounds(0, 0, width, height)
}
}
}
}
override fun paint(g: Graphics) {
super.paint(g)
if (divider.isVisible) {
g.color = UIManager.getColor("controlShadow")
g.fillRect(splitPane.dividerLocation, 0, 1, topOffset.get())
}
}
}
override fun doLayout() {
super.doLayout()
layeredPane.doLayout()
}
}

View File

@@ -9,6 +9,12 @@ import java.awt.Window
object NativeMacLibrary {
private val log = LoggerFactory.getLogger(NativeMacLibrary::class.java)
enum class NSWindowButton {
NSWindowCloseButton,
NSWindowMiniaturizeButton,
NSWindowZoomButton,
}
fun getNSWindow(window: Window): Long? {
try {
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)
try {
Foundation.executeOnMainThread(true, true) {
for (i in 0..2) {
val button = Foundation.invoke(nsWindow, "standardWindowButton:", i)
Foundation.invoke(button, "setHidden:", !visible)
for (button in buttons) {
val button = Foundation.invoke(nsWindow, "standardWindowButton:", button.ordinal)
Foundation.invoke(button, "setHidden:", visible.not())
}
}
} catch (e: Exception) {

View File

@@ -1,5 +1,6 @@
package app.termora
import app.termora.account.AccountOwner
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.protocol.*
@@ -18,7 +19,11 @@ import java.awt.Dimension
import java.awt.Window
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 {
var card: ProtocolHostPanel? = null
@@ -65,11 +70,11 @@ class NewHostDialogV2(owner: Window, private val editHost: Host? = null) : Dialo
toolbar.add(Box.createHorizontalGlue())
val extensions = ProtocolHostPanelExtension.extensions
.filter { it.canCreateProtocolHostPanel() }
.filter { it.canCreateProtocolHostPanel(accountOwner) }
for ((index, extension) in extensions.withIndex()) {
val protocol = extension.getProtocolProvider().getProtocol()
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) }
button.setVerticalTextPosition(SwingConstants.BOTTOM)
button.setHorizontalTextPosition(SwingConstants.CENTER)

View File

@@ -23,7 +23,10 @@ abstract class PtyHostTerminalTab(
private var readerJob: Job? = null
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()
override fun start() {
@@ -37,7 +40,7 @@ abstract class PtyHostTerminalTab(
}
// 开启 PTY
val ptyConnector = openPtyConnector()
val ptyConnector = loginScriptsPtyConnector(host, openPtyConnector())
ptyConnectorDelegate.ptyConnector = ptyConnector
// 开启 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) {
ptyConnector.write(bytes)
}

View File

@@ -33,7 +33,7 @@ open class Scope(
return get(clazz)
}
synchronized(clazz) {
synchronized(this) {
if (beans.containsKey(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.FlatPopupMenu
import com.formdev.flatlaf.extras.components.FlatToolBar
import com.formdev.flatlaf.util.FontUtils
import com.formdev.flatlaf.util.SystemInfo
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
@@ -29,7 +28,6 @@ import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.SystemUtils
import java.awt.BorderLayout
import java.awt.Component
import java.awt.Dimension
import java.awt.Toolkit
import java.awt.event.ActionEvent
import java.awt.event.ItemEvent
@@ -112,6 +110,7 @@ class SettingsOptionsPane : OptionsPane() {
private inner class AppearanceOption : JPanel(BorderLayout()), Option {
val themeManager = ThemeManager.getInstance()
val themeComboBox = FlatComboBox<String>()
val layoutComboBox = FlatComboBox<TermoraLayout>()
val languageComboBox = FlatComboBox<String>()
val backgroundComBoBox = YesOrNoComboBox()
val confirmTabCloseComBoBox = YesOrNoComboBox()
@@ -129,6 +128,38 @@ class SettingsOptionsPane : OptionsPane() {
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
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 {
val opacity = opacitySpinner.value
if (opacity is Double) {
@@ -307,7 +349,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun getFormPanel(): JPanel {
val layout = FormLayout(
"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()
box.add(followSystemCheckBox)
@@ -329,6 +371,9 @@ class SettingsOptionsPane : OptionsPane() {
})).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)
.add(opacitySpinner).xy(3, rows).apply { rows += step }
@@ -352,7 +397,8 @@ class SettingsOptionsPane : OptionsPane() {
private val debugComboBox = YesOrNoComboBox()
private val beepComboBox = 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 maxRowsTextField = IntSpinner(0, 0)
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 ->
if (e.stateChange == ItemEvent.SELECTED) {
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.Bar)
cursorStyleComboBox.addItem(CursorStyle.Underline)
@@ -519,29 +545,18 @@ class SettingsOptionsPane : OptionsPane() {
shellComboBox.selectedItem = terminalSetting.localShell
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 {
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
if (!fontsLoaded) {
val selectedItem = fontComboBox.selectedItem
fontComboBox.removeAllItems();
fontComboBox.addItem("JetBrains Mono")
fontComboBox.addItem("Source Code Pro")
fontComboBox.addItem("Monospaced")
FontUtils.getAvailableFontFamilyNames().forEach {
fontComboBox.addItem(it)
}
fontComboBox.selectedItem = selectedItem
fontsLoaded = true
}
}
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {}
override fun popupMenuCanceled(e: PopupMenuEvent) {}
})
if (terminalSetting.fallbackFont.isNotBlank()) {
fallbackFontComboBox.addItem(StringUtils.EMPTY)
}
fallbackFontComboBox.addItem(terminalSetting.fallbackFont)
fontComboBox.selectedItem = terminalSetting.font
fallbackFontComboBox.selectedItem = terminalSetting.fallbackFont
debugComboBox.selectedItem = terminalSetting.debug
beepComboBox.selectedItem = terminalSetting.beep
hyperlinkComboBox.selectedItem = terminalSetting.hyperlink
@@ -580,7 +595,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"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)
@@ -596,6 +611,8 @@ class SettingsOptionsPane : OptionsPane() {
.add(fontComboBox).xy(3, rows)
.add("${I18n.getString("termora.settings.terminal.size")}:").xy(5, rows)
.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(maxRowsTextField).xy(3, rows).apply { rows += step }
.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 terminalPanel = TerminalPanel(terminal, writer)
val terminalPanel = TerminalPanel(tab, terminal, writer)
// processDeviceStatusReport
terminal.getTerminalModel().setData(DataKey.TerminalWriter, writer)

View File

@@ -1,6 +1,7 @@
package app.termora
import app.termora.account.AccountManager
import app.termora.actions.*
import app.termora.database.DatabaseChangedExtension
import app.termora.database.DatabaseManager
@@ -8,10 +9,8 @@ import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
import app.termora.findeverywhere.FindEverywhereProvider
import app.termora.findeverywhere.FindEverywhereProviderExtension
import app.termora.findeverywhere.FindEverywhereResult
import app.termora.plugin.ExtensionManager
import app.termora.plugin.internal.extension.DynamicExtensionHandler
import app.termora.plugin.internal.sftppty.SFTPPtyProtocolProvider
import app.termora.plugin.internal.sftppty.SFTPPtyTerminalTab
import app.termora.plugin.internal.ssh.SSHProtocolProvider
import app.termora.terminal.DataKey
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.components.FlatPopupMenu
@@ -23,7 +22,6 @@ import java.awt.event.ActionEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.beans.PropertyChangeListener
import java.util.*
import javax.swing.*
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
import kotlin.math.min
@@ -32,6 +30,7 @@ class TerminalTabbed(
private val windowScope: WindowScope,
private val termoraToolBar: TermoraToolBar,
private val tabbedPane: FlatTabbedPane,
private val layout: TermoraLayout,
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager, DataProvider {
private val tabs = mutableListOf<TerminalTab>()
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
@@ -110,20 +109,6 @@ class TerminalTabbed(
}
})
// 点击
tabbedPane.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
if (SwingUtilities.isLeftMouseButton(e)) {
val index = tabbedPane.indexAtLocation(e.x, e.y)
if (index > 0) {
tabbedPane.getComponentAt(index).requestFocusInWindow()
}
}
}
})
// 注册全局搜索
DynamicExtensionHandler.getInstance()
.register(FindEverywhereProviderExtension::class.java, object : FindEverywhereProviderExtension {
@@ -206,11 +191,13 @@ class TerminalTabbed(
// remove ele
tabs.removeAt(index)
// 新的获取到焦点
tabs[tabbedPane.selectedIndex].onGrabFocus()
if (tabbedPane.tabCount > 0) {
// 新的获取到焦点
tabs[tabbedPane.selectedIndex].onGrabFocus()
// 新的真正获取焦点
tabbedPane.getComponentAt(tabbedPane.selectedIndex).requestFocusInWindow()
// 新的真正获取焦点
tabbedPane.getComponentAt(tabbedPane.selectedIndex).requestFocusInWindow()
}
if (disposable) {
Disposer.dispose(tab)
@@ -221,6 +208,15 @@ class TerminalTabbed(
private fun showContextMenu(tabIndex: Int, e: MouseEvent) {
val c = tabbedPane.getComponentAt(tabIndex) as JComponent
val tab = tabs[tabIndex]
val extensions = ExtensionManager.getInstance().getExtensions(TerminalTabbedContextMenuExtension::class.java)
val menuItems = mutableListOf<JMenuItem>()
for (extension in extensions) {
try {
menuItems.add(extension.createJMenuItem(windowScope, tab))
} catch (_: UnsupportedOperationException) {
continue
}
}
val popupMenu = FlatPopupMenu()
@@ -242,7 +238,7 @@ class TerminalTabbed(
}
// 克隆
val clone = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.clone"))
val clone = popupMenu.add(I18n.getString("termora.copy"))
clone.addActionListener { evt ->
if (tab is HostTerminalTab) {
actionManager
@@ -255,10 +251,15 @@ class TerminalTabbed(
val edit = popupMenu.add(I18n.getString("termora.keymgr.edit"))
edit.addActionListener(object : AnAction() {
private val hostManager get() = HostManager.getInstance()
private val accountManager get() = AccountManager.getInstance()
override fun actionPerformed(evt: AnActionEvent) {
if (tab is HostTerminalTab) {
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.isVisible = true
@@ -289,14 +290,10 @@ class TerminalTabbed(
}
})
if (tab is HostTerminalTab) {
val openHostAction = actionManager.getAction(OpenHostAction.OPEN_HOST)
if (openHostAction != null) {
if (tab.host.protocol == SSHProtocolProvider.PROTOCOL || tab.host.protocol == SFTPPtyProtocolProvider.PROTOCOL) {
popupMenu.addSeparator()
val sftpCommand = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
sftpCommand.addActionListener { openSFTPPtyTab(tab, openHostAction, it) }
}
if (menuItems.isNotEmpty()) {
popupMenu.addSeparator()
for (item in menuItems) {
popupMenu.add(item)
}
}
@@ -388,36 +385,6 @@ class TerminalTabbed(
}
private fun openSFTPPtyTab(tab: HostTerminalTab, openHostAction: Action, evt: EventObject) {
if (!SFTPPtyTerminalTab.canSupports) {
OptionPane.showMessageDialog(
SwingUtilities.getWindowAncestor(this),
I18n.getString("termora.tabbed.contextmenu.sftp-not-install"),
messageType = JOptionPane.ERROR_MESSAGE
)
return
}
var host = tab.host
if (host.protocol == SSHProtocolProvider.PROTOCOL) {
val envs = tab.host.options.envs().toMutableMap()
val currentDir = tab.getData(DataProviders.Terminal)?.getTerminalModel()
?.getData(DataKey.CurrentDir, StringUtils.EMPTY) ?: StringUtils.EMPTY
if (currentDir.isNotBlank()) {
envs["CurrentDir"] = currentDir
}
host = host.copy(
protocol = SFTPPtyProtocolProvider.PROTOCOL,
options = host.options.copy(env = envs.toPropertiesString())
)
}
openHostAction.actionPerformed(OpenHostActionEvent(this, host, evt))
}
/**
* 对着 ToolBar 右键
*/
@@ -497,6 +464,32 @@ class TerminalTabbed(
}
}
override fun paint(g: Graphics) {
super.paint(g)
}
private val border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
private val banner = BannerPanel(fontSize = 13).apply {
foreground = UIManager.getColor("textInactiveText")
}
override fun paintComponent(g: Graphics) {
super.paintComponent(g)
if (layout == TermoraLayout.Fence) {
if (g is Graphics2D) {
if (tabbedPane.tabCount < 1) {
border.paintBorder(this, g, 0, tabbedPane.tabHeight, width, tabbedPane.tabHeight)
banner.setBounds(0, 0, width, height)
g.save()
g.translate(0, 180)
banner.paintComponent(g)
g.restore()
}
}
}
}
override fun dispose() {
}

View File

@@ -0,0 +1,12 @@
package app.termora
import app.termora.plugin.Extension
import javax.swing.JMenuItem
interface TerminalTabbedContextMenuExtension : Extension {
/**
* 抛出 [UnsupportedOperationException] 表示不支持
*/
fun createJMenuItem(windowScope: WindowScope, tab: TerminalTab): JMenuItem
}

View File

@@ -0,0 +1,163 @@
package app.termora
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.tree.NewHostTree
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.extras.components.FlatToolBar
import com.formdev.flatlaf.util.SystemInfo
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.Font
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter
import javax.swing.*
import kotlin.math.max
class TermoraFencePanel(
private val terminalTabbed: TerminalTabbed,
private val tabbed: FlatTabbedPane,
private val moveMouseAdapter: MouseAdapter,
) : JPanel(BorderLayout()), Disposable {
private val splitPane = object : JSplitPane() {
override fun updateUI() {
setUI(SplitPaneUI())
revalidate()
}
}
private val leftTreePanel = LeftTreePanel()
private val mySplitPane = JSplitPaneWithZeroSizeDivider(splitPane) { tabbed.tabHeight }
private val enableManager get() = EnableManager.getInstance()
private val toolbar = FlatToolBar().apply { isFloatable = false }
private var dividerLocation = 0
init {
initView()
initEvents()
}
private fun initView() {
splitPane.border = null
splitPane.leftComponent = leftTreePanel
splitPane.rightComponent = terminalTabbed
splitPane.dividerSize = 0
splitPane.dividerLocation = enableManager.getFlag("Termora.Fence.dividerLocation", 220)
leftTreePanel.preferredSize = Dimension(180, -1)
tabbed.tabType = FlatTabbedPane.TabType.underlined
tabbed.tabAreaInsets = null
// macOS 避开控制栏
if (SystemInfo.isMacOS) {
toolbar.add(Box.createHorizontalStrut(76))
}
toolbar.add(createColspanAction())
tabbed.leadingComponent = toolbar
toolbar.isVisible = false
add(mySplitPane, BorderLayout.CENTER)
}
private fun initEvents() {
Disposer.register(this, leftTreePanel)
splitPane.addPropertyChangeListener("dividerLocation") { mySplitPane.doLayout() }
leftTreePanel.addComponentListener(object : ComponentAdapter() {
override fun componentHidden(e: ComponentEvent) {
toolbar.isVisible = true
}
override fun componentShown(e: ComponentEvent) {
toolbar.isVisible = false
}
})
actionMap.put("toggle", createColspanAction())
getInputMap(WHEN_IN_FOCUSED_WINDOW).put(
KeyStroke.getKeyStroke(
KeyEvent.VK_B,
toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK
), "toggle"
)
}
private inner class LeftTreePanel : JPanel(BorderLayout()), Disposable {
val hostTree = NewHostTree()
private val box = JToolBar().apply { isFloatable = false }
init {
initView()
initEvents()
}
private fun initView() {
val scrollPane = JScrollPane(hostTree)
hostTree.name = "FenceHostTree"
hostTree.restoreExpansions()
box.preferredSize = Dimension(-1, tabbed.tabHeight)
val label = JLabel(Application.getName())
label.foreground = UIManager.getColor("textInactiveText")
label.font = label.font.deriveFont(Font.BOLD)
// 与最后一个按钮对冲,使其宽度和谐
box.add(JButton(Icons.empty))
box.add(Box.createHorizontalGlue())
if (SystemInfo.isMacOS.not()) {
box.add(label)
}
box.add(Box.createHorizontalGlue())
box.add(createColspanAction())
if (SystemInfo.isMacOS || SystemInfo.isLinux) {
box.addMouseListener(moveMouseAdapter)
box.addMouseMotionListener(moveMouseAdapter)
}
scrollPane.verticalScrollBar.unitIncrement = 16
scrollPane.horizontalScrollBar.unitIncrement = 16
scrollPane.border = BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor),
BorderFactory.createEmptyBorder(4, 4, 0, 4)
)
add(box, BorderLayout.NORTH)
add(scrollPane, BorderLayout.CENTER)
}
private fun initEvents() {
Disposer.register(this, hostTree)
}
}
private fun createColspanAction(): Action {
return object : AnAction(Icons.dataColumn) {
init {
val text = I18n.getString("termora.welcome.toggle-sidebar")
putValue(SHORT_DESCRIPTION, "$text (${if (SystemInfo.isMacOS) '⌘' else "Ctrl"} + Shift + B)")
}
override fun actionPerformed(evt: AnActionEvent) {
if (leftTreePanel.isVisible) dividerLocation = splitPane.dividerLocation
leftTreePanel.isVisible = leftTreePanel.isVisible.not()
if (leftTreePanel.isVisible) splitPane.dividerLocation = dividerLocation
}
}
}
override fun dispose() {
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
}
fun getHostTree(): NewHostTree {
return leftTreePanel.hostTree
}
}

View File

@@ -4,21 +4,29 @@ package app.termora
import app.termora.actions.DataProvider
import app.termora.actions.DataProviderSupport
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.internal.extension.DynamicExtensionHandler
import app.termora.plugin.internal.ssh.SSHProtocolProvider
import app.termora.terminal.DataKey
import app.termora.tree.NewHostTreeModel
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.ui.FlatRootPaneUI
import com.formdev.flatlaf.ui.FlatTitlePane
import com.formdev.flatlaf.util.SystemInfo
import com.jetbrains.JBR
import org.apache.commons.lang3.ArrayUtils
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionManager
import java.awt.*
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.MouseListener
import java.awt.event.MouseMotionListener
import java.awt.event.*
import java.util.*
import javax.imageio.ImageIO
import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.JFrame
import javax.swing.SwingUtilities.isEventDispatchThread
@@ -32,15 +40,16 @@ fun assertEventDispatchThread() {
class TermoraFrame : JFrame(), DataProvider {
private val layout get() = TermoraLayout.Layout
private val titleBarHeight = computedTitleBarHeight()
private val id = UUID.randomUUID().toString()
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 terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane)
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane, layout)
private val dataProviderSupport = DataProviderSupport()
private val welcomePanel = WelcomePanel(windowScope)
private var notifyListeners = emptyArray<NotifyListener>()
private val moveMouseAdapter = createMoveMouseAdaptor()
init {
@@ -50,7 +59,201 @@ class TermoraFrame : JFrame(), DataProvider {
private fun initEvents() {
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)
}
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) {
getMouseHandler()?.mouseClicked(e)
}
@@ -97,8 +300,6 @@ class TermoraFrame : JFrame(), DataProvider {
return titlePaneField.get(ui) as? FlatTitlePane
}
}
toolbar.getJToolBar().addMouseListener(mouseAdapter)
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
}
/// force hit
@@ -145,111 +346,13 @@ class TermoraFrame : JFrame(), DataProvider {
}
}
terminalTabbed.addMouseListener(mouseAdapter)
terminalTabbed.addMouseMotionListener(mouseAdapter)
tabbedPane.addMouseListener(mouseAdapter)
tabbedPane.addMouseMotionListener(mouseAdapter)
toolbar.getJToolBar().addMouseListener(mouseAdapter)
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
JBR.getWindowDecorations().setCustomTitleBar(this, customTitleBar)
return 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() }
return object : MouseAdapter() {}
}

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

@@ -10,7 +10,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory
import java.util.function.Consumer
import javax.swing.PopupFactory
import javax.swing.SwingUtilities
import javax.swing.UIManager
@@ -118,11 +117,7 @@ internal class ThemeManager private constructor() {
private fun immediateChange(classname: String) {
try {
val oldPopupFactory = PopupFactory.getSharedInstance()
UIManager.setLookAndFeel(classname)
PopupFactory.setSharedInstance(oldPopupFactory)
} catch (ex: Exception) {
log.error(ex.message, ex)
}

View File

@@ -4,14 +4,9 @@ package app.termora
import app.termora.actions.*
import app.termora.database.DatabaseManager
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.tree.*
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.FlatSVGIcon
import com.formdev.flatlaf.extras.components.FlatButton
import org.apache.commons.lang3.StringUtils
@@ -24,8 +19,7 @@ import java.awt.event.*
import javax.swing.*
import kotlin.math.max
class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()), Disposable, TerminalTab,
DataProvider {
class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProvider {
private val properties get() = DatabaseManager.getInstance().properties
private val rootPanel = JPanel(BorderLayout())
@@ -52,6 +46,7 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
val panel = JPanel(BorderLayout())
panel.add(createSearchPanel(), BorderLayout.NORTH)
panel.add(createHostPanel(), BorderLayout.CENTER)
bannerPanel.foreground = UIManager.getColor("TextField.placeholderForeground")
if (!fullContent) {
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() {
@@ -302,40 +259,6 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
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? {
return dataProviderSupport.getData(dataKey)
}

View File

@@ -183,8 +183,8 @@ object AccountHttp {
if (isRefreshing.compareAndSet(false, true)) {
try {
// 刷新 token
accountManager.refreshToken()
// 刷新 token 和用户
accountManager.refresh()
} finally {
lock.withLock {
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.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import java.security.PrivateKey
import java.security.PublicKey
import javax.swing.SwingUtilities
class AccountManager private constructor() : ApplicationRunnerExtension {
companion object {
private val log = LoggerFactory.getLogger(AccountManager::class.java)
fun getInstance(): AccountManager {
return ApplicationScope.forApplicationScope()
.getOrCreate(AccountManager::class) { AccountManager() }
@@ -30,6 +33,7 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
}
}
private val serverManager get() = ServerManager.getInstance()
private var account = locally()
private val accountProperties get() = AccountProperties.getInstance()
@@ -48,10 +52,14 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
fun getAccessToken() = account.accessToken
fun getRefreshToken() = account.refreshToken
fun getOwnerIds() = account.teams.map { it.id }.toMutableList().apply { add(getAccountId()) }.toSet()
fun getOwners() =
account.teams.map { AccountOwner(it.id, it.name, OwnerType.Team) }
.toMutableList().apply { AccountOwner(getAccountId(), getEmail(), OwnerType.User) }
.toSet()
fun getOwners(): Set<AccountOwner> {
val owners = mutableSetOf<AccountOwner>()
owners.add(AccountOwner(getAccountId(), getEmail(), OwnerType.User))
for (team in getTeams()) {
owners.add(AccountOwner(team.id, team.name, OwnerType.Team))
}
return owners
}
fun isFreePlan(): Boolean {
return isLocally() || getSubscription().plan == SubscriptionPlan.Free
@@ -126,37 +134,39 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
* 设置账户信息,可以多次调用,每次修改用户信息都要通过这个方法
*/
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()
accountProperties.id = account.id
accountProperties.server = account.server
accountProperties.email = account.email
accountProperties.teams = ohMyJson.encodeToString(account.teams)
accountProperties.subscriptions = ohMyJson.encodeToString(account.subscriptions)
accountProperties.accessToken = account.accessToken
accountProperties.refreshToken = account.refreshToken
accountProperties.secretKey = ohMyJson.encodeToString(account.secretKey)
// 立即保存到数据库
val accountProperties = AccountProperties.getInstance()
accountProperties.id = account.id
accountProperties.server = account.server
accountProperties.email = account.email
accountProperties.teams = ohMyJson.encodeToString(account.teams)
accountProperties.subscriptions = ohMyJson.encodeToString(account.subscriptions)
accountProperties.accessToken = account.accessToken
accountProperties.refreshToken = account.refreshToken
accountProperties.secretKey = ohMyJson.encodeToString(account.secretKey)
// 如果变更账户了那么同步时间从0开始
if (oldAccount.id != account.id) {
accountProperties.nextSynchronizationSince = 0
// 如果变更账户了那么同步时间从0开始
if (oldAccount.id != account.id) {
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) {
@@ -220,7 +230,7 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
override fun ready() {
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 {

View File

@@ -75,8 +75,10 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
val subscription = accountManager.getSubscription()
val isFreePlan = accountManager.isFreePlan()
val isLocally = accountManager.isLocally()
val validTo = if (isFreePlan) "-" else if (subscription.endAt >= Long.MAX_VALUE)
I18n.getString("termora.settings.account.lifetime") else
val validTo = if (isFreePlan) "-"
else if (subscription.endAt >= Long.MAX_VALUE)
I18n.getString("termora.settings.account.lifetime")
else
DateFormatUtils.format(Date(subscription.endAt), I18n.getString("termora.date-format"))
val lastSynchronizationOn = if (isFreePlan) "-" else
DateFormatUtils.format(
@@ -158,9 +160,19 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
if (isFreePlan.not()) {
actions.add(JXHyperlink(object : AnAction(I18n.getString("termora.settings.account.sync-now")) {
override fun actionPerformed(evt: AnActionEvent) {
// 全量同步
accountProperties.nextSynchronizationSince = 0
// 拉取
PullService.getInstance().trigger()
// 推送
PushService.getInstance().trigger()
swingCoroutineScope.launch(Dispatchers.IO) {
// 刷新账户
accountManager.refreshAccount()
withContext(Dispatchers.Swing) {
isEnabled = false
lastSynchronizationOnLabel.text = DateFormatUtils.format(

View File

@@ -2,5 +2,4 @@ package app.termora.account
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")
private val chinaServer =
Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn")
private val serverManager get() = ServerManager.getInstance()
init {
isModal = true
@@ -59,6 +60,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
size = Dimension(max(preferredSize.width, UIManager.getInt("Dialog.width") - 250), preferredSize.height)
setLocationRelativeTo(owner)
passwordField.putClientProperty(FlatClientProperties.STYLE, mapOf("showCapsLock" to true))
addWindowListener(object : WindowAdapter() {
override fun windowOpened(e: WindowEvent) {
@@ -358,7 +360,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
val loginJob = swingCoroutineScope.launch(Dispatchers.IO) {
try {
ServerManager.getInstance().login(
serverManager.login(
server, usernameTextField.text,
String(passwordField.password), mfaTextField.text.trim()
)

View File

@@ -45,6 +45,15 @@ class PullService private constructor() : SyncService(), Disposable, Application
lastChangeHash = StringUtils.EMPTY
}
// 团队变了,全量同步
if (oldAccount.id == newAccount.id) {
if (oldAccount.teams != newAccount.teams) {
accountProperties.nextSynchronizationSince = 0
trigger()
return
}
}
if (oldAccount.isLocally && newAccount.isLocally.not()) {
trigger()
}
@@ -213,7 +222,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
log.debug("拉取数据: {} 成功, 响应码: {}", id, response.code)
}
if(response.isSuccessful.not()){
if (response.isSuccessful.not()) {
IOUtils.closeQuietly(response)
}
@@ -281,7 +290,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
ownerId = ownerId,
ownerType = ownerType,
type = type,
data = decryptData(id, data),
data = decryptData(id, data, ownerId),
version = version,
// 因为已经是拉取最新版本了,所以这里无需再同步了
synced = true,
@@ -298,11 +307,8 @@ class PullService private constructor() : SyncService(), Disposable, Application
if (log.isDebugEnabled) {
log.debug("数据: {}, 类型: {} 云端已经删除,本地即将删除", id, type)
}
databaseManager.delete(
id, type,
DatabaseChangedExtension.Source.Sync
)
databaseManager.delete(id, type, DatabaseChangedExtension.Source.Sync)
if (log.isInfoEnabled) {
log.info("数据: {}, 类型: {} 已从本地删除", id, type)
@@ -340,7 +346,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
ownerId = ownerId,
ownerType = ownerType,
type = type,
data = decryptData(id, data),
data = decryptData(id, data, ownerId),
version = version,
// 因为已经是拉取最新版本了,所以这里无需再同步了
synced = true,
@@ -377,7 +383,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
pullChanges()
// N 秒拉一次
val result = withTimeoutOrNull(Random.nextInt(5, 15).seconds) {
val result = withTimeoutOrNull(Random.nextInt(3, 10).seconds) {
channel.receiveCatching()
} ?: continue
if (result.isFailure) break

View File

@@ -4,6 +4,7 @@ import app.termora.*
import app.termora.Application.ohMyJson
import app.termora.database.Data
import app.termora.database.DatabaseChangedExtension
import app.termora.database.OwnerType
import app.termora.plugin.DispatchThread
import app.termora.plugin.internal.extension.DynamicExtensionHandler
import kotlinx.coroutines.Dispatchers
@@ -106,7 +107,20 @@ class PushService private constructor() : SyncService(), Disposable, Application
.delete()
.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)
@@ -153,6 +167,12 @@ class PushService private constructor() : SyncService(), Disposable, Application
}
// 标记为已经同步
updateData(data.id, synced = true, version = data.version)
// 如果是 Team 发现没有权限,那么很有可能是被提出团队
if (data.ownerType == OwnerType.Team.name) {
// 刷新用户
accountManager.refreshAccount()
}
return
} else if (response.code == 409) { // 版本冲突,一般来说是云端版本大于本地版本
val json = ohMyJson.decodeFromString<JsonObject>(text)

View File

@@ -54,7 +54,7 @@ class ServerManager private constructor() {
val loginResponse = callLogin(serverInfo, server, username, password, mfa)
// call me
val meResponse = callMe(server, loginResponse.accessToken)
val meResponse = callMe(server.server, loginResponse.accessToken)
// 解密
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()
.url("${server.server}/v1/users/me")
.url("${server}/v1/users/me")
.header("Authorization", "Bearer $accessToken")
.build()
val text = AccountHttp.execute(request = request)
@@ -149,13 +149,13 @@ class ServerManager private constructor() {
}
@Serializable
private data class ServerInfo(val salt: String)
data class ServerInfo(val salt: String)
@Serializable
private data class LoginResponse(val accessToken: String, val refreshToken: String)
data class LoginResponse(val accessToken: String, val refreshToken: String)
@Serializable
private data class MeResponse(
data class MeResponse(
val id: String,
val email: String,
val publicKey: String,
@@ -167,5 +167,5 @@ class ServerManager private constructor() {
@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 {
val iv = DigestUtils.sha256(id).copyOf(12)
var secretKey = EMPTY_BYTE_ARRAY
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()
}
val secretKey = getSecretKey(ownerId)
if (secretKey.isEmpty()) return StringUtils.EMPTY
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)
return String(
AES.GCM.decrypt(
accountManager.getSecretKey(), iv,
Base64.decodeBase64(data)
)
)
val secretKey = getSecretKey(ownerId)
if (secretKey.isEmpty()) throw IllegalStateException("根据 ownerId 无法获取对应密钥")
return String(AES.GCM.decrypt(secretKey, iv, Base64.decodeBase64(data)))
}
}

View File

@@ -26,4 +26,22 @@ class Team(
* 所属角色
*/
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
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.BoundAction
import java.awt.event.ActionEvent
import javax.swing.Icon
@@ -20,7 +21,7 @@ abstract class AnAction : BoundAction {
if (evt is AnActionEvent) {
actionPerformed(evt)
} 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
import app.termora.NewHostDialogV2
import app.termora.account.AccountManager
import app.termora.tree.HostTreeNode
import javax.swing.tree.TreePath
@@ -14,6 +15,8 @@ class NewHostAction : AnAction() {
}
private val accountManager get() = AccountManager.getInstance()
override fun actionPerformed(evt: AnActionEvent) {
val tree = evt.getData(DataProviders.Welcome.HostTree) ?: return
var lastNode = (tree.lastSelectedPathComponent ?: tree.model.root) as? HostTreeNode ?: return
@@ -27,7 +30,7 @@ class NewHostAction : AnAction() {
}
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.isVisible = true
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.AccountExtension
import app.termora.account.AccountManager
import app.termora.account.AccountOwner
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.snippet.SnippetManager
import app.termora.terminal.CursorStyle
import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.StringUtils
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.statements.StatementType
import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.*
import java.util.concurrent.locks.ReentrantLock
@@ -27,11 +32,8 @@ import kotlin.reflect.KProperty
class DatabaseManager private constructor() : Disposable {
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 {
return ApplicationScope.forApplicationScope()
.getOrCreate(DatabaseManager::class) { DatabaseManager() }
@@ -47,14 +49,6 @@ class DatabaseManager private constructor() : Disposable {
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 accountManager get() = AccountManager.getInstance()
@@ -96,11 +90,6 @@ class DatabaseManager private constructor() : Disposable {
// 注册动态扩展
registerDynamicExtensions()
for (extension in ExtensionManager.getInstance().getExtensions(DatabaseReadyExtension::class.java)) {
extension.ready(this)
}
}
private fun registerDynamicExtensions() {
@@ -303,6 +292,7 @@ class DatabaseManager private constructor() : Disposable {
DataEntity.update({ DataEntity.id eq id }) {
it[DataEntity.deleted] = true
// 如果是本地用户,那么删除是不需要同步的,云端用户才需要同步
// 云端用户也会判断,如果来源 Sync 那么默认同步了
it[DataEntity.synced] = accountManager.isLocally()
it[DataEntity.data] = StringUtils.EMPTY
}
@@ -362,7 +352,6 @@ class DatabaseManager private constructor() : Disposable {
private inner class AccountDataTransferExtension : AccountExtension {
private val hostManager get() = HostManager.getInstance()
override fun onAccountChanged(oldAccount: Account, newAccount: Account) {
if (oldAccount.isLocally && newAccount.isLocally) {
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) {
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()) {
// 已经删除,则忽略
if (host.deleted) continue
// 不是用户数据,那么忽略
if (host.ownerType.isNotBlank() && host.ownerType != OwnerType.User.name) continue
// 不是本地用户数据,那么忽略
if (AccountManager.isLocally(host.ownerId).not()) continue
// 转移资产
val newHost = host.copy(
id = randomUUID(),
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)
// 先删除,因为 ID 没有改变,改变的只是 owner 信息
silentDelete(host.id)
hostManager.addHost(host.copy(ownerId = accountOwner.id, ownerType = accountOwner.type.name))
}
if (deleteIds.isNotEmpty()) {
lock.withLock {
transaction(database) {
DataEntity.deleteWhere { DataEntity.id.inList(deleteIds) }
}
}
for (snippet in snippetManager.snippets()) {
if (snippet.deleted) continue
silentDelete(snippet.id)
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 {
@@ -452,19 +465,17 @@ class DatabaseManager private constructor() : Disposable {
return
}
// 如果团队变更,那么删除所有旧的团队数据,静默删除
if (oldAccount.id == newAccount.id) {
return
}
for (team in oldAccount.teams) {
// 如果被踢出团队,那么移除该团队的所有资产
if (newAccount.teams.none { it.id == team.id }) {
lock.withLock {
transaction(database) {
DataEntity.deleteWhere {
DataEntity.ownerId.eq(team.id) and (DataEntity.ownerType.eq(
OwnerType.Team.name
))
if (oldAccount.teams != newAccount.teams) {
for (team in oldAccount.teams) {
lock.withLock {
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 fallbackFont by StringPropertyDelegate(StringUtils.EMPTY)
/**
* 默认终端
*/
@@ -693,6 +709,11 @@ class DatabaseManager private constructor() : Disposable {
*/
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)),
I18n.getString("termora.highlight.background-color")
)
val matchCaseBtn = JToggleButton(Icons.matchCase)
val regexBtn = JToggleButton(Icons.regex)
val matchCaseBtn = JToggleButton(Icons.matchCase).apply { toolTipText = I18n.getString("termora.match-case") }
val regexBtn = JToggleButton(Icons.regex).apply { toolTipText = I18n.getString("termora.regex") }
private val textColorRevert = JButton(Icons.revert)

View File

@@ -107,7 +107,7 @@ class KeymapManager private constructor() : Disposable {
id = keymap.id,
ownerId = accountId,
ownerType = OwnerType.User.name,
type = DataType.KeywordHighlight.name,
type = DataType.Keymap.name,
data = keymap.toJSON(),
)
)

View File

@@ -130,7 +130,7 @@ class KeymapPanel : JPanel(BorderLayout()) {
)
if (!text.isNullOrBlank()) {
if (text != keymap.name) {
keymapManager.removeKeymap(keymap.name)
keymapManager.removeKeymap(keymap.id)
val newKeymap = cloneKeymap(text, keymap)
keymapManager.addKeymap(newKeymap)
keymapModel.removeElementAt(index)
@@ -152,7 +152,7 @@ class KeymapPanel : JPanel(BorderLayout()) {
messageType = JOptionPane.WARNING_MESSAGE
) == JOptionPane.YES_OPTION
) {
keymapManager.removeKeymap(keymap.name)
keymapManager.removeKeymap(keymap.id)
keymapModel.removeElementAt(index)
}
}

View File

@@ -19,6 +19,7 @@ class KeyManagerDialog(
owner: Window,
private val selectMode: Boolean = false,
size: Dimension = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height")),
private val accountOwner: AccountOwner? = null,
) : DialogWrapper(owner) {
var ok: Boolean = false
@@ -56,12 +57,40 @@ class KeyManagerDialog(
tabbed.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
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()) {
for (team in accountManager.getTeams()) {

View File

@@ -2,6 +2,7 @@ package app.termora.keymgr
import app.termora.*
import app.termora.keyboardinteractive.TerminalUserInteraction
import app.termora.plugin.internal.ssh.SshClients
import app.termora.terminal.ControlCharacters
import app.termora.terminal.DataKey
import app.termora.terminal.PtyConnectorDelegate
@@ -40,7 +41,7 @@ class SSHCopyIdDialog(
}
}
private val terminalPanel by lazy {
terminalPanelFactory.createTerminalPanel(terminal, PtyConnectorDelegate())
terminalPanelFactory.createTerminalPanel(null, terminal, PtyConnectorDelegate())
.apply { enableFloatingToolbar = false }
}
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

View File

@@ -11,8 +11,10 @@ import app.termora.plugin.internal.rdp.RDPInternalPlugin
import app.termora.plugin.internal.serial.SerialInternalPlugin
import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin
import app.termora.plugin.internal.ssh.SSHInternalPlugin
import app.termora.plugin.internal.telnet.TelnetInternalPlugin
import app.termora.plugin.internal.wsl.WSLInternalPlugin
import app.termora.swingCoroutineScope
import app.termora.terminal.panel.vw.FloatingToolbarPlugin
import app.termora.transfer.internal.local.LocalPlugin
import app.termora.transfer.internal.sftp.SFTPPlugin
import com.formdev.flatlaf.util.SystemInfo
@@ -110,12 +112,14 @@ internal class PluginManager private constructor() {
// ssh plugin
plugins.add(PluginDescriptor(SSHInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// serial plugin
plugins.add(PluginDescriptor(SerialInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// local plugin
plugins.add(PluginDescriptor(LocalInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// rdp plugin
plugins.add(PluginDescriptor(RDPInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// telnet plugin
plugins.add(PluginDescriptor(TelnetInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// serial plugin
plugins.add(PluginDescriptor(SerialInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// wsl plugin
if (SystemUtils.IS_OS_WINDOWS) {
plugins.add(PluginDescriptor(WSLInternalPlugin(), origin = PluginOrigin.Internal, version = version))
@@ -127,6 +131,9 @@ internal class PluginManager private constructor() {
plugins.add(PluginDescriptor(LocalPlugin(), origin = PluginOrigin.Internal, version = version))
// sftp transfer plugin
plugins.add(PluginDescriptor(SFTPPlugin(), origin = PluginOrigin.Internal, version = version))
// floating
plugins.add(PluginDescriptor(FloatingToolbarPlugin(), origin = PluginOrigin.Internal, version = version))
}
private fun loadSystemPlugins() {

View File

@@ -10,7 +10,10 @@ import java.awt.Component
import java.awt.event.ItemEvent
import javax.swing.*
class BasicProxyOption(private val proxyTypes: List<ProxyType> = listOf(ProxyType.HTTP, ProxyType.SOCKS5)) :
class BasicProxyOption(
private val proxyTypes: List<ProxyType> = listOf(ProxyType.HTTP, ProxyType.SOCKS5),
private val authenticationTypes: List<AuthenticationType> = listOf(AuthenticationType.Password),
) :
JPanel(BorderLayout()), Option {
private val formMargin = "7dlu"
@@ -21,6 +24,10 @@ class BasicProxyOption(private val proxyTypes: List<ProxyType> = listOf(ProxyTyp
val proxyPortTextField = PortSpinner(1080)
val proxyAuthenticationTypeComboBox = FlatComboBox<AuthenticationType>()
constructor(proxyTypes: List<ProxyType> = listOf(ProxyType.HTTP, ProxyType.SOCKS5)) : this(
proxyTypes,
listOf(AuthenticationType.Password)
)
init {
initView()
@@ -67,7 +74,9 @@ class BasicProxyOption(private val proxyTypes: List<ProxyType> = listOf(ProxyTyp
}
proxyAuthenticationTypeComboBox.addItem(AuthenticationType.No)
proxyAuthenticationTypeComboBox.addItem(AuthenticationType.Password)
for (type in authenticationTypes) {
proxyAuthenticationTypeComboBox.addItem(type)
}
proxyUsernameTextField.text = "root"

View File

@@ -1,5 +1,6 @@
package app.termora.plugin.internal.local
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -14,7 +15,11 @@ internal class LocalProtocolHostPanelExtension private constructor() : ProtocolH
return LocalProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return LocalProtocolHostPanel()
}
override fun ordered(): Long {
return 1
}
}

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