Compare commits

...

18 Commits
1.0.2 ... 1.0.3

Author SHA1 Message Date
hstyi
0345848418 release: 1.0.3 2025-01-22 19:17:52 +08:00
hstyi
f1073fb53f fix: deadlock 2025-01-22 15:50:36 +08:00
hstyi
ce1924c422 docs: README 2025-01-22 15:47:36 +08:00
hstyi
d6de0922c6 feat: Device Status Report (DSR) 2025-01-22 15:32:10 +08:00
hstyi
d5157d3a16 feat: left right key 2025-01-22 15:01:40 +08:00
hstyi
63b27a2f83 feat: improved keyPair comboBox (#92) 2025-01-16 18:18:58 +08:00
hstyi
992015c8e5 feat: GitHub actions 2025-01-16 17:31:53 +08:00
hstyi
5d459f9b0d fix: FindEverywhereAction name (#89) 2025-01-16 16:09:23 +08:00
hstyi
88f20c4898 feat: SFTP supports pasting files for upload (#87) 2025-01-16 14:59:01 +08:00
hstyi
314c112d4b feat: Windows keyboard shortcut (#86) 2025-01-16 12:38:43 +08:00
hstyi
0cd818e9a0 feat: support fast reconnect 2025-01-15 23:02:05 +08:00
hstyi
0884486e91 feat: theme sync with OS (#82) 2025-01-15 22:24:19 +08:00
hstyi
e30316eab3 feat: support keymap sync 2025-01-15 20:05:26 +08:00
hstyi
d321e766b1 docs: README 2025-01-15 17:22:02 +08:00
hstyi
6aaed92f2c feat: SFTP 支持不显示隐藏文件 2025-01-15 16:52:58 +08:00
hstyi
21cf22906b fix: 修复可能导致内存泄漏的问题 2025-01-15 15:14:30 +08:00
hstyi
1476368673 feat: support jump hosts 2025-01-15 15:14:30 +08:00
hstyi
45ea822fd6 feat: 改进事件系统与全局快捷键 (#62) 2025-01-15 14:54:39 +08:00
171 changed files with 4441 additions and 1236 deletions

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

@@ -0,0 +1,33 @@
name: Linux x86-64
on: [ push, pull_request ]
jobs:
build:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# download jdk
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.5-linux-x64-b509.30.tar.gz
# install jdk
- name: Installing Java
uses: actions/setup-java@v4
with:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.5'
architecture: x64
# dist
- run: |
./gradlew dist --no-daemon
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-linux-x86-64
path: build/distributions/*.tar.gz

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

@@ -0,0 +1,35 @@
name: macOS aarch64
on: [ push, pull_request ]
jobs:
build:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# download jdk
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.5-osx-aarch64-b509.30.tar.gz
# install jdk
- name: Installing Java
uses: actions/setup-java@v4
with:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.5'
architecture: aarch64
# dist
- run: |
./gradlew dist --no-daemon
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-osx-aarch64
path: build/distributions/*.dmg

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

@@ -0,0 +1,34 @@
name: macOS x86-64
on: [ push, pull_request ]
jobs:
build:
runs-on: macos-13
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# download jdk
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.5-osx-x64-b509.30.tar.gz
# install jdk
- name: Installing Java
uses: actions/setup-java@v4
with:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.5'
architecture: x64
# dist
- run: |
./gradlew dist --no-daemon
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-osx-x86-64
path: build/distributions/*.dmg

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

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

View File

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

43
README.zh_CN.md Normal file
View File

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

View File

@@ -14,7 +14,7 @@ plugins {
group = "app.termora"
version = "1.0.2"
version = "1.0.3"
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
val arch: Architecture = DefaultNativePlatform.getCurrentArchitecture()
@@ -42,14 +42,14 @@ dependencies {
testImplementation(kotlin("test"))
testImplementation(libs.hutool)
testImplementation(libs.sshj)
testImplementation(platform(libs.koin.bom))
testImplementation(libs.koin.core)
testImplementation(libs.jsch)
testImplementation(libs.rhino)
testImplementation(libs.delight.rhino.sandbox)
testImplementation(platform(libs.testcontainers.bom))
testImplementation(libs.testcontainers)
// implementation(platform(libs.koin.bom))
// implementation(libs.koin.core)
implementation(libs.slf4j.api)
implementation(libs.pty4j)
implementation(libs.slf4j.tinylog)
@@ -109,6 +109,12 @@ dependencies {
application {
val args = mutableListOf(
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
"-Xmx2g",
"-XX:+UseZGC",
"-XX:+ZUncommit",
"-XX:+ZGenerational",
"-XX:ZUncommitDelay=60",
"-XX:SoftMaxHeapSize=64m"
)
if (os.isMacOsX) {
@@ -215,6 +221,11 @@ tasks.register<Exec>("jpackage") {
val options = mutableListOf(
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
"-Xmx2g",
"-XX:+UseZGC",
"-XX:+ZUncommit",
"-XX:+ZGenerational",
"-XX:ZUncommitDelay=60",
"-XX:SoftMaxHeapSize=64m",
"-XX:+HeapDumpOnOutOfMemoryError",
"-Dlogger.console.level=off",
"-Dkotlinx.coroutines.debug=off",

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 61 KiB

BIN
docs/readme-zh_CN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 96 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package app.termora
import app.termora.db.Database
import app.termora.actions.ActionManager
import app.termora.keymap.KeymapManager
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatSystemProperties
import com.formdev.flatlaf.extras.FlatInspector
@@ -28,8 +29,8 @@ import java.io.RandomAccessFile
import java.nio.channels.FileLock
import java.util.*
import javax.swing.*
import javax.swing.WindowConstants.DISPOSE_ON_CLOSE
import kotlin.system.exitProcess
import kotlin.system.measureTimeMillis
class ApplicationRunner {
private lateinit var singletonLock: FileLock
@@ -41,39 +42,62 @@ class ApplicationRunner {
}
fun run() {
// 覆盖 tinylog 配置
setupTinylog()
measureTimeMillis {
// 覆盖 tinylog 配置
val setupTinylog = measureTimeMillis { setupTinylog() }
// 是否单例
checkSingleton()
// 是否单例
val checkSingleton = measureTimeMillis { checkSingleton() }
// 打印系统信息
printSystemInfo()
// 打印系统信息
val printSystemInfo = measureTimeMillis { printSystemInfo() }
SwingUtilities.invokeAndWait {
// 打开数据库
openDatabase()
val openDatabase = measureTimeMillis { openDatabase() }
// 加载设置
loadSettings()
val loadSettings = measureTimeMillis { loadSettings() }
// 统计
enableAnalytics()
val enableAnalytics = measureTimeMillis { enableAnalytics() }
// init ActionManager、KeymapManager
@Suppress("OPT_IN_USAGE")
GlobalScope.launch(Dispatchers.IO) {
ActionManager.getInstance()
KeymapManager.getInstance()
}
// 设置 LAF
setupLaf()
val setupLaf = measureTimeMillis { setupLaf() }
// 解密数据
openDoor()
val openDoor = measureTimeMillis { openDoor() }
// 启动主窗口
startMainFrame()
val startMainFrame = measureTimeMillis { startMainFrame() }
if (log.isDebugEnabled) {
log.debug("setupTinylog: {}ms", setupTinylog)
log.debug("checkSingleton: {}ms", checkSingleton)
log.debug("printSystemInfo: {}ms", printSystemInfo)
log.debug("openDatabase: {}ms", openDatabase)
log.debug("loadSettings: {}ms", loadSettings)
log.debug("enableAnalytics: {}ms", enableAnalytics)
log.debug("setupLaf: {}ms", setupLaf)
log.debug("openDoor: {}ms", openDoor)
log.debug("startMainFrame: {}ms", startMainFrame)
}
}.let {
if (log.isDebugEnabled) {
log.debug("run: {}ms", it)
}
}
}
private fun openDoor() {
if (Doorman.instance.isWorking()) {
if (Doorman.getInstance().isWorking()) {
if (!DoormanDialog(null).open()) {
exitProcess(1)
}
@@ -81,17 +105,11 @@ class ApplicationRunner {
}
private fun startMainFrame() {
val frame = TermoraFrame()
frame.title = if (SystemInfo.isLinux) null else Application.getName()
frame.defaultCloseOperation = DISPOSE_ON_CLOSE
frame.setSize(1280, 800)
frame.setLocationRelativeTo(null)
frame.isVisible = true
TermoraFrameManager.getInstance().createWindow().isVisible = true
}
private fun loadSettings() {
val language = Database.instance.appearance.language
val language = Database.getDatabase().appearance.language
val locale = runCatching { LocaleUtils.toLocale(language) }.getOrElse { Locale.getDefault() }
if (log.isInfoEnabled) {
log.info("Language: {} , Locale: {}", language, locale)
@@ -110,22 +128,22 @@ class ApplicationRunner {
JDialog.setDefaultLookAndFeelDecorated(true)
}
val themeManager = ThemeManager.instance
val settings = Database.instance
var theme = settings.appearance.theme
// 如果是跟随系统或者不存在样式,那么使用默认的
if (settings.appearance.followSystem || !themeManager.themes.containsKey(theme)) {
val themeManager = ThemeManager.getInstance()
val appearance = Database.getDatabase().appearance
var theme = appearance.theme
// 如果是跟随系统
if (appearance.followSystem) {
theme = if (OsThemeDetector.getDetector().isDark) {
"Dark"
appearance.darkTheme
} else {
"Light"
appearance.lightTheme
}
}
themeManager.change(theme, true)
FlatInspector.install("ctrl shift alt X");
if (Application.isUnknownVersion())
FlatInspector.install("ctrl shift alt X");
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false)
@@ -173,21 +191,21 @@ class ApplicationRunner {
}
private fun printSystemInfo() {
if (log.isInfoEnabled) {
log.info("Welcome to ${Application.getName()} ${Application.getVersion()}!")
log.info(
if (log.isDebugEnabled) {
log.debug("Welcome to ${Application.getName()} ${Application.getVersion()}!")
log.debug(
"JVM name: {} , vendor: {} , version: {}",
SystemUtils.JAVA_VM_NAME,
SystemUtils.JAVA_VM_VENDOR,
SystemUtils.JAVA_VM_VERSION,
)
log.info(
log.debug(
"OS name: {} , version: {} , arch: {}",
SystemUtils.OS_NAME,
SystemUtils.OS_VERSION,
SystemUtils.OS_ARCH
)
log.info("Base config dir: ${Application.getBaseDataDir().absolutePath}")
log.debug("Base config dir: ${Application.getBaseDataDir().absolutePath}")
}
}
@@ -245,9 +263,8 @@ class ApplicationRunner {
private fun openDatabase() {
val dir = Application.getDatabaseFile()
try {
Database.open(dir)
Database.getDatabase()
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
@@ -296,10 +313,10 @@ class ApplicationRunner {
}
private fun getAnalyticsUserID(): String {
var id = Database.instance.properties.getString("AnalyticsUserID")
var id = Database.getDatabase().properties.getString("AnalyticsUserID")
if (id.isNullOrBlank()) {
id = UUID.randomUUID().toSimpleString()
Database.instance.properties.putString("AnalyticsUserID", id)
Database.getDatabase().properties.putString("AnalyticsUserID", id)
}
return id
}

View File

@@ -1,7 +1,7 @@
package app.termora
import app.termora.Application.ohMyJson
import app.termora.db.Database
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import kotlinx.serialization.encodeToString
@@ -358,7 +358,8 @@ class CustomizeToolBarDialog(
actions.add(ToolBarAction(leftList.model.getElementAt(i).id, false))
}
Database.instance.properties.putString("Termora.ToolBar.Actions", ohMyJson.encodeToString(actions))
Database.getDatabase()
.properties.putString("Termora.ToolBar.Actions", ohMyJson.encodeToString(actions))
super.doOKAction()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,13 @@
package app.termora
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import org.apache.commons.lang3.exception.ExceptionUtils
import org.apache.sshd.client.SshClient
import org.apache.sshd.client.session.ClientSession
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.Window
import java.awt.event.ActionEvent
import javax.swing.*
class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
@@ -40,7 +41,7 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
private fun createTestConnectionAction(): AbstractAction {
return object : AnAction(I18n.getString("termora.new-host.test-connection")) {
override fun actionPerformed(e: ActionEvent) {
override fun actionPerformed(evt: AnActionEvent) {
if (!pane.validateFields()) {
return
}

View File

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

View File

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

View File

@@ -9,8 +9,9 @@ import java.beans.PropertyChangeEvent
import javax.swing.Icon
abstract class HostTerminalTab(
val windowScope: WindowScope,
val host: Host,
protected val terminal: Terminal = TerminalFactory.instance.createTerminal()
protected val terminal: Terminal = TerminalFactory.getInstance(windowScope).createTerminal()
) : PropertyTerminalTab() {
companion object {
val Host = DataKey(app.termora.Host::class)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,12 +8,17 @@ object Icons {
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
val close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") }
val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
val eye by lazy { DynamicIcon("icons/eye.svg", "icons/eye_dark.svg") }
val eyeClose by lazy { DynamicIcon("icons/eyeClose.svg", "icons/eyeClose_dark.svg") }
val matchCase by lazy { DynamicIcon("icons/matchCase.svg", "icons/matchCase_dark.svg") }
val regex by lazy { DynamicIcon("icons/regex.svg", "icons/regex_dark.svg") }
val vcs by lazy { DynamicIcon("icons/vcs.svg", "icons/vcs_dark.svg") }
val dumpThreads by lazy { DynamicIcon("icons/dumpThreads.svg", "icons/dumpThreads_dark.svg") }
val supertypes by lazy { DynamicIcon("icons/supertypes.svg", "icons/supertypes_dark.svg") }
val fitContent by lazy { DynamicIcon("icons/fitContent.svg", "icons/fitContent_dark.svg") }
val settings by lazy { DynamicIcon("icons/settings.svg", "icons/settings_dark.svg") }
val copy by lazy { DynamicIcon("icons/copy.svg", "icons/copy_dark.svg") }
val delete by lazy { DynamicIcon("icons/delete.svg", "icons/delete_dark.svg") }
val pin by lazy { DynamicIcon("icons/pin.svg", "icons/pin_dark.svg") }
val empty by lazy { DynamicIcon("icons/empty.svg") }
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") }

View File

@@ -8,6 +8,11 @@ import com.formdev.flatlaf.FlatPropertiesLaf
import com.formdev.flatlaf.util.SystemInfo
import java.util.*
interface LafTag
interface LightLafTag : LafTag
interface DarkLafTag : LafTag
class DraculaLaf : FlatPropertiesLaf("Dracula", Properties().apply {
putAll(
mapOf(
@@ -16,7 +21,7 @@ class DraculaLaf : FlatPropertiesLaf("Dracula", Properties().apply {
"@windowText" to "#eaeaea",
)
)
}), ColorTheme {
}), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Basic.BACKGROUND -> 0x282935
@@ -54,7 +59,8 @@ class DraculaLaf : FlatPropertiesLaf("Dracula", Properties().apply {
}
class LightLaf : FlatLightLaf(), ColorTheme {
class LightLaf : FlatLightLaf(), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0
@@ -81,7 +87,7 @@ class LightLaf : FlatLightLaf(), ColorTheme {
}
class DarkLaf : FlatDarkLaf(), ColorTheme {
class DarkLaf : FlatDarkLaf(), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0
@@ -110,7 +116,7 @@ class DarkLaf : FlatDarkLaf(), ColorTheme {
}
}
class iTerm2DarkLaf : FlatDarkLaf(), ColorTheme {
class iTerm2DarkLaf : FlatDarkLaf(), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
@@ -158,7 +164,7 @@ class TermiusLightLaf : FlatPropertiesLaf("Termius Light", Properties().apply {
"@windowText" to "#32364a",
)
)
}), ColorTheme {
}), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int {
@@ -201,7 +207,7 @@ class TermiusDarkLaf : FlatPropertiesLaf("Termius Dark", Properties().apply {
"@windowText" to "#21b568",
)
)
}), ColorTheme {
}), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
@@ -243,7 +249,7 @@ class NovelLaf : FlatPropertiesLaf("Novel", Properties().apply {
"@windowText" to "#3b2322",
)
)
}), ColorTheme {
}), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x000000
@@ -282,7 +288,7 @@ class AtomOneDarkLaf : FlatPropertiesLaf("Atom One Dark", Properties().apply {
"@windowText" to "#abb2bf",
)
)
}), ColorTheme {
}), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x000000
@@ -320,7 +326,7 @@ class AtomOneLightLaf : FlatPropertiesLaf("Atom One Light", Properties().apply {
"@windowText" to "#383a42",
)
)
}), ColorTheme {
}), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x000000
@@ -358,7 +364,7 @@ class EverforestDarkLaf : FlatPropertiesLaf("Everforest Dark", Properties().appl
"@windowText" to "#d3c6aa",
)
)
}), ColorTheme {
}), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x42494e
@@ -395,7 +401,7 @@ class EverforestLightLaf : FlatPropertiesLaf("Everforest Light", Properties().ap
"@windowText" to "#5c6a72",
)
)
}), ColorTheme {
}), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x42494e
@@ -432,7 +438,7 @@ class NightOwlLaf : FlatPropertiesLaf("Night Owl", Properties().apply {
"@windowText" to "#d6deeb",
)
)
}), ColorTheme {
}), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x072945
@@ -469,7 +475,7 @@ class LightOwlLaf : FlatPropertiesLaf("Light Owl", Properties().apply {
"@windowText" to "#403f53",
)
)
}), ColorTheme {
}), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x403f53
@@ -506,7 +512,7 @@ class AuraLaf : FlatPropertiesLaf("Aura", Properties().apply {
"@windowText" to "#edecee",
)
)
}), ColorTheme {
}), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x1c1b22
@@ -543,7 +549,7 @@ class Cobalt2Laf : FlatPropertiesLaf("Cobalt2", Properties().apply {
"@windowText" to "#ffffff",
)
)
}), ColorTheme {
}), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x000000
@@ -580,7 +586,7 @@ class OctocatDarkLaf : FlatPropertiesLaf("Octocat Dark", Properties().apply {
"@windowText" to "#8b949e",
)
)
}), ColorTheme {
}), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x000000
@@ -617,7 +623,7 @@ class OctocatLightLaf : FlatPropertiesLaf("Octocat Light", Properties().apply {
"@windowText" to "#3e3e3e",
)
)
}), ColorTheme {
}), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x000000
@@ -654,7 +660,7 @@ class AyuDarkLaf : FlatPropertiesLaf("Ayu Dark", Properties().apply {
"@windowText" to "#e6e1cf",
)
)
}), ColorTheme {
}), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x000000
@@ -691,7 +697,7 @@ class AyuLightLaf : FlatPropertiesLaf("Ayu Light", Properties().apply {
"@windowText" to "#5c6773",
)
)
}), ColorTheme {
}), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x000000
@@ -728,7 +734,7 @@ class HomebrewLaf : FlatPropertiesLaf("Homebrew", Properties().apply {
"@windowText" to "#00ff00",
)
)
}), ColorTheme {
}), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x2e2e2e
@@ -767,7 +773,7 @@ class ProLaf : FlatPropertiesLaf("Pro", Properties().apply {
"@windowText" to "#f2f2f2",
)
)
}), ColorTheme {
}), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x2e2e2e
@@ -806,7 +812,7 @@ class NordLightLaf : FlatPropertiesLaf("Nord Light", Properties().apply {
"@windowText" to "#414858",
)
)
}), ColorTheme {
}), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x2c3344
@@ -845,7 +851,7 @@ class NordDarkLaf : FlatPropertiesLaf("Nord Dark", Properties().apply {
"@windowText" to "#d8dee9",
)
)
}), ColorTheme {
}), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x3b4252
@@ -885,7 +891,7 @@ class GitHubLightLaf : FlatPropertiesLaf("GitHub Light", Properties().apply {
"@windowText" to "#3e3e3e",
)
)
}), ColorTheme {
}), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x3e3e3e
@@ -924,7 +930,7 @@ class GitHubDarkLaf : FlatPropertiesLaf("GitHub Dark", Properties().apply {
"@windowText" to "#8b949e",
)
)
}), ColorTheme {
}), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x000000
@@ -964,7 +970,7 @@ class ChalkLaf : FlatPropertiesLaf("Chalk", Properties().apply {
"@windowText" to "#d2d8d9",
)
)
}), ColorTheme {
}), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int {
return when (color) {
TerminalColor.Normal.BLACK -> 0x7d8b8f

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ class MyTabbedPane : FlatTabbedPane() {
override fun setSelectedIndex(index: Int) {
val oldIndex = selectedIndex
super.setSelectedIndex(index)
firePropertyChange("selectedIndex", oldIndex,index)
firePropertyChange("selectedIndex", oldIndex, index)
}
}

View File

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

View File

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

View File

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

View File

@@ -10,9 +10,10 @@ import javax.swing.JComponent
import kotlin.time.Duration.Companion.milliseconds
abstract class PtyHostTerminalTab(
windowScope: WindowScope,
host: Host,
terminal: Terminal = TerminalFactory.instance.createTerminal()
) : HostTerminalTab(host, terminal) {
terminal: Terminal = TerminalFactory.getInstance(windowScope).createTerminal()
) : HostTerminalTab(windowScope, host, terminal) {
companion object {
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
@@ -22,8 +23,13 @@ abstract class PtyHostTerminalTab(
private var readerJob: Job? = null
private val ptyConnectorDelegate = PtyConnectorDelegate()
protected val terminalPanel = TerminalPanelFactory.instance.createTerminalPanel(terminal, ptyConnectorDelegate)
protected val ptyConnectorFactory get() = PtyConnectorFactory.instance
protected val terminalPanel =
TerminalPanelFactory.getInstance(windowScope).createTerminalPanel(terminal, ptyConnectorDelegate)
protected val ptyConnectorFactory get() = PtyConnectorFactory.getInstance(windowScope)
init {
terminal.getTerminalModel().setData(DataKey.PtyConnector, ptyConnectorDelegate)
}
override fun start() {
coroutineScope.launch(Dispatchers.IO) {

View File

@@ -1,5 +1,6 @@
package app.termora
import app.termora.transport.TransportDataProviders
import app.termora.transport.TransportPanel
import java.beans.PropertyChangeListener
import javax.swing.Icon
@@ -40,8 +41,8 @@ class SFTPTerminalTab : Disposable, TerminalTab {
override fun canClose(): Boolean {
assertEventDispatchThread()
if (transportPanel.transportManager.getTransports().isEmpty()) {
val transportManager = transportPanel.getData(TransportDataProviders.TransportManager) ?: return true
if (transportManager.getTransports().isEmpty()) {
return true
}

View File

@@ -1,7 +1,10 @@
package app.termora
import app.termora.actions.TabReconnectAction
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
import app.termora.keyboardinteractive.TerminalUserInteraction
import app.termora.keymap.KeyShortcut
import app.termora.keymap.KeymapManager
import app.termora.terminal.ControlCharacters
import app.termora.terminal.DataKey
import app.termora.terminal.PtyConnector
@@ -28,7 +31,7 @@ import javax.swing.JComponent
import javax.swing.SwingUtilities
class SSHTerminalTab(host: Host) : PtyHostTerminalTab(host) {
class SSHTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
companion object {
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
}
@@ -109,10 +112,18 @@ class SSHTerminalTab(host: Host) : PtyHostTerminalTab(host) {
channel.addChannelListener(object : ChannelListener {
private val reconnectShortcut
get() = KeymapManager.getInstance().getActiveKeymap()
.getShortcut(TabReconnectAction.RECONNECT_TAB).firstOrNull()
override fun channelClosed(channel: Channel, reason: Throwable?) {
coroutineScope.launch(Dispatchers.Swing) {
terminal.write("\r\n${ControlCharacters.ESC}[31m")
terminal.write("Channel has been disconnected.\r\n")
terminal.write("\r\n\r\n${ControlCharacters.ESC}[31m")
terminal.write("Channel has been disconnected.")
if (reconnectShortcut is KeyShortcut) {
terminal.write(" Type $reconnectShortcut to reconnect.")
}
terminal.write("\r\n")
terminal.write("${ControlCharacters.ESC}[0m")
terminalModel.setData(DataKey.ShowCursor, false)
}

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,10 @@ package app.termora
import app.termora.AES.encodeBase64String
import app.termora.Application.ohMyJson
import app.termora.db.Database
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.highlight.KeywordHighlightManager
import app.termora.keymap.KeymapPanel
import app.termora.keymgr.KeyManager
import app.termora.macro.MacroManager
import app.termora.native.FileChooser
@@ -15,11 +17,8 @@ import app.termora.terminal.CursorStyle
import app.termora.terminal.DataKey
import app.termora.terminal.panel.TerminalPanel
import cash.z.ecc.android.bip39.Mnemonics
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.FlatSVGIcon
import com.formdev.flatlaf.extras.components.FlatButton
import com.formdev.flatlaf.extras.components.FlatComboBox
import com.formdev.flatlaf.extras.components.FlatLabel
import com.formdev.flatlaf.extras.components.*
import com.formdev.flatlaf.util.SystemInfo
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
@@ -40,7 +39,6 @@ import org.slf4j.LoggerFactory
import java.awt.BorderLayout
import java.awt.Component
import java.awt.datatransfer.StringSelection
import java.awt.event.ActionEvent
import java.awt.event.ItemEvent
import java.io.File
import java.net.URI
@@ -48,12 +46,14 @@ import java.nio.charset.StandardCharsets
import java.util.*
import javax.swing.*
import javax.swing.event.DocumentEvent
import javax.swing.event.PopupMenuEvent
import javax.swing.event.PopupMenuListener
import kotlin.time.Duration.Companion.milliseconds
class SettingsOptionsPane : OptionsPane() {
private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane)
private val database get() = Database.instance
private val database get() = Database.getDatabase()
companion object {
private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java)
@@ -96,6 +96,7 @@ class SettingsOptionsPane : OptionsPane() {
init {
addOption(AppearanceOption())
addOption(TerminalOption())
addOption(KeyShortcutsOption())
addOption(CloudSyncOption())
addOption(DoormanOption())
addOption(AboutOption())
@@ -103,10 +104,11 @@ class SettingsOptionsPane : OptionsPane() {
}
private inner class AppearanceOption : JPanel(BorderLayout()), Option {
val themeManager = ThemeManager.instance
val themeManager = ThemeManager.getInstance()
val themeComboBox = FlatComboBox<String>()
val languageComboBox = FlatComboBox<String>()
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
val preferredThemeBtn = JButton(Icons.settings)
private val appearance get() = database.appearance
init {
@@ -117,6 +119,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun initView() {
followSystemCheckBox.isSelected = appearance.followSystem
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
themeManager.themes.keys.forEach { themeComboBox.addItem(it) }
@@ -156,19 +159,17 @@ class SettingsOptionsPane : OptionsPane() {
followSystemCheckBox.addActionListener {
appearance.followSystem = followSystemCheckBox.isSelected
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
appearance.theme = themeComboBox.selectedItem as String
if (followSystemCheckBox.isSelected) {
SwingUtilities.invokeLater {
if (OsThemeDetector.getDetector().isDark) {
if (!FlatLaf.isLafDark()) {
themeManager.change("Dark")
themeComboBox.selectedItem = "Dark"
}
themeManager.change(appearance.darkTheme)
themeComboBox.selectedItem = appearance.darkTheme
} else {
if (FlatLaf.isLafDark()) {
themeManager.change("Light")
themeComboBox.selectedItem = "Light"
}
themeManager.change(appearance.lightTheme)
themeComboBox.selectedItem = appearance.lightTheme
}
}
}
@@ -187,6 +188,8 @@ class SettingsOptionsPane : OptionsPane() {
}
}
}
preferredThemeBtn.addActionListener { showPreferredThemeContextmenu() }
}
override fun getIcon(isSelected: Boolean): Icon {
@@ -201,23 +204,78 @@ class SettingsOptionsPane : OptionsPane() {
return this
}
private fun showPreferredThemeContextmenu() {
val popupMenu = FlatPopupMenu()
val dark = JMenu("For Dark OS")
val light = JMenu("For Light OS")
val darkTheme = appearance.darkTheme
val lightTheme = appearance.lightTheme
for (e in themeManager.themes) {
val clazz = Class.forName(e.value)
val item = JCheckBoxMenuItem(e.key)
item.isSelected = e.key == lightTheme || e.key == darkTheme
if (clazz.interfaces.contains(DarkLafTag::class.java)) {
dark.add(item).addActionListener {
if (e.key != darkTheme) {
appearance.darkTheme = e.key
if (OsThemeDetector.getDetector().isDark) {
themeComboBox.selectedItem = e.key
}
}
}
} else if (clazz.interfaces.contains(LightLafTag::class.java)) {
light.add(item).addActionListener {
if (e.key != lightTheme) {
appearance.lightTheme = e.key
if (!OsThemeDetector.getDetector().isDark) {
themeComboBox.selectedItem = e.key
}
}
}
}
}
popupMenu.add(dark)
popupMenu.addSeparator()
popupMenu.add(light)
popupMenu.addPopupMenuListener(object : PopupMenuListener {
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
}
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {
}
override fun popupMenuCanceled(e: PopupMenuEvent) {
}
})
popupMenu.show(preferredThemeBtn, 0, preferredThemeBtn.height + 2)
}
private fun getFormPanel(): JPanel {
val layout = FormLayout(
"left:pref, $formMargin, default:grow, $formMargin, default, default:grow",
"pref, $formMargin, pref, $formMargin"
)
val box = FlatToolBar()
box.add(followSystemCheckBox)
box.add(Box.createHorizontalStrut(2))
box.add(preferredThemeBtn)
var rows = 1
val step = 2
return FormBuilder.create().layout(layout)
.add("${I18n.getString("termora.settings.appearance.theme")}:").xy(1, rows)
.add(themeComboBox).xy(3, rows)
.add(followSystemCheckBox).xy(5, rows).apply { rows += step }
.add(box).xy(5, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.appearance.language")}:").xy(1, rows)
.add(languageComboBox).xy(3, rows)
.add(Hyperlink(object : AnAction(I18n.getString("termora.settings.appearance.i-want-to-translate")) {
override fun actionPerformed(evt: ActionEvent) {
override fun actionPerformed(evt: AnActionEvent) {
Application.browse(URI.create("https://github.com/TermoraDev/termora/tree/main/src/main/resources/i18n"))
}
})).xy(5, rows).apply { rows += step }
@@ -234,7 +292,7 @@ class SettingsOptionsPane : OptionsPane() {
private val shellComboBox = FlatComboBox<String>()
private val maxRowsTextField = IntSpinner(0, 0)
private val fontSizeTextField = IntSpinner(0, 9, 99)
private val terminalSetting get() = Database.instance.terminal
private val terminalSetting get() = Database.getDatabase().terminal
private val selectCopyComboBox = YesOrNoComboBox()
init {
@@ -270,7 +328,7 @@ class SettingsOptionsPane : OptionsPane() {
if (it.stateChange == ItemEvent.SELECTED) {
val style = cursorStyleComboBox.selectedItem as CursorStyle
terminalSetting.cursor = style
TerminalFactory.instance.getTerminals().forEach { e ->
TerminalFactory.getInstance(ApplicationScope.forWindowScope(owner)).getTerminals().forEach { e ->
e.getTerminalModel().setData(DataKey.CursorStyle, style)
}
}
@@ -280,7 +338,7 @@ class SettingsOptionsPane : OptionsPane() {
debugComboBox.addItemListener { e ->
if (e.stateChange == ItemEvent.SELECTED) {
terminalSetting.debug = debugComboBox.selectedItem as Boolean
TerminalFactory.instance.getTerminals().forEach {
TerminalFactory.getInstance(ApplicationScope.forWindowScope(owner)).getTerminals().forEach {
it.getTerminalModel().setData(TerminalPanel.Debug, terminalSetting.debug)
}
}
@@ -296,7 +354,10 @@ class SettingsOptionsPane : OptionsPane() {
}
private fun fireFontChanged() {
TerminalPanelFactory.instance.fireResize()
ApplicationScope.windowScopes().forEach {
TerminalPanelFactory.getInstance(it)
.fireResize()
}
}
private fun initView() {
@@ -397,6 +458,7 @@ class SettingsOptionsPane : OptionsPane() {
val keysCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keys"))
val keywordHighlightsCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keyword-highlights"))
val macrosCheckBox = JCheckBox(I18n.getString("termora.macro"))
val keymapCheckBox = JCheckBox(I18n.getString("termora.settings.keymap"))
val visitGistBtn = JButton(Icons.externalLink)
val getTokenBtn = JButton(Icons.externalLink)
@@ -489,7 +551,11 @@ class SettingsOptionsPane : OptionsPane() {
getTokenBtn.addActionListener {
when (typeComboBox.selectedItem) {
SyncType.GitLab -> Application.browse(URI.create("https://gitlab.com/-/user_settings/personal_access_tokens"))
SyncType.GitLab -> {
val uri = URI.create(domainTextField.text)
Application.browse(URI.create("${uri.scheme}://${uri.host}/-/user_settings/personal_access_tokens?name=Termora%20Sync%20Config&scopes=api"))
}
SyncType.GitHub -> Application.browse(URI.create("https://github.com/settings/tokens"))
SyncType.Gitee -> Application.browse(URI.create("https://gitee.com/profile/personal_access_tokens"))
}
@@ -537,21 +603,21 @@ class SettingsOptionsPane : OptionsPane() {
put("os", SystemUtils.OS_NAME)
put("exportDateHuman", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(Date(now)))
if (syncConfig.ranges.contains(SyncRange.Hosts)) {
put("hosts", ohMyJson.encodeToJsonElement(HostManager.instance.hosts()))
put("hosts", ohMyJson.encodeToJsonElement(HostManager.getInstance().hosts()))
}
if (syncConfig.ranges.contains(SyncRange.KeyPairs)) {
put("keyPairs", ohMyJson.encodeToJsonElement(KeyManager.instance.getOhKeyPairs()))
put("keyPairs", ohMyJson.encodeToJsonElement(KeyManager.getInstance().getOhKeyPairs()))
}
if (syncConfig.ranges.contains(SyncRange.KeywordHighlights)) {
put(
"keywordHighlights",
ohMyJson.encodeToJsonElement(KeywordHighlightManager.instance.getKeywordHighlights())
ohMyJson.encodeToJsonElement(KeywordHighlightManager.getInstance().getKeywordHighlights())
)
}
if (syncConfig.ranges.contains(SyncRange.Macros)) {
put(
"macros",
ohMyJson.encodeToJsonElement(MacroManager.instance.getMacros())
ohMyJson.encodeToJsonElement(MacroManager.getInstance().getMacros())
)
}
put("settings", buildJsonObject {
@@ -584,6 +650,9 @@ class SettingsOptionsPane : OptionsPane() {
if (macrosCheckBox.isSelected) {
range.add(SyncRange.Macros)
}
if (keymapCheckBox.isSelected) {
range.add(SyncRange.Keymap)
}
return SyncConfig(
type = typeComboBox.selectedItem as SyncType,
token = String(tokenTextField.password),
@@ -655,6 +724,7 @@ class SettingsOptionsPane : OptionsPane() {
tokenTextField.isEnabled = false
keysCheckBox.isEnabled = false
macrosCheckBox.isEnabled = false
keymapCheckBox.isEnabled = false
keywordHighlightsCheckBox.isEnabled = false
hostsCheckBox.isEnabled = false
domainTextField.isEnabled = false
@@ -670,7 +740,7 @@ class SettingsOptionsPane : OptionsPane() {
// sync
val syncResult = kotlin.runCatching {
val syncer = SyncerProvider.instance.getSyncer(syncConfig.type)
val syncer = SyncerProvider.getInstance().getSyncer(syncConfig.type)
if (push) {
syncer.push(syncConfig)
} else {
@@ -687,6 +757,7 @@ class SettingsOptionsPane : OptionsPane() {
hostsCheckBox.isEnabled = true
typeComboBox.isEnabled = true
macrosCheckBox.isEnabled = true
keymapCheckBox.isEnabled = true
gistTextField.isEnabled = true
tokenTextField.isEnabled = true
domainTextField.isEnabled = true
@@ -747,11 +818,13 @@ class SettingsOptionsPane : OptionsPane() {
keysCheckBox.isFocusable = false
keywordHighlightsCheckBox.isFocusable = false
macrosCheckBox.isFocusable = false
keymapCheckBox.isFocusable = false
hostsCheckBox.isSelected = sync.rangeHosts
keysCheckBox.isSelected = sync.rangeKeyPairs
keywordHighlightsCheckBox.isSelected = sync.rangeKeywordHighlights
macrosCheckBox.isSelected = sync.rangeMacros
keymapCheckBox.isSelected = sync.rangeKeymap
typeComboBox.selectedItem = sync.type
gistTextField.text = sync.gist
@@ -814,6 +887,7 @@ class SettingsOptionsPane : OptionsPane() {
.add(keysCheckBox).xy(3, 1)
.add(keywordHighlightsCheckBox).xy(5, 1)
.add(macrosCheckBox).xy(1, 3)
.add(keymapCheckBox).xy(3, 3)
.build()
var rows = 1
@@ -905,10 +979,10 @@ class SettingsOptionsPane : OptionsPane() {
private fun createHyperlink(url: String, text: String = url): Hyperlink {
return Hyperlink(object : AnAction(text) {
override fun actionPerformed(evt: ActionEvent) {
override fun actionPerformed(evt: AnActionEvent) {
Application.browse(URI.create(url))
}
});
})
}
private fun initEvents() {}
@@ -934,9 +1008,9 @@ class SettingsOptionsPane : OptionsPane() {
private val twoPasswordTextField = OutlinePasswordField(255)
private val tip = FlatLabel()
private val safeBtn = FlatButton()
private val doorman get() = Doorman.instance
private val hostManager get() = HostManager.instance
private val keyManager get() = KeyManager.instance
private val doorman get() = Doorman.getInstance()
private val hostManager get() = HostManager.getInstance()
private val keyManager get() = KeyManager.getInstance()
init {
initView()
@@ -1159,5 +1233,35 @@ class SettingsOptionsPane : OptionsPane() {
}
private inner class KeyShortcutsOption : JPanel(BorderLayout()), Option {
private val keymapPanel = KeymapPanel()
init {
initView()
initEvents()
}
private fun initView() {
add(keymapPanel, BorderLayout.CENTER)
}
private fun initEvents() {}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.fitContent
}
override fun getTitle(): String {
return I18n.getString("termora.settings.keymap")
}
override fun getJComponent(): JComponent {
return this
}
}
}

View File

@@ -5,10 +5,12 @@ import app.termora.terminal.TerminalSize
import org.apache.sshd.client.ClientBuilder
import org.apache.sshd.client.SshClient
import org.apache.sshd.client.channel.ChannelShell
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver
import org.apache.sshd.client.session.ClientSession
import org.apache.sshd.common.SshException
import org.apache.sshd.common.channel.PtyChannelConfiguration
import org.apache.sshd.common.global.KeepAliveHandler
import org.apache.sshd.common.util.net.SshdSocketAddress
import org.apache.sshd.core.CoreModuleProperties
import org.apache.sshd.server.forward.AcceptAllForwardingFilter
import org.apache.sshd.server.forward.RejectAllForwardingFilter
@@ -16,14 +18,15 @@ import org.eclipse.jgit.internal.transport.sshd.JGitSshClient
import org.eclipse.jgit.transport.CredentialsProvider
import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
import org.eclipse.jgit.transport.sshd.ProxyData
import org.slf4j.LoggerFactory
import java.net.InetSocketAddress
import java.net.Proxy
import java.time.Duration
import kotlin.math.max
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver
object SshClients {
private val timeout = Duration.ofSeconds(30)
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
/**
* 打开一个 Shell
@@ -57,6 +60,54 @@ object SshClients {
* 打开一个会话
*/
fun openSession(host: Host, client: SshClient): ClientSession {
// 如果没有跳板机直接连接
if (host.options.jumpHosts.isEmpty()) {
return doOpenSession(host, client)
}
val jumpHosts = mutableListOf<Host>()
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
for (jumpHostId in host.options.jumpHosts) {
val e = hosts[jumpHostId]
if (e == null) {
if (log.isWarnEnabled) {
log.warn("Failed to find jump host: $jumpHostId")
}
continue
}
jumpHosts.add(e)
}
// 最后一跳是目标机器
jumpHosts.add(host)
val sessions = mutableListOf<ClientSession>()
for (i in 0 until jumpHosts.size) {
val currentHost = jumpHosts[i]
sessions.add(doOpenSession(currentHost, client))
// 如果有下一跳
if (i < jumpHosts.size - 1) {
val nextHost = jumpHosts[i + 1]
// 通过 currentHost 的 Session 将远程端口映射到本地
val address = sessions.last().startLocalPortForwarding(
SshdSocketAddress.LOCALHOST_ADDRESS,
SshdSocketAddress(nextHost.host, nextHost.port),
)
if (log.isInfoEnabled) {
log.info("jump host: ${currentHost.host}:${currentHost.port} , next host: ${nextHost.host}:${nextHost.port} , local address: ${address.hostName}:${address.port}")
}
// 映射完毕之后修改Host和端口
jumpHosts[i + 1] = nextHost.copy(host = address.hostName, port = address.port)
}
}
return sessions.last()
}
private fun doOpenSession(host: Host, client: SshClient): ClientSession {
val session = client.connect(host.username, host.host, host.port)
.verify(timeout).session
if (host.authentication.type == AuthenticationType.Password) {
@@ -73,6 +124,7 @@ object SshClients {
return session
}
/**
* 打开一个客户端
*/
@@ -81,7 +133,7 @@ object SshClients {
builder.globalRequestHandlers(listOf(KeepAliveHandler.INSTANCE))
.factory { JGitSshClient() }
if (host.tunnelings.isEmpty()) {
if (host.tunnelings.isEmpty() && host.options.jumpHosts.isEmpty()) {
builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE)
} else {
builder.forwardingFilter(AcceptAllForwardingFilter.INSTANCE)

View File

@@ -1,19 +1,21 @@
package app.termora
import app.termora.db.Database
import app.termora.terminal.*
import app.termora.terminal.panel.TerminalPanel
import app.termora.tlog.TerminalLoggerDataListener
import java.awt.Color
import javax.swing.UIManager
class TerminalFactory {
class TerminalFactory private constructor() : Disposable {
private val terminals = mutableListOf<Terminal>()
companion object {
val instance by lazy { TerminalFactory() }
fun getInstance(scope: WindowScope): TerminalFactory {
return scope.getOrCreate(TerminalFactory::class) { TerminalFactory() }
}
}
fun createTerminal(): Terminal {
val terminal = MyVisualTerminal()
@@ -38,7 +40,7 @@ class TerminalFactory {
open class MyTerminalModel(terminal: Terminal) : TerminalModelImpl(terminal) {
private val colorPalette by lazy { MyColorPalette(terminal) }
private val config get() = Database.instance.terminal
private val config get() = Database.getDatabase().terminal
init {
this.setData(DataKey.CursorStyle, config.cursor)
@@ -95,7 +97,7 @@ class TerminalFactory {
TerminalColor.Basic.SELECTION_FOREGROUND
)
else -> DefaultColorTheme.instance.getColor(color)
else -> DefaultColorTheme.getInstance().getColor(color)
}
}
@@ -108,4 +110,6 @@ class TerminalFactory {
return colorTheme
}
}
}

View File

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

View File

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

View File

@@ -1,28 +1,35 @@
package app.termora
import app.termora.actions.*
import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
import app.termora.findeverywhere.FindEverywhere
import app.termora.findeverywhere.FindEverywhereProvider
import app.termora.findeverywhere.FindEverywhereResult
import app.termora.terminal.DataKey
import app.termora.transport.TransportPanel
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import org.jdesktop.swingx.action.ActionManager
import java.awt.*
import java.awt.event.*
import java.awt.event.AWTEventListener
import java.awt.event.ActionEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.beans.PropertyChangeListener
import javax.swing.*
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
import kotlin.math.min
class TerminalTabbed(
private val windowScope: WindowScope,
private val termoraToolBar: TermoraToolBar,
private val tabbedPane: FlatTabbedPane,
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager {
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager, DataProvider {
private val tabs = mutableListOf<TerminalTab>()
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
private val toolbar = termoraToolBar.getJToolBar()
private val actionManager = ActionManager.getInstance()
private val dataProviderSupport = DataProviderSupport()
private val iconListener = PropertyChangeListener { e ->
val source = e.source
@@ -52,6 +59,10 @@ class TerminalTabbed(
add(tabbedPane, BorderLayout.CENTER)
windowScope.getOrCreate(TerminalTabbedManager::class) { this }
dataProviderSupport.addData(DataProviders.TerminalTabbed, this)
dataProviderSupport.addData(DataProviders.TerminalTabbedManager, this)
}
@@ -79,35 +90,6 @@ class TerminalTabbed(
}
}
// 快捷键
val inputMap = getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
for (i in KeyEvent.VK_1..KeyEvent.VK_9) {
val tabIndex = i - KeyEvent.VK_1 + 1
val actionKey = "select_$tabIndex"
actionMap.put(actionKey, object : AnAction() {
override fun actionPerformed(e: ActionEvent) {
tabbedPane.selectedIndex = if (i == KeyEvent.VK_9 || tabIndex > tabbedPane.tabCount) {
tabbedPane.tabCount - 1
} else {
tabIndex - 1
}
}
})
inputMap.put(KeyStroke.getKeyStroke(i, toolkit.menuShortcutKeyMaskEx), actionKey)
}
// 关闭 tab
actionMap.put("closeTab", object : AnAction() {
override fun actionPerformed(e: ActionEvent) {
if (tabbedPane.selectedIndex >= 0) {
tabbedPane.tabCloseCallback?.accept(tabbedPane, tabbedPane.selectedIndex)
}
}
})
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, toolkit.menuShortcutKeyMaskEx), "closeTab")
// 右键菜单
tabbedPane.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
@@ -136,44 +118,35 @@ class TerminalTabbed(
})
// 注册全局搜索
FindEverywhere.registerProvider(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider {
override fun find(pattern: String): List<FindEverywhereResult> {
val results = mutableListOf<FindEverywhereResult>()
for (i in 0 until tabbedPane.tabCount) {
val c = tabbedPane.getComponentAt(i)
if (c is WelcomePanel || c is TransportPanel) {
continue
}
results.add(
SwitchFindEverywhereResult(
tabbedPane.getTitleAt(i),
tabbedPane.getIconAt(i),
tabbedPane.getComponentAt(i)
FindEverywhereProvider.getFindEverywhereProviders(windowScope)
.add(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider {
override fun find(pattern: String): List<FindEverywhereResult> {
val results = mutableListOf<FindEverywhereResult>()
for (i in 0 until tabbedPane.tabCount) {
val c = tabbedPane.getComponentAt(i)
if (c is WelcomePanel || c is TransportPanel) {
continue
}
results.add(
SwitchFindEverywhereResult(
tabbedPane.getTitleAt(i),
tabbedPane.getIconAt(i),
tabbedPane.getComponentAt(i)
)
)
)
}
return results
}
return results
}
override fun group(): String {
return I18n.getString("termora.find-everywhere.groups.opened-hosts")
}
override fun order(): Int {
return Integer.MIN_VALUE + 1
}
}))
// 打开 Host
ActionManager.getInstance().addAction(Actions.OPEN_HOST, object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
if (e !is OpenHostActionEvent) {
return
override fun group(): String {
return I18n.getString("termora.find-everywhere.groups.opened-hosts")
}
openHost(e.host)
}
})
override fun order(): Int {
return Integer.MIN_VALUE + 1
}
}))
// 监听全局事件
toolkit.addAWTEventListener(customizeToolBarAWTEventListener, AWTEvent.MOUSE_EVENT_MASK)
@@ -208,14 +181,6 @@ class TerminalTabbed(
}
}
private fun openHost(host: Host) {
val tab = if (host.protocol == Protocol.SSH) SSHTerminalTab(host) else LocalTerminalTab(host)
addTab(tab)
tab.start()
}
private fun showContextMenu(tabIndex: Int, e: MouseEvent) {
val c = tabbedPane.getComponentAt(tabIndex) as JComponent
val tab = tabs[tabIndex]
@@ -242,32 +207,34 @@ class TerminalTabbed(
// 克隆
val clone = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.clone"))
clone.addActionListener {
clone.addActionListener { evt ->
if (tab is HostTerminalTab) {
ActionManager.getInstance()
.getAction(Actions.OPEN_HOST)
.actionPerformed(OpenHostActionEvent(this, tab.host))
actionManager
.getAction(OpenHostAction.OPEN_HOST)
.actionPerformed(OpenHostActionEvent(this, tab.host, evt))
}
}
// 在新窗口中打开
val openInNewWindow = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.open-in-new-window"))
openInNewWindow.addActionListener {
val index = tabbedPane.selectedIndex
if (index > 0) {
val title = tabbedPane.getTitleAt(index)
removeTabAt(index, false)
val dialog = TerminalTabDialog(
owner = SwingUtilities.getWindowAncestor(this),
terminalTab = tab,
size = Dimension(min(size.width, 1280), min(size.height, 800))
)
dialog.title = title
Disposer.register(dialog, tab)
Disposer.register(this, dialog)
dialog.isVisible = true
openInNewWindow.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
val owner = evt.getData(DataProviders.TermoraFrame) ?: return
if (tabIndex > 0) {
val title = tabbedPane.getTitleAt(tabIndex)
removeTabAt(tabIndex, false)
val dialog = TerminalTabDialog(
owner = owner,
terminalTab = tab,
size = Dimension(min(size.width, 1280), min(size.height, 800))
)
dialog.title = title
Disposer.register(dialog, tab)
Disposer.register(this@TerminalTabbed, dialog)
dialog.isVisible = true
}
}
}
})
popupMenu.addSeparator()
@@ -451,5 +418,24 @@ class TerminalTabbed(
}
}
override fun closeTerminalTab(tab: TerminalTab) {
for (i in 0 until tabs.size) {
if (tabs[i] == tab) {
removeTabAt(i, true)
break
}
}
}
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
if (dataKey == DataProviders.TerminalTab) {
dataProviderSupport.removeData(dataKey)
if (tabbedPane.selectedIndex >= 0 && tabs.size > tabbedPane.selectedIndex) {
dataProviderSupport.addData(dataKey, tabs[tabbedPane.selectedIndex])
}
}
return dataProviderSupport.getData(dataKey)
}
}

View File

@@ -5,4 +5,5 @@ interface TerminalTabbedManager {
fun getSelectedTerminalTab(): TerminalTab?
fun getTerminalTabs(): List<TerminalTab>
fun setSelectedTerminalTab(tab: TerminalTab)
fun closeTerminalTab(tab: TerminalTab)
}

View File

@@ -1,84 +1,53 @@
package app.termora
import app.termora.findeverywhere.FindEverywhere
import app.termora.highlight.KeywordHighlightDialog
import app.termora.keymgr.KeyManagerDialog
import app.termora.macro.MacroAction
import app.termora.tlog.TerminalLoggerAction
import app.termora.transport.SFTPAction
import app.termora.actions.ActionManager
import app.termora.actions.DataProvider
import app.termora.actions.DataProviderSupport
import app.termora.actions.DataProviders
import app.termora.terminal.DataKey
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.FlatDesktop
import com.formdev.flatlaf.util.SystemInfo
import com.jetbrains.JBR
import io.github.g00fy2.versioncompare.Version
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.JXEditorPane
import org.jdesktop.swingx.action.ActionManager
import org.slf4j.LoggerFactory
import java.awt.Dimension
import java.awt.Insets
import java.awt.KeyEventDispatcher
import java.awt.KeyboardFocusManager
import java.awt.event.*
import java.net.URI
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.util.*
import javax.imageio.ImageIO
import javax.swing.*
import javax.swing.Box
import javax.swing.JFrame
import javax.swing.SwingUtilities
import javax.swing.SwingUtilities.isEventDispatchThread
import javax.swing.event.HyperlinkEvent
import kotlin.concurrent.fixedRateTimer
import javax.swing.UIManager
import kotlin.math.max
import kotlin.system.exitProcess
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
fun assertEventDispatchThread() {
if (!isEventDispatchThread()) throw WrongThreadException("AWT EventQueue")
}
class TermoraFrame : JFrame() {
class TermoraFrame : JFrame(), DataProvider {
companion object {
private val log = LoggerFactory.getLogger(TermoraFrame::class.java)
}
private val actionManager get() = ActionManager.getInstance()
private val id = UUID.randomUUID().toString()
private val windowScope = ApplicationScope.forWindowScope(this)
private val titleBar = LogicCustomTitleBar.createCustomTitleBar(this)
private val tabbedPane = MyTabbedPane()
private val toolbar = TermoraToolBar(titleBar, tabbedPane)
private lateinit var terminalTabbed: TerminalTabbed
private val disposable = Disposer.newDisposable()
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane)
private val isWindowDecorationsSupported by lazy { JBR.isWindowDecorationsSupported() }
private val updaterManager get() = UpdaterManager.instance
private val dataProviderSupport = DataProviderSupport()
private val welcomePanel = WelcomePanel(windowScope)
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
private val preferencesHandler = object : Runnable {
override fun run() {
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow ?: this@TermoraFrame
if (owner != this@TermoraFrame) {
return
}
val that = this
FlatDesktop.setPreferencesHandler {}
val dialog = SettingsDialog(owner)
dialog.addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
FlatDesktop.setPreferencesHandler(that)
}
})
dialog.setLocationRelativeTo(owner)
dialog.isVisible = true
}
}
init {
initActions()
initView()
initEvents()
initDesktopHandler()
scheduleUpdate()
}
private fun initEvents() {
@@ -97,154 +66,19 @@ class TermoraFrame : JFrame() {
}
}
// global shortcuts
rootPane.actionMap.put(Actions.FIND_EVERYWHERE, ActionManager.getInstance().getAction(Actions.FIND_EVERYWHERE))
rootPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_T, toolkit.menuShortcutKeyMaskEx), Actions.FIND_EVERYWHERE)
// double shift
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(object : KeyEventDispatcher {
private var lastTime = -1L
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
if (e.keyCode == KeyEvent.VK_SHIFT && e.id == KeyEvent.KEY_PRESSED) {
val now = System.currentTimeMillis()
if (now - 250 < lastTime) {
ActionManager.getInstance().getAction(Actions.FIND_EVERYWHERE)
.actionPerformed(ActionEvent(rootPane, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
}
lastTime = now
} else if (e.keyCode != KeyEvent.VK_SHIFT) { // 如果不是 Shift 键,那么就阻断了连续性,重置时间
lastTime = -1
}
return false
}
})
// 监听主题变化 需要动态修改控制栏颜色
if (SystemInfo.isWindows && isWindowDecorationsSupported) {
ThemeManager.instance.addThemeChangeListener(object : ThemeChangeListener {
ThemeManager.getInstance().addThemeChangeListener(object : ThemeChangeListener {
override fun onChanged() {
titleBar.putProperty("controls.dark", FlatLaf.isLafDark())
}
})
}
// dispose
addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
Disposer.dispose(disposable)
Disposer.dispose(ApplicationDisposable.instance)
try {
Disposer.getTree().assertIsEmpty(true)
} catch (e: Exception) {
log.error(e.message)
}
exitProcess(0)
}
})
}
private fun initActions() {
// SETTING
ActionManager.getInstance().addAction(Actions.SETTING, object : AnAction(
I18n.getString("termora.setting"),
Icons.settings
) {
override fun actionPerformed(e: ActionEvent) {
preferencesHandler.run()
}
})
// MULTIPLE
ActionManager.getInstance().addAction(Actions.MULTIPLE, object : AnAction(
I18n.getString("termora.tools.multiple"),
Icons.vcs
) {
init {
setStateAction()
}
override fun actionPerformed(evt: ActionEvent) {
TerminalPanelFactory.instance.repaintAll()
}
})
// Keyword Highlight
ActionManager.getInstance().addAction(Actions.KEYWORD_HIGHLIGHT, object : AnAction(
I18n.getString("termora.highlight"),
Icons.edit
) {
override fun actionPerformed(evt: ActionEvent) {
KeywordHighlightDialog(this@TermoraFrame).isVisible = true
}
})
// app update
ActionManager.getInstance().addAction(Actions.APP_UPDATE, object :
AnAction(
StringUtils.EMPTY,
Icons.ideUpdate
) {
init {
isEnabled = false
}
override fun actionPerformed(evt: ActionEvent) {
showUpdateDialog()
}
})
// 终端日志记录
ActionManager.getInstance().addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
// SFTP
ActionManager.getInstance().addAction(Actions.SFTP, SFTPAction())
// macro
ActionManager.getInstance().addAction(Actions.MACRO, MacroAction())
// FIND_EVERYWHERE
ActionManager.getInstance().addAction(Actions.FIND_EVERYWHERE, object : AnAction(
I18n.getString("termora.find-everywhere"),
Icons.find
) {
override fun actionPerformed(evt: ActionEvent) {
if (this.isEnabled) {
val focusWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
val frame = this@TermoraFrame
if (focusWindow == frame) {
val dialog = FindEverywhere(frame)
dialog.setLocationRelativeTo(frame)
dialog.isVisible = true
}
}
}
})
// Key manager
ActionManager.getInstance().addAction(Actions.KEY_MANAGER, object : AnAction(
I18n.getString("termora.keymgr.title"),
Icons.greyKey
) {
override fun actionPerformed(evt: ActionEvent) {
if (this.isEnabled) {
KeyManagerDialog(this@TermoraFrame).isVisible = true
}
}
})
}
private fun initView() {
if (isWindowDecorationsSupported) {
titleBar.height = UIManager.getInt("TabbedPane.tabHeight").toFloat()
@@ -267,10 +101,7 @@ class TermoraFrame : JFrame() {
}
minimumSize = Dimension(640, 400)
terminalTabbed = TerminalTabbed(toolbar, tabbedPane).apply {
Application.registerService(TerminalTabbedManager::class, this)
}
terminalTabbed.addTab(WelcomePanel())
terminalTabbed.addTab(welcomePanel)
// macOS 要避开左边的控制栏
if (SystemInfo.isMacOS) {
@@ -282,89 +113,13 @@ class TermoraFrame : JFrame() {
}
}
Disposer.register(disposable, terminalTabbed)
Disposer.register(windowScope, terminalTabbed)
add(terminalTabbed)
dataProviderSupport.addData(DataProviders.TermoraFrame, this)
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
}
private fun showUpdateDialog() {
val lastVersion = updaterManager.lastVersion
val editorPane = JXEditorPane()
editorPane.contentType = "text/html"
editorPane.text = lastVersion.htmlBody
editorPane.isEditable = false
editorPane.addHyperlinkListener {
if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) {
Application.browse(it.url.toURI())
}
}
editorPane.background = DynamicColor("window")
val scrollPane = JScrollPane(editorPane)
scrollPane.border = BorderFactory.createEmptyBorder()
scrollPane.preferredSize = Dimension(
UIManager.getInt("Dialog.width") - 100,
UIManager.getInt("Dialog.height") - 100
)
val option = OptionPane.showConfirmDialog(
this,
scrollPane,
title = I18n.getString("termora.update.title"),
messageType = JOptionPane.PLAIN_MESSAGE,
optionType = JOptionPane.YES_NO_CANCEL_OPTION,
options = arrayOf(
I18n.getString("termora.update.update"),
I18n.getString("termora.update.ignore"),
I18n.getString("termora.cancel")
),
initialValue = I18n.getString("termora.update.update")
)
if (option == JOptionPane.CANCEL_OPTION) {
return
} else if (option == JOptionPane.NO_OPTION) {
ActionManager.getInstance().setEnabled(Actions.APP_UPDATE, false)
updaterManager.ignore(updaterManager.lastVersion.version)
} else if (option == JOptionPane.YES_OPTION) {
ActionManager.getInstance()
.setEnabled(Actions.APP_UPDATE, false)
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${lastVersion.version}"))
}
}
@OptIn(DelicateCoroutinesApi::class)
private fun scheduleUpdate() {
fixedRateTimer(
name = "check-update-timer",
initialDelay = 3.minutes.inWholeMilliseconds,
period = 5.hours.inWholeMilliseconds, daemon = true
) {
GlobalScope.launch(Dispatchers.IO) { supervisorScope { launch { checkUpdate() } } }
}
}
private suspend fun checkUpdate() {
val latestVersion = updaterManager.fetchLatestVersion()
if (latestVersion.isSelf) {
return
}
val newVersion = Version(latestVersion.version)
val version = Version(Application.getVersion())
if (newVersion <= version) {
return
}
if (updaterManager.isIgnored(latestVersion.version)) {
return
}
withContext(Dispatchers.Swing) {
ActionManager.getInstance()
.setEnabled(Actions.APP_UPDATE, true)
}
}
private fun forceHitTest() {
val mouseAdapter = object : MouseAdapter() {
@@ -423,11 +178,25 @@ class TermoraFrame : JFrame() {
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
}
private fun initDesktopHandler() {
if (SystemInfo.isMacOS) {
FlatDesktop.setPreferencesHandler {
preferencesHandler.run()
}
}
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()
}
}

View File

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

View File

@@ -1,16 +1,18 @@
package app.termora
import app.termora.Application.ohMyJson
import app.termora.db.Database
import app.termora.actions.ActionManager
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.actions.SettingsAction
import app.termora.findeverywhere.FindEverywhereAction
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.util.SystemInfo
import com.jetbrains.WindowDecorations
import kotlinx.serialization.Serializable
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionContainerFactory
import org.jdesktop.swingx.action.ActionManager
import java.awt.Insets
import java.awt.event.ActionEvent
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.Box
@@ -27,7 +29,7 @@ class TermoraToolBar(
private val titleBar: WindowDecorations.CustomTitleBar,
private val tabbedPane: FlatTabbedPane
) {
private val properties by lazy { Database.instance.properties }
private val properties by lazy { Database.getDatabase().properties }
private val toolbar by lazy { MyToolBar().apply { rebuild(this) } }
@@ -46,8 +48,8 @@ class TermoraToolBar(
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
ToolBarAction(Actions.KEY_MANAGER, true),
ToolBarAction(Actions.MULTIPLE, true),
ToolBarAction(Actions.FIND_EVERYWHERE, true),
ToolBarAction(Actions.SETTING, true),
ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
ToolBarAction(SettingsAction.SETTING, true),
)
}
@@ -96,12 +98,12 @@ class TermoraToolBar(
toolbar.removeAll()
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
override fun actionPerformed(e: ActionEvent?) {
actionManager.getAction(Actions.FIND_EVERYWHERE)?.actionPerformed(e)
override fun actionPerformed(evt: AnActionEvent) {
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
}
override fun isEnabled(): Boolean {
return actionManager.getAction(Actions.FIND_EVERYWHERE)?.isEnabled ?: false
return actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.isEnabled ?: false
}
}))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,117 @@
package app.termora.actions
import app.termora.*
import io.github.g00fy2.versioncompare.Version
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.JXEditorPane
import java.awt.Dimension
import java.awt.KeyboardFocusManager
import java.net.URI
import javax.swing.BorderFactory
import javax.swing.JOptionPane
import javax.swing.JScrollPane
import javax.swing.UIManager
import javax.swing.event.HyperlinkEvent
import kotlin.concurrent.fixedRateTimer
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
class AppUpdateAction : AnAction(
StringUtils.EMPTY,
Icons.ideUpdate
) {
private val updaterManager get() = UpdaterManager.getInstance()
init {
isEnabled = false
scheduleUpdate()
}
override fun actionPerformed(evt: AnActionEvent) {
showUpdateDialog()
}
@OptIn(DelicateCoroutinesApi::class)
private fun scheduleUpdate() {
fixedRateTimer(
name = "check-update-timer",
initialDelay = 3.minutes.inWholeMilliseconds,
period = 5.hours.inWholeMilliseconds, daemon = true
) {
GlobalScope.launch(Dispatchers.IO) { supervisorScope { launch { checkUpdate() } } }
}
}
private suspend fun checkUpdate() {
val latestVersion = updaterManager.fetchLatestVersion()
if (latestVersion.isSelf) {
return
}
val newVersion = Version(latestVersion.version)
val version = Version(Application.getVersion())
if (newVersion <= version) {
return
}
if (updaterManager.isIgnored(latestVersion.version)) {
return
}
withContext(Dispatchers.Swing) {
ActionManager.getInstance()
.setEnabled(Actions.APP_UPDATE, true)
}
}
private fun showUpdateDialog() {
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
val lastVersion = updaterManager.lastVersion
val editorPane = JXEditorPane()
editorPane.contentType = "text/html"
editorPane.text = lastVersion.htmlBody
editorPane.isEditable = false
editorPane.addHyperlinkListener {
if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) {
Application.browse(it.url.toURI())
}
}
editorPane.background = DynamicColor("window")
val scrollPane = JScrollPane(editorPane)
scrollPane.border = BorderFactory.createEmptyBorder()
scrollPane.preferredSize = Dimension(
UIManager.getInt("Dialog.width") - 100,
UIManager.getInt("Dialog.height") - 100
)
val option = OptionPane.showConfirmDialog(
owner,
scrollPane,
title = I18n.getString("termora.update.title"),
messageType = JOptionPane.PLAIN_MESSAGE,
optionType = JOptionPane.YES_NO_CANCEL_OPTION,
options = arrayOf(
I18n.getString("termora.update.update"),
I18n.getString("termora.update.ignore"),
I18n.getString("termora.cancel")
),
initialValue = I18n.getString("termora.update.update")
)
if (option == JOptionPane.CANCEL_OPTION) {
return
} else if (option == JOptionPane.NO_OPTION) {
ActionManager.getInstance().setEnabled(Actions.APP_UPDATE, false)
updaterManager.ignore(updaterManager.lastVersion.version)
} else if (option == JOptionPane.YES_OPTION) {
ActionManager.getInstance()
.setEnabled(Actions.APP_UPDATE, false)
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${lastVersion.version}"))
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package app.termora.findeverywhere
import app.termora.Actions
import app.termora.I18n
import org.jdesktop.swingx.action.ActionManager
class QuickActionsFindEverywhereProvider : FindEverywhereProvider {

View File

@@ -1,9 +1,12 @@
package app.termora.findeverywhere
import app.termora.*
import app.termora.Actions
import app.termora.I18n
import app.termora.Icons
import app.termora.actions.NewHostAction
import app.termora.actions.OpenLocalTerminalAction
import com.formdev.flatlaf.FlatLaf
import org.jdesktop.swingx.action.ActionManager
import java.awt.event.ActionEvent
import javax.swing.Icon
class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
@@ -11,26 +14,12 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
override fun find(pattern: String): List<FindEverywhereResult> {
val list = mutableListOf<FindEverywhereResult>()
actionManager?.let {
list.add(CreateHostFindEverywhereResult())
}
actionManager.let { list.add(CreateHostFindEverywhereResult()) }
// Local terminal
list.add(ActionFindEverywhereResult(object : AnAction(
I18n.getString("termora.find-everywhere.quick-command.local-terminal"),
Icons.terminal
) {
override fun actionPerformed(evt: ActionEvent) {
actionManager.getAction(Actions.OPEN_HOST)?.actionPerformed(
OpenHostActionEvent(
this, Host(
name = name,
protocol = Protocol.Local
)
)
)
}
}))
actionManager.getAction(OpenLocalTerminalAction.LOCAL_TERMINAL)?.let {
list.add(ActionFindEverywhereResult(it))
}
// SFTP
actionManager.getAction(Actions.SFTP)?.let {
@@ -50,7 +39,7 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
}
private class CreateHostFindEverywhereResult : ActionFindEverywhereResult(
ActionManager.getInstance().getAction(Actions.ADD_HOST)
ActionManager.getInstance().getAction(NewHostAction.NEW_HOST)
) {
override fun getIcon(isSelected: Boolean): Icon {
if (isSelected) {

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,22 @@
package app.termora.highlight
import app.termora.ApplicationScope
import app.termora.TerminalPanelFactory
import app.termora.db.Database
import app.termora.Database
import org.slf4j.LoggerFactory
class KeywordHighlightManager private constructor() {
companion object {
val instance by lazy { KeywordHighlightManager() }
fun getInstance(): KeywordHighlightManager {
return ApplicationScope.forApplicationScope()
.getOrCreate(KeywordHighlightManager::class) { KeywordHighlightManager() }
}
private val log = LoggerFactory.getLogger(KeywordHighlightManager::class.java)
}
private val database by lazy { Database.instance }
private val database by lazy { Database.getDatabase() }
private val keywordHighlights = mutableMapOf<String, KeywordHighlight>()
init {
@@ -22,7 +27,7 @@ class KeywordHighlightManager private constructor() {
fun addKeywordHighlight(keywordHighlight: KeywordHighlight) {
database.addKeywordHighlight(keywordHighlight)
keywordHighlights[keywordHighlight.id] = keywordHighlight
TerminalPanelFactory.instance.repaintAll()
ApplicationScope.windowScopes().forEach { TerminalPanelFactory.getInstance(it).repaintAll() }
if (log.isDebugEnabled) {
log.debug("Keyword highlighter added. {}", keywordHighlight)
@@ -32,7 +37,7 @@ class KeywordHighlightManager private constructor() {
fun removeKeywordHighlight(id: String) {
database.removeKeywordHighlight(id)
keywordHighlights.remove(id)
TerminalPanelFactory.instance.repaintAll()
ApplicationScope.windowScopes().forEach { TerminalPanelFactory.getInstance(it).repaintAll() }
if (log.isDebugEnabled) {
log.debug("Keyword highlighter removed. {}", id)

View File

@@ -1,5 +1,6 @@
package app.termora.highlight
import app.termora.ApplicationScope
import app.termora.terminal.*
import app.termora.terminal.panel.TerminalDisplay
import app.termora.terminal.panel.TerminalPaintListener
@@ -11,11 +12,15 @@ import kotlin.random.Random
class KeywordHighlightPaintListener private constructor() : TerminalPaintListener {
companion object {
val instance by lazy { KeywordHighlightPaintListener() }
fun getInstance(): KeywordHighlightPaintListener {
return ApplicationScope.forApplicationScope()
.getOrCreate(KeywordHighlightPaintListener::class) { KeywordHighlightPaintListener() }
}
private val tag = Random.nextInt()
}
private val keywordHighlightManager by lazy { KeywordHighlightManager.instance }
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
override fun before(
offset: Int,

View File

@@ -3,7 +3,7 @@ package app.termora.highlight
import javax.swing.table.DefaultTableModel
class KeywordHighlightTableModel : DefaultTableModel() {
private val rows get() = KeywordHighlightManager.instance.getKeywordHighlights()
private val rows get() = KeywordHighlightManager.getInstance().getKeywordHighlights()
override fun isCellEditable(row: Int, column: Int): Boolean {
return false

View File

@@ -4,7 +4,7 @@ import app.termora.DialogWrapper
import app.termora.DynamicColor
import app.termora.I18n
import app.termora.Icons
import app.termora.db.Database
import app.termora.Database
import app.termora.terminal.ColorPalette
import app.termora.terminal.TerminalColor
import com.formdev.flatlaf.FlatClientProperties
@@ -29,7 +29,7 @@ class NewKeywordHighlightDialog(
val colorPalette: ColorPalette
) : DialogWrapper(owner) {
private val formMargin = "7dlu"
private val keywordHighlightView by lazy { KeywordHighlightView(fontSize = Database.instance.terminal.fontSize) }
private val keywordHighlightView by lazy { KeywordHighlightView(fontSize = Database.getDatabase().terminal.fontSize) }
val keywordTextField = FlatTextField()
val descriptionTextField = FlatTextField()

View File

@@ -0,0 +1,50 @@
package app.termora.keymap
import org.apache.commons.lang3.StringUtils
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
class KeyShortcut(val keyStroke: KeyStroke) : Shortcut() {
companion object {
fun toHumanText(keyStroke: KeyStroke): String {
var text = keyStroke.toString()
text = text.replace("shift", "")
text = text.replace("ctrl", "")
text = text.replace("meta", "")
text = text.replace("alt", "")
text = text.replace("pressed", StringUtils.EMPTY)
text = text.replace(StringUtils.SPACE, StringUtils.EMPTY)
if (keyStroke.keyCode == KeyEvent.VK_EQUALS) {
text = text.replace("EQUALS", "+")
} else if (keyStroke.keyCode == KeyEvent.VK_MINUS) {
text = text.replace("MINUS", "-")
}
return text.toCharArray().joinToString(" + ")
}
}
override fun isKeyboard(): Boolean {
return true
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as KeyShortcut
return keyStroke == other.keyStroke
}
override fun hashCode(): Int {
return keyStroke.hashCode()
}
override fun toString(): String {
return toHumanText(keyStroke)
}
}

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