mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Init Commit
This commit is contained in:
14
src/main/java/app/termora/Disposable.java
Normal file
14
src/main/java/app/termora/Disposable.java
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package app.termora;
|
||||
|
||||
public interface Disposable {
|
||||
|
||||
default void dispose() {
|
||||
|
||||
}
|
||||
|
||||
interface Parent extends Disposable {
|
||||
void beforeTreeDispose();
|
||||
}
|
||||
|
||||
}
|
||||
192
src/main/java/app/termora/Disposer.java
Normal file
192
src/main/java/app/termora/Disposer.java
Normal file
@@ -0,0 +1,192 @@
|
||||
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package app.termora;
|
||||
|
||||
import org.jetbrains.annotations.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* <p>Manages a parent-child relation of chained objects requiring cleanup.</p>
|
||||
*
|
||||
* <p>A root node can be created via {@link #newDisposable()}, to which children are attached via subsequent calls to {@link #register(Disposable, Disposable)}.
|
||||
* Invoking {@link #dispose(Disposable)} will process all its registered children's {@link Disposable#dispose()} method.</p>
|
||||
* <p>
|
||||
* See <a href="https://www.jetbrains.org/intellij/sdk/docs/basics/disposers.html">Disposer and Disposable</a> in SDK Docs.
|
||||
*
|
||||
* @see Disposable
|
||||
*/
|
||||
public final class Disposer {
|
||||
private static final ObjectTree ourTree = new ObjectTree();
|
||||
|
||||
public static boolean isDebugDisposerOn() {
|
||||
return "on".equals(System.getProperty("idea.disposer.debug"));
|
||||
}
|
||||
|
||||
private static boolean ourDebugMode;
|
||||
|
||||
private Disposer() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Contract(pure = true, value = "->new")
|
||||
public static Disposable newDisposable() {
|
||||
// must not be lambda because we care about identity in ObjectTree.myObject2NodeMap
|
||||
return newDisposable("");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Contract(pure = true, value = "_->new")
|
||||
public static Disposable newDisposable(@NotNull @NonNls String debugName) {
|
||||
// must not be lambda because we care about identity in ObjectTree.myObject2NodeMap
|
||||
return new Disposable() {
|
||||
@Override
|
||||
public void dispose() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return debugName;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Contract(pure = true, value = "_,_->new")
|
||||
public static @NotNull Disposable newDisposable(@NotNull Disposable parentDisposable, @NotNull String debugName) {
|
||||
Disposable result = newDisposable(debugName);
|
||||
register(parentDisposable, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final Map<String, Disposable> ourKeyDisposables = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
|
||||
public static void register(@NotNull Disposable parent, @NotNull Disposable child) {
|
||||
RuntimeException e = ourTree.register(parent, child);
|
||||
if (e != null) throw e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers child disposable under parent unless the parent has already been disposed
|
||||
*
|
||||
* @return whether the registration succeeded
|
||||
*/
|
||||
public static boolean tryRegister(@NotNull Disposable parent, @NotNull Disposable child) {
|
||||
return ourTree.register(parent, child) == null;
|
||||
}
|
||||
|
||||
public static void register(@NotNull Disposable parent, @NotNull Disposable child, @NonNls @NotNull final String key) {
|
||||
register(parent, child);
|
||||
Disposable v = get(key);
|
||||
if (v != null) throw new IllegalArgumentException("Key " + key + " already registered: " + v);
|
||||
ourKeyDisposables.put(key, child);
|
||||
register(child, new KeyDisposable(key));
|
||||
}
|
||||
|
||||
private static final class KeyDisposable implements Disposable {
|
||||
@NotNull
|
||||
private final String myKey;
|
||||
|
||||
KeyDisposable(@NotNull String key) {
|
||||
myKey = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
ourKeyDisposables.remove(myKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "KeyDisposable (" + myKey + ")";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if {@code disposable} is disposed or being disposed (i.e. its {@link Disposable#dispose()} method is executing).
|
||||
*/
|
||||
public static boolean isDisposed(@NotNull Disposable disposable) {
|
||||
return ourTree.getDisposalInfo(disposable) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link #isDisposed(Disposable)} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public static boolean isDisposing(@NotNull Disposable disposable) {
|
||||
return isDisposed(disposable);
|
||||
}
|
||||
|
||||
public static Disposable get(@NotNull String key) {
|
||||
return ourKeyDisposables.get(key);
|
||||
}
|
||||
|
||||
public static void dispose(@NotNull Disposable disposable) {
|
||||
dispose(disposable, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code predicate} is used only for direct children.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public static void disposeChildren(@NotNull Disposable disposable, @Nullable Predicate<? super Disposable> predicate) {
|
||||
ourTree.executeAllChildren(disposable, predicate);
|
||||
}
|
||||
|
||||
public static void dispose(@NotNull Disposable disposable, boolean processUnregistered) {
|
||||
ourTree.executeAll(disposable, processUnregistered);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static ObjectTree getTree() {
|
||||
return ourTree;
|
||||
}
|
||||
|
||||
public static void assertIsEmpty() {
|
||||
assertIsEmpty(false);
|
||||
}
|
||||
|
||||
public static void assertIsEmpty(boolean throwError) {
|
||||
if (ourDebugMode) {
|
||||
ourTree.assertIsEmpty(throwError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return old value
|
||||
*/
|
||||
public static boolean setDebugMode(boolean debugMode) {
|
||||
if (debugMode) {
|
||||
debugMode = !"off".equals(System.getProperty("idea.disposer.debug"));
|
||||
}
|
||||
boolean oldValue = ourDebugMode;
|
||||
ourDebugMode = debugMode;
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
public static boolean isDebugMode() {
|
||||
return ourDebugMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return object registered on {@code parentDisposable} which is equal to object, or {@code null} if not found
|
||||
*/
|
||||
@Nullable
|
||||
public static <T extends Disposable> T findRegisteredObject(@NotNull Disposable parentDisposable, @NotNull T object) {
|
||||
return ourTree.findRegisteredObject(parentDisposable, object);
|
||||
}
|
||||
|
||||
public static Throwable getDisposalTrace(@NotNull Disposable disposable) {
|
||||
if (getTree().getDisposalInfo(disposable) instanceof Throwable) {
|
||||
return (Throwable) disposable;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public static void clearDisposalTraces() {
|
||||
ourTree.clearDisposedObjectTraces();
|
||||
}
|
||||
}
|
||||
128
src/main/java/app/termora/ObjectNode.java
Normal file
128
src/main/java/app/termora/ObjectNode.java
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package app.termora;
|
||||
|
||||
import org.jetbrains.annotations.NonNls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.TestOnly;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
final class ObjectNode {
|
||||
private final ObjectTree myTree;
|
||||
|
||||
ObjectNode myParent; // guarded by myTree.treeLock
|
||||
private final Disposable myObject;
|
||||
|
||||
private List<ObjectNode> myChildren; // guarded by myTree.treeLock
|
||||
private Throwable myTrace; // guarded by myTree.treeLock
|
||||
|
||||
ObjectNode(@NotNull ObjectTree tree,
|
||||
@Nullable ObjectNode parentNode,
|
||||
@NotNull Disposable object) {
|
||||
myTree = tree;
|
||||
myParent = parentNode;
|
||||
myObject = object;
|
||||
|
||||
}
|
||||
|
||||
void addChild(@NotNull ObjectNode child) {
|
||||
List<ObjectNode> children = myChildren;
|
||||
if (children == null) {
|
||||
myChildren = new ArrayList<>();
|
||||
myChildren.add(child);
|
||||
} else {
|
||||
children.add(child);
|
||||
}
|
||||
child.myParent = this;
|
||||
}
|
||||
|
||||
void removeChild(@NotNull ObjectNode child) {
|
||||
List<ObjectNode> children = myChildren;
|
||||
if (children != null) {
|
||||
// optimisation: iterate backwards
|
||||
for (int i = children.size() - 1; i >= 0; i--) {
|
||||
ObjectNode node = children.get(i);
|
||||
if (node.equals(child)) {
|
||||
children.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
child.myParent = null;
|
||||
}
|
||||
|
||||
ObjectNode getParent() {
|
||||
return myParent;
|
||||
}
|
||||
|
||||
void getAndRemoveRecursively(@NotNull List<? super Disposable> result) {
|
||||
getAndRemoveChildrenRecursively(result, null);
|
||||
myTree.removeObjectFromTree(this);
|
||||
// already disposed. may happen when someone does `register(obj, ()->Disposer.dispose(t));` abomination
|
||||
if (myTree.rememberDisposedTrace(myObject) == null) {
|
||||
result.add(myObject);
|
||||
}
|
||||
myChildren = null;
|
||||
myParent = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code predicate} is used only for direct children.
|
||||
*/
|
||||
void getAndRemoveChildrenRecursively(@NotNull List<? super Disposable> result, @Nullable Predicate<? super Disposable> predicate) {
|
||||
if (myChildren != null) {
|
||||
for (int i = myChildren.size() - 1; i >= 0; i--) {
|
||||
ObjectNode childNode = myChildren.get(i);
|
||||
if (predicate == null || predicate.test(childNode.getObject())) {
|
||||
childNode.getAndRemoveRecursively(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Disposable getObject() {
|
||||
return myObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNls
|
||||
public String toString() {
|
||||
return "Node: " + myObject;
|
||||
}
|
||||
|
||||
Throwable getTrace() {
|
||||
return myTrace;
|
||||
}
|
||||
|
||||
void clearTrace() {
|
||||
myTrace = null;
|
||||
}
|
||||
|
||||
@TestOnly
|
||||
void assertNoReferencesKept(@NotNull Disposable aDisposable) {
|
||||
assert getObject() != aDisposable;
|
||||
if (myChildren != null) {
|
||||
for (ObjectNode node : myChildren) {
|
||||
node.assertNoReferencesKept(aDisposable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<D extends Disposable> D findChildEqualTo(@NotNull D object) {
|
||||
List<ObjectNode> children = myChildren;
|
||||
if (children != null) {
|
||||
for (ObjectNode node : children) {
|
||||
Disposable nodeObject = node.getObject();
|
||||
if (nodeObject.equals(object)) {
|
||||
//noinspection unchecked
|
||||
return (D) nodeObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
246
src/main/java/app/termora/ObjectTree.java
Normal file
246
src/main/java/app/termora/ObjectTree.java
Normal file
@@ -0,0 +1,246 @@
|
||||
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package app.termora;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.TestOnly;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
final class ObjectTree {
|
||||
private static final ThreadLocal<Throwable> ourTopmostDisposeTrace = new ThreadLocal<>();
|
||||
|
||||
private final Set<Disposable> myRootObjects = new HashSet<>();
|
||||
// guarded by treeLock
|
||||
private final Map<Disposable, ObjectNode> myObject2NodeMap = new HashMap<>();
|
||||
// Disposable -> trace or boolean marker (if trace unavailable)
|
||||
private final Map<Disposable, Object> myDisposedObjects = new WeakHashMap<>(); // guarded by treeLock
|
||||
|
||||
private final Object treeLock = new Object();
|
||||
|
||||
private ObjectNode getNode(@NotNull Disposable object) {
|
||||
return myObject2NodeMap.get(object);
|
||||
}
|
||||
|
||||
private void putNode(@NotNull Disposable object, @Nullable("null means remove") ObjectNode node) {
|
||||
if (node == null) {
|
||||
myObject2NodeMap.remove(object);
|
||||
} else {
|
||||
myObject2NodeMap.put(object, node);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
final RuntimeException register(@NotNull Disposable parent, @NotNull Disposable child) {
|
||||
if (parent == child) return new IllegalArgumentException("Cannot register to itself: " + parent);
|
||||
synchronized (treeLock) {
|
||||
Object wasDisposed = getDisposalInfo(parent);
|
||||
if (wasDisposed != null) {
|
||||
return new IllegalStateException("Sorry but parent: " + parent + " has already been disposed " +
|
||||
"(see the cause for stacktrace) so the child: " + child + " will never be disposed",
|
||||
wasDisposed instanceof Throwable ? (Throwable) wasDisposed : null);
|
||||
}
|
||||
|
||||
myDisposedObjects.remove(child); // if we dispose thing and then register it back it means it's not disposed anymore
|
||||
ObjectNode parentNode = getNode(parent);
|
||||
if (parentNode == null) parentNode = createNodeFor(parent, null);
|
||||
|
||||
ObjectNode childNode = getNode(child);
|
||||
if (childNode == null) {
|
||||
childNode = createNodeFor(child, parentNode);
|
||||
} else {
|
||||
ObjectNode oldParent = childNode.getParent();
|
||||
if (oldParent != null) {
|
||||
oldParent.removeChild(childNode);
|
||||
}
|
||||
}
|
||||
myRootObjects.remove(child);
|
||||
|
||||
RuntimeException e = checkWasNotAddedAlready(parentNode, childNode);
|
||||
if (e != null) return e;
|
||||
|
||||
parentNode.addChild(childNode);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Object getDisposalInfo(@NotNull Disposable object) {
|
||||
synchronized (treeLock) {
|
||||
return myDisposedObjects.get(object);
|
||||
}
|
||||
}
|
||||
|
||||
private static RuntimeException checkWasNotAddedAlready(@NotNull ObjectNode childNode, @NotNull ObjectNode parentNode) {
|
||||
for (ObjectNode node = childNode; node != null; node = node.getParent()) {
|
||||
if (node == parentNode) {
|
||||
return new IllegalStateException("'" + childNode.getObject() + "' was already added as a child of '" + parentNode.getObject() + "'");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ObjectNode createNodeFor(@NotNull Disposable object, @Nullable ObjectNode parentNode) {
|
||||
final ObjectNode newNode = new ObjectNode(this, parentNode, object);
|
||||
if (parentNode == null) {
|
||||
myRootObjects.add(object);
|
||||
}
|
||||
putNode(object, newNode);
|
||||
return newNode;
|
||||
}
|
||||
|
||||
private void runWithTrace(@NotNull Supplier<? extends @NotNull List<Disposable>> removeFromTreeAction) {
|
||||
boolean needTrace = Disposer.isDebugMode() && ourTopmostDisposeTrace.get() == null;
|
||||
if (needTrace) {
|
||||
ourTopmostDisposeTrace.set(new Throwable());
|
||||
}
|
||||
|
||||
// first, atomically remove disposables from the tree to avoid "register during dispose" race conditions
|
||||
List<Disposable> disposables;
|
||||
synchronized (treeLock) {
|
||||
disposables = removeFromTreeAction.get();
|
||||
}
|
||||
|
||||
// second, call "beforeTreeDispose" in pre-order (some clients are hardcoded to see parents-then-children order in "beforeTreeDispose")
|
||||
List<Throwable> exceptions = null;
|
||||
for (int i = disposables.size() - 1; i >= 0; i--) {
|
||||
Disposable disposable = disposables.get(i);
|
||||
if (disposable instanceof Disposable.Parent) {
|
||||
try {
|
||||
((Disposable.Parent) disposable).beforeTreeDispose();
|
||||
} catch (Throwable t) {
|
||||
if (exceptions == null) exceptions = new ArrayList<>();
|
||||
exceptions.add(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// third, dispose in post-order (bottom-up)
|
||||
for (Disposable disposable : disposables) {
|
||||
try {
|
||||
//noinspection SSBasedInspection
|
||||
disposable.dispose();
|
||||
} catch (Throwable e) {
|
||||
if (exceptions == null) exceptions = new ArrayList<>();
|
||||
exceptions.add(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (needTrace) {
|
||||
ourTopmostDisposeTrace.remove();
|
||||
}
|
||||
if (exceptions != null) {
|
||||
handleExceptions(exceptions);
|
||||
}
|
||||
}
|
||||
|
||||
void executeAllChildren(@NotNull Disposable object, @Nullable Predicate<? super Disposable> predicate) {
|
||||
runWithTrace(() -> {
|
||||
ObjectNode node = getNode(object);
|
||||
if (node == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Disposable> disposables = new ArrayList<>();
|
||||
node.getAndRemoveChildrenRecursively(disposables, predicate);
|
||||
return disposables;
|
||||
});
|
||||
}
|
||||
|
||||
void executeAll(@NotNull Disposable object, boolean processUnregistered) {
|
||||
runWithTrace(() -> {
|
||||
ObjectNode node = getNode(object);
|
||||
if (node == null && !processUnregistered) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Disposable> disposables = new ArrayList<>();
|
||||
if (node == null) {
|
||||
if (rememberDisposedTrace(object) == null) {
|
||||
disposables.add(object);
|
||||
}
|
||||
} else {
|
||||
node.getAndRemoveRecursively(disposables);
|
||||
}
|
||||
return disposables;
|
||||
});
|
||||
}
|
||||
|
||||
private static void handleExceptions(@NotNull List<? extends Throwable> exceptions) {
|
||||
|
||||
}
|
||||
|
||||
@TestOnly
|
||||
void assertNoReferenceKeptInTree(@NotNull Disposable disposable) {
|
||||
synchronized (treeLock) {
|
||||
for (Map.Entry<Disposable, ObjectNode> entry : myObject2NodeMap.entrySet()) {
|
||||
Disposable key = entry.getKey();
|
||||
assert key != disposable;
|
||||
ObjectNode node = entry.getValue();
|
||||
node.assertNoReferencesKept(disposable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void assertIsEmpty(boolean throwError) {
|
||||
synchronized (treeLock) {
|
||||
for (Disposable object : myRootObjects) {
|
||||
if (object == null) continue;
|
||||
ObjectNode objectNode = getNode(object);
|
||||
if (objectNode == null) continue;
|
||||
while (objectNode.getParent() != null) {
|
||||
objectNode = objectNode.getParent();
|
||||
}
|
||||
final Throwable trace = objectNode.getTrace();
|
||||
RuntimeException exception = new RuntimeException("Memory leak detected: '" + object + "' of " + object.getClass()
|
||||
+ "\nSee the cause for the corresponding Disposer.register() stacktrace:\n",
|
||||
trace);
|
||||
if (throwError) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// return old value
|
||||
Object rememberDisposedTrace(@NotNull Disposable object) {
|
||||
synchronized (treeLock) {
|
||||
Throwable trace = ourTopmostDisposeTrace.get();
|
||||
return myDisposedObjects.put(object, trace != null ? trace : Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
void clearDisposedObjectTraces() {
|
||||
synchronized (treeLock) {
|
||||
myDisposedObjects.clear();
|
||||
for (ObjectNode value : myObject2NodeMap.values()) {
|
||||
value.clearTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
<D extends Disposable> D findRegisteredObject(@NotNull Disposable parentDisposable, @NotNull D object) {
|
||||
synchronized (treeLock) {
|
||||
ObjectNode parentNode = getNode(parentDisposable);
|
||||
if (parentNode == null) return null;
|
||||
return parentNode.findChildEqualTo(object);
|
||||
}
|
||||
}
|
||||
|
||||
void removeObjectFromTree(@NotNull ObjectNode node) {
|
||||
synchronized (treeLock) {
|
||||
Disposable myObject = node.getObject();
|
||||
putNode(myObject, null);
|
||||
ObjectNode parent = node.getParent();
|
||||
if (parent == null) {
|
||||
myRootObjects.remove(myObject);
|
||||
} else {
|
||||
parent.removeChild(node);
|
||||
}
|
||||
node.myParent = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/main/java/zmodem/FileCopyStreamEvent.kt
Normal file
34
src/main/java/zmodem/FileCopyStreamEvent.kt
Normal file
@@ -0,0 +1,34 @@
|
||||
package zmodem
|
||||
|
||||
import org.apache.commons.net.io.CopyStreamEvent
|
||||
|
||||
/**
|
||||
* 如果一共两个文件,并且传输第一个文件时:
|
||||
*
|
||||
* remaining = 1
|
||||
* index = 1
|
||||
*/
|
||||
class FileCopyStreamEvent(
|
||||
source: Any,
|
||||
// 本次传输的文件名
|
||||
val filename: String,
|
||||
// 剩余未传输的文件数量
|
||||
val remaining: Int,
|
||||
// 第几个文件
|
||||
val index: Int,
|
||||
// 总字节数
|
||||
totalBytesTransferred: Long,
|
||||
// 已经传输完成的字节数
|
||||
bytesTransferred: Int,
|
||||
// 本次传输的字节数
|
||||
streamSize: Long,
|
||||
/**
|
||||
* 这个文件被跳过了
|
||||
*/
|
||||
val skip: Boolean = false,
|
||||
) :
|
||||
CopyStreamEvent(
|
||||
source, totalBytesTransferred,
|
||||
bytesTransferred,
|
||||
streamSize
|
||||
)
|
||||
24
src/main/java/zmodem/XModem.java
Normal file
24
src/main/java/zmodem/XModem.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package zmodem;
|
||||
|
||||
import zmodem.xfer.zm.util.Modem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class XModem {
|
||||
private Modem modem;
|
||||
|
||||
public XModem(InputStream inputStream, OutputStream outputStream) {
|
||||
this.modem = new Modem(inputStream, outputStream);
|
||||
}
|
||||
|
||||
public void send(Path file, boolean useBlock1K) throws IOException, InterruptedException {
|
||||
modem.send(file, useBlock1K);
|
||||
}
|
||||
|
||||
public void receive(Path file) throws IOException {
|
||||
modem.receive(file, false);
|
||||
}
|
||||
}
|
||||
207
src/main/java/zmodem/YModem.java
Normal file
207
src/main/java/zmodem/YModem.java
Normal file
@@ -0,0 +1,207 @@
|
||||
package zmodem;
|
||||
|
||||
import zmodem.xfer.util.CRC16;
|
||||
import zmodem.xfer.util.CRC8;
|
||||
import zmodem.xfer.util.XCRC;
|
||||
import zmodem.xfer.zm.util.Modem;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* YModem.<br/>
|
||||
* Block 0 contain minimal file information (only filename)<br/>
|
||||
* <p>
|
||||
* Created by Anton Sirotinkin (aesirot@mail.ru), Moscow 2014<br/>
|
||||
* I hope you will find this program useful.<br/>
|
||||
* You are free to use/modify the code for any purpose, but please leave a reference to me.<br/>
|
||||
* <br/>
|
||||
*/
|
||||
public class YModem {
|
||||
private Modem modem;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param inputStream stream for reading received data from other side
|
||||
* @param outputStream stream for writing data to other side
|
||||
*/
|
||||
public YModem(InputStream inputStream, OutputStream outputStream) {
|
||||
this.modem = new Modem(inputStream, outputStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a file.<br/>
|
||||
* <p>
|
||||
* This method support correct thread interruption, when thread is interrupted "cancel of transmission" will be send.
|
||||
* So you can move long transmission to other thread and interrupt it according to your algorithm.
|
||||
*
|
||||
* @param file
|
||||
* @throws IOException
|
||||
*/
|
||||
public void send(Path file) throws IOException {
|
||||
//check filename
|
||||
// if (!file.getFileName().toString().matches("\\w{1,8}\\.\\w{1,3}")) {
|
||||
// throw new IOException("Filename must be in DOS style (no spaces, max 8.3)");
|
||||
// }
|
||||
|
||||
//open file
|
||||
try (DataInputStream dataStream = new DataInputStream(Files.newInputStream(file))) {
|
||||
|
||||
boolean useCRC16 = modem.waitReceiverRequest();
|
||||
XCRC crc;
|
||||
if (useCRC16)
|
||||
crc = new CRC16();
|
||||
else
|
||||
crc = new CRC8();
|
||||
|
||||
//send block 0
|
||||
BasicFileAttributes readAttributes = Files.readAttributes(file, BasicFileAttributes.class);
|
||||
String fileNameString = file.getFileName().toString() + (char) 0 + ((Long) Files.size(file)).toString() + " " + Long.toOctalString(readAttributes.lastModifiedTime().toMillis() / 1000);
|
||||
byte[] fileNameBytes = Arrays.copyOf(fileNameString.getBytes(), 128);
|
||||
modem.sendBlock(0, Arrays.copyOf(fileNameBytes, 128), 128, crc);
|
||||
|
||||
modem.waitReceiverRequest();
|
||||
//send data
|
||||
byte[] block = new byte[1024];
|
||||
modem.sendDataBlocks(dataStream, 1, crc, block);
|
||||
|
||||
modem.sendEOT();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send files in batch mode.<br/>
|
||||
* <p>
|
||||
* This method support correct thread interruption, when thread is interrupted "cancel of transmission" will be send.
|
||||
* So you can move long transmission to other thread and interrupt it according to your algorithm.
|
||||
*
|
||||
* @param files
|
||||
* @throws IOException
|
||||
*/
|
||||
public void batchSend(Path... files) throws IOException {
|
||||
for (Path file : files) {
|
||||
send(file);
|
||||
}
|
||||
|
||||
sendBatchStop();
|
||||
}
|
||||
|
||||
private void sendBatchStop() throws IOException {
|
||||
boolean useCRC16 = modem.waitReceiverRequest();
|
||||
XCRC crc;
|
||||
if (useCRC16)
|
||||
crc = new CRC16();
|
||||
else
|
||||
crc = new CRC8();
|
||||
|
||||
//send block 0
|
||||
byte[] bytes = new byte[128];
|
||||
modem.sendBlock(0, bytes, bytes.length, crc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive single file <br/>
|
||||
* <p>
|
||||
* This method support correct thread interruption, when thread is interrupted "cancel of transmission" will be send.
|
||||
* So you can move long transmission to other thread and interrupt it according to your algorithm.
|
||||
*
|
||||
* @param directory directory where file will be saved
|
||||
* @return path to created file
|
||||
* @throws IOException
|
||||
*/
|
||||
public Path receiveSingleFileInDirectory(Path directory) throws IOException {
|
||||
return receive(directory, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive files in batch mode <br/>
|
||||
* <p>
|
||||
* This method support correct thread interruption, when thread is interrupted "cancel of transmission" will be send.
|
||||
* So you can move long transmission to other thread and interrupt it according to your algorithm.
|
||||
*
|
||||
* @param directory directory where files will be saved
|
||||
* @throws IOException
|
||||
*/
|
||||
public void receiveFilesInDirectory(Path directory) throws IOException {
|
||||
while (receive(directory, true) != null) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive path <br/>
|
||||
* <p>
|
||||
* This method support correct thread interruption, when thread is interrupted "cancel of transmission" will be send.
|
||||
* So you can move long transmission to other thread and interrupt it according to your algorithm.
|
||||
*
|
||||
* @param path path to file where data will be saved
|
||||
* @return path to file
|
||||
* @throws IOException
|
||||
*/
|
||||
public Path receive(Path path) throws IOException {
|
||||
return receive(path, false);
|
||||
}
|
||||
|
||||
private Path receive(Path path, boolean inDirectory) throws IOException {
|
||||
DataOutputStream dataOutput = null;
|
||||
Path filePath;
|
||||
try {
|
||||
XCRC crc = new CRC16();
|
||||
int errorCount = 0;
|
||||
|
||||
// process block 0
|
||||
byte[] block;
|
||||
int character;
|
||||
while (true) {
|
||||
character = modem.requestTransmissionStart(true);
|
||||
try {
|
||||
// read file name from zero block
|
||||
block = modem.readBlock(0, (character == Modem.SOH), crc);
|
||||
|
||||
if (inDirectory) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (block[0] == 0) {
|
||||
//this is stop block of batch file transfer
|
||||
modem.sendByte(Modem.ACK);
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < block.length; i++) {
|
||||
if (block[i] == 0) {
|
||||
break;
|
||||
}
|
||||
sb.append((char) block[i]);
|
||||
}
|
||||
filePath = path.resolve(sb.toString());
|
||||
} else {
|
||||
filePath = path;
|
||||
}
|
||||
dataOutput = new DataOutputStream(Files.newOutputStream(filePath));
|
||||
modem.sendByte(Modem.ACK);
|
||||
break;
|
||||
} catch (Modem.InvalidBlockException e) {
|
||||
errorCount++;
|
||||
if (errorCount == Modem.MAXERRORS) {
|
||||
modem.interruptTransmission();
|
||||
throw new IOException("Transmission aborted, error count exceeded max");
|
||||
}
|
||||
modem.sendByte(Modem.NAK);
|
||||
} catch (Modem.RepeatedBlockException | Modem.SynchronizationLostException e) {
|
||||
//fatal transmission error
|
||||
modem.interruptTransmission();
|
||||
throw new IOException("Fatal transmission error", e);
|
||||
}
|
||||
}
|
||||
|
||||
//receive data blocks
|
||||
modem.receive(filePath, true);
|
||||
} finally {
|
||||
if (dataOutput != null) {
|
||||
dataOutput.close();
|
||||
}
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
46
src/main/java/zmodem/ZModem.java
Normal file
46
src/main/java/zmodem/ZModem.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package zmodem;
|
||||
|
||||
|
||||
import org.apache.commons.net.io.CopyStreamListener;
|
||||
import zmodem.util.FileAdapter;
|
||||
import zmodem.xfer.zm.util.ZModemReceive;
|
||||
import zmodem.xfer.zm.util.ZModemSend;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
|
||||
public class ZModem {
|
||||
|
||||
private final InputStream netIs;
|
||||
private final OutputStream netOs;
|
||||
private final AtomicBoolean isCancelled = new AtomicBoolean(false);
|
||||
|
||||
|
||||
public ZModem(InputStream netin, OutputStream netout) {
|
||||
netIs = netin;
|
||||
netOs = netout;
|
||||
}
|
||||
|
||||
public void receive(Supplier<FileAdapter> destDir, CopyStreamListener listener) throws IOException {
|
||||
ZModemReceive sender = new ZModemReceive(destDir, netIs, netOs);
|
||||
sender.addCopyStreamListener(listener);
|
||||
sender.receive(isCancelled::get);
|
||||
netOs.flush();
|
||||
}
|
||||
|
||||
public void send(Supplier<List<FileAdapter>> filesSupplier, CopyStreamListener listener) throws IOException {
|
||||
ZModemSend sender = new ZModemSend(filesSupplier, netIs, netOs);
|
||||
sender.addCopyStreamListener(listener);
|
||||
sender.send(isCancelled::get);
|
||||
netOs.flush();
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
isCancelled.compareAndSet(false, true);
|
||||
}
|
||||
}
|
||||
4
src/main/java/zmodem/package-info.java
Normal file
4
src/main/java/zmodem/package-info.java
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* https://github.com/scraymer/Zmodem-in-Java
|
||||
*/
|
||||
package zmodem;
|
||||
65
src/main/java/zmodem/util/CustomFile.java
Normal file
65
src/main/java/zmodem/util/CustomFile.java
Normal file
@@ -0,0 +1,65 @@
|
||||
package zmodem.util;
|
||||
|
||||
import org.apache.commons.io.input.RandomAccessFileInputStream;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
public class CustomFile implements FileAdapter {
|
||||
File file = null;
|
||||
|
||||
public CustomFile(File file) {
|
||||
super();
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return file.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return RandomAccessFileInputStream.builder()
|
||||
.setCloseOnClose(true)
|
||||
.setRandomAccessFile(new RandomAccessFile(file, "r"))
|
||||
.setBufferSize(1024 * 8)
|
||||
.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() throws IOException {
|
||||
return getOutputStream(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream(boolean append) throws IOException {
|
||||
return new BufferedOutputStream(new FileOutputStream(file, append));
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileAdapter getChild(String name) {
|
||||
if (name.equals(file.getName())) {
|
||||
return this;
|
||||
} else if (file.isDirectory()) {
|
||||
return new CustomFile(new File(file.getAbsolutePath(), name));
|
||||
}
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public long length() {
|
||||
return file.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory() {
|
||||
return file.isDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return file.exists();
|
||||
}
|
||||
|
||||
}
|
||||
42
src/main/java/zmodem/util/EmptyFileAdapter.kt
Normal file
42
src/main/java/zmodem/util/EmptyFileAdapter.kt
Normal file
@@ -0,0 +1,42 @@
|
||||
package zmodem.util
|
||||
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
class EmptyFileAdapter private constructor() : FileAdapter {
|
||||
companion object {
|
||||
val instance by lazy { EmptyFileAdapter() }
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getInputStream(): InputStream {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getOutputStream(): OutputStream {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getOutputStream(append: Boolean): OutputStream {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getChild(name: String?): FileAdapter {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun length(): Long {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isDirectory(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun exists(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
25
src/main/java/zmodem/util/FileAdapter.java
Normal file
25
src/main/java/zmodem/util/FileAdapter.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package zmodem.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public interface FileAdapter {
|
||||
public String getName();
|
||||
|
||||
public InputStream getInputStream() throws IOException;
|
||||
|
||||
public OutputStream getOutputStream() throws IOException;
|
||||
|
||||
public OutputStream getOutputStream(boolean append) throws IOException;
|
||||
|
||||
public FileAdapter getChild(String name);
|
||||
|
||||
public long length();
|
||||
|
||||
public boolean isDirectory();
|
||||
|
||||
public boolean exists();
|
||||
|
||||
public String toString();
|
||||
}
|
||||
7
src/main/java/zmodem/xfer/io/ObjectInputStream.java
Normal file
7
src/main/java/zmodem/xfer/io/ObjectInputStream.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package zmodem.xfer.io;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public abstract class ObjectInputStream<T> {
|
||||
public abstract T read() throws IOException;
|
||||
}
|
||||
7
src/main/java/zmodem/xfer/io/ObjectOutputStream.java
Normal file
7
src/main/java/zmodem/xfer/io/ObjectOutputStream.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package zmodem.xfer.io;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public abstract class ObjectOutputStream<T> {
|
||||
public abstract void write(T o) throws IOException;
|
||||
}
|
||||
27
src/main/java/zmodem/xfer/util/ASCII.java
Normal file
27
src/main/java/zmodem/xfer/util/ASCII.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package zmodem.xfer.util;
|
||||
|
||||
public enum ASCII {
|
||||
|
||||
SOH((byte) 0x01),
|
||||
STX((byte) 0x02),
|
||||
EOT((byte) 0x04),
|
||||
ENQ((byte) 0x05),
|
||||
ACK((byte) 0x06),
|
||||
BS((byte) 0x08),
|
||||
LF((byte) 0x0a),
|
||||
CR((byte) 0x0d),
|
||||
XON((byte) 0x11),
|
||||
XOFF((byte) 0x13),
|
||||
NAK((byte) 0x15),
|
||||
CAN((byte) 0x18);
|
||||
|
||||
private byte value;
|
||||
|
||||
private ASCII(byte b) {
|
||||
value = b;
|
||||
}
|
||||
|
||||
public byte value() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
91
src/main/java/zmodem/xfer/util/Arrays.java
Normal file
91
src/main/java/zmodem/xfer/util/Arrays.java
Normal file
@@ -0,0 +1,91 @@
|
||||
package zmodem.xfer.util;
|
||||
|
||||
/**
|
||||
* copyOf is not in java 5 java.util.Array
|
||||
*
|
||||
* @author justin
|
||||
*/
|
||||
public class Arrays {
|
||||
public enum Endianness {Little, Big;}
|
||||
|
||||
public static byte[] copyOf(byte[] original, int newLength) {
|
||||
byte[] copy = new byte[newLength];
|
||||
System.arraycopy(original, 0, copy, 0,
|
||||
Math.min(original.length, newLength));
|
||||
return copy;
|
||||
}
|
||||
|
||||
public static boolean equals(byte[] a, byte[] a2) {
|
||||
return java.util.Arrays.equals(a, a2);
|
||||
}
|
||||
|
||||
public static long toInteger(byte[] array, int size, Endianness endian) {
|
||||
long n = 0;
|
||||
int offset = 0, increment = 1;
|
||||
switch (endian) {
|
||||
case Little:
|
||||
increment = 1;
|
||||
offset = 0;
|
||||
break;
|
||||
case Big:
|
||||
increment = -1;
|
||||
offset = size - 1;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
n += (0xff & array[offset]) * (0x1 << i * 8);
|
||||
offset += increment;
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
public static short toShort(byte[] array, Endianness endian) {
|
||||
return (short) toInteger(array, 2, endian);
|
||||
}
|
||||
|
||||
public static int toInt(byte[] array, Endianness endian) {
|
||||
return (int) toInteger(array, 4, endian);
|
||||
}
|
||||
|
||||
public static long toLong(byte[] array, Endianness endian) {
|
||||
return toInteger(array, 8, endian);
|
||||
}
|
||||
|
||||
public static byte[] fromInteger(long n, int size, Endianness endian) {
|
||||
byte[] ret = new byte[size];
|
||||
int offset = 0, increment = 1;
|
||||
|
||||
switch (endian) {
|
||||
case Big:
|
||||
increment = -1;
|
||||
offset = size - 1;
|
||||
break;
|
||||
case Little:
|
||||
increment = 1;
|
||||
offset = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
ret[offset] = (byte) ((n >> (i * 8)) & 0xFF);
|
||||
offset += increment;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static byte[] fromShort(short s, Endianness endian) {
|
||||
return fromInteger(s, 2, endian);
|
||||
}
|
||||
|
||||
public static byte[] fromInt(int i, Endianness endian) {
|
||||
return fromInteger(i, 4, endian);
|
||||
}
|
||||
|
||||
public static byte[] fromLong(long i, Endianness endian) {
|
||||
return fromInteger(i, 8, endian);
|
||||
}
|
||||
}
|
||||
38
src/main/java/zmodem/xfer/util/Buffer.java
Normal file
38
src/main/java/zmodem/xfer/util/Buffer.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package zmodem.xfer.util;
|
||||
|
||||
public interface Buffer {
|
||||
public byte get();
|
||||
|
||||
public Buffer get(byte[] dst, int offset, int len);
|
||||
|
||||
public Buffer get(byte[] dst);
|
||||
|
||||
public Buffer put(byte b);
|
||||
|
||||
public Buffer put(byte[] dst, int offset, int len);
|
||||
|
||||
public Buffer put(byte[] dst);
|
||||
|
||||
public byte get(int index);
|
||||
|
||||
public Buffer get(int index, byte[] dst, int offset, int len);
|
||||
|
||||
public Buffer get(int index, byte[] dst);
|
||||
|
||||
public Buffer put(int index, byte b);
|
||||
|
||||
public Buffer put(int index, byte[] dst, int offset, int len);
|
||||
|
||||
public Buffer put(int index, byte[] dst);
|
||||
|
||||
public void flip();
|
||||
|
||||
public int remaining();
|
||||
|
||||
public boolean hasRemaining();
|
||||
|
||||
public HexBuffer asHexBuffer();
|
||||
|
||||
public ByteBuffer asByteBuffer();
|
||||
|
||||
}
|
||||
193
src/main/java/zmodem/xfer/util/ByteBuffer.java
Normal file
193
src/main/java/zmodem/xfer/util/ByteBuffer.java
Normal file
@@ -0,0 +1,193 @@
|
||||
package zmodem.xfer.util;
|
||||
|
||||
|
||||
public class ByteBuffer implements Buffer {
|
||||
|
||||
private java.nio.ByteBuffer _wrapped;
|
||||
|
||||
public ByteBuffer(java.nio.ByteBuffer b) {
|
||||
_wrapped = b;
|
||||
}
|
||||
|
||||
public static ByteBuffer allocate(int capacity) {
|
||||
return new ByteBuffer(java.nio.ByteBuffer.allocate(capacity));
|
||||
}
|
||||
|
||||
public static ByteBuffer allocateDirect(int capacity) {
|
||||
return new ByteBuffer(java.nio.ByteBuffer.allocateDirect(capacity));
|
||||
}
|
||||
|
||||
public Buffer slice() {
|
||||
return new ByteBuffer(_wrapped.slice());
|
||||
}
|
||||
|
||||
public Buffer duplicate() {
|
||||
return new ByteBuffer(_wrapped.duplicate());
|
||||
}
|
||||
|
||||
public Buffer asReadOnlyBuffer() {
|
||||
return new ByteBuffer(_wrapped.asReadOnlyBuffer());
|
||||
}
|
||||
|
||||
public byte get() {
|
||||
return _wrapped.get();
|
||||
}
|
||||
|
||||
public Buffer get(byte[] dst, int offset, int len) {
|
||||
for (; offset < len; offset++)
|
||||
dst[offset] = get();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Buffer get(byte[] dst) {
|
||||
return get(dst, 0, dst.length);
|
||||
}
|
||||
|
||||
public Buffer put(byte b) {
|
||||
_wrapped.put(b);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Buffer put(byte[] dst, int offset, int len) {
|
||||
for (; offset < len; offset++)
|
||||
put(dst[offset]);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Buffer put(byte[] dst) {
|
||||
return put(dst, 0, dst.length);
|
||||
}
|
||||
|
||||
public byte get(int index) {
|
||||
return _wrapped.get(index);
|
||||
}
|
||||
|
||||
public Buffer get(int index, byte[] dst, int offset, int len) {
|
||||
for (; offset < len; offset++)
|
||||
dst[offset] = get(index++);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Buffer get(int index, byte[] dst) {
|
||||
return get(index, dst, 0, dst.length);
|
||||
}
|
||||
|
||||
public Buffer put(int index, byte b) {
|
||||
_wrapped.put(index, b);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Buffer put(int index, byte[] dst, int offset, int len) {
|
||||
for (; offset < len; offset++)
|
||||
put(index++, dst[offset]);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Buffer put(int index, byte[] dst) {
|
||||
return put(index, dst, 0, dst.length);
|
||||
}
|
||||
|
||||
public Buffer compact() {
|
||||
_wrapped.compact();
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isDirect() {
|
||||
return _wrapped.isDirect();
|
||||
}
|
||||
|
||||
public char getChar() {
|
||||
return (char) get();
|
||||
}
|
||||
|
||||
public Buffer putChar(char value) {
|
||||
return put((byte) value);
|
||||
}
|
||||
|
||||
public char getChar(int index) {
|
||||
return (char) get(index);
|
||||
}
|
||||
|
||||
public Buffer putChar(int index, char value) {
|
||||
return put(index, (byte) value);
|
||||
}
|
||||
|
||||
public HexBuffer asHexBuffer() {
|
||||
return new HexBuffer(_wrapped);
|
||||
}
|
||||
|
||||
public short getShort() {
|
||||
return Arrays.toShort(new byte[]{get(), get()}, Arrays.Endianness.Little);
|
||||
}
|
||||
|
||||
public Buffer putShort(short value) {
|
||||
return put(Arrays.fromShort(value, Arrays.Endianness.Little));
|
||||
}
|
||||
|
||||
public short getShort(int index) {
|
||||
return Arrays.toShort(new byte[]{get(index), get(index + 1)}, Arrays.Endianness.Little);
|
||||
}
|
||||
|
||||
public Buffer putShort(int index, short value) {
|
||||
return put(index, Arrays.fromShort(value, Arrays.Endianness.Little));
|
||||
}
|
||||
|
||||
public int getInt() {
|
||||
return Arrays.toInt(new byte[]{get(), get(), get(), get()}, Arrays.Endianness.Little);
|
||||
}
|
||||
|
||||
public Buffer putInt(int value) {
|
||||
return put(Arrays.fromInt(value, Arrays.Endianness.Little));
|
||||
}
|
||||
|
||||
public int getInt(int index) {
|
||||
return Arrays.toInt(new byte[]{get(index), get(index + 1), get(index + 2), get(index + 3)}, Arrays.Endianness.Little);
|
||||
}
|
||||
|
||||
public Buffer putInt(int index, int value) {
|
||||
return put(index, Arrays.fromInt(value, Arrays.Endianness.Little));
|
||||
}
|
||||
|
||||
public long getLong() {
|
||||
return Arrays.toLong(new byte[]{get(), get(), get(), get(), get(), get(), get(), get()}, Arrays.Endianness.Little);
|
||||
}
|
||||
|
||||
public Buffer putLong(long value) {
|
||||
return put(Arrays.fromLong(value, Arrays.Endianness.Little));
|
||||
}
|
||||
|
||||
public long getLong(int index) {
|
||||
return Arrays.toLong(new byte[]{get(index), get(index + 1), get(index + 2), get(index + 3), get(index + 4), get(index + 5), get(index + 6), get(index + 7)}, Arrays.Endianness.Little);
|
||||
}
|
||||
|
||||
public Buffer putLong(int index, long value) {
|
||||
return put(index, Arrays.fromLong(value, Arrays.Endianness.Little));
|
||||
}
|
||||
|
||||
public boolean isReadOnly() {
|
||||
return _wrapped.isReadOnly();
|
||||
}
|
||||
|
||||
public void flip() {
|
||||
_wrapped.flip();
|
||||
}
|
||||
|
||||
public int remaining() {
|
||||
return _wrapped.remaining();
|
||||
}
|
||||
|
||||
public boolean hasRemaining() {
|
||||
return remaining() > 0;
|
||||
}
|
||||
|
||||
public ByteBuffer asByteBuffer() {
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
232
src/main/java/zmodem/xfer/util/CRC.java
Normal file
232
src/main/java/zmodem/xfer/util/CRC.java
Normal file
@@ -0,0 +1,232 @@
|
||||
package zmodem.xfer.util;
|
||||
|
||||
|
||||
public class CRC {
|
||||
|
||||
public static enum Type {
|
||||
CRC16(2, 0),
|
||||
CRC32(4, 0xffffffff);
|
||||
private int numbytes;
|
||||
private int initial;
|
||||
|
||||
private Type(int s, int i) {
|
||||
numbytes = s;
|
||||
initial = i;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return numbytes;
|
||||
}
|
||||
|
||||
public int initial() {
|
||||
return initial;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied from the original zmodem source
|
||||
*/
|
||||
private static final int crctab[] = {
|
||||
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
|
||||
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
|
||||
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
|
||||
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
|
||||
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
|
||||
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
|
||||
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
|
||||
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
|
||||
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
|
||||
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
|
||||
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
|
||||
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
|
||||
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
|
||||
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
|
||||
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
|
||||
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
|
||||
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
|
||||
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
|
||||
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
|
||||
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
|
||||
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
|
||||
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
|
||||
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
|
||||
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
|
||||
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
|
||||
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
|
||||
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
|
||||
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
|
||||
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
|
||||
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
|
||||
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
|
||||
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
|
||||
};
|
||||
/**
|
||||
* Copied from the original zmodem source
|
||||
*/
|
||||
private static final int cr3tab[] = { /* CRC polynomial 0xedb88320 */
|
||||
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
|
||||
0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
|
||||
0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
|
||||
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
|
||||
0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
|
||||
0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
|
||||
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
|
||||
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
|
||||
0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
|
||||
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
|
||||
0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
|
||||
0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
|
||||
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
|
||||
0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
|
||||
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
|
||||
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
|
||||
0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
|
||||
0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
|
||||
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
|
||||
0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
|
||||
0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
|
||||
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
|
||||
0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
|
||||
0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
|
||||
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
|
||||
0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
|
||||
0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
|
||||
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
|
||||
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
|
||||
0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
|
||||
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
|
||||
0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
|
||||
};
|
||||
|
||||
private static int updcrc(int cp, int crc) {
|
||||
//System.out.printf("updcrc: %08x / %08x\n",cp,crc);
|
||||
return (crctab[(crc >> 8) & 0xff] ^ (crc << 8) ^ cp);
|
||||
}
|
||||
|
||||
private static int updcrc32(int b, int c) {
|
||||
return (cr3tab[(c ^ b) & 0xff] ^ ((c >> 8) & 0x00FFFFFF));
|
||||
}
|
||||
|
||||
|
||||
public static byte[] arrayCRC(Type t, byte[] bytes) {
|
||||
switch (t) {
|
||||
case CRC16:
|
||||
return arrayCRC16(bytes);
|
||||
case CRC32:
|
||||
return arrayCRC32(bytes);
|
||||
}
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bytes
|
||||
* @return
|
||||
*/
|
||||
public static byte[] arrayCRC16(byte[] bytes) {
|
||||
byte[] bb = new byte[2];
|
||||
int value = CRC16(bytes);
|
||||
bb[1] = (byte) (value & 0xFF);
|
||||
bb[0] = (byte) ((value >> 8) & 0xFF);
|
||||
return bb;
|
||||
|
||||
}
|
||||
|
||||
public static int CRC16(byte[] bytes) {
|
||||
int crc = 0;
|
||||
|
||||
for (byte b : bytes)
|
||||
crc = updcrc(0xff & b, crc);
|
||||
|
||||
crc = updcrc(0, updcrc(0, crc));
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bytes
|
||||
* @return
|
||||
*/
|
||||
public static byte[] arrayCRC32(byte[] bytes) {
|
||||
byte[] bb = new byte[4];
|
||||
long value = CRC32(bytes);
|
||||
bb[3] = (byte) (value & 0xFF);
|
||||
bb[2] = (byte) ((value >> 8) & 0xFF);
|
||||
bb[1] = (byte) ((value >> 16) & 0xFF);
|
||||
bb[0] = (byte) ((value >> 24) & 0xFF);
|
||||
return bb;
|
||||
|
||||
}
|
||||
|
||||
public static int CRC32(byte[] bytes) {
|
||||
int crc = 0xFFFFFFFF;
|
||||
|
||||
for (byte b : bytes)
|
||||
crc = updcrc32(b, crc);
|
||||
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
private Type type;
|
||||
private int crc;
|
||||
|
||||
public CRC(Type t) {
|
||||
type = t;
|
||||
crc = type.initial();
|
||||
}
|
||||
|
||||
public void update(byte b) {
|
||||
switch (type) {
|
||||
case CRC16:
|
||||
crc = updcrc(0xff & b, crc);
|
||||
break;
|
||||
case CRC32:
|
||||
crc = updcrc32((0xff & b), crc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void update(byte[] array) {
|
||||
for (byte b : array)
|
||||
update(b);
|
||||
}
|
||||
|
||||
public void finalized() {
|
||||
switch (type) {
|
||||
case CRC16:
|
||||
crc = 0xffff & updcrc(0, updcrc(0, crc));
|
||||
break;
|
||||
case CRC32:
|
||||
crc = ~crc;
|
||||
break;
|
||||
}
|
||||
//System.out.printf("crc: %08x\n",crc);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return type.size();
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
byte[] bb;
|
||||
switch (type) {
|
||||
case CRC16:
|
||||
bb = new byte[2];
|
||||
bb[1] = (byte) (crc & 0xFF);
|
||||
bb[0] = (byte) ((crc >> 8) & 0xFF);
|
||||
return bb;
|
||||
case CRC32:
|
||||
bb = new byte[4];
|
||||
bb[3] = (byte) (crc & 0xFF);
|
||||
bb[2] = (byte) ((crc >> 8) & 0xFF);
|
||||
bb[1] = (byte) ((crc >> 16) & 0xFF);
|
||||
bb[0] = (byte) ((crc >> 24) & 0xFF);
|
||||
return bb;
|
||||
}
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
public Type type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
}
|
||||
58
src/main/java/zmodem/xfer/util/CRC16.java
Normal file
58
src/main/java/zmodem/xfer/util/CRC16.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package zmodem.xfer.util;
|
||||
|
||||
/**
|
||||
* Uses table for irreducible polynomial: 1 + x^2 + x^15 + x^16
|
||||
*/
|
||||
|
||||
public class CRC16 implements XCRC {
|
||||
|
||||
private static int[] table = {
|
||||
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
|
||||
0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
|
||||
0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
|
||||
0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
|
||||
0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,
|
||||
0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D,
|
||||
0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
|
||||
0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,
|
||||
0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
|
||||
0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,
|
||||
0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
|
||||
0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,
|
||||
0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
|
||||
0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
|
||||
0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,
|
||||
0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78,
|
||||
0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F,
|
||||
0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,
|
||||
0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,
|
||||
0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256,
|
||||
0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
|
||||
0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
|
||||
0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,
|
||||
0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
|
||||
0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,
|
||||
0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
|
||||
0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
|
||||
0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
|
||||
0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9,
|
||||
0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
|
||||
0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8,
|
||||
0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0,
|
||||
};
|
||||
|
||||
@Override
|
||||
public int getCRCLength() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long calcCRC(byte[] block) {
|
||||
int crc = 0x0000;
|
||||
for (byte b : block) {
|
||||
crc = ((crc << 8) ^ table[((crc >> 8) ^ (0xff & b))]) & 0xFFFF;
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
}
|
||||
21
src/main/java/zmodem/xfer/util/CRC8.java
Normal file
21
src/main/java/zmodem/xfer/util/CRC8.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package zmodem.xfer.util;
|
||||
|
||||
/**
|
||||
* Created by asirotinkin on 11.11.2014.
|
||||
*/
|
||||
public class CRC8 implements XCRC {
|
||||
@Override
|
||||
public int getCRCLength() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long calcCRC(byte[] block) {
|
||||
byte checkSumma = 0;
|
||||
for (int i = 0; i < block.length; i++) {
|
||||
checkSumma += block[i];
|
||||
}
|
||||
return checkSumma;
|
||||
}
|
||||
|
||||
}
|
||||
235
src/main/java/zmodem/xfer/util/HexBuffer.java
Normal file
235
src/main/java/zmodem/xfer/util/HexBuffer.java
Normal file
@@ -0,0 +1,235 @@
|
||||
package zmodem.xfer.util;
|
||||
|
||||
|
||||
import zmodem.xfer.util.Arrays.Endianness;
|
||||
|
||||
public class HexBuffer implements Buffer {
|
||||
|
||||
private static final byte[] hx = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
|
||||
|
||||
public static byte[] binToHex(byte[] bin) {
|
||||
byte[] hex = new byte[bin.length * 2];
|
||||
for (int i = 0; i < bin.length; i++)
|
||||
System.arraycopy(toHex(bin[i]), 0, hex, i * 2, 2);
|
||||
|
||||
return hex;
|
||||
}
|
||||
|
||||
public static byte[] hexToBin(byte[] hex) {
|
||||
byte[] bin = new byte[hex.length / 2];
|
||||
for (int i = 0; i < bin.length; i++) {
|
||||
byte[] bn = new byte[2];
|
||||
System.arraycopy(hex, i * 2, bn, 0, 2);
|
||||
bin[i] = toByte(bn);
|
||||
}
|
||||
|
||||
return bin;
|
||||
}
|
||||
|
||||
private static byte toByte(byte[] array) {
|
||||
int d;
|
||||
|
||||
d = java.util.Arrays.binarySearch(hx, array[0]) * 16;
|
||||
d += java.util.Arrays.binarySearch(hx, array[1]);
|
||||
|
||||
return (byte) d;
|
||||
}
|
||||
|
||||
private static byte[] toHex(byte b) {
|
||||
byte[] array = new byte[2];
|
||||
|
||||
array[0] = hx[((b >> 4) & 0xF)];
|
||||
array[1] = hx[(b & 0xF)];
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
private java.nio.ByteBuffer _wrapped;
|
||||
|
||||
protected HexBuffer(java.nio.ByteBuffer b) {
|
||||
_wrapped = b;
|
||||
}
|
||||
|
||||
public static HexBuffer allocate(int capacity) {
|
||||
return new HexBuffer(java.nio.ByteBuffer.allocate(capacity * 2));
|
||||
}
|
||||
|
||||
public static HexBuffer allocateDirect(int capacity) {
|
||||
return new HexBuffer(java.nio.ByteBuffer.allocateDirect(capacity * 2));
|
||||
}
|
||||
|
||||
public Buffer slice() {
|
||||
return new HexBuffer(_wrapped.slice());
|
||||
}
|
||||
|
||||
public Buffer duplicate() {
|
||||
return new HexBuffer(_wrapped.duplicate());
|
||||
}
|
||||
|
||||
public Buffer asReadOnlyBuffer() {
|
||||
return new HexBuffer(_wrapped.asReadOnlyBuffer());
|
||||
}
|
||||
|
||||
public byte get() {
|
||||
return toByte(new byte[]{_wrapped.get(), _wrapped.get()});
|
||||
}
|
||||
|
||||
public Buffer get(byte[] dst, int offset, int len) {
|
||||
for (; offset < len; offset++)
|
||||
dst[offset] = get();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Buffer get(byte[] dst) {
|
||||
return get(dst, 0, dst.length);
|
||||
}
|
||||
|
||||
public Buffer put(byte b) {
|
||||
_wrapped.put(toHex(b));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Buffer put(byte[] dst, int offset, int len) {
|
||||
for (; offset < len; offset++)
|
||||
put(dst[offset]);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Buffer put(byte[] dst) {
|
||||
return put(dst, 0, dst.length);
|
||||
}
|
||||
|
||||
public byte get(int index) {
|
||||
return toByte(new byte[]{_wrapped.get(index * 2), _wrapped.get(index * 2 + 1)});
|
||||
}
|
||||
|
||||
public Buffer get(int index, byte[] dst, int offset, int len) {
|
||||
for (; offset < len; offset++)
|
||||
dst[offset] = get(index++);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Buffer get(int index, byte[] dst) {
|
||||
return get(index, dst, 0, dst.length);
|
||||
}
|
||||
|
||||
public Buffer put(int index, byte b) {
|
||||
byte[] array = toHex(b);
|
||||
_wrapped.put(index * 2, array[0]);
|
||||
_wrapped.put(index * 2 + 1, array[1]);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Buffer put(int index, byte[] dst, int offset, int len) {
|
||||
for (; offset < len; offset++)
|
||||
put(index++, dst[offset]);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Buffer put(int index, byte[] dst) {
|
||||
return put(index, dst, 0, dst.length);
|
||||
}
|
||||
|
||||
public Buffer compact() {
|
||||
_wrapped.compact();
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isDirect() {
|
||||
return _wrapped.isDirect();
|
||||
}
|
||||
|
||||
public char getChar() {
|
||||
return (char) get();
|
||||
}
|
||||
|
||||
public Buffer putChar(char value) {
|
||||
return put((byte) value);
|
||||
}
|
||||
|
||||
public char getChar(int index) {
|
||||
return (char) get(index);
|
||||
}
|
||||
|
||||
public Buffer putChar(int index, char value) {
|
||||
return put(index, (byte) value);
|
||||
}
|
||||
|
||||
public ByteBuffer asByteBuffer() {
|
||||
return new ByteBuffer(_wrapped);
|
||||
}
|
||||
|
||||
public short getShort() {
|
||||
return Arrays.toShort(new byte[]{get(), get()}, Endianness.Little);
|
||||
}
|
||||
|
||||
public Buffer putShort(short value) {
|
||||
return put(Arrays.fromShort(value, Endianness.Little));
|
||||
}
|
||||
|
||||
public short getShort(int index) {
|
||||
return Arrays.toShort(new byte[]{get(index), get(index + 1)}, Endianness.Little);
|
||||
}
|
||||
|
||||
public Buffer putShort(int index, short value) {
|
||||
return put(index, Arrays.fromShort(value, Endianness.Little));
|
||||
}
|
||||
|
||||
public int getInt() {
|
||||
return Arrays.toInt(new byte[]{get(), get(), get(), get()}, Endianness.Little);
|
||||
}
|
||||
|
||||
public Buffer putInt(int value) {
|
||||
return put(Arrays.fromInt(value, Endianness.Little));
|
||||
}
|
||||
|
||||
public int getInt(int index) {
|
||||
return Arrays.toInt(new byte[]{get(index), get(index + 1), get(index + 2), get(index + 3)}, Endianness.Little);
|
||||
}
|
||||
|
||||
public Buffer putInt(int index, int value) {
|
||||
return put(index, Arrays.fromInt(value, Endianness.Little));
|
||||
}
|
||||
|
||||
public long getLong() {
|
||||
return Arrays.toLong(new byte[]{get(), get(), get(), get(), get(), get(), get(), get()}, Endianness.Little);
|
||||
}
|
||||
|
||||
public Buffer putLong(long value) {
|
||||
return put(Arrays.fromLong(value, Endianness.Little));
|
||||
}
|
||||
|
||||
public long getLong(int index) {
|
||||
return Arrays.toLong(new byte[]{get(index), get(index + 1), get(index + 2), get(index + 3), get(index + 4), get(index + 5), get(index + 6), get(index + 7)}, Endianness.Little);
|
||||
}
|
||||
|
||||
public Buffer putLong(int index, long value) {
|
||||
return put(index, Arrays.fromLong(value, Endianness.Little));
|
||||
}
|
||||
|
||||
public boolean isReadOnly() {
|
||||
return _wrapped.isReadOnly();
|
||||
}
|
||||
|
||||
public void flip() {
|
||||
_wrapped.flip();
|
||||
}
|
||||
|
||||
public int remaining() {
|
||||
double rem = ((double) _wrapped.remaining() / 2.0d);
|
||||
return (int) Math.floor(rem);
|
||||
}
|
||||
|
||||
public boolean hasRemaining() {
|
||||
return (_wrapped.remaining() > 1);
|
||||
}
|
||||
|
||||
public HexBuffer asHexBuffer() {
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package zmodem.xfer.util;
|
||||
|
||||
public class InvalidChecksumException extends RuntimeException {
|
||||
private static final long serialVersionUID = 3864874377147160043L;
|
||||
|
||||
}
|
||||
7
src/main/java/zmodem/xfer/util/TimeoutException.java
Normal file
7
src/main/java/zmodem/xfer/util/TimeoutException.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package zmodem.xfer.util;
|
||||
|
||||
/**
|
||||
* Created by asirotinkin on 12.11.2014.
|
||||
*/
|
||||
class TimeoutException extends Exception {
|
||||
}
|
||||
10
src/main/java/zmodem/xfer/util/XCRC.java
Normal file
10
src/main/java/zmodem/xfer/util/XCRC.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package zmodem.xfer.util;
|
||||
|
||||
/**
|
||||
* Created by Muzeffer on 2016/6/30.
|
||||
*/
|
||||
public interface XCRC {
|
||||
int getCRCLength();
|
||||
|
||||
long calcCRC(byte[] block);
|
||||
}
|
||||
29
src/main/java/zmodem/xfer/zm/packet/Cancel.java
Normal file
29
src/main/java/zmodem/xfer/zm/packet/Cancel.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package zmodem.xfer.zm.packet;
|
||||
|
||||
|
||||
import zmodem.xfer.util.ASCII;
|
||||
import zmodem.xfer.util.Buffer;
|
||||
import zmodem.xfer.util.ByteBuffer;
|
||||
import zmodem.xfer.zm.util.ZMPacket;
|
||||
|
||||
public class Cancel extends ZMPacket {
|
||||
|
||||
@Override
|
||||
public Buffer marshall() {
|
||||
ByteBuffer buff = ByteBuffer.allocate(16);
|
||||
|
||||
for (int i = 0; i < 8; i++)
|
||||
buff.put(ASCII.CAN.value());
|
||||
for (int i = 0; i < 8; i++)
|
||||
buff.put(ASCII.BS.value());
|
||||
|
||||
buff.flip();
|
||||
|
||||
return buff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Cancel: CAN * 8 + BS * 8";
|
||||
}
|
||||
}
|
||||
92
src/main/java/zmodem/xfer/zm/packet/DataPacket.java
Normal file
92
src/main/java/zmodem/xfer/zm/packet/DataPacket.java
Normal file
@@ -0,0 +1,92 @@
|
||||
package zmodem.xfer.zm.packet;
|
||||
|
||||
|
||||
import zmodem.xfer.util.*;
|
||||
import zmodem.xfer.zm.util.ZDLEEncoder;
|
||||
import zmodem.xfer.zm.util.ZMPacket;
|
||||
import zmodem.xfer.zm.util.ZModemCharacter;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
public class DataPacket extends ZMPacket {
|
||||
|
||||
public static DataPacket unmarshall(Buffer buff, CRC crc) {
|
||||
byte[] data = new byte[buff.remaining() - crc.size() - 1];
|
||||
|
||||
buff.get(data);
|
||||
|
||||
ZModemCharacter type;
|
||||
type = ZModemCharacter.forbyte(buff.get());
|
||||
|
||||
|
||||
byte[] netCrc = new byte[crc.size()];
|
||||
buff.get(netCrc);
|
||||
|
||||
if (!Arrays.equals(netCrc, crc.getBytes()))
|
||||
throw new InvalidChecksumException();
|
||||
|
||||
return new DataPacket(type, data);
|
||||
}
|
||||
|
||||
|
||||
private final ZModemCharacter type;
|
||||
private byte[] data = new byte[0];
|
||||
|
||||
public DataPacket(ZModemCharacter fe) {
|
||||
type = fe;
|
||||
}
|
||||
|
||||
public DataPacket(ZModemCharacter fr, byte[] d) {
|
||||
this(fr);
|
||||
data = d;
|
||||
}
|
||||
|
||||
public ZModemCharacter type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public byte[] data() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(byte[] d) {
|
||||
data = d;
|
||||
}
|
||||
|
||||
public void copyData(byte[] d) {
|
||||
data = Arrays.copyOf(d, d.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Buffer marshall() {
|
||||
ZDLEEncoder encoder;
|
||||
ByteBuffer buff = ByteBuffer.allocate(data.length * 2 + 64);
|
||||
|
||||
CRC crc = new CRC(CRC.Type.CRC16);
|
||||
|
||||
encoder = new ZDLEEncoder(data);
|
||||
|
||||
crc.update(data);
|
||||
buff.put(encoder.zdle(), 0, encoder.zdleLen());
|
||||
|
||||
buff.put(ZModemCharacter.ZDLE.value());
|
||||
|
||||
crc.update(type.value());
|
||||
buff.put(type.value());
|
||||
|
||||
crc.finalized();
|
||||
|
||||
encoder = new ZDLEEncoder(crc.getBytes());
|
||||
buff.put(encoder.zdle(), 0, encoder.zdleLen());
|
||||
|
||||
buff.flip();
|
||||
|
||||
return buff;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return type + ":" + data.length + " bytes";
|
||||
}
|
||||
}
|
||||
27
src/main/java/zmodem/xfer/zm/packet/Finish.java
Normal file
27
src/main/java/zmodem/xfer/zm/packet/Finish.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package zmodem.xfer.zm.packet;
|
||||
|
||||
|
||||
import zmodem.xfer.util.Buffer;
|
||||
import zmodem.xfer.util.ByteBuffer;
|
||||
import zmodem.xfer.zm.util.ZMPacket;
|
||||
|
||||
public class Finish extends ZMPacket {
|
||||
|
||||
@Override
|
||||
public Buffer marshall() {
|
||||
ByteBuffer buff = ByteBuffer.allocate(16);
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
buff.put((byte) 'O');
|
||||
|
||||
buff.flip();
|
||||
|
||||
return buff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Finish: OO";
|
||||
}
|
||||
|
||||
}
|
||||
48
src/main/java/zmodem/xfer/zm/packet/Format.java
Normal file
48
src/main/java/zmodem/xfer/zm/packet/Format.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package zmodem.xfer.zm.packet;
|
||||
|
||||
|
||||
import zmodem.xfer.util.CRC;
|
||||
import zmodem.xfer.zm.util.ZModemCharacter;
|
||||
|
||||
public enum Format {
|
||||
|
||||
BIN32(1, CRC.Type.CRC32, ZModemCharacter.ZBIN32),
|
||||
BIN(1, CRC.Type.CRC16, ZModemCharacter.ZBIN),
|
||||
HEX(2, CRC.Type.CRC16, ZModemCharacter.ZHEX);
|
||||
|
||||
|
||||
public static Format fromByte(byte b) {
|
||||
for (Format ft : values()) {
|
||||
if (ft.character() == b)
|
||||
return ft;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private int width;
|
||||
private CRC.Type crc;
|
||||
private ZModemCharacter character;
|
||||
|
||||
private Format(int bw, CRC.Type crct, ZModemCharacter fmt) {
|
||||
width = bw;
|
||||
crc = crct;
|
||||
character = fmt;
|
||||
}
|
||||
|
||||
public CRC.Type crc() {
|
||||
return crc;
|
||||
}
|
||||
|
||||
public byte character() {
|
||||
return character.value();
|
||||
}
|
||||
|
||||
public int width() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public boolean hex() {
|
||||
return (this == HEX);
|
||||
}
|
||||
|
||||
}
|
||||
130
src/main/java/zmodem/xfer/zm/packet/Header.java
Normal file
130
src/main/java/zmodem/xfer/zm/packet/Header.java
Normal file
@@ -0,0 +1,130 @@
|
||||
package zmodem.xfer.zm.packet;
|
||||
|
||||
|
||||
import zmodem.xfer.util.*;
|
||||
import zmodem.xfer.zm.util.ZDLEEncoder;
|
||||
import zmodem.xfer.zm.util.ZMPacket;
|
||||
import zmodem.xfer.zm.util.ZModemCharacter;
|
||||
|
||||
public class Header extends ZMPacket {
|
||||
|
||||
public static Header unmarshall(Buffer buff) {
|
||||
|
||||
Format fmt = null;
|
||||
|
||||
while (fmt == null)
|
||||
fmt = Format.fromByte(buff.get());
|
||||
|
||||
if (fmt.hex())
|
||||
buff = buff.asHexBuffer();
|
||||
|
||||
CRC crc = new CRC(fmt.crc());
|
||||
byte b;
|
||||
|
||||
b = buff.get();
|
||||
crc.update(b);
|
||||
ZModemCharacter type = ZModemCharacter.forbyte(b);
|
||||
|
||||
byte[] data = new byte[4];
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
b = buff.get();
|
||||
crc.update(b);
|
||||
data[i] = b;
|
||||
}
|
||||
crc.finalized();
|
||||
|
||||
byte[] netCrc = new byte[crc.size()];
|
||||
buff.get(netCrc);
|
||||
|
||||
if (!Arrays.equals(netCrc, crc.getBytes()))
|
||||
throw new InvalidChecksumException();
|
||||
|
||||
return new Header(fmt, type, data);
|
||||
}
|
||||
|
||||
|
||||
private Format format;
|
||||
private ZModemCharacter type;
|
||||
private byte[] data = {0, 0, 0, 0};
|
||||
|
||||
private Header(Format fFmt) {
|
||||
format = fFmt;
|
||||
}
|
||||
|
||||
public Header(Format fFmt, ZModemCharacter fType) {
|
||||
this(fFmt);
|
||||
type = fType;
|
||||
}
|
||||
|
||||
public Header(Format fFmt, ZModemCharacter fType, byte[] flags) {
|
||||
this(fFmt, fType);
|
||||
setFlags(flags);
|
||||
}
|
||||
|
||||
public Header(Format fFmt, ZModemCharacter fType, int pos) {
|
||||
this(fFmt, fType);
|
||||
setPos(pos);
|
||||
}
|
||||
|
||||
|
||||
public ZModemCharacter type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Format format() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public void setFlags(byte[] flags) {
|
||||
data = Arrays.copyOf(flags, flags.length);
|
||||
}
|
||||
|
||||
public byte[] getFlags() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setPos(int num) {
|
||||
data = Arrays.fromInt(num, Arrays.Endianness.Little);
|
||||
}
|
||||
|
||||
public int getPos() {
|
||||
return Arrays.toInt(data, Arrays.Endianness.Little);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Buffer marshall() {
|
||||
ZDLEEncoder encoder;
|
||||
|
||||
Buffer buff;
|
||||
if (format.hex())
|
||||
buff = HexBuffer.allocate(16);
|
||||
else
|
||||
buff = ByteBuffer.allocate(32);
|
||||
|
||||
CRC crc = new CRC(format.crc());
|
||||
|
||||
crc.update(type.value());
|
||||
buff.put(type.value());
|
||||
|
||||
crc.update(data);
|
||||
encoder = new ZDLEEncoder(data, format);
|
||||
buff.put(encoder.zdle(), 0, encoder.zdleLen());
|
||||
|
||||
|
||||
crc.finalized();
|
||||
|
||||
encoder = new ZDLEEncoder(crc.getBytes(), format);
|
||||
buff.put(encoder.zdle(), 0, encoder.zdleLen());
|
||||
|
||||
buff.flip();
|
||||
|
||||
return buff.asByteBuffer();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return type + ", " + format + ", " + "{" + data[0] + "," + data[1] + "," + data[2] + "," + data[3] + "}";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package zmodem.xfer.zm.packet;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class InvalidPacketException extends IOException {
|
||||
|
||||
private static final long serialVersionUID = 6436104259898858243L;
|
||||
|
||||
}
|
||||
5
src/main/java/zmodem/xfer/zm/proto/Action.java
Normal file
5
src/main/java/zmodem/xfer/zm/proto/Action.java
Normal file
@@ -0,0 +1,5 @@
|
||||
package zmodem.xfer.zm.proto;
|
||||
|
||||
public enum Action {
|
||||
ESCAPE, DATA, HEADER, CANCEL, FINISH;
|
||||
}
|
||||
99
src/main/java/zmodem/xfer/zm/proto/Escape.java
Normal file
99
src/main/java/zmodem/xfer/zm/proto/Escape.java
Normal file
@@ -0,0 +1,99 @@
|
||||
package zmodem.xfer.zm.proto;
|
||||
|
||||
import zmodem.xfer.zm.util.ZModemCharacter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class Escape {
|
||||
|
||||
private int len = 0;
|
||||
private Action action = Action.ESCAPE;
|
||||
|
||||
|
||||
public Escape(Action a) {
|
||||
this(a, 0);
|
||||
}
|
||||
|
||||
|
||||
public Escape(Action a, int l) {
|
||||
len = l;
|
||||
action = a;
|
||||
}
|
||||
|
||||
public Action action() {
|
||||
return action;
|
||||
}
|
||||
|
||||
|
||||
public int len() {
|
||||
return len;
|
||||
}
|
||||
|
||||
|
||||
private static Map<Byte, Escape> _specials = new HashMap<Byte, Escape>();
|
||||
|
||||
static {
|
||||
_specials.put(ZModemCharacter.ZBIN.value(), new Escape(Action.HEADER, 7));
|
||||
_specials.put(ZModemCharacter.ZHEX.value(), new Escape(Action.HEADER, 16));
|
||||
_specials.put(ZModemCharacter.ZBIN32.value(), new Escape(Action.HEADER, 9));
|
||||
_specials.put(ZModemCharacter.ZCRCE.value(), new Escape(Action.DATA, 2));
|
||||
_specials.put(ZModemCharacter.ZCRCG.value(), new Escape(Action.DATA, 2));
|
||||
_specials.put(ZModemCharacter.ZCRCQ.value(), new Escape(Action.DATA, 2));
|
||||
_specials.put(ZModemCharacter.ZCRCW.value(), new Escape(Action.DATA, 2));
|
||||
}
|
||||
|
||||
|
||||
public static Escape detect(byte b, boolean acceptsHeader) {
|
||||
Escape r = _specials.get(b);
|
||||
|
||||
|
||||
if (r == null || ((!acceptsHeader) && r.action() == Action.HEADER))
|
||||
return new Escape(Action.ESCAPE);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
public static boolean mustEscape(byte b, byte previous, boolean escapeCtl) {
|
||||
switch (b) {
|
||||
case 0xd:
|
||||
case (byte) 0x8d:
|
||||
if (escapeCtl && previous == '@')
|
||||
return true;
|
||||
break;
|
||||
case 0x18:
|
||||
case 0x10:
|
||||
case 0x11:
|
||||
case 0x13:
|
||||
case (byte) 0x7f:
|
||||
case (byte) 0x90:
|
||||
case (byte) 0x91:
|
||||
case (byte) 0x93:
|
||||
case (byte) 0xff:
|
||||
return true;
|
||||
default:
|
||||
if (escapeCtl && ((b & 0x60) == 0))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static byte escapeIt(byte b) {
|
||||
if (b == (byte) 0x7f)
|
||||
return ZModemCharacter.ZRUB0.value();
|
||||
if (b == (byte) 0xff)
|
||||
return ZModemCharacter.ZRUB1.value();
|
||||
if (b == (byte) ZModemCharacter.ZRUB0.value())
|
||||
return 0x7f;
|
||||
if (b == (byte) ZModemCharacter.ZRUB1.value())
|
||||
return (byte) 0xff;
|
||||
|
||||
return (byte) (b ^ 0x40);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Action=" + action + ", len=" + len;
|
||||
}
|
||||
}
|
||||
401
src/main/java/zmodem/xfer/zm/util/Modem.java
Normal file
401
src/main/java/zmodem/xfer/zm/util/Modem.java
Normal file
@@ -0,0 +1,401 @@
|
||||
package zmodem.xfer.zm.util;
|
||||
|
||||
import zmodem.xfer.util.CRC16;
|
||||
import zmodem.xfer.util.CRC8;
|
||||
import zmodem.xfer.util.XCRC;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* This is core Modem class supporting XModem (and some extensions XModem-1K, XModem-CRC), and YModem.<br/>
|
||||
* YModem support is limited (currently block 0 is ignored).<br/>
|
||||
* <br/>
|
||||
* Created by Anton Sirotinkin (aesirot@mail.ru), Moscow 2014 <br/>
|
||||
* I hope you will find this program useful.<br/>
|
||||
* You are free to use/modify the code for any purpose, but please leave a reference to me.<br/>
|
||||
*/
|
||||
public class Modem {
|
||||
|
||||
public static final byte SOH = 0x01; /* Start Of Header */
|
||||
public static final byte STX = 0x02; /* Start Of Text (used like SOH but means 1024 block size) */
|
||||
public static final byte EOT = 0x04; /* End Of Transmission */
|
||||
public static final byte ACK = 0x06; /* ACKnowlege */
|
||||
public static final byte NAK = 0x15; /* Negative AcKnowlege */
|
||||
public static final byte CAN = 0x18; /* CANcel character */
|
||||
|
||||
public static final byte CPMEOF = 0x1A;
|
||||
public static final byte ST_C = 'C';
|
||||
|
||||
public static final int MAXERRORS = 10;
|
||||
|
||||
public static final int BLOCK_TIMEOUT = 1000;
|
||||
public static final int REQUEST_TIMEOUT = 3000;
|
||||
public static final int WAIT_FOR_RECEIVER_TIMEOUT = 60_000;
|
||||
public static final int SEND_BLOCK_TIMEOUT = 10_000;
|
||||
|
||||
private final InputStream inputStream;
|
||||
private final OutputStream outputStream;
|
||||
|
||||
private final byte[] shortBlockBuffer;
|
||||
private final byte[] longBlockBuffer;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param inputStream stream for reading received data from other side
|
||||
* @param outputStream stream for writing data to other side
|
||||
*/
|
||||
public Modem(InputStream inputStream, OutputStream outputStream) {
|
||||
this.inputStream = inputStream;
|
||||
this.outputStream = outputStream;
|
||||
shortBlockBuffer = new byte[128];
|
||||
longBlockBuffer = new byte[1024];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for receiver request for transmission
|
||||
*
|
||||
* @return TRUE if receiver requested CRC-16 checksum, FALSE if 8bit checksum
|
||||
* @throws IOException
|
||||
*/
|
||||
public boolean waitReceiverRequest() throws IOException {
|
||||
int character;
|
||||
while (true) {
|
||||
character = readByte();
|
||||
if (character == NAK)
|
||||
return false;
|
||||
if (character == ST_C) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a file. <br/>
|
||||
* <p>
|
||||
* This method support correct thread interruption, when thread is interrupted "cancel of transmission" will be send.
|
||||
* So you can move long transmission to other thread and interrupt it according to your algorithm.
|
||||
*
|
||||
* @param file
|
||||
* @param useBlock1K
|
||||
* @throws IOException
|
||||
*/
|
||||
public void send(Path file, boolean useBlock1K) throws IOException {
|
||||
//open file
|
||||
try (DataInputStream dataStream = new DataInputStream(Files.newInputStream(file))) {
|
||||
|
||||
boolean useCRC16 = waitReceiverRequest();
|
||||
XCRC crc;
|
||||
if (useCRC16)
|
||||
crc = new CRC16();
|
||||
else
|
||||
crc = new CRC8();
|
||||
|
||||
byte[] block;
|
||||
if (useBlock1K)
|
||||
block = new byte[1024];
|
||||
else
|
||||
block = new byte[128];
|
||||
sendDataBlocks(dataStream, 1, crc, block);
|
||||
|
||||
sendEOT();
|
||||
}
|
||||
}
|
||||
|
||||
public void sendDataBlocks(DataInputStream dataStream, int blockNumber, XCRC crc, byte[] block) throws IOException {
|
||||
int dataLength;
|
||||
while ((dataLength = dataStream.read(block)) != -1) {
|
||||
sendBlock(blockNumber++, block, dataLength, crc);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendEOT() throws IOException {
|
||||
int errorCount = 0;
|
||||
int character;
|
||||
while (errorCount < 10) {
|
||||
sendByte(EOT);
|
||||
character = readByte();
|
||||
|
||||
if (character == ACK) {
|
||||
return;
|
||||
} else if (character == CAN) {
|
||||
throw new IOException("Transmission terminated");
|
||||
}
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
public void sendBlock(int blockNumber, byte[] block, int dataLength, XCRC crc) throws IOException {
|
||||
int errorCount;
|
||||
int character;
|
||||
|
||||
if (dataLength < block.length) {
|
||||
block[dataLength] = CPMEOF;
|
||||
}
|
||||
errorCount = 0;
|
||||
|
||||
while (errorCount < MAXERRORS) {
|
||||
|
||||
if (block.length == 1024)
|
||||
outputStream.write(STX);
|
||||
else //128
|
||||
outputStream.write(SOH);
|
||||
outputStream.write(blockNumber);
|
||||
outputStream.write(~blockNumber);
|
||||
|
||||
outputStream.write(block);
|
||||
writeCRC(block, crc);
|
||||
outputStream.flush();
|
||||
|
||||
while (true) {
|
||||
character = readByte();
|
||||
if (character == ACK) {
|
||||
return;
|
||||
} else if (character == NAK) {
|
||||
errorCount++;
|
||||
break;
|
||||
} else if (character == CAN) {
|
||||
throw new IOException("Transmission terminated");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
throw new IOException("Too many errors caught, abandoning transfer");
|
||||
}
|
||||
|
||||
private void writeCRC(byte[] block, XCRC crc) throws IOException {
|
||||
byte[] crcBytes = new byte[crc.getCRCLength()];
|
||||
long crcValue = crc.calcCRC(block);
|
||||
for (int i = 0; i < crc.getCRCLength(); i++) {
|
||||
crcBytes[crc.getCRCLength() - i - 1] = (byte) ((crcValue >> (8 * i)) & 0xFF);
|
||||
}
|
||||
outputStream.write(crcBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive file <br/>
|
||||
* <p>
|
||||
* This method support correct thread interruption, when thread is interrupted "cancel of transmission" will be send.
|
||||
* So you can move long transmission to other thread and interrupt it according to your algorithm.
|
||||
*
|
||||
* @param file file path for storing
|
||||
* @throws IOException
|
||||
*/
|
||||
public void receive(Path file, boolean useCRC16) throws IOException {
|
||||
try (DataOutputStream dataOutput = new DataOutputStream(Files.newOutputStream(file))) {
|
||||
int available;
|
||||
// clean input stream
|
||||
if ((available = inputStream.available()) > 0) {
|
||||
inputStream.skip(available);
|
||||
}
|
||||
|
||||
int character = requestTransmissionStart(useCRC16);
|
||||
|
||||
XCRC crc;
|
||||
if (useCRC16)
|
||||
crc = new CRC16();
|
||||
else
|
||||
crc = new CRC8();
|
||||
|
||||
|
||||
processDataBlocks(crc, 1, character, dataOutput);
|
||||
}
|
||||
}
|
||||
|
||||
public void processDataBlocks(XCRC crc, int blockNumber, int blockInitialCharacter, DataOutputStream dataOutput) throws IOException {
|
||||
// read blocks until EOT
|
||||
boolean result = false;
|
||||
boolean shortBlock;
|
||||
byte[] block;
|
||||
while (true) {
|
||||
int errorCount = 0;
|
||||
if (blockInitialCharacter == EOT) {
|
||||
// end of transmission
|
||||
sendByte(ACK);
|
||||
return;
|
||||
}
|
||||
|
||||
//read and process block
|
||||
shortBlock = (blockInitialCharacter == SOH);
|
||||
try {
|
||||
block = readBlock(blockNumber, shortBlock, crc);
|
||||
dataOutput.write(block);
|
||||
blockNumber++;
|
||||
errorCount = 0;
|
||||
result = true;
|
||||
sendByte(ACK);
|
||||
} catch (InvalidBlockException e) {
|
||||
errorCount++;
|
||||
if (errorCount == MAXERRORS) {
|
||||
interruptTransmission();
|
||||
throw new IOException("Transmission aborted, error count exceeded max");
|
||||
}
|
||||
sendByte(NAK);
|
||||
result = false;
|
||||
} catch (RepeatedBlockException e) {
|
||||
//thats ok, accept and wait for next block
|
||||
sendByte(ACK);
|
||||
} catch (SynchronizationLostException e) {
|
||||
//fatal transmission error
|
||||
interruptTransmission();
|
||||
throw new IOException("Fatal transmission error", e);
|
||||
}
|
||||
|
||||
//wait for next block
|
||||
blockInitialCharacter = readNextBlockStart(result);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendByte(byte b) throws IOException {
|
||||
outputStream.write(b);
|
||||
outputStream.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request transmission start and return first byte of "first" block from sender (block 1 for XModem, block 0 for YModem)
|
||||
*
|
||||
* @param useCRC16
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public int requestTransmissionStart(boolean useCRC16) throws IOException {
|
||||
int character;
|
||||
int errorCount = 0;
|
||||
byte requestStartByte;
|
||||
if (!useCRC16) {
|
||||
requestStartByte = NAK;
|
||||
} else {
|
||||
requestStartByte = ST_C;
|
||||
}
|
||||
|
||||
// wait for first block start
|
||||
// request transmission start (will be repeated after 10 second timeout for 10 times)
|
||||
sendByte(requestStartByte);
|
||||
while (true) {
|
||||
character = readByte();
|
||||
|
||||
if (character == SOH || character == STX) {
|
||||
return character;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int readNextBlockStart(boolean lastBlockResult) throws IOException {
|
||||
int character;
|
||||
int errorCount = 0;
|
||||
while (true) {
|
||||
while (true) {
|
||||
character = readByte();
|
||||
if (character == SOH || character == STX || character == EOT) {
|
||||
return character;
|
||||
}
|
||||
}
|
||||
// repeat last block result and wait for next block one more time
|
||||
// if (++errorCount < MAXERRORS) {
|
||||
// sendByte(lastBlockResult ? ACK : NAK);
|
||||
// } else {
|
||||
// interruptTransmission();
|
||||
// throw new RuntimeException("Timeout, no data received from transmitter");
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
private void shortSleep() {
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
} catch (InterruptedException e) {
|
||||
try {
|
||||
interruptTransmission();
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
throw new RuntimeException("Transmission was interrupted", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* send CAN to interrupt seance
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public void interruptTransmission() throws IOException {
|
||||
sendByte(CAN);
|
||||
sendByte(CAN);
|
||||
}
|
||||
|
||||
public byte[] readBlock(int blockNumber, boolean shortBlock, XCRC crc) throws IOException, RepeatedBlockException, SynchronizationLostException, InvalidBlockException {
|
||||
byte[] block;
|
||||
|
||||
if (shortBlock) {
|
||||
block = shortBlockBuffer;
|
||||
} else {
|
||||
block = longBlockBuffer;
|
||||
}
|
||||
byte character;
|
||||
|
||||
character = readByte();
|
||||
|
||||
if (character == blockNumber - 1) {
|
||||
// this is repeating of last block, possible ACK lost
|
||||
throw new RepeatedBlockException();
|
||||
}
|
||||
if (character != blockNumber) {
|
||||
// wrong block - fatal loss of synchronization
|
||||
throw new SynchronizationLostException();
|
||||
}
|
||||
|
||||
character = readByte();
|
||||
|
||||
if (character != ~blockNumber) {
|
||||
throw new InvalidBlockException();
|
||||
}
|
||||
|
||||
// data
|
||||
for (int i = 0; i < block.length; i++) {
|
||||
block[i] = readByte();
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (inputStream.available() >= crc.getCRCLength()) {
|
||||
if (crc.calcCRC(block) != readCRC(crc)) {
|
||||
throw new InvalidBlockException();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
shortSleep();
|
||||
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
private long readCRC(XCRC crc) throws IOException {
|
||||
long checkSumma = 0;
|
||||
for (int j = 0; j < crc.getCRCLength(); j++) {
|
||||
checkSumma = (checkSumma << 8) + inputStream.read();
|
||||
}
|
||||
return checkSumma;
|
||||
}
|
||||
|
||||
private byte readByte() throws IOException {
|
||||
while (true) {
|
||||
if (inputStream.available() > 0) {
|
||||
int b = inputStream.read();
|
||||
return (byte) b;
|
||||
}
|
||||
shortSleep();
|
||||
}
|
||||
}
|
||||
|
||||
public class RepeatedBlockException extends Exception {
|
||||
}
|
||||
|
||||
public class SynchronizationLostException extends Exception {
|
||||
}
|
||||
|
||||
public class InvalidBlockException extends Exception {
|
||||
}
|
||||
}
|
||||
58
src/main/java/zmodem/xfer/zm/util/ZDLEEncoder.java
Normal file
58
src/main/java/zmodem/xfer/zm/util/ZDLEEncoder.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package zmodem.xfer.zm.util;
|
||||
|
||||
|
||||
import zmodem.xfer.zm.packet.Format;
|
||||
import zmodem.xfer.zm.proto.Escape;
|
||||
|
||||
public class ZDLEEncoder {
|
||||
|
||||
private byte[] raw;
|
||||
private byte[] zdle;
|
||||
private int zdleLen;
|
||||
private Format format;
|
||||
|
||||
|
||||
public ZDLEEncoder(byte[] data) {
|
||||
this(data, Format.BIN);
|
||||
}
|
||||
|
||||
public ZDLEEncoder(byte[] data, Format fmt) {
|
||||
raw = data;
|
||||
format = fmt;
|
||||
zdle = new byte[raw.length * 2];
|
||||
encode();
|
||||
}
|
||||
|
||||
private void putZdle(byte b) {
|
||||
zdle[zdleLen] = b;
|
||||
zdleLen++;
|
||||
}
|
||||
|
||||
private void encode() {
|
||||
byte previous = 0;
|
||||
for (byte b : raw) {
|
||||
|
||||
if ((!format.hex()) && Escape.mustEscape(b, previous, false)) {
|
||||
putZdle(ZModemCharacter.ZDLE.value());
|
||||
b = Escape.escapeIt(b);
|
||||
}
|
||||
|
||||
putZdle(b);
|
||||
previous = b;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] raw() {
|
||||
return raw;
|
||||
}
|
||||
|
||||
public int zdleLen() {
|
||||
return zdleLen;
|
||||
}
|
||||
|
||||
public byte[] zdle() {
|
||||
return zdle;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
49
src/main/java/zmodem/xfer/zm/util/ZMOptions.java
Normal file
49
src/main/java/zmodem/xfer/zm/util/ZMOptions.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package zmodem.xfer.zm.util;
|
||||
|
||||
public enum ZMOptions {
|
||||
|
||||
CANFDX(0x01), /* Rx can send and receive true FDX */
|
||||
CANOVIO(0x02), /* Rx can receive data during disk I/O */
|
||||
CANBRK(0x04), /* Rx can send a break signal */
|
||||
CANCRY(0x08), /* Receiver can decrypt */
|
||||
CANLZW(0x10), /* Receiver can uncompress */
|
||||
CANFC32(0x20), /* Receiver can use 32 bit Frame Check */
|
||||
ESCCTL(0x40), /* Receiver expects ctl chars to be escaped */
|
||||
ESC8(0x80), /* Receiver expects 8th bit to be escaped */
|
||||
ZCBIN(0x01);
|
||||
|
||||
private byte value;
|
||||
|
||||
private ZMOptions(char b) {
|
||||
value = (byte) b;
|
||||
}
|
||||
|
||||
private ZMOptions(int b) {
|
||||
value = (byte) b;
|
||||
}
|
||||
|
||||
private ZMOptions(byte b) {
|
||||
value = b;
|
||||
}
|
||||
|
||||
|
||||
public byte value() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static byte with(ZMOptions... oo) {
|
||||
byte r = 0;
|
||||
for (ZMOptions o : oo)
|
||||
r = (byte) (r | o.value());
|
||||
return r;
|
||||
}
|
||||
|
||||
public static ZMOptions forbyte(byte b) {
|
||||
for (ZMOptions zb : values()) {
|
||||
if (zb.value() == b)
|
||||
return zb;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
9
src/main/java/zmodem/xfer/zm/util/ZMPacket.java
Normal file
9
src/main/java/zmodem/xfer/zm/util/ZMPacket.java
Normal file
@@ -0,0 +1,9 @@
|
||||
package zmodem.xfer.zm.util;
|
||||
|
||||
|
||||
import zmodem.xfer.util.Buffer;
|
||||
|
||||
public abstract class ZMPacket {
|
||||
public abstract Buffer marshall();
|
||||
|
||||
}
|
||||
39
src/main/java/zmodem/xfer/zm/util/ZMPacketFactory.java
Normal file
39
src/main/java/zmodem/xfer/zm/util/ZMPacketFactory.java
Normal file
@@ -0,0 +1,39 @@
|
||||
package zmodem.xfer.zm.util;
|
||||
|
||||
|
||||
import zmodem.xfer.zm.packet.DataPacket;
|
||||
|
||||
public class ZMPacketFactory {
|
||||
|
||||
public ZMPacketFactory() {
|
||||
}
|
||||
|
||||
public DataPacket createZFilePacket(String pathname, long flen) {
|
||||
return createZFilePacket(pathname, flen, 0, "0", 0, 0);
|
||||
}
|
||||
|
||||
public DataPacket createZFilePacket(String pathname, long flen, long ts, String mode/*octal*/
|
||||
, int remainingfiles, long remainingBytes) {
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.append(pathname);
|
||||
builder.append('\0');
|
||||
builder.append(flen);
|
||||
builder.append(' ');
|
||||
builder.append(ts);
|
||||
builder.append(' ');
|
||||
builder.append(mode);
|
||||
builder.append(' ');
|
||||
builder.append('0');
|
||||
builder.append(' ');
|
||||
builder.append(remainingfiles);
|
||||
builder.append(' ');
|
||||
builder.append(remainingBytes);
|
||||
builder.append('0');
|
||||
|
||||
return new DataPacket(ZModemCharacter.ZCRCW, builder.toString().getBytes());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
64
src/main/java/zmodem/xfer/zm/util/ZModemCharacter.java
Normal file
64
src/main/java/zmodem/xfer/zm/util/ZModemCharacter.java
Normal file
@@ -0,0 +1,64 @@
|
||||
package zmodem.xfer.zm.util;
|
||||
|
||||
public enum ZModemCharacter {
|
||||
ZPAD('*'),
|
||||
ZDLE(0x18),
|
||||
ZDLEE(ZDLE.value() ^ 0x40),
|
||||
ZBIN('A'),
|
||||
ZHEX('B'),
|
||||
ZBIN32('C'),
|
||||
ZCRCE('h'),
|
||||
ZCRCG('i'),
|
||||
ZCRCQ('j'),
|
||||
ZCRCW('k'),
|
||||
ZRUB0('l'),
|
||||
ZRUB1('m'),
|
||||
ZRQINIT(0),
|
||||
ZRINIT(1),
|
||||
ZSINIT(2),
|
||||
ZACK(3),
|
||||
ZFILE(4),
|
||||
ZSKIP(5),
|
||||
ZNAK(6),
|
||||
ZABORT(7),
|
||||
ZFIN(8),
|
||||
ZRPOS(9),
|
||||
ZDATA(10),
|
||||
ZEOF(11),
|
||||
ZFERR(12),
|
||||
ZCRC(13),
|
||||
ZCHALLENGE(14),
|
||||
ZCOMPL(15),
|
||||
ZCAN(16),
|
||||
ZFREECNT(17),
|
||||
ZCOMMAND(18),
|
||||
ZSTDERR(19);
|
||||
|
||||
private byte value;
|
||||
|
||||
private ZModemCharacter(char b) {
|
||||
value = (byte) b;
|
||||
}
|
||||
|
||||
private ZModemCharacter(int b) {
|
||||
value = (byte) b;
|
||||
}
|
||||
|
||||
private ZModemCharacter(byte b) {
|
||||
value = b;
|
||||
}
|
||||
|
||||
|
||||
public byte value() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static ZModemCharacter forbyte(byte b) {
|
||||
for (ZModemCharacter zb : values()) {
|
||||
if (zb.value() == b)
|
||||
return zb;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
259
src/main/java/zmodem/xfer/zm/util/ZModemReceive.java
Normal file
259
src/main/java/zmodem/xfer/zm/util/ZModemReceive.java
Normal file
@@ -0,0 +1,259 @@
|
||||
package zmodem.xfer.zm.util;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
import org.apache.commons.net.io.CopyStreamAdapter;
|
||||
import org.apache.commons.net.io.CopyStreamListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import zmodem.FileCopyStreamEvent;
|
||||
import zmodem.util.EmptyFileAdapter;
|
||||
import zmodem.util.FileAdapter;
|
||||
import zmodem.xfer.util.InvalidChecksumException;
|
||||
import zmodem.xfer.zm.packet.*;
|
||||
import zmodem.zm.io.ZMPacketInputStream;
|
||||
import zmodem.zm.io.ZMPacketOutputStream;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
|
||||
public class ZModemReceive {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ZModemReceive.class);
|
||||
|
||||
private final CopyStreamAdapter adapter = new CopyStreamAdapter();
|
||||
private final Supplier<FileAdapter> destinationSupplier;
|
||||
private FileAdapter destination;
|
||||
|
||||
private FileAdapter file;
|
||||
private int fOffset = 0;
|
||||
private Long filesize;
|
||||
private int remaining = 0;
|
||||
private int index = 0;
|
||||
|
||||
private OutputStream fileOs = null;
|
||||
|
||||
private final InputStream netIs;
|
||||
private final OutputStream netOs;
|
||||
|
||||
private enum Expect {
|
||||
FILENAME, DATA, NOTHING;
|
||||
}
|
||||
|
||||
|
||||
public ZModemReceive(Supplier<FileAdapter> destDir, InputStream netin, OutputStream netout) throws IOException {
|
||||
destinationSupplier = destDir;
|
||||
netIs = netin;
|
||||
netOs = netout;
|
||||
}
|
||||
|
||||
private void open(int offset) throws IOException {
|
||||
boolean append = false;
|
||||
|
||||
if (offset != 0) {
|
||||
if (file.exists() && file.length() == offset)
|
||||
append = true;
|
||||
else
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
IOUtils.closeQuietly(fileOs);
|
||||
|
||||
fileOs = file.getOutputStream(append);
|
||||
fOffset = offset;
|
||||
|
||||
}
|
||||
|
||||
private void decodeFileNameData(DataPacket p) {
|
||||
ByteArrayOutputStream filename = new ByteArrayOutputStream();
|
||||
StringBuilder extract = new StringBuilder();
|
||||
byte[] data = p.data();
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
byte b = data[i];
|
||||
if (b == 0) {
|
||||
for (int j = i + 1; j < data.length; j++) {
|
||||
b = data[j];
|
||||
if (b == 0) {
|
||||
break;
|
||||
}
|
||||
extract.append((char) b);
|
||||
}
|
||||
break;
|
||||
}
|
||||
filename.write(b);
|
||||
}
|
||||
|
||||
final String[] segments = extract.toString().split(StringUtils.SPACE);
|
||||
if (ArrayUtils.isNotEmpty(segments)) {
|
||||
// filesize
|
||||
if (segments.length >= 1) {
|
||||
this.filesize = NumberUtils.toLong(segments[0]);
|
||||
}
|
||||
// remaining
|
||||
if (segments.length >= 5) {
|
||||
this.remaining = NumberUtils.toInt(segments[4]);
|
||||
}
|
||||
}
|
||||
|
||||
file = destination.getChild(filename.toString());
|
||||
fOffset = 0;
|
||||
|
||||
index++;
|
||||
|
||||
adapter.bytesTransferred(new FileCopyStreamEvent(this, file.getName(), remaining - index, index,
|
||||
this.filesize, fOffset, 0, false));
|
||||
}
|
||||
|
||||
public void addCopyStreamListener(CopyStreamListener listener) {
|
||||
adapter.addCopyStreamListener(listener);
|
||||
}
|
||||
|
||||
public void removeCopyStreamListener(CopyStreamListener listener) {
|
||||
adapter.removeCopyStreamListener(listener);
|
||||
}
|
||||
|
||||
private void writeData(DataPacket p) throws IOException {
|
||||
final byte[] data = p.data();
|
||||
|
||||
fileOs.write(data);
|
||||
fOffset += data.length;
|
||||
|
||||
// 开始传输
|
||||
adapter.bytesTransferred(new FileCopyStreamEvent(this, file.getName(), remaining, index,
|
||||
this.filesize, fOffset, 0, false));
|
||||
}
|
||||
|
||||
private boolean initDestination() {
|
||||
if (destination != null) {
|
||||
return true;
|
||||
}
|
||||
destination = destinationSupplier.get();
|
||||
return !(destination instanceof EmptyFileAdapter);
|
||||
}
|
||||
|
||||
public void receive(Supplier<Boolean> isCancelled) {
|
||||
ZMPacketInputStream is = new ZMPacketInputStream(netIs);
|
||||
ZMPacketOutputStream os = new ZMPacketOutputStream(netOs);
|
||||
|
||||
Expect expect = Expect.NOTHING;
|
||||
|
||||
byte[] recvOpt = {0, 4, 0, ZMOptions.with(ZMOptions.ESCCTL, ZMOptions.ESC8)};
|
||||
|
||||
try {
|
||||
|
||||
boolean end = false;
|
||||
int errorCount = 0;
|
||||
ZMPacket packet = null;
|
||||
while (!end) {
|
||||
try {
|
||||
packet = is.read();
|
||||
} catch (InvalidChecksumException ice) {
|
||||
if (log.isErrorEnabled()) {
|
||||
log.error(ice.getMessage(), ice);
|
||||
}
|
||||
++errorCount;
|
||||
if (errorCount >= 3) {
|
||||
os.write(new Cancel());
|
||||
end = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (packet instanceof Cancel) {
|
||||
end = true;
|
||||
} else if (packet instanceof Finish) {
|
||||
end = true;
|
||||
}
|
||||
|
||||
if (isCancelled.get()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果重定向为空,则终止传输
|
||||
if (destination instanceof EmptyFileAdapter) {
|
||||
os.write(new Cancel());
|
||||
break;
|
||||
}
|
||||
|
||||
if (packet instanceof Header header) {
|
||||
switch (header.type()) {
|
||||
case ZRQINIT:
|
||||
os.write(new Header(Format.HEX, ZModemCharacter.ZRINIT, recvOpt));
|
||||
break;
|
||||
case ZFILE:
|
||||
expect = Expect.FILENAME;
|
||||
break;
|
||||
case ZEOF:
|
||||
os.write(new Header(Format.HEX, ZModemCharacter.ZRINIT, recvOpt));
|
||||
expect = Expect.NOTHING;
|
||||
file = null;
|
||||
fileOs.flush();
|
||||
IOUtils.closeQuietly(fileOs);
|
||||
fileOs = null;
|
||||
break;
|
||||
case ZDATA:
|
||||
open(header.getPos());
|
||||
expect = Expect.DATA;
|
||||
break;
|
||||
case ZFIN:
|
||||
os.write(new Header(Format.HEX, ZModemCharacter.ZFIN));
|
||||
end = true;
|
||||
break;
|
||||
default:
|
||||
end = true;
|
||||
os.write(new Cancel());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (packet instanceof DataPacket data) {
|
||||
switch (expect) {
|
||||
case NOTHING:
|
||||
os.write(new Header(Format.HEX, ZModemCharacter.ZRINIT, recvOpt));
|
||||
break;
|
||||
case FILENAME:
|
||||
if (!initDestination()) {
|
||||
end = true;
|
||||
os.write(new Cancel());
|
||||
break;
|
||||
}
|
||||
decodeFileNameData(data);
|
||||
if (file.length() == filesize) {
|
||||
os.write(new Header(Format.HEX, ZModemCharacter.ZSKIP));
|
||||
adapter.bytesTransferred(new FileCopyStreamEvent(this, file.getName(), remaining, index,
|
||||
this.filesize, fOffset, 0, true));
|
||||
} else {
|
||||
os.write(new Header(Format.HEX, ZModemCharacter.ZRPOS, (int) file.length()));
|
||||
}
|
||||
expect = Expect.NOTHING;
|
||||
break;
|
||||
case DATA:
|
||||
writeData(data);
|
||||
switch (data.type()) {
|
||||
case ZCRCW:
|
||||
expect = Expect.NOTHING;
|
||||
case ZCRCQ:
|
||||
os.write(new Header(Format.HEX, ZModemCharacter.ZACK, fOffset));
|
||||
break;
|
||||
case ZCRCE:
|
||||
expect = Expect.NOTHING;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (log.isErrorEnabled()) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
} finally {
|
||||
IOUtils.closeQuietly(fileOs);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
213
src/main/java/zmodem/xfer/zm/util/ZModemSend.java
Normal file
213
src/main/java/zmodem/xfer/zm/util/ZModemSend.java
Normal file
@@ -0,0 +1,213 @@
|
||||
package zmodem.xfer.zm.util;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.net.io.CopyStreamAdapter;
|
||||
import org.apache.commons.net.io.CopyStreamListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import zmodem.FileCopyStreamEvent;
|
||||
import zmodem.util.FileAdapter;
|
||||
import zmodem.xfer.util.InvalidChecksumException;
|
||||
import zmodem.xfer.zm.packet.*;
|
||||
import zmodem.zm.io.ZMPacketInputStream;
|
||||
import zmodem.zm.io.ZMPacketOutputStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
|
||||
public class ZModemSend {
|
||||
|
||||
private static final int packLen = 1024 * 8;
|
||||
private static final Logger log = LoggerFactory.getLogger(ZModemSend.class);
|
||||
|
||||
private final byte[] data = new byte[packLen];
|
||||
private final CopyStreamAdapter adapter = new CopyStreamAdapter();
|
||||
private final Supplier<List<FileAdapter>> destinationSupplier;
|
||||
private final InputStream netIs;
|
||||
private final OutputStream netOs;
|
||||
|
||||
private List<FileAdapter> files;
|
||||
private Iterator<FileAdapter> iter;
|
||||
|
||||
private FileAdapter file;
|
||||
private int fOffset = 0;
|
||||
private int index = 0;
|
||||
private int filesize = 0;
|
||||
private boolean atEof = false;
|
||||
private InputStream fileIs;
|
||||
|
||||
|
||||
public ZModemSend(Supplier<List<FileAdapter>> destinationSupplier, InputStream netin, OutputStream netout) throws IOException {
|
||||
this.destinationSupplier = destinationSupplier;
|
||||
netIs = netin;
|
||||
netOs = netout;
|
||||
}
|
||||
|
||||
public boolean nextFile() throws IOException {
|
||||
|
||||
IOUtils.closeQuietly(fileIs);
|
||||
|
||||
if (files == null) {
|
||||
files = destinationSupplier.get();
|
||||
iter = files.iterator();
|
||||
}
|
||||
|
||||
if (!iter.hasNext())
|
||||
return false;
|
||||
|
||||
|
||||
file = iter.next();
|
||||
fileIs = file.getInputStream();
|
||||
filesize = fileIs.available();
|
||||
fOffset = 0;
|
||||
atEof = false;
|
||||
index++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public void addCopyStreamListener(CopyStreamListener listener) {
|
||||
adapter.addCopyStreamListener(listener);
|
||||
}
|
||||
|
||||
public void removeCopyStreamListener(CopyStreamListener listener) {
|
||||
adapter.removeCopyStreamListener(listener);
|
||||
}
|
||||
|
||||
|
||||
private void position(int offset) throws IOException {
|
||||
if (offset != fOffset) {
|
||||
fileIs.skipNBytes(offset);
|
||||
fOffset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getNextBlock() throws IOException {
|
||||
final int len = fileIs.read(data);
|
||||
|
||||
/* we know it is a file: all the data is locally available.*/
|
||||
if (len < data.length)
|
||||
atEof = true;
|
||||
else if (fileIs.available() == 0)
|
||||
atEof = true;
|
||||
|
||||
if (len == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
fOffset += len;
|
||||
|
||||
if (len != data.length)
|
||||
return ArrayUtils.subarray(data, 0, len);
|
||||
else
|
||||
return data;
|
||||
}
|
||||
|
||||
private DataPacket getNextDataPacket() throws IOException {
|
||||
byte[] data = getNextBlock();
|
||||
|
||||
ZModemCharacter fe = ZModemCharacter.ZCRCW;
|
||||
if (atEof) {
|
||||
fe = ZModemCharacter.ZCRCE;
|
||||
fileIs.close();
|
||||
}
|
||||
|
||||
if (data == null) {
|
||||
return new DataPacket(fe);
|
||||
}
|
||||
|
||||
return new DataPacket(fe, data);
|
||||
}
|
||||
|
||||
public void send(Supplier<Boolean> isCancelled) {
|
||||
ZMPacketFactory factory = new ZMPacketFactory();
|
||||
|
||||
ZMPacketInputStream is = new ZMPacketInputStream(netIs);
|
||||
ZMPacketOutputStream os = new ZMPacketOutputStream(netOs);
|
||||
|
||||
|
||||
try {
|
||||
|
||||
boolean end = false;
|
||||
int errorCount = 0;
|
||||
ZMPacket packet = null;
|
||||
|
||||
while (!end) {
|
||||
try {
|
||||
packet = is.read();
|
||||
} catch (InvalidChecksumException ice) {
|
||||
++errorCount;
|
||||
if (errorCount > 20) {
|
||||
os.write(new Cancel());
|
||||
end = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (packet instanceof Cancel) {
|
||||
end = true;
|
||||
} else if (isCancelled.get()) {
|
||||
os.write(new Cancel());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (packet instanceof Header header) {
|
||||
switch (header.type()) {
|
||||
case ZSKIP:
|
||||
fireBytesTransferred(true);
|
||||
case ZRINIT:
|
||||
if (!nextFile()) {
|
||||
os.write(new Header(Format.BIN, ZModemCharacter.ZFIN));
|
||||
} else {
|
||||
os.write(new Header(Format.BIN, ZModemCharacter.ZFILE, new byte[]{0, 0, 0, ZMOptions.with(ZMOptions.ZCBIN)}));
|
||||
os.write(factory.createZFilePacket(file.getName(), filesize));
|
||||
fireBytesTransferred(false);
|
||||
}
|
||||
break;
|
||||
case ZRPOS:
|
||||
if (!atEof)
|
||||
position(header.getPos());
|
||||
case ZACK:
|
||||
os.write(new Header(Format.BIN, ZModemCharacter.ZDATA, fOffset));
|
||||
os.write(getNextDataPacket());
|
||||
if (atEof) {
|
||||
os.write(new Header(Format.HEX, ZModemCharacter.ZEOF, fOffset));
|
||||
}
|
||||
fireBytesTransferred(false);
|
||||
break;
|
||||
case ZFIN:
|
||||
end = true;
|
||||
os.write(new Finish());
|
||||
break;
|
||||
default:
|
||||
end = true;
|
||||
os.write(new Cancel());
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (log.isErrorEnabled()) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
} finally {
|
||||
IOUtils.closeQuietly(fileIs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void fireBytesTransferred(boolean skip) {
|
||||
if (this.filesize == fOffset) {
|
||||
System.out.println();
|
||||
}
|
||||
adapter.bytesTransferred(new FileCopyStreamEvent(this, file.getName(), files.size() - index + 1, index,
|
||||
this.filesize, fOffset, 0, skip));
|
||||
}
|
||||
}
|
||||
145
src/main/java/zmodem/zm/io/ZMPacketInputStream.java
Normal file
145
src/main/java/zmodem/zm/io/ZMPacketInputStream.java
Normal file
@@ -0,0 +1,145 @@
|
||||
package zmodem.zm.io;
|
||||
|
||||
import zmodem.xfer.io.ObjectInputStream;
|
||||
import zmodem.xfer.util.ByteBuffer;
|
||||
import zmodem.xfer.util.CRC;
|
||||
import zmodem.xfer.zm.packet.*;
|
||||
import zmodem.xfer.zm.proto.Action;
|
||||
import zmodem.xfer.zm.proto.Escape;
|
||||
import zmodem.xfer.zm.util.ZMPacket;
|
||||
import zmodem.xfer.zm.util.ZModemCharacter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
|
||||
public class ZMPacketInputStream extends ObjectInputStream<ZMPacket> {
|
||||
|
||||
private final InputStream netIs;
|
||||
private CRC dataCRC = new CRC(CRC.Type.CRC16);
|
||||
private boolean gotFIN = false;
|
||||
private boolean acceptsHeader = true;
|
||||
|
||||
public ZMPacketInputStream(InputStream is) {
|
||||
netIs = is;
|
||||
}
|
||||
|
||||
private boolean ignored(int b) {
|
||||
return b == 0x11 || b == 0x13 || b == 0x91 || b == 0x93;
|
||||
}
|
||||
|
||||
private byte implRead() throws IOException {
|
||||
int n;
|
||||
do {
|
||||
n = netIs.read();
|
||||
} while (ignored(n));
|
||||
|
||||
if (n == -1) {
|
||||
throw new IOException("Closed");
|
||||
}
|
||||
|
||||
return (byte) n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZMPacket read() throws IOException {
|
||||
ByteBuffer zbuff = ByteBuffer.allocate(1024 * 10);
|
||||
boolean doread = true;
|
||||
Action action = Action.ESCAPE;
|
||||
|
||||
int beforeStop = -1;
|
||||
int countCan = 0;
|
||||
|
||||
while (doread) {
|
||||
byte n = implRead();
|
||||
|
||||
if (gotFIN && n == 'O') {
|
||||
n = implRead();
|
||||
if (n == 'O') {
|
||||
return new Finish();
|
||||
}
|
||||
}
|
||||
|
||||
if (n == ZModemCharacter.ZDLE.value()) {
|
||||
n = (byte) netIs.read();
|
||||
|
||||
if (n == ZModemCharacter.ZDLE.value())
|
||||
countCan += 2;
|
||||
else
|
||||
countCan = 0;
|
||||
|
||||
Escape escape = Escape.detect(n, acceptsHeader);
|
||||
|
||||
if (escape.action() != Action.ESCAPE && beforeStop < 0) {
|
||||
action = escape.action();
|
||||
|
||||
if (escape.action() == Action.DATA)
|
||||
beforeStop = dataCRC.size();
|
||||
else
|
||||
beforeStop = escape.len();
|
||||
|
||||
dataCRC.update(n);
|
||||
} else {
|
||||
n = Escape.escapeIt(n);
|
||||
}
|
||||
|
||||
}
|
||||
zbuff.put(n);
|
||||
|
||||
if (beforeStop < 0)
|
||||
dataCRC.update(n);
|
||||
|
||||
if (beforeStop == 0)
|
||||
doread = false;
|
||||
|
||||
if (beforeStop > 0)
|
||||
beforeStop--;
|
||||
|
||||
if (countCan >= 5) {
|
||||
doread = false;
|
||||
action = Action.CANCEL;
|
||||
}
|
||||
|
||||
}
|
||||
zbuff.flip();
|
||||
|
||||
ZMPacket r = null;
|
||||
switch (action) {
|
||||
case HEADER:
|
||||
r = Header.unmarshall(zbuff);
|
||||
|
||||
|
||||
if (((Header) r).format() == Format.BIN32)
|
||||
dataCRC = new CRC(CRC.Type.CRC32);
|
||||
else
|
||||
dataCRC = new CRC(CRC.Type.CRC16);
|
||||
|
||||
if (((Header) r).type() == ZModemCharacter.ZFIN)
|
||||
gotFIN = true;
|
||||
if (((Header) r).type() == ZModemCharacter.ZDATA || ((Header) r).type() == ZModemCharacter.ZFILE)
|
||||
acceptsHeader = false;
|
||||
|
||||
break;
|
||||
case DATA:
|
||||
dataCRC.finalized();
|
||||
|
||||
r = DataPacket.unmarshall(zbuff, dataCRC);
|
||||
|
||||
dataCRC = new CRC(dataCRC.type());
|
||||
|
||||
if (((DataPacket) r).type() == ZModemCharacter.ZCRCG)
|
||||
acceptsHeader = false;
|
||||
else
|
||||
acceptsHeader = true;
|
||||
|
||||
break;
|
||||
case CANCEL:
|
||||
r = new Cancel();
|
||||
dataCRC = new CRC(dataCRC.type());
|
||||
break;
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
}
|
||||
67
src/main/java/zmodem/zm/io/ZMPacketOutputStream.java
Normal file
67
src/main/java/zmodem/zm/io/ZMPacketOutputStream.java
Normal file
@@ -0,0 +1,67 @@
|
||||
package zmodem.zm.io;
|
||||
|
||||
import zmodem.xfer.io.ObjectOutputStream;
|
||||
import zmodem.xfer.util.ASCII;
|
||||
import zmodem.xfer.util.Buffer;
|
||||
import zmodem.xfer.zm.packet.DataPacket;
|
||||
import zmodem.xfer.zm.packet.Format;
|
||||
import zmodem.xfer.zm.packet.Header;
|
||||
import zmodem.xfer.zm.util.ZMPacket;
|
||||
import zmodem.xfer.zm.util.ZModemCharacter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
|
||||
public class ZMPacketOutputStream extends ObjectOutputStream<ZMPacket> {
|
||||
|
||||
private final OutputStream os;
|
||||
|
||||
public ZMPacketOutputStream(OutputStream netOs) {
|
||||
os = netOs;
|
||||
}
|
||||
|
||||
public void implWrite(byte b) throws IOException {
|
||||
//System.out.printf("%02x",b);
|
||||
os.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(ZMPacket o) throws IOException {
|
||||
Buffer buff = o.marshall();
|
||||
Format fmt = null;
|
||||
|
||||
if (o instanceof Header)
|
||||
fmt = ((Header) o).format();
|
||||
|
||||
|
||||
if (fmt != null) {
|
||||
for (int i = 0; i < fmt.width(); i++)
|
||||
implWrite(ZModemCharacter.ZPAD.value());
|
||||
|
||||
implWrite(ZModemCharacter.ZDLE.value());
|
||||
implWrite(fmt.character());
|
||||
}
|
||||
|
||||
|
||||
if (buff.hasRemaining()) {
|
||||
byte[] buf = new byte[buff.remaining()];
|
||||
buff.get(buf);
|
||||
os.write(buf);
|
||||
}
|
||||
|
||||
if (fmt != null) if (fmt.hex()) {
|
||||
implWrite(ASCII.CR.value());
|
||||
implWrite(ASCII.LF.value());
|
||||
implWrite(ASCII.XON.value());
|
||||
}
|
||||
|
||||
if (o instanceof DataPacket) if (((DataPacket) o).type() == ZModemCharacter.ZCRCW)
|
||||
implWrite(ASCII.XON.value());
|
||||
|
||||
|
||||
os.flush();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
50
src/main/kotlin/app/termora/Actions.kt
Normal file
50
src/main/kotlin/app/termora/Actions.kt
Normal file
@@ -0,0 +1,50 @@
|
||||
package app.termora
|
||||
|
||||
object Actions {
|
||||
|
||||
/**
|
||||
* 打开设置
|
||||
*/
|
||||
const val SETTING = "SettingAction"
|
||||
|
||||
/**
|
||||
* 将命令发送到多个会话
|
||||
*/
|
||||
const val MULTIPLE = "MultipleAction"
|
||||
|
||||
/**
|
||||
* 查找
|
||||
*/
|
||||
const val FIND_EVERYWHERE = "FindEverywhereAction"
|
||||
|
||||
/**
|
||||
* 关键词高亮
|
||||
*/
|
||||
const val KEYWORD_HIGHLIGHT_EVERYWHERE = "KeywordHighlightAction"
|
||||
|
||||
/**
|
||||
* Key manager
|
||||
*/
|
||||
const val KEY_MANAGER = "KeyManagerAction"
|
||||
|
||||
/**
|
||||
* 更新
|
||||
*/
|
||||
const val APP_UPDATE = "AppUpdateAction"
|
||||
|
||||
|
||||
/**
|
||||
* 宏
|
||||
*/
|
||||
const val MACRO = "MacroAction"
|
||||
|
||||
/**
|
||||
* 添加主机对话框
|
||||
*/
|
||||
const val ADD_HOST = "AddHostAction"
|
||||
|
||||
/**
|
||||
* 打开一个主机
|
||||
*/
|
||||
const val OPEN_HOST = "OpenHostAction"
|
||||
}
|
||||
16
src/main/kotlin/app/termora/AnAction.kt
Normal file
16
src/main/kotlin/app/termora/AnAction.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
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)
|
||||
|
||||
}
|
||||
146
src/main/kotlin/app/termora/Application.kt
Normal file
146
src/main/kotlin/app/termora/Application.kt
Normal file
@@ -0,0 +1,146 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jthemedetecor.util.OsInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Desktop
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
object Application {
|
||||
private val services = Collections.synchronizedMap(mutableMapOf<KClass<*>, Any>())
|
||||
private lateinit var baseDataDir: File
|
||||
|
||||
|
||||
val ohMyJson = Json {
|
||||
ignoreUnknownKeys = true
|
||||
// 默认值不输出
|
||||
encodeDefaults = false
|
||||
}
|
||||
|
||||
|
||||
val httpClient by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.callTimeout(Duration.ofSeconds(60))
|
||||
.writeTimeout(Duration.ofSeconds(60))
|
||||
.readTimeout(Duration.ofSeconds(60))
|
||||
.addInterceptor(
|
||||
HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
|
||||
private val log = LoggerFactory.getLogger(HttpLoggingInterceptor::class.java)
|
||||
override fun log(message: String) {
|
||||
if (log.isDebugEnabled) log.debug(message)
|
||||
}
|
||||
}).setLevel(HttpLoggingInterceptor.Level.BASIC)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun getDefaultShell(): String {
|
||||
if (SystemInfo.isWindows) {
|
||||
return "cmd.exe"
|
||||
} else {
|
||||
val shell = System.getenv("SHELL")
|
||||
if (shell != null && shell.isNotBlank()) {
|
||||
return shell
|
||||
}
|
||||
}
|
||||
return "/bin/bash"
|
||||
}
|
||||
|
||||
fun getBaseDataDir(): File {
|
||||
if (::baseDataDir.isInitialized) {
|
||||
return baseDataDir
|
||||
}
|
||||
|
||||
// 从启动参数取
|
||||
var baseDataDir = System.getProperty("${getName()}.base-data-dir".lowercase())
|
||||
// 取不到从环境取
|
||||
if (StringUtils.isBlank(baseDataDir)) {
|
||||
baseDataDir = System.getenv("${getName()}-BASE-DATA-DIR".uppercase())
|
||||
}
|
||||
|
||||
var dir = File(SystemUtils.getUserHome(), ".${getName()}".lowercase())
|
||||
if (StringUtils.isNotBlank(baseDataDir)) {
|
||||
dir = File(baseDataDir)
|
||||
}
|
||||
|
||||
|
||||
FileUtils.forceMkdir(dir)
|
||||
Application.baseDataDir = dir
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
fun getDatabaseFile(): File {
|
||||
return FileUtils.getFile(getBaseDataDir(), "storage")
|
||||
}
|
||||
|
||||
fun getVersion(): String {
|
||||
var version = System.getProperty("jpackage.app-version")
|
||||
if (version.isNullOrBlank()) {
|
||||
version = System.getProperty("app-version")
|
||||
}
|
||||
if (version.isNullOrBlank()) {
|
||||
version = "unknown"
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
fun getAppPath(): String {
|
||||
return StringUtils.defaultString(System.getProperty("jpackage.app-path"))
|
||||
}
|
||||
|
||||
fun getName(): String {
|
||||
return "Termora"
|
||||
}
|
||||
|
||||
fun browse(uri: URI, async: Boolean = true) {
|
||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||
Desktop.getDesktop().browse(uri)
|
||||
} else if (async) {
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
GlobalScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
||||
} else {
|
||||
tryBrowse(uri)
|
||||
}
|
||||
}
|
||||
|
||||
@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()
|
||||
} else if (SystemInfo.isMacOS) {
|
||||
ProcessBuilder("open", uri.toString()).start()
|
||||
} else if (SystemInfo.isLinux && OsInfo.isGnome()) {
|
||||
ProcessBuilder("xdg-open", uri.toString()).start()
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/main/kotlin/app/termora/ApplicationDisposable.kt
Normal file
10
src/main/kotlin/app/termora/ApplicationDisposable.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package app.termora
|
||||
|
||||
/**
|
||||
* 将在 JVM 进程退出时释放
|
||||
*/
|
||||
class ApplicationDisposable : Disposable {
|
||||
companion object {
|
||||
val instance by lazy { ApplicationDisposable() }
|
||||
}
|
||||
}
|
||||
254
src/main/kotlin/app/termora/ApplicationRunner.kt
Normal file
254
src/main/kotlin/app/termora/ApplicationRunner.kt
Normal file
@@ -0,0 +1,254 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.db.Database
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.FlatSystemProperties
|
||||
import com.formdev.flatlaf.extras.FlatInspector
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jthemedetecor.OsThemeDetector
|
||||
import com.sun.jna.platform.WindowUtils
|
||||
import com.sun.jna.platform.win32.User32
|
||||
import com.sun.jna.ptr.IntByReference
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.lang3.LocaleUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
import org.apache.commons.lang3.math.NumberUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.tinylog.configuration.Configuration
|
||||
import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.channels.FileLock
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.util.*
|
||||
import javax.swing.*
|
||||
import javax.swing.WindowConstants.DISPOSE_ON_CLOSE
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class ApplicationRunner {
|
||||
private lateinit var singletonLock: FileLock
|
||||
private val log by lazy {
|
||||
if (!::singletonLock.isInitialized) {
|
||||
throw UnsupportedOperationException("Singleton lock is not initialized")
|
||||
}
|
||||
LoggerFactory.getLogger("Main")
|
||||
}
|
||||
|
||||
fun run() {
|
||||
// 覆盖 tinylog 配置
|
||||
setupTinylog()
|
||||
|
||||
// 是否单例
|
||||
checkSingleton()
|
||||
|
||||
// 打印系统信息
|
||||
printSystemInfo()
|
||||
|
||||
SwingUtilities.invokeAndWait {
|
||||
// 打开数据库
|
||||
openDatabase()
|
||||
|
||||
// 加载设置
|
||||
loadSettings()
|
||||
|
||||
// 设置 LAF
|
||||
setupLaf()
|
||||
|
||||
// 解密数据
|
||||
openDoor()
|
||||
|
||||
// 启动主窗口
|
||||
startMainFrame()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun openDoor() {
|
||||
if (Doorman.instance.isWorking()) {
|
||||
if (!DoormanDialog(null).open()) {
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
private fun loadSettings() {
|
||||
val language = Database.instance.appearance.language
|
||||
val locale = runCatching { LocaleUtils.toLocale(language) }.getOrElse { Locale.getDefault() }
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("Language: {} , Locale: {}", language, locale)
|
||||
}
|
||||
Locale.setDefault(locale)
|
||||
}
|
||||
|
||||
|
||||
private fun setupLaf() {
|
||||
|
||||
System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux}")
|
||||
System.setProperty(FlatSystemProperties.USE_ROUNDED_POPUP_BORDER, "false")
|
||||
|
||||
if (SystemInfo.isLinux) {
|
||||
JFrame.setDefaultLookAndFeelDecorated(true)
|
||||
JDialog.setDefaultLookAndFeelDecorated(true)
|
||||
}
|
||||
|
||||
val themeManager = ThemeManager.instance
|
||||
val settings = Database.instance
|
||||
var theme = settings.appearance.theme
|
||||
|
||||
// 如果是跟随系统或者不存在样式,那么使用默认的
|
||||
if (settings.appearance.followSystem || !themeManager.themes.containsKey(theme)) {
|
||||
theme = if (OsThemeDetector.getDetector().isDark) {
|
||||
"Dark"
|
||||
} else {
|
||||
"Light"
|
||||
}
|
||||
}
|
||||
|
||||
themeManager.change(theme, true)
|
||||
|
||||
FlatInspector.install("ctrl shift alt X");
|
||||
|
||||
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
||||
UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false)
|
||||
UIManager.put("TitlePane.useWindowDecorations", false)
|
||||
|
||||
UIManager.put("Component.arc", 5)
|
||||
UIManager.put("TextComponent.arc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("Component.hideMnemonics", false)
|
||||
|
||||
UIManager.put("TitleBar.height", 36)
|
||||
|
||||
UIManager.put("Dialog.width", 650)
|
||||
UIManager.put("Dialog.height", 550)
|
||||
|
||||
|
||||
if (SystemInfo.isMacOS) {
|
||||
UIManager.put("TabbedPane.tabHeight", UIManager.getInt("TitleBar.height"))
|
||||
} else if (SystemInfo.isLinux) {
|
||||
UIManager.put("TabbedPane.tabHeight", UIManager.getInt("TitleBar.height") - 4)
|
||||
} else {
|
||||
UIManager.put("TabbedPane.tabHeight", UIManager.getInt("TitleBar.height") - 6)
|
||||
}
|
||||
|
||||
if (SystemInfo.isLinux) {
|
||||
UIManager.put("TitlePane.centerTitle", true)
|
||||
UIManager.put("TitlePane.showIcon", false)
|
||||
UIManager.put("TitlePane.showIconInDialogs", false)
|
||||
}
|
||||
|
||||
UIManager.put("Table.rowHeight", 24)
|
||||
UIManager.put("Table.cellNoFocusBorder", BorderFactory.createEmptyBorder())
|
||||
UIManager.put("Table.focusCellHighlightBorder", BorderFactory.createEmptyBorder())
|
||||
UIManager.put("Table.focusSelectedCellHighlightBorder", BorderFactory.createEmptyBorder())
|
||||
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
||||
|
||||
UIManager.put("Tree.rowHeight", 24)
|
||||
UIManager.put("Tree.background", DynamicColor("window"))
|
||||
UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("Tree.showCellFocusIndicator", false)
|
||||
UIManager.put("Tree.repaintWholeRow", true)
|
||||
|
||||
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun printSystemInfo() {
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("Welcome to ${Application.getName()} ${Application.getVersion()}!")
|
||||
log.info(
|
||||
"JVM name: {} , vendor: {} , version: {}",
|
||||
SystemUtils.JAVA_VM_NAME,
|
||||
SystemUtils.JAVA_VM_VENDOR,
|
||||
SystemUtils.JAVA_VM_VERSION,
|
||||
)
|
||||
log.info(
|
||||
"OS name: {} , version: {} , arch: {}",
|
||||
SystemUtils.OS_NAME,
|
||||
SystemUtils.OS_VERSION,
|
||||
SystemUtils.OS_ARCH
|
||||
)
|
||||
log.info("Base config dir: ${Application.getBaseDataDir().absolutePath}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Windows 情况覆盖
|
||||
*/
|
||||
private fun setupTinylog() {
|
||||
if (SystemInfo.isWindows) {
|
||||
val dir = File(Application.getBaseDataDir(), "logs")
|
||||
FileUtils.forceMkdir(dir)
|
||||
Configuration.set("writer_file.latest", "${dir.absolutePath}/${Application.getName().lowercase()}.log")
|
||||
Configuration.set("writer_file.file", "${dir.absolutePath}/{date:yyyy}-{date:MM}-{date:dd}.log")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun checkSingleton() {
|
||||
val file = File(Application.getBaseDataDir(), "lock")
|
||||
val pidFile = File(Application.getBaseDataDir(), "pid")
|
||||
|
||||
|
||||
val raf = RandomAccessFile(file, "rw")
|
||||
val lock = raf.channel.tryLock()
|
||||
|
||||
if (lock != null) {
|
||||
pidFile.writeText(ProcessHandle.current().pid().toString())
|
||||
pidFile.deleteOnExit()
|
||||
file.deleteOnExit()
|
||||
} else {
|
||||
if (SystemInfo.isWindows && pidFile.exists()) {
|
||||
val pid = NumberUtils.toLong(pidFile.readText())
|
||||
for (window in WindowUtils.getAllWindows(false)) {
|
||||
if (pid > 0) {
|
||||
val processId = IntByReference()
|
||||
User32.INSTANCE.GetWindowThreadProcessId(window.hwnd, processId)
|
||||
if (processId.value.toLong() != pid) {
|
||||
continue
|
||||
}
|
||||
} else if (window.title != Application.getName() || window.filePath.endsWith("explorer.exe")) {
|
||||
continue
|
||||
}
|
||||
User32.INSTANCE.ShowWindow(window.hwnd, User32.SW_SHOWNOACTIVATE)
|
||||
User32.INSTANCE.SetForegroundWindow(window.hwnd)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
System.err.println("Program is already running")
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
singletonLock = lock
|
||||
}
|
||||
|
||||
|
||||
private fun openDatabase() {
|
||||
val dir = Application.getDatabaseFile()
|
||||
try {
|
||||
Database.open(dir)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
JOptionPane.showMessageDialog(
|
||||
null, "Unable to open database",
|
||||
I18n.getString("termora.title"), JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
68
src/main/kotlin/app/termora/BannerPanel.kt
Normal file
68
src/main/kotlin/app/termora/BannerPanel.kt
Normal file
@@ -0,0 +1,68 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import org.apache.commons.lang3.RandomUtils
|
||||
import java.awt.*
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.UIManager
|
||||
|
||||
class BannerPanel(fontSize: Int = 11, val beautiful: Boolean = false) : JComponent() {
|
||||
private val banner = """
|
||||
______
|
||||
/_ __/__ _________ ___ ____ _________ _
|
||||
/ / / _ \/ ___/ __ `__ \/ __ \/ ___/ __ `/
|
||||
/ / / __/ / / / / / / / /_/ / / / /_/ /
|
||||
/_/ \___/_/ /_/ /_/ /_/\____/_/ \__,_/
|
||||
""".trimIndent().lines()
|
||||
|
||||
private val colors = mutableListOf<Color>()
|
||||
|
||||
init {
|
||||
font = Font("JetBrains Mono", Font.PLAIN, fontSize)
|
||||
preferredSize = Dimension(width, getFontMetrics(font).height * banner.size)
|
||||
size = preferredSize
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
if (g is Graphics2D) {
|
||||
g.setRenderingHints(
|
||||
RenderingHints(
|
||||
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
g.font = font
|
||||
g.color = UIManager.getColor("TextField.placeholderForeground")
|
||||
|
||||
val height = g.fontMetrics.height
|
||||
val descent = g.fontMetrics.descent
|
||||
val offset = width / 2 - g.fontMetrics.stringWidth(banner.maxBy { it.length }) / 2
|
||||
val insecure = RandomUtils.insecure()
|
||||
var index = 0
|
||||
|
||||
for (i in banner.indices) {
|
||||
var x = offset
|
||||
val y = height * (i + 1) - descent
|
||||
val chars = banner[i].toCharArray()
|
||||
for (j in chars.indices) {
|
||||
if (beautiful) {
|
||||
if (colors.size <= index) {
|
||||
colors.add(
|
||||
Color(
|
||||
insecure.randomInt(0, 255),
|
||||
insecure.randomInt(0, 255),
|
||||
insecure.randomInt(0, 255)
|
||||
)
|
||||
)
|
||||
}
|
||||
val color = colors[index++]
|
||||
g.color = if (FlatLaf.isLafDark()) color.brighter() else color.darker()
|
||||
}
|
||||
g.drawChars(chars, j, 1, x, y)
|
||||
x += g.fontMetrics.charWidth(chars[j])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
41
src/main/kotlin/app/termora/ChannelShellPtyConnector.kt
Normal file
41
src/main/kotlin/app/termora/ChannelShellPtyConnector.kt
Normal file
@@ -0,0 +1,41 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.terminal.StreamPtyConnector
|
||||
import org.apache.sshd.client.channel.ChannelShell
|
||||
import org.apache.sshd.client.channel.ClientChannelEvent
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.charset.Charset
|
||||
|
||||
class ChannelShellPtyConnector(
|
||||
private val channel: ChannelShell,
|
||||
private val charset: Charset = Charsets.UTF_8
|
||||
) : StreamPtyConnector(channel.invertedOut, channel.invertedIn) {
|
||||
|
||||
private val reader = InputStreamReader(input, charset)
|
||||
|
||||
override fun read(buffer: CharArray): Int {
|
||||
return reader.read(buffer)
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteArray, offset: Int, len: Int) {
|
||||
output.write(buffer, offset, len)
|
||||
output.flush()
|
||||
}
|
||||
|
||||
override fun write(buffer: String) {
|
||||
write(buffer.toByteArray(charset))
|
||||
}
|
||||
|
||||
override fun resize(rows: Int, cols: Int) {
|
||||
channel.sendWindowChange(cols, rows)
|
||||
}
|
||||
|
||||
override fun waitFor(): Int {
|
||||
channel.waitFor(listOf(ClientChannelEvent.CLOSED), Long.MAX_VALUE)
|
||||
return channel.exitStatus
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
channel.close(true)
|
||||
}
|
||||
}
|
||||
155
src/main/kotlin/app/termora/Crypto.kt
Normal file
155
src/main/kotlin/app/termora/Crypto.kt
Normal file
@@ -0,0 +1,155 @@
|
||||
package app.termora
|
||||
|
||||
import org.apache.commons.codec.binary.Base64
|
||||
import org.apache.commons.lang3.RandomUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.security.*
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.time.measureTime
|
||||
|
||||
object AES {
|
||||
private const val ALGORITHM = "AES"
|
||||
|
||||
/**
|
||||
* ECB 没有 IV
|
||||
*/
|
||||
object ECB {
|
||||
private const val TRANSFORMATION = "AES/ECB/PKCS5Padding"
|
||||
|
||||
fun encrypt(key: ByteArray, data: ByteArray): ByteArray {
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, ALGORITHM))
|
||||
return cipher.doFinal(data)
|
||||
}
|
||||
|
||||
fun decrypt(key: ByteArray, data: ByteArray): ByteArray {
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, ALGORITHM))
|
||||
return cipher.doFinal(data)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 携带 IV
|
||||
*/
|
||||
object CBC {
|
||||
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
|
||||
|
||||
fun encrypt(key: ByteArray, iv: ByteArray, data: ByteArray): ByteArray {
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, ALGORITHM), IvParameterSpec(iv))
|
||||
return cipher.doFinal(data)
|
||||
}
|
||||
|
||||
fun decrypt(key: ByteArray, iv: ByteArray, data: ByteArray): ByteArray {
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, ALGORITHM), IvParameterSpec(iv))
|
||||
return cipher.doFinal(data)
|
||||
}
|
||||
|
||||
|
||||
fun String.aesCBCEncrypt(key: ByteArray, iv: ByteArray): ByteArray {
|
||||
return encrypt(key, iv, toByteArray())
|
||||
}
|
||||
|
||||
fun ByteArray.aesCBCEncrypt(key: ByteArray, iv: ByteArray): ByteArray {
|
||||
return encrypt(key, iv, this)
|
||||
}
|
||||
|
||||
fun ByteArray.aesCBCDecrypt(key: ByteArray, iv: ByteArray): ByteArray {
|
||||
return decrypt(key, iv, this)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun randomBytes(size: Int = 32): ByteArray {
|
||||
return RandomUtils.secureStrong().randomBytes(size)
|
||||
}
|
||||
|
||||
fun ByteArray.encodeBase64String(): String {
|
||||
return Base64.encodeBase64String(this)
|
||||
}
|
||||
|
||||
fun String.decodeBase64(): ByteArray {
|
||||
return Base64.decodeBase64(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
object PBKDF2 {
|
||||
|
||||
private const val ALGORITHM = "PBKDF2WithHmacSHA512"
|
||||
private val log = LoggerFactory.getLogger(PBKDF2::class.java)
|
||||
|
||||
fun generateSecret(
|
||||
password: CharArray,
|
||||
salt: ByteArray,
|
||||
iterationCount: Int = 150000,
|
||||
keyLength: Int = 256
|
||||
): ByteArray {
|
||||
val bytes: ByteArray
|
||||
val time = measureTime {
|
||||
bytes = SecretKeyFactory.getInstance(ALGORITHM)
|
||||
.generateSecret(PBEKeySpec(password, salt, iterationCount, keyLength))
|
||||
.encoded
|
||||
}
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Secret generated $time")
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
object RSA {
|
||||
|
||||
private const val TRANSFORMATION = "RSA"
|
||||
|
||||
fun encrypt(publicKey: PublicKey, data: ByteArray): ByteArray {
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
|
||||
return cipher.doFinal(data)
|
||||
}
|
||||
|
||||
fun decrypt(privateKey: PrivateKey, data: ByteArray): ByteArray {
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.DECRYPT_MODE, privateKey)
|
||||
return cipher.doFinal(data)
|
||||
}
|
||||
|
||||
fun encrypt(privateKey: PrivateKey, data: ByteArray): ByteArray {
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, privateKey)
|
||||
return cipher.doFinal(data)
|
||||
}
|
||||
|
||||
fun decrypt(publicKey: PublicKey, data: ByteArray): ByteArray {
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.DECRYPT_MODE, publicKey)
|
||||
return cipher.doFinal(data)
|
||||
}
|
||||
|
||||
fun generatePublic(publicKey: ByteArray): PublicKey {
|
||||
return KeyFactory.getInstance(TRANSFORMATION)
|
||||
.generatePublic(X509EncodedKeySpec(publicKey))
|
||||
}
|
||||
|
||||
fun generatePrivate(privateKey: ByteArray): PrivateKey {
|
||||
return KeyFactory.getInstance(TRANSFORMATION)
|
||||
.generatePrivate(PKCS8EncodedKeySpec(privateKey))
|
||||
}
|
||||
|
||||
fun generateKeyPair(keySize: Int = 2048): KeyPair {
|
||||
val generator = KeyPairGenerator.getInstance(TRANSFORMATION)
|
||||
generator.initialize(keySize)
|
||||
return generator.generateKeyPair()
|
||||
}
|
||||
}
|
||||
200
src/main/kotlin/app/termora/DialogWrapper.kt
Normal file
200
src/main/kotlin/app/termora/DialogWrapper.kt
Normal file
@@ -0,0 +1,200 @@
|
||||
package app.termora
|
||||
|
||||
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.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()
|
||||
private val titleBar by lazy { LogicCustomTitleBar.createCustomTitleBar(this) }
|
||||
val disposable = Disposer.newDisposable()
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_ACTION = "DEFAULT_ACTION"
|
||||
}
|
||||
|
||||
|
||||
protected var controlsVisible = true
|
||||
set(value) {
|
||||
field = value
|
||||
titleBar.putProperty("controls.visible", value)
|
||||
}
|
||||
|
||||
protected var titleBarHeight = UIManager.getInt("TabbedPane.tabHeight").toFloat()
|
||||
set(value) {
|
||||
titleBar.height = value
|
||||
field = value
|
||||
}
|
||||
|
||||
protected var lostFocusDispose = false
|
||||
protected var escapeDispose = true
|
||||
|
||||
protected fun init() {
|
||||
|
||||
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
|
||||
|
||||
initTitleBar()
|
||||
initEvents()
|
||||
|
||||
if (JBR.isWindowDecorationsSupported()) {
|
||||
if (rootPane.getClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE) != false) {
|
||||
val titlePanel = createTitlePanel()
|
||||
if (titlePanel != null) {
|
||||
rootPanel.add(titlePanel, BorderLayout.NORTH)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rootPanel.add(createCenterPanel(), BorderLayout.CENTER)
|
||||
|
||||
val southPanel = createSouthPanel()
|
||||
if (southPanel != null) {
|
||||
rootPanel.add(southPanel, BorderLayout.SOUTH)
|
||||
}
|
||||
|
||||
rootPane.contentPane = rootPanel
|
||||
}
|
||||
|
||||
protected open fun createSouthPanel(): JComponent? {
|
||||
val box = Box.createHorizontalBox()
|
||||
box.border = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor),
|
||||
BorderFactory.createEmptyBorder(8, 12, 8, 12)
|
||||
)
|
||||
|
||||
val okButton = createJButtonForAction(createOkAction())
|
||||
box.add(Box.createHorizontalGlue())
|
||||
box.add(createJButtonForAction(CancelAction()))
|
||||
box.add(Box.createHorizontalStrut(8))
|
||||
box.add(okButton)
|
||||
|
||||
|
||||
return box
|
||||
}
|
||||
|
||||
protected open fun createOkAction(): AbstractAction {
|
||||
return OkAction()
|
||||
}
|
||||
|
||||
protected open fun createJButtonForAction(action: Action): JButton {
|
||||
val button = JButton(action)
|
||||
val value = action.getValue(DEFAULT_ACTION)
|
||||
if (value is Boolean && value) {
|
||||
rootPane.defaultButton = button
|
||||
}
|
||||
return button
|
||||
}
|
||||
|
||||
protected open fun createTitlePanel(): JPanel? {
|
||||
titleLabel.horizontalAlignment = SwingConstants.CENTER
|
||||
titleLabel.verticalAlignment = SwingConstants.CENTER
|
||||
titleLabel.text = title
|
||||
titleLabel.putClientProperty("FlatLaf.style", "font: bold")
|
||||
|
||||
val panel = JPanel(BorderLayout())
|
||||
panel.add(titleLabel, BorderLayout.CENTER)
|
||||
panel.preferredSize = Dimension(-1, titleBar.height.toInt())
|
||||
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
override fun setTitle(title: String?) {
|
||||
super.setTitle(title)
|
||||
titleLabel.text = title
|
||||
}
|
||||
|
||||
protected abstract fun createCenterPanel(): JComponent
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
val inputMap = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
|
||||
if (escapeDispose) {
|
||||
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "close")
|
||||
}
|
||||
|
||||
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, toolkit.menuShortcutKeyMaskEx), "close")
|
||||
|
||||
rootPane.actionMap.put("close", object : AnAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
doCancelAction()
|
||||
}
|
||||
})
|
||||
|
||||
addWindowFocusListener(object : WindowAdapter() {
|
||||
override fun windowLostFocus(e: WindowEvent) {
|
||||
if (lostFocusDispose) {
|
||||
SwingUtilities.invokeLater { doCancelAction() }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
Disposer.dispose(disposable)
|
||||
}
|
||||
})
|
||||
|
||||
if (SystemInfo.isWindows) {
|
||||
addWindowListener(object : WindowAdapter(), ThemeChangeListener {
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
ThemeManager.instance.removeThemeChangeListener(this)
|
||||
}
|
||||
|
||||
override fun windowOpened(e: WindowEvent) {
|
||||
onChanged()
|
||||
ThemeManager.instance.addThemeChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onChanged() {
|
||||
titleBar.putProperty("controls.dark", FlatLaf.isLafDark())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun initTitleBar() {
|
||||
titleBar.height = titleBarHeight
|
||||
titleBar.putProperty("controls.visible", controlsVisible)
|
||||
if (JBR.isWindowDecorationsSupported()) {
|
||||
JBR.getWindowDecorations().setCustomTitleBar(this, titleBar)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun doOKAction() {
|
||||
dispose()
|
||||
}
|
||||
|
||||
protected open fun doCancelAction() {
|
||||
dispose()
|
||||
}
|
||||
|
||||
protected inner class OkAction(text: String = I18n.getString("termora.confirm")) : AnAction(text) {
|
||||
init {
|
||||
putValue(DEFAULT_ACTION, true)
|
||||
}
|
||||
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
doOKAction()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected inner class CancelAction : AnAction(I18n.getString("termora.cancel")) {
|
||||
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
doCancelAction()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
18
src/main/kotlin/app/termora/DocumentAdaptor.kt
Normal file
18
src/main/kotlin/app/termora/DocumentAdaptor.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package app.termora
|
||||
|
||||
import javax.swing.event.DocumentEvent
|
||||
import javax.swing.event.DocumentListener
|
||||
|
||||
abstract class DocumentAdaptor : DocumentListener {
|
||||
override fun insertUpdate(e: DocumentEvent) {
|
||||
changedUpdate(e)
|
||||
}
|
||||
|
||||
override fun removeUpdate(e: DocumentEvent) {
|
||||
changedUpdate(e)
|
||||
}
|
||||
|
||||
override fun changedUpdate(e: DocumentEvent) {
|
||||
|
||||
}
|
||||
}
|
||||
85
src/main/kotlin/app/termora/Doorman.kt
Normal file
85
src/main/kotlin/app/termora/Doorman.kt
Normal file
@@ -0,0 +1,85 @@
|
||||
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
|
||||
private var key = byteArrayOf()
|
||||
|
||||
companion object {
|
||||
val instance by lazy { Doorman() }
|
||||
}
|
||||
|
||||
fun isWorking(): Boolean {
|
||||
return properties.getString("doorman", "false").toBoolean()
|
||||
}
|
||||
|
||||
fun encrypt(text: String): String {
|
||||
checkIsWorking()
|
||||
return AES.ECB.encrypt(key, text.toByteArray()).encodeBase64String()
|
||||
}
|
||||
|
||||
|
||||
fun decrypt(text: String): String {
|
||||
checkIsWorking()
|
||||
return AES.ECB.decrypt(key, text.decodeBase64()).decodeToString()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回钥匙
|
||||
*/
|
||||
fun work(password: CharArray): ByteArray {
|
||||
if (key.isNotEmpty()) {
|
||||
throw IllegalStateException("Working")
|
||||
}
|
||||
return work(convertKey(password))
|
||||
}
|
||||
|
||||
fun work(key: ByteArray): ByteArray {
|
||||
val verify = properties.getString("doorman-verify")
|
||||
if (verify == null) {
|
||||
properties.putString(
|
||||
"doorman-verify",
|
||||
AES.ECB.encrypt(key, factor()).encodeBase64String()
|
||||
)
|
||||
} else {
|
||||
try {
|
||||
if (!AES.ECB.decrypt(key, verify.decodeBase64()).contentEquals(factor())) {
|
||||
throw PasswordWrongException()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw PasswordWrongException()
|
||||
}
|
||||
}
|
||||
|
||||
this.key = key
|
||||
properties.putString("doorman", "true")
|
||||
|
||||
return this.key
|
||||
}
|
||||
|
||||
|
||||
private fun convertKey(password: CharArray): ByteArray {
|
||||
return PBKDF2.generateSecret(password, factor())
|
||||
}
|
||||
|
||||
|
||||
private fun checkIsWorking() {
|
||||
if (key.isEmpty() || !isWorking()) {
|
||||
throw UnsupportedOperationException("Doorman is not working")
|
||||
}
|
||||
}
|
||||
|
||||
private fun factor(): ByteArray {
|
||||
return Application.getName().toByteArray()
|
||||
}
|
||||
|
||||
fun test(password: CharArray): Boolean {
|
||||
checkIsWorking()
|
||||
return key.contentEquals(convertKey(password))
|
||||
}
|
||||
}
|
||||
309
src/main/kotlin/app/termora/DoormanDialog.kt
Normal file
309
src/main/kotlin/app/termora/DoormanDialog.kt
Normal file
@@ -0,0 +1,309 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.AES.decodeBase64
|
||||
import app.termora.db.Database
|
||||
import app.termora.terminal.ControlCharacters
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||
import com.formdev.flatlaf.extras.components.FlatButton
|
||||
import com.formdev.flatlaf.extras.components.FlatLabel
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.JXHyperlink
|
||||
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
|
||||
import javax.swing.*
|
||||
import kotlin.math.max
|
||||
|
||||
class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(DoormanDialog::class.java)
|
||||
}
|
||||
|
||||
private val formMargin = "7dlu"
|
||||
private val label = FlatLabel()
|
||||
private val icon = JLabel()
|
||||
private val passwordTextField = OutlinePasswordField()
|
||||
private val tip = FlatLabel()
|
||||
private val safeBtn = FlatButton()
|
||||
|
||||
var isOpened = false
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
||||
isModal = true
|
||||
isResizable = false
|
||||
controlsVisible = false
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
title = I18n.getString("termora.doorman.safe")
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||
}
|
||||
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
val sizes = listOf(16, 20, 24, 28, 32, 48, 64)
|
||||
val loader = TermoraFrame::class.java.classLoader
|
||||
val images = sizes.mapNotNull { e ->
|
||||
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
|
||||
}
|
||||
iconImages = images
|
||||
}
|
||||
|
||||
setLocationRelativeTo(null)
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
label.text = I18n.getString("termora.doorman.safe")
|
||||
tip.text = I18n.getString("termora.doorman.unlock-data")
|
||||
icon.icon = FlatSVGIcon(Icons.role.name, 80, 80)
|
||||
safeBtn.icon = Icons.unlocked
|
||||
|
||||
|
||||
label.labelType = FlatLabel.LabelType.h2
|
||||
label.horizontalAlignment = SwingConstants.CENTER
|
||||
safeBtn.isFocusable = false
|
||||
tip.foreground = UIManager.getColor("TextField.placeholderForeground")
|
||||
icon.horizontalAlignment = SwingConstants.CENTER
|
||||
|
||||
|
||||
safeBtn.addActionListener { doOKAction() }
|
||||
passwordTextField.addActionListener { doOKAction() }
|
||||
|
||||
var rows = 2
|
||||
val step = 2
|
||||
return FormBuilder.create().debug(false)
|
||||
.layout(
|
||||
FormLayout(
|
||||
"$formMargin, default:grow, 4dlu, pref, $formMargin",
|
||||
"${if (SystemInfo.isWindows) "20dlu" else "0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
|
||||
)
|
||||
)
|
||||
.add(icon).xyw(2, rows, 4).apply { rows += step }
|
||||
.add(label).xyw(2, rows, 4).apply { rows += step }
|
||||
.add(passwordTextField).xy(2, rows)
|
||||
.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) {
|
||||
val option = OptionPane.showConfirmDialog(
|
||||
this@DoormanDialog, I18n.getString("termora.doorman.forget-password-message"),
|
||||
options = arrayOf(
|
||||
I18n.getString("termora.doorman.have-a-mnemonic"),
|
||||
I18n.getString("termora.doorman.dont-have-a-mnemonic"),
|
||||
),
|
||||
optionType = JOptionPane.YES_NO_OPTION,
|
||||
messageType = JOptionPane.INFORMATION_MESSAGE,
|
||||
initialValue = I18n.getString("termora.doorman.have-a-mnemonic")
|
||||
)
|
||||
if (option == JOptionPane.YES_OPTION) {
|
||||
showMnemonicsDialog()
|
||||
} else if (option == JOptionPane.NO_OPTION) {
|
||||
OptionPane.showMessageDialog(
|
||||
this@DoormanDialog,
|
||||
I18n.getString("termora.doorman.delete-data"),
|
||||
messageType = JOptionPane.WARNING_MESSAGE
|
||||
)
|
||||
Application.browse(Application.getDatabaseFile().toURI())
|
||||
}
|
||||
}
|
||||
}).apply { isFocusable = false }).xyw(2, rows, 4, "center, fill")
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun showMnemonicsDialog() {
|
||||
val dialog = MnemonicsDialog(this@DoormanDialog)
|
||||
dialog.isVisible = true
|
||||
val entropy = dialog.entropy
|
||||
if (entropy.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val keyBackup = Database.instance.properties.getString("doorman-key-backup")
|
||||
?: throw IllegalStateException("doorman-key-backup is null")
|
||||
val key = AES.ECB.decrypt(entropy, keyBackup.decodeBase64())
|
||||
Doorman.instance.work(key)
|
||||
} catch (e: Exception) {
|
||||
OptionPane.showMessageDialog(
|
||||
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
passwordTextField.outline = "error"
|
||||
passwordTextField.requestFocus()
|
||||
return
|
||||
}
|
||||
|
||||
isOpened = true
|
||||
super.doOKAction()
|
||||
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
if (passwordTextField.password.isEmpty()) {
|
||||
passwordTextField.outline = "error"
|
||||
passwordTextField.requestFocus()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Doorman.instance.work(passwordTextField.password)
|
||||
} catch (e: Exception) {
|
||||
if (e is PasswordWrongException) {
|
||||
OptionPane.showMessageDialog(
|
||||
this, I18n.getString("termora.doorman.password-wrong"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
passwordTextField.outline = "error"
|
||||
passwordTextField.requestFocus()
|
||||
return
|
||||
}
|
||||
|
||||
isOpened = true
|
||||
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
fun open(): Boolean {
|
||||
isModal = true
|
||||
isVisible = true
|
||||
return isOpened
|
||||
}
|
||||
|
||||
|
||||
private class MnemonicsDialog(owner: Window) : DialogWrapper(owner) {
|
||||
|
||||
private val textFields = (1..12).map { PasteTextField(it) }
|
||||
var entropy = byteArrayOf()
|
||||
private set
|
||||
|
||||
init {
|
||||
isModal = true
|
||||
isResizable = true
|
||||
controlsVisible = false
|
||||
title = I18n.getString("termora.doorman.mnemonic.title")
|
||||
init()
|
||||
pack()
|
||||
size = Dimension(max(size.width, UIManager.getInt("Dialog.width") - 250), size.height)
|
||||
setLocationRelativeTo(null)
|
||||
}
|
||||
|
||||
fun getWords(): List<String> {
|
||||
val words = mutableListOf<String>()
|
||||
for (e in textFields) {
|
||||
if (e.text.isBlank()) {
|
||||
return emptyList()
|
||||
}
|
||||
words.add(e.text)
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val formMargin = "4dlu"
|
||||
val layout = FormLayout(
|
||||
"default:grow, $formMargin, default:grow, $formMargin, default:grow, $formMargin, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
|
||||
.layout(layout).debug(true)
|
||||
val iterator = textFields.iterator()
|
||||
for (i in 1..5 step 2) {
|
||||
for (j in 1..7 step 2) {
|
||||
builder.add(iterator.next()).xy(j, i)
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
for (textField in textFields) {
|
||||
if (textField.text.isBlank()) {
|
||||
textField.outline = "error"
|
||||
textField.requestFocusInWindow()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Mnemonics.MnemonicCode(getWords().joinToString(StringUtils.SPACE)).use {
|
||||
it.validate()
|
||||
entropy = it.toEntropy()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
OptionPane.showMessageDialog(
|
||||
this,
|
||||
I18n.getString("termora.doorman.mnemonic.incorrect"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
override fun doCancelAction() {
|
||||
entropy = byteArrayOf()
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
private inner class PasteTextField(private val index: Int) : OutlineTextField() {
|
||||
init {
|
||||
addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
if (e.keyCode == KeyEvent.VK_BACK_SPACE) {
|
||||
if (text.isEmpty() && index != 1) {
|
||||
textFields[index - 2].requestFocusInWindow()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun paste() {
|
||||
if (!toolkit.systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||
return
|
||||
}
|
||||
|
||||
val text = toolkit.systemClipboard.getData(DataFlavor.stringFlavor)?.toString() ?: return
|
||||
if (text.isBlank()) {
|
||||
return
|
||||
}
|
||||
val words = mutableListOf<String>()
|
||||
if (text.count { it == ControlCharacters.SP } > text.count { it == ControlCharacters.LF }) {
|
||||
words.addAll(text.split(StringUtils.SPACE))
|
||||
} else {
|
||||
words.addAll(text.split(ControlCharacters.LF))
|
||||
}
|
||||
val iterator = words.iterator()
|
||||
for (i in index..textFields.size) {
|
||||
if (iterator.hasNext()) {
|
||||
textFields[i - 1].text = iterator.next()
|
||||
textFields[i - 1].requestFocusInWindow()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun createSouthPanel(): JComponent? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
63
src/main/kotlin/app/termora/DynamicColor.kt
Normal file
63
src/main/kotlin/app/termora/DynamicColor.kt
Normal file
@@ -0,0 +1,63 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import java.awt.Color
|
||||
import javax.swing.UIManager
|
||||
|
||||
open class DynamicColor : Color {
|
||||
private var regular: Color?
|
||||
private val dark: Color?
|
||||
private var colorKey: String? = null
|
||||
private val color: Color
|
||||
get() {
|
||||
val r = regular
|
||||
val d = dark
|
||||
if (r == null || d == null) {
|
||||
return UIManager.getColor(colorKey)
|
||||
}
|
||||
return if (FlatLaf.isLafDark()) d else r
|
||||
}
|
||||
|
||||
constructor(regular: Color, dark: Color) : super(regular.rgb, regular.alpha != 255) {
|
||||
this.regular = regular
|
||||
this.dark = dark
|
||||
}
|
||||
|
||||
companion object {
|
||||
val BorderColor = DynamicColor("Component.borderColor")
|
||||
}
|
||||
|
||||
constructor(key: String) : super(0) {
|
||||
this.regular = null
|
||||
this.dark = null
|
||||
this.colorKey = key
|
||||
}
|
||||
|
||||
override fun getRed(): Int {
|
||||
return color.red
|
||||
}
|
||||
|
||||
override fun getGreen(): Int {
|
||||
return color.green
|
||||
}
|
||||
|
||||
override fun getBlue(): Int {
|
||||
return color.blue
|
||||
}
|
||||
|
||||
override fun getAlpha(): Int {
|
||||
return color.alpha
|
||||
}
|
||||
|
||||
override fun getRGB(): Int {
|
||||
return color.rgb
|
||||
}
|
||||
|
||||
override fun brighter(): Color {
|
||||
return color.brighter()
|
||||
}
|
||||
|
||||
override fun darker(): Color {
|
||||
return color.darker()
|
||||
}
|
||||
}
|
||||
10
src/main/kotlin/app/termora/DynamicIcon.kt
Normal file
10
src/main/kotlin/app/termora/DynamicIcon.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||
|
||||
open class DynamicIcon(name: String, private val darkName: String) : FlatSVGIcon(name) {
|
||||
constructor(name: String) : this(name, name)
|
||||
|
||||
val dark by lazy { DynamicIcon(darkName, name) }
|
||||
|
||||
}
|
||||
54
src/main/kotlin/app/termora/EditHostOptionsPane.kt
Normal file
54
src/main/kotlin/app/termora/EditHostOptionsPane.kt
Normal file
@@ -0,0 +1,54 @@
|
||||
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
|
||||
generalOption.nameTextField.text = host.name
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||
proxyOption.proxyHostTextField.text = host.proxy.host
|
||||
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
||||
proxyOption.proxyUsernameTextField.text = host.proxy.username
|
||||
proxyOption.proxyPortTextField.value = host.proxy.port
|
||||
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
|
||||
|
||||
terminalOption.charsetComboBox.selectedItem = host.options.encoding
|
||||
terminalOption.environmentTextArea.text = host.options.env
|
||||
terminalOption.startupCommandTextField.text = host.options.startupCommand
|
||||
|
||||
tunnelingOption.tunnelings.addAll(host.tunnelings)
|
||||
}
|
||||
|
||||
override fun getHost(): Host {
|
||||
val newHost = super.getHost()
|
||||
return host.copy(
|
||||
name = newHost.name,
|
||||
protocol = newHost.protocol,
|
||||
host = newHost.host,
|
||||
port = newHost.port,
|
||||
username = newHost.username,
|
||||
authentication = newHost.authentication,
|
||||
proxy = newHost.proxy,
|
||||
remark = newHost.remark,
|
||||
updateDate = System.currentTimeMillis(),
|
||||
options = newHost.options,
|
||||
tunnelings = newHost.tunnelings,
|
||||
)
|
||||
}
|
||||
}
|
||||
236
src/main/kotlin/app/termora/Host.kt
Normal file
236
src/main/kotlin/app/termora/Host.kt
Normal file
@@ -0,0 +1,236 @@
|
||||
package app.termora
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.util.*
|
||||
|
||||
|
||||
fun UUID.toSimpleString(): String {
|
||||
return toString().replace("-", StringUtils.EMPTY)
|
||||
}
|
||||
|
||||
enum class Protocol {
|
||||
Folder,
|
||||
SSH,
|
||||
Local,
|
||||
}
|
||||
|
||||
|
||||
enum class AuthenticationType {
|
||||
No,
|
||||
Password,
|
||||
PublicKey,
|
||||
KeyboardInteractive,
|
||||
}
|
||||
|
||||
enum class ProxyType {
|
||||
No,
|
||||
HTTP,
|
||||
SOCKS5,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Authentication(
|
||||
val type: AuthenticationType,
|
||||
val password: String,
|
||||
) {
|
||||
companion object {
|
||||
val No = Authentication(AuthenticationType.No, String())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
data class Options(
|
||||
/**
|
||||
* 跳板机
|
||||
*/
|
||||
val jumpHosts: List<String> = mutableListOf(),
|
||||
/**
|
||||
* 编码
|
||||
*/
|
||||
val encoding: String = "UTF-8",
|
||||
/**
|
||||
* 环境变量
|
||||
*/
|
||||
val env: String = StringUtils.EMPTY,
|
||||
/**
|
||||
* 连接成功后立即发送命令
|
||||
*/
|
||||
val startupCommand: String = StringUtils.EMPTY,
|
||||
) {
|
||||
companion object {
|
||||
val Default = Options()
|
||||
}
|
||||
|
||||
fun envs(): Map<String, String> {
|
||||
if (env.isBlank()) return emptyMap()
|
||||
val envs = mutableMapOf<String, String>()
|
||||
for (line in env.lines()) {
|
||||
if (line.isBlank()) continue
|
||||
val vars = line.split("=", limit = 2)
|
||||
if (vars.size != 2) continue
|
||||
envs[vars[0]] = vars[1]
|
||||
}
|
||||
return envs
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Proxy(
|
||||
val type: ProxyType,
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val authenticationType: AuthenticationType = AuthenticationType.No,
|
||||
val username: String,
|
||||
val password: String,
|
||||
) {
|
||||
companion object {
|
||||
val No = Proxy(
|
||||
ProxyType.No,
|
||||
host = StringUtils.EMPTY,
|
||||
port = 7890,
|
||||
username = StringUtils.EMPTY,
|
||||
password = StringUtils.EMPTY
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class TunnelingType {
|
||||
Local,
|
||||
Remote,
|
||||
Dynamic
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Tunneling(
|
||||
val name: String = StringUtils.EMPTY,
|
||||
val type: TunnelingType = TunnelingType.Local,
|
||||
val sourceHost: String = StringUtils.EMPTY,
|
||||
val sourcePort: Int = 0,
|
||||
val destinationHost: String = StringUtils.EMPTY,
|
||||
val destinationPort: Int = 0,
|
||||
)
|
||||
|
||||
|
||||
@Serializable
|
||||
data class EncryptedHost(
|
||||
var id: String = StringUtils.EMPTY,
|
||||
var name: String = StringUtils.EMPTY,
|
||||
var protocol: String = StringUtils.EMPTY,
|
||||
var host: String = StringUtils.EMPTY,
|
||||
var port: String = StringUtils.EMPTY,
|
||||
var username: String = StringUtils.EMPTY,
|
||||
var remark: String = StringUtils.EMPTY,
|
||||
var authentication: String = StringUtils.EMPTY,
|
||||
var proxy: String = StringUtils.EMPTY,
|
||||
var options: String = StringUtils.EMPTY,
|
||||
var tunnelings: String = StringUtils.EMPTY,
|
||||
var sort: Long = 0L,
|
||||
var deleted: Boolean = false,
|
||||
var parentId: String = StringUtils.EMPTY,
|
||||
var ownerId: String = StringUtils.EMPTY,
|
||||
var creatorId: String = StringUtils.EMPTY,
|
||||
var createDate: Long = 0L,
|
||||
var updateDate: Long = 0L,
|
||||
)
|
||||
|
||||
|
||||
@Serializable
|
||||
data class Host(
|
||||
/**
|
||||
* 唯一ID
|
||||
*/
|
||||
val id: String = UUID.randomUUID().toSimpleString(),
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
val name: String,
|
||||
/**
|
||||
* 协议
|
||||
*/
|
||||
val protocol: Protocol,
|
||||
/**
|
||||
* 主机
|
||||
*/
|
||||
val host: String = StringUtils.EMPTY,
|
||||
/**
|
||||
* 端口
|
||||
*/
|
||||
val port: Int = 0,
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
val username: String = StringUtils.EMPTY,
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
val remark: String = StringUtils.EMPTY,
|
||||
/**
|
||||
* 认证信息
|
||||
*/
|
||||
val authentication: Authentication = Authentication.No,
|
||||
/**
|
||||
* 代理
|
||||
*/
|
||||
val proxy: Proxy = Proxy.No,
|
||||
|
||||
/**
|
||||
* 选项,备用字段
|
||||
*/
|
||||
val options: Options = Options.Default,
|
||||
|
||||
/**
|
||||
* 隧道
|
||||
*/
|
||||
val tunnelings: List<Tunneling> = emptyList(),
|
||||
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
val sort: Long = 0,
|
||||
/**
|
||||
* 父ID
|
||||
*/
|
||||
val parentId: String = "0",
|
||||
/**
|
||||
* 所属者
|
||||
*/
|
||||
val ownerId: String = "0",
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
val creatorId: String = "0",
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
val createDate: Long = System.currentTimeMillis(),
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
val updateDate: Long = System.currentTimeMillis(),
|
||||
|
||||
/**
|
||||
* 是否已经删除
|
||||
*/
|
||||
val deleted: Boolean = false
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Host
|
||||
|
||||
if (id != other.id) return false
|
||||
if (ownerId != other.ownerId) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + ownerId.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
46
src/main/kotlin/app/termora/HostDialog.kt
Normal file
46
src/main/kotlin/app/termora/HostDialog.kt
Normal file
@@ -0,0 +1,46 @@
|
||||
package app.termora
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.UIManager
|
||||
|
||||
class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
||||
private val pane = if (host != null) EditHostOptionsPane(host) else HostOptionsPane()
|
||||
var host: Host? = host
|
||||
private set
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||
isModal = true
|
||||
title = I18n.getString("termora.new-host.title")
|
||||
setLocationRelativeTo(null)
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
pane.background = UIManager.getColor("window")
|
||||
|
||||
val panel = JPanel(BorderLayout())
|
||||
panel.add(pane, BorderLayout.CENTER)
|
||||
panel.background = UIManager.getColor("window")
|
||||
panel.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
|
||||
override fun doOKAction() {
|
||||
if (!pane.validateFields()) {
|
||||
return
|
||||
}
|
||||
host = pane.getHost()
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
54
src/main/kotlin/app/termora/HostManager.kt
Normal file
54
src/main/kotlin/app/termora/HostManager.kt
Normal file
@@ -0,0 +1,54 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.db.Database
|
||||
import java.util.*
|
||||
|
||||
interface HostListener : EventListener {
|
||||
fun hostAdded(host: Host) {}
|
||||
fun hostRemoved(id: String) {}
|
||||
fun hostsChanged() {}
|
||||
}
|
||||
|
||||
|
||||
class HostManager private constructor() {
|
||||
companion object {
|
||||
val instance by lazy { HostManager() }
|
||||
}
|
||||
|
||||
private val database get() = Database.instance
|
||||
private val listeners = mutableListOf<HostListener>()
|
||||
|
||||
fun addHost(host: Host, notify: Boolean = true) {
|
||||
assertEventDispatchThread()
|
||||
database.addHost(host)
|
||||
if (notify) listeners.forEach { it.hostAdded(host) }
|
||||
}
|
||||
|
||||
fun removeHost(id: String) {
|
||||
assertEventDispatchThread()
|
||||
database.removeHost(id)
|
||||
listeners.forEach { it.hostRemoved(id) }
|
||||
|
||||
}
|
||||
|
||||
fun hosts(): List<Host> {
|
||||
return database.getHosts()
|
||||
.sortedWith(compareBy<Host> { if (it.protocol == Protocol.Folder) 0 else 1 }.thenBy { it.sort })
|
||||
}
|
||||
|
||||
fun removeAll() {
|
||||
assertEventDispatchThread()
|
||||
database.removeAllHost()
|
||||
listeners.forEach { it.hostsChanged() }
|
||||
}
|
||||
|
||||
fun addHostListener(listener: HostListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeHostListener(listener: HostListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
842
src/main/kotlin/app/termora/HostOptionsPane.kt
Normal file
842
src/main/kotlin/app/termora/HostOptionsPane.kt
Normal file
@@ -0,0 +1,842 @@
|
||||
package app.termora
|
||||
|
||||
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
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.*
|
||||
import java.awt.event.*
|
||||
import java.nio.charset.Charset
|
||||
import javax.swing.*
|
||||
import javax.swing.table.DefaultTableModel
|
||||
|
||||
|
||||
open class HostOptionsPane : OptionsPane() {
|
||||
protected val tunnelingOption = TunnelingOption()
|
||||
protected val generalOption = GeneralOption()
|
||||
protected val proxyOption = ProxyOption()
|
||||
protected val terminalOption = TerminalOption()
|
||||
protected val owner: Window? get() = SwingUtilities.getWindowAncestor(this)
|
||||
|
||||
init {
|
||||
addOption(generalOption)
|
||||
addOption(proxyOption)
|
||||
addOption(tunnelingOption)
|
||||
addOption(terminalOption)
|
||||
|
||||
setContentBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
||||
}
|
||||
|
||||
|
||||
open fun getHost(): Host {
|
||||
val name = generalOption.nameTextField.text
|
||||
val protocol = generalOption.protocolTypeComboBox.selectedItem as Protocol
|
||||
val host = generalOption.hostTextField.text
|
||||
val port = (generalOption.portTextField.value ?: 22) as Int
|
||||
var authentication = Authentication.No
|
||||
var proxy = Proxy.No
|
||||
|
||||
if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.Password) {
|
||||
authentication = authentication.copy(
|
||||
type = AuthenticationType.Password,
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||
proxy = proxy.copy(
|
||||
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
|
||||
host = proxyOption.proxyHostTextField.text,
|
||||
username = proxyOption.proxyUsernameTextField.text,
|
||||
password = String(proxyOption.proxyPasswordTextField.password),
|
||||
port = proxyOption.proxyPortTextField.value as Int,
|
||||
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
|
||||
)
|
||||
}
|
||||
|
||||
val options = Options.Default.copy(
|
||||
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
||||
env = terminalOption.environmentTextArea.text,
|
||||
startupCommand = terminalOption.startupCommandTextField.text
|
||||
)
|
||||
|
||||
return Host(
|
||||
name = name,
|
||||
protocol = protocol,
|
||||
host = host,
|
||||
port = port,
|
||||
username = generalOption.usernameTextField.text,
|
||||
authentication = authentication,
|
||||
proxy = proxy,
|
||||
sort = System.currentTimeMillis(),
|
||||
remark = generalOption.remarkTextArea.text,
|
||||
options = options,
|
||||
tunnelings = tunnelingOption.tunnelings
|
||||
)
|
||||
}
|
||||
|
||||
fun validateFields(): Boolean {
|
||||
val host = getHost()
|
||||
|
||||
// general
|
||||
if (validateField(generalOption.nameTextField)
|
||||
|| validateField(generalOption.hostTextField)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (host.protocol == Protocol.SSH) {
|
||||
if (validateField(generalOption.usernameTextField)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (host.authentication.type == AuthenticationType.Password) {
|
||||
if (validateField(generalOption.passwordTextField)) {
|
||||
return false
|
||||
}
|
||||
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||
if (validateField(generalOption.publicKeyTextField)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// proxy
|
||||
if (host.proxy.type != ProxyType.No) {
|
||||
if (validateField(proxyOption.proxyHostTextField)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (host.proxy.authenticationType != AuthenticationType.No) {
|
||||
if (validateField(proxyOption.proxyUsernameTextField)
|
||||
|| validateField(proxyOption.proxyPasswordTextField)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 true 表示有错误
|
||||
*/
|
||||
private fun validateField(textField: JTextField): Boolean {
|
||||
if (textField.isEnabled && textField.text.isBlank()) {
|
||||
selectOptionJComponent(textField)
|
||||
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||
textField.requestFocusInWindow()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
protected inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||
val portTextField = PortSpinner()
|
||||
val nameTextField = OutlineTextField(128)
|
||||
val protocolTypeComboBox = FlatComboBox<Protocol>()
|
||||
val usernameTextField = OutlineTextField(128)
|
||||
val hostTextField = OutlineTextField(255)
|
||||
private val passwordPanel = JPanel(BorderLayout())
|
||||
private val chooseKeyBtn = JButton(Icons.greyKey)
|
||||
val passwordTextField = OutlinePasswordField(255)
|
||||
val publicKeyTextField = OutlineTextField()
|
||||
val remarkTextArea = FixedLengthTextArea(512)
|
||||
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
|
||||
publicKeyTextField.isEditable = false
|
||||
chooseKeyBtn.isFocusable = false
|
||||
|
||||
protocolTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
value.toString().uppercase(),
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = value?.toString() ?: ""
|
||||
when (value) {
|
||||
AuthenticationType.Password -> {
|
||||
text = "Password"
|
||||
}
|
||||
|
||||
AuthenticationType.PublicKey -> {
|
||||
text = "Public Key"
|
||||
}
|
||||
|
||||
AuthenticationType.KeyboardInteractive -> {
|
||||
text = "Keyboard Interactive"
|
||||
}
|
||||
}
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
text,
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protocolTypeComboBox.addItem(Protocol.SSH)
|
||||
protocolTypeComboBox.addItem(Protocol.Local)
|
||||
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.PublicKey)
|
||||
|
||||
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
|
||||
|
||||
refreshStates()
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
protocolTypeComboBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
refreshStates()
|
||||
}
|
||||
}
|
||||
|
||||
authenticationTypeComboBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
refreshStates()
|
||||
switchPasswordComponent()
|
||||
}
|
||||
}
|
||||
|
||||
chooseKeyBtn.addActionListener {
|
||||
chooseKeyPair()
|
||||
}
|
||||
|
||||
addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentResized(e: ComponentEvent) {
|
||||
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
|
||||
removeComponentListener(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun chooseKeyPair() {
|
||||
val dialog = KeyManagerDialog(
|
||||
SwingUtilities.getWindowAncestor(this),
|
||||
selectMode = true,
|
||||
)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshStates() {
|
||||
hostTextField.isEnabled = true
|
||||
portTextField.isEnabled = true
|
||||
usernameTextField.isEnabled = true
|
||||
authenticationTypeComboBox.isEnabled = true
|
||||
passwordTextField.isEnabled = true
|
||||
chooseKeyBtn.isEnabled = true
|
||||
|
||||
if (protocolTypeComboBox.selectedItem == Protocol.Local) {
|
||||
hostTextField.isEnabled = false
|
||||
portTextField.isEnabled = false
|
||||
usernameTextField.isEnabled = false
|
||||
authenticationTypeComboBox.isEnabled = false
|
||||
passwordTextField.isEnabled = false
|
||||
chooseKeyBtn.isEnabled = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.settings
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return I18n.getString("termora.new-host.general")
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow, $formMargin, pref, $formMargin, default, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
remarkTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
remarkTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
|
||||
remarkTextArea.rows = 8
|
||||
remarkTextArea.lineWrap = true
|
||||
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
|
||||
switchPasswordComponent()
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
|
||||
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.protocol")}:").xy(1, rows)
|
||||
.add(protocolTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
||||
.add(hostTextField).xy(3, rows)
|
||||
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||
.add(portTextField).xy(7, rows).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
||||
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
|
||||
.add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
||||
.add(passwordPanel).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
||||
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
||||
.xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.build()
|
||||
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
private fun switchPasswordComponent() {
|
||||
passwordPanel.removeAll()
|
||||
|
||||
if (authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
|
||||
passwordPanel.add(
|
||||
FormBuilder.create()
|
||||
.layout(FormLayout("default:grow, 4dlu, left:pref", "pref"))
|
||||
.add(publicKeyTextField).xy(1, 1)
|
||||
.add(chooseKeyBtn).xy(3, 1)
|
||||
.build(), BorderLayout.CENTER
|
||||
)
|
||||
} else {
|
||||
passwordPanel.add(passwordTextField, BorderLayout.CENTER)
|
||||
}
|
||||
passwordPanel.revalidate()
|
||||
passwordPanel.repaint()
|
||||
}
|
||||
}
|
||||
|
||||
protected inner class ProxyOption : JPanel(BorderLayout()), Option {
|
||||
val proxyTypeComboBox = FlatComboBox<ProxyType>()
|
||||
val proxyHostTextField = OutlineTextField()
|
||||
val proxyPasswordTextField = OutlinePasswordField()
|
||||
val proxyUsernameTextField = OutlineTextField()
|
||||
val proxyPortTextField = PortSpinner(1080)
|
||||
val proxyAuthenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
proxyAuthenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = value?.toString() ?: ""
|
||||
when (value) {
|
||||
AuthenticationType.Password -> {
|
||||
text = "Password"
|
||||
}
|
||||
|
||||
AuthenticationType.PublicKey -> {
|
||||
text = "Public Key"
|
||||
}
|
||||
|
||||
AuthenticationType.KeyboardInteractive -> {
|
||||
text = "Keyboard Interactive"
|
||||
}
|
||||
}
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
text,
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
proxyTypeComboBox.addItem(ProxyType.No)
|
||||
proxyTypeComboBox.addItem(ProxyType.HTTP)
|
||||
proxyTypeComboBox.addItem(ProxyType.SOCKS5)
|
||||
|
||||
proxyAuthenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||
proxyAuthenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||
|
||||
proxyUsernameTextField.text = "root"
|
||||
|
||||
refreshStates()
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
proxyTypeComboBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
refreshStates()
|
||||
}
|
||||
}
|
||||
proxyAuthenticationTypeComboBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
refreshStates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshStates() {
|
||||
proxyHostTextField.isEnabled = proxyTypeComboBox.selectedItem != ProxyType.No
|
||||
proxyPortTextField.isEnabled = proxyHostTextField.isEnabled
|
||||
|
||||
proxyAuthenticationTypeComboBox.isEnabled = proxyHostTextField.isEnabled
|
||||
proxyUsernameTextField.isEnabled = proxyAuthenticationTypeComboBox.selectedItem != AuthenticationType.No
|
||||
proxyPasswordTextField.isEnabled = proxyUsernameTextField.isEnabled
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.network
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return I18n.getString("termora.new-host.proxy")
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow, $formMargin, pref, $formMargin, default, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
.add("${I18n.getString("termora.new-host.general.protocol")}:").xy(1, rows)
|
||||
.add(proxyTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
||||
.add(proxyHostTextField).xy(3, rows)
|
||||
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||
.add(proxyPortTextField).xy(7, rows).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
|
||||
.add(proxyAuthenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
||||
.add(proxyUsernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
||||
.add(proxyPasswordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.build()
|
||||
|
||||
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
protected inner class TerminalOption : JPanel(BorderLayout()), Option {
|
||||
val charsetComboBox = JComboBox<String>()
|
||||
val startupCommandTextField = OutlineTextField()
|
||||
val environmentTextArea = FixedLengthTextArea(2048)
|
||||
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
|
||||
|
||||
environmentTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
environmentTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
|
||||
environmentTextArea.rows = 8
|
||||
environmentTextArea.lineWrap = true
|
||||
environmentTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
|
||||
for (e in Charset.availableCharsets()) {
|
||||
charsetComboBox.addItem(e.key)
|
||||
}
|
||||
|
||||
charsetComboBox.selectedItem = "UTF-8"
|
||||
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.terminal
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return I18n.getString("termora.new-host.terminal")
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow, $formMargin, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin"
|
||||
)
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
|
||||
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows)
|
||||
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
|
||||
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
|
||||
.apply { rows += step }
|
||||
.build()
|
||||
|
||||
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
protected inner class TunnelingOption : JPanel(BorderLayout()), Option {
|
||||
val tunnelings = mutableListOf<Tunneling>()
|
||||
|
||||
private val model = object : DefaultTableModel() {
|
||||
override fun getRowCount(): Int {
|
||||
return tunnelings.size
|
||||
}
|
||||
|
||||
override fun isCellEditable(row: Int, column: Int): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
fun addRow(tunneling: Tunneling) {
|
||||
val rowCount = super.getRowCount()
|
||||
tunnelings.add(tunneling)
|
||||
super.fireTableRowsInserted(rowCount, rowCount + 1)
|
||||
}
|
||||
|
||||
override fun getValueAt(row: Int, column: Int): Any {
|
||||
val tunneling = tunnelings[row]
|
||||
return when (column) {
|
||||
0 -> tunneling.name
|
||||
1 -> tunneling.type
|
||||
2 -> "${tunneling.sourceHost}:${tunneling.sourcePort}"
|
||||
3 -> "${tunneling.destinationHost}:${tunneling.destinationPort}"
|
||||
else -> super.getValueAt(row, column)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val table = JTable(model)
|
||||
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
|
||||
private val editBtn = JButton(I18n.getString("termora.new-host.tunneling.edit"))
|
||||
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.tunneling.table.name"))
|
||||
model.addColumn(I18n.getString("termora.new-host.tunneling.table.type"))
|
||||
model.addColumn(I18n.getString("termora.new-host.tunneling.table.source"))
|
||||
model.addColumn(I18n.getString("termora.new-host.tunneling.table.destination"))
|
||||
|
||||
|
||||
table.autoResizeMode = JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS
|
||||
table.border = BorderFactory.createEmptyBorder()
|
||||
table.fillsViewportHeight = true
|
||||
scrollPane.border = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createEmptyBorder(4, 0, 4, 0),
|
||||
BorderFactory.createMatteBorder(1, 1, 1, 1, DynamicColor.BorderColor)
|
||||
)
|
||||
|
||||
deleteBtn.isFocusable = false
|
||||
addBtn.isFocusable = false
|
||||
editBtn.isFocusable = false
|
||||
|
||||
editBtn.isEnabled = false
|
||||
deleteBtn.isEnabled = false
|
||||
|
||||
val box = Box.createHorizontalBox()
|
||||
box.add(addBtn)
|
||||
box.add(Box.createHorizontalStrut(4))
|
||||
box.add(editBtn)
|
||||
box.add(Box.createHorizontalStrut(4))
|
||||
box.add(deleteBtn)
|
||||
|
||||
add(JLabel("TCP/IP Forwarding:"), BorderLayout.NORTH)
|
||||
add(scrollPane, BorderLayout.CENTER)
|
||||
add(box, BorderLayout.SOUTH)
|
||||
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
addBtn.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent?) {
|
||||
val dialog = PortForwardingDialog(SwingUtilities.getWindowAncestor(this@HostOptionsPane))
|
||||
dialog.isVisible = true
|
||||
val tunneling = dialog.tunneling ?: return
|
||||
model.addRow(tunneling)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
editBtn.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent?) {
|
||||
val row = table.selectedRow
|
||||
if (row < 0) {
|
||||
return
|
||||
}
|
||||
val dialog = PortForwardingDialog(
|
||||
SwingUtilities.getWindowAncestor(this@HostOptionsPane),
|
||||
tunnelings[row]
|
||||
)
|
||||
dialog.isVisible = true
|
||||
tunnelings[row] = dialog.tunneling ?: return
|
||||
model.fireTableRowsUpdated(row, row)
|
||||
}
|
||||
})
|
||||
|
||||
deleteBtn.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val rows = table.selectedRows
|
||||
if (rows.isEmpty()) return
|
||||
rows.sortDescending()
|
||||
for (row in rows) {
|
||||
tunnelings.removeAt(row)
|
||||
model.fireTableRowsDeleted(row, row)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
table.selectionModel.addListSelectionListener {
|
||||
editBtn.isEnabled = table.selectedRowCount > 0
|
||||
deleteBtn.isEnabled = editBtn.isEnabled
|
||||
}
|
||||
|
||||
table.addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (e.clickCount % 2 == 0 && SwingUtilities.isLeftMouseButton(e)) {
|
||||
editBtn.actionListeners.forEach {
|
||||
it.actionPerformed(
|
||||
ActionEvent(
|
||||
editBtn,
|
||||
ActionEvent.ACTION_PERFORMED,
|
||||
StringUtils.EMPTY
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.showWriteAccess
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return I18n.getString("termora.new-host.tunneling")
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return this
|
||||
}
|
||||
|
||||
private inner class PortForwardingDialog(
|
||||
owner: Window,
|
||||
var tunneling: Tunneling? = null
|
||||
) : DialogWrapper(owner) {
|
||||
private val formMargin = "4dlu"
|
||||
private val typeComboBox = FlatComboBox<TunnelingType>()
|
||||
private val nameTextField = OutlineTextField(32)
|
||||
private val localHostTextField = OutlineTextField()
|
||||
private val localPortSpinner = PortSpinner()
|
||||
private val remoteHostTextField = OutlineTextField()
|
||||
private val remotePortSpinner = PortSpinner()
|
||||
|
||||
init {
|
||||
isModal = true
|
||||
title = I18n.getString("termora.new-host.tunneling")
|
||||
controlsVisible = false
|
||||
|
||||
typeComboBox.addItem(TunnelingType.Local)
|
||||
typeComboBox.addItem(TunnelingType.Remote)
|
||||
typeComboBox.addItem(TunnelingType.Dynamic)
|
||||
|
||||
localHostTextField.text = "127.0.0.1"
|
||||
localPortSpinner.value = 1080
|
||||
|
||||
remoteHostTextField.text = "127.0.0.1"
|
||||
|
||||
typeComboBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
remoteHostTextField.isEnabled = typeComboBox.selectedItem != TunnelingType.Dynamic
|
||||
remotePortSpinner.isEnabled = remoteHostTextField.isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
tunneling?.let {
|
||||
localHostTextField.text = it.sourceHost
|
||||
localPortSpinner.value = it.sourcePort
|
||||
remoteHostTextField.text = it.destinationHost
|
||||
remotePortSpinner.value = it.destinationPort
|
||||
nameTextField.text = it.name
|
||||
typeComboBox.selectedItem = it.type
|
||||
}
|
||||
|
||||
init()
|
||||
pack()
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 300, size.height)
|
||||
setLocationRelativeTo(null)
|
||||
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
if (nameTextField.text.isBlank()) {
|
||||
nameTextField.outline = "error"
|
||||
nameTextField.requestFocusInWindow()
|
||||
return
|
||||
} else if (localHostTextField.text.isBlank()) {
|
||||
localHostTextField.outline = "error"
|
||||
localHostTextField.requestFocusInWindow()
|
||||
return
|
||||
} else if (remoteHostTextField.text.isBlank()) {
|
||||
remoteHostTextField.outline = "error"
|
||||
remoteHostTextField.requestFocusInWindow()
|
||||
return
|
||||
}
|
||||
|
||||
tunneling = Tunneling(
|
||||
name = nameTextField.text,
|
||||
type = typeComboBox.selectedItem as TunnelingType,
|
||||
sourceHost = localHostTextField.text,
|
||||
sourcePort = localPortSpinner.value as Int,
|
||||
destinationHost = remoteHostTextField.text,
|
||||
destinationPort = remotePortSpinner.value as Int,
|
||||
)
|
||||
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
override fun doCancelAction() {
|
||||
tunneling = null
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow, $formMargin, pref",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
return FormBuilder.create().layout(layout).padding("0dlu, $formMargin, $formMargin, $formMargin")
|
||||
.add("${I18n.getString("termora.new-host.tunneling.table.name")}:").xy(1, rows)
|
||||
.add(nameTextField).xyw(3, rows, 3).apply { rows += step }
|
||||
.add("${I18n.getString("termora.new-host.tunneling.table.type")}:").xy(1, rows)
|
||||
.add(typeComboBox).xyw(3, rows, 3).apply { rows += step }
|
||||
.add("${I18n.getString("termora.new-host.tunneling.table.source")}:").xy(1, rows)
|
||||
.add(localHostTextField).xy(3, rows)
|
||||
.add(localPortSpinner).xy(5, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.new-host.tunneling.table.destination")}:").xy(1, rows)
|
||||
.add(remoteHostTextField).xy(3, rows)
|
||||
.add(remotePortSpinner).xy(5, rows).apply { rows += step }
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
63
src/main/kotlin/app/termora/HostTerminalTab.kt
Normal file
63
src/main/kotlin/app/termora/HostTerminalTab.kt
Normal file
@@ -0,0 +1,63 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.terminal.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import java.beans.PropertyChangeEvent
|
||||
import javax.swing.Icon
|
||||
|
||||
abstract class HostTerminalTab(val host: Host) : PropertyTerminalTab() {
|
||||
protected val coroutineScope by lazy { CoroutineScope(Dispatchers.Swing) }
|
||||
protected val terminal = TerminalFactory.instance.createTerminal()
|
||||
protected val terminalModel get() = terminal.getTerminalModel()
|
||||
protected var unread = false
|
||||
set(value) {
|
||||
field = value
|
||||
firePropertyChange(PropertyChangeEvent(this, "icon", null, null))
|
||||
}
|
||||
|
||||
|
||||
/* visualTerminal */
|
||||
protected fun Terminal.clearScreen() {
|
||||
this.write("${ControlCharacters.ESC}[3J")
|
||||
}
|
||||
|
||||
init {
|
||||
terminal.getTerminalModel().addDataListener(object : DataListener {
|
||||
override fun onChanged(key: DataKey<*>, data: Any) {
|
||||
if (key == VisualTerminal.Written) {
|
||||
if (hasFocus || unread) {
|
||||
return
|
||||
}
|
||||
unread = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
open fun start() {}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return host.name
|
||||
}
|
||||
|
||||
override fun getIcon(): Icon {
|
||||
if (host.protocol == Protocol.Local || host.protocol == Protocol.SSH) {
|
||||
return if (unread) Icons.terminalUnread else Icons.terminal
|
||||
}
|
||||
return Icons.terminal
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
override fun onGrabFocus() {
|
||||
super.onGrabFocus()
|
||||
if (!unread) return
|
||||
unread = false
|
||||
}
|
||||
|
||||
}
|
||||
583
src/main/kotlin/app/termora/HostTree.kt
Normal file
583
src/main/kotlin/app/termora/HostTree.kt
Normal file
@@ -0,0 +1,583 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.db.Database
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
||||
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.Transferable
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.*
|
||||
import javax.swing.*
|
||||
import javax.swing.event.CellEditorListener
|
||||
import javax.swing.event.ChangeEvent
|
||||
import javax.swing.event.PopupMenuEvent
|
||||
import javax.swing.event.PopupMenuListener
|
||||
import javax.swing.tree.TreePath
|
||||
import javax.swing.tree.TreeSelectionModel
|
||||
|
||||
|
||||
class HostTree : JTree(), Disposable {
|
||||
private val hostManager get() = HostManager.instance
|
||||
private val editor = OutlineTextField(64)
|
||||
|
||||
val model = HostTreeModel()
|
||||
val searchableModel = SearchableHostTreeModel(model)
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
|
||||
private fun initView() {
|
||||
setModel(model)
|
||||
isEditable = true
|
||||
dropMode = DropMode.ON_OR_INSERT
|
||||
dragEnabled = true
|
||||
selectionModel.selectionMode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION
|
||||
editor.preferredSize = Dimension(220, 0)
|
||||
|
||||
setCellRenderer(object : DefaultXTreeCellRenderer() {
|
||||
override fun getTreeCellRendererComponent(
|
||||
tree: JTree,
|
||||
value: Any,
|
||||
sel: Boolean,
|
||||
expanded: Boolean,
|
||||
leaf: Boolean,
|
||||
row: Int,
|
||||
hasFocus: Boolean
|
||||
): Component {
|
||||
val host = value as Host
|
||||
val c = super.getTreeCellRendererComponent(tree, host, sel, expanded, leaf, row, hasFocus)
|
||||
if (host.protocol == Protocol.Folder) {
|
||||
icon = if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||
} else if (host.protocol == Protocol.SSH || host.protocol == Protocol.Local) {
|
||||
icon = if (sel && this@HostTree.hasFocus()) Icons.terminal.dark else Icons.terminal
|
||||
}
|
||||
return c
|
||||
}
|
||||
})
|
||||
|
||||
setCellEditor(object : DefaultCellEditor(editor) {
|
||||
override fun isCellEditable(e: EventObject?): Boolean {
|
||||
if (e is MouseEvent) {
|
||||
return false
|
||||
}
|
||||
return super.isCellEditable(e)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
val state = Database.instance.properties.getString("HostTreeExpansionState")
|
||||
if (state != null) {
|
||||
TreeUtils.loadExpansionState(this@HostTree, state)
|
||||
}
|
||||
}
|
||||
|
||||
override fun convertValueToText(
|
||||
value: Any?,
|
||||
selected: Boolean,
|
||||
expanded: Boolean,
|
||||
leaf: Boolean,
|
||||
row: Int,
|
||||
hasFocus: Boolean
|
||||
): String {
|
||||
if (value is Host) {
|
||||
return value.name
|
||||
}
|
||||
return super.convertValueToText(value, selected, expanded, leaf, row, hasFocus)
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
// 右键选中
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (!SwingUtilities.isRightMouseButton(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
requestFocusInWindow()
|
||||
|
||||
val selectionRows = selectionModel.selectionRows
|
||||
|
||||
val selRow = getClosestRowForLocation(e.x, e.y)
|
||||
if (selRow < 0) {
|
||||
selectionModel.clearSelection()
|
||||
return
|
||||
} else if (selectionRows != null && selectionRows.contains(selRow)) {
|
||||
return
|
||||
}
|
||||
|
||||
selectionPath = getPathForLocation(e.x, e.y)
|
||||
|
||||
setSelectionRow(selRow)
|
||||
}
|
||||
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (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))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// contextmenu
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (!(SwingUtilities.isRightMouseButton(e))) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Objects.isNull(lastSelectedPathComponent)) {
|
||||
return
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater { showContextMenu(e) }
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// rename
|
||||
getCellEditor().addCellEditorListener(object : CellEditorListener {
|
||||
override fun editingStopped(e: ChangeEvent) {
|
||||
val lastHost = lastSelectedPathComponent
|
||||
if (lastHost !is Host || editor.text.isBlank() || editor.text == lastHost.name) {
|
||||
return
|
||||
}
|
||||
runCatchingHost(lastHost.copy(name = editor.text))
|
||||
}
|
||||
|
||||
override fun editingCanceled(e: ChangeEvent) {
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// drag
|
||||
transferHandler = object : TransferHandler() {
|
||||
|
||||
override fun createTransferable(c: JComponent): Transferable {
|
||||
val nodes = selectionModel.selectionPaths
|
||||
.map { it.lastPathComponent }
|
||||
.filterIsInstance<Host>()
|
||||
.toMutableList()
|
||||
|
||||
val iterator = nodes.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val node = iterator.next()
|
||||
val parents = model.getPathToRoot(node).filter { it != node }
|
||||
if (parents.any { nodes.contains(it) }) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
|
||||
return MoveHostTransferable(nodes)
|
||||
}
|
||||
|
||||
override fun getSourceActions(c: JComponent?): Int {
|
||||
return MOVE
|
||||
}
|
||||
|
||||
override fun canImport(support: TransferSupport): Boolean {
|
||||
if (!support.isDrop) {
|
||||
return false
|
||||
}
|
||||
val dropLocation = support.dropLocation
|
||||
if (dropLocation !is JTree.DropLocation || support.component != this@HostTree
|
||||
|| dropLocation.childIndex != -1
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
val lastNode = dropLocation.path.lastPathComponent
|
||||
if (lastNode !is Host || lastNode.protocol != Protocol.Folder) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (support.isDataFlavorSupported(MoveHostTransferable.dataFlavor)) {
|
||||
val nodes = support.transferable.getTransferData(MoveHostTransferable.dataFlavor) as List<*>
|
||||
if (nodes.any { it == lastNode }) {
|
||||
return false
|
||||
}
|
||||
for (parent in model.getPathToRoot(lastNode).filter { it != lastNode }) {
|
||||
if (nodes.any { it == parent }) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
support.setShowDropLocation(true)
|
||||
return support.isDataFlavorSupported(MoveHostTransferable.dataFlavor)
|
||||
}
|
||||
|
||||
override fun importData(support: TransferSupport): Boolean {
|
||||
if (!support.isDrop) {
|
||||
return false
|
||||
}
|
||||
|
||||
val dropLocation = support.dropLocation
|
||||
if (dropLocation !is JTree.DropLocation) {
|
||||
return false
|
||||
}
|
||||
|
||||
val lastNode = dropLocation.path.lastPathComponent
|
||||
if (lastNode !is Host || lastNode.protocol != Protocol.Folder) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!support.isDataFlavorSupported(MoveHostTransferable.dataFlavor)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val hosts = (support.transferable.getTransferData(MoveHostTransferable.dataFlavor) as List<*>)
|
||||
.filterIsInstance<Host>().toMutableList()
|
||||
if (hosts.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 记录展开的节点
|
||||
val expandedHosts = mutableListOf<String>()
|
||||
for (host in hosts) {
|
||||
model.visit(host) {
|
||||
if (it.protocol == Protocol.Folder) {
|
||||
if (isExpanded(TreePath(model.getPathToRoot(it)))) {
|
||||
expandedHosts.addFirst(it.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var now = System.currentTimeMillis()
|
||||
for (host in hosts) {
|
||||
model.removeNodeFromParent(host)
|
||||
val newHost = host.copy(
|
||||
parentId = lastNode.id,
|
||||
sort = ++now,
|
||||
updateDate = now
|
||||
)
|
||||
runCatchingHost(newHost)
|
||||
}
|
||||
|
||||
expandNode(lastNode)
|
||||
|
||||
// 展开
|
||||
for (id in expandedHosts) {
|
||||
model.getHost(id)?.let { expandNode(it) }
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun isPathEditable(path: TreePath?): Boolean {
|
||||
if (path == null) return false
|
||||
if (path.lastPathComponent == model.root) return false
|
||||
return super.isPathEditable(path)
|
||||
}
|
||||
|
||||
override fun getLastSelectedPathComponent(): Any? {
|
||||
val last = super.getLastSelectedPathComponent() ?: return null
|
||||
if (last is Host) {
|
||||
return model.getHost(last.id) ?: last
|
||||
}
|
||||
return last
|
||||
}
|
||||
|
||||
private fun showContextMenu(event: MouseEvent) {
|
||||
val lastHost = lastSelectedPathComponent
|
||||
if (lastHost !is Host) {
|
||||
return
|
||||
}
|
||||
|
||||
val popupMenu = FlatPopupMenu()
|
||||
val newMenu = JMenu(I18n.getString("termora.welcome.contextmenu.new"))
|
||||
val newFolder = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.folder"))
|
||||
val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host"))
|
||||
|
||||
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open"))
|
||||
popupMenu.addSeparator()
|
||||
val copy = popupMenu.add(I18n.getString("termora.welcome.contextmenu.copy"))
|
||||
val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove"))
|
||||
val rename = popupMenu.add(I18n.getString("termora.welcome.contextmenu.rename"))
|
||||
popupMenu.addSeparator()
|
||||
val expandAll = popupMenu.add(I18n.getString("termora.welcome.contextmenu.expand-all"))
|
||||
val colspanAll = popupMenu.add(I18n.getString("termora.welcome.contextmenu.collapse-all"))
|
||||
popupMenu.addSeparator()
|
||||
popupMenu.add(newMenu)
|
||||
popupMenu.addSeparator()
|
||||
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
|
||||
|
||||
open.addActionListener {
|
||||
getSelectionNodes()
|
||||
.filter { it.protocol != Protocol.Folder }
|
||||
.forEach {
|
||||
ActionManager.getInstance()
|
||||
.getAction(Actions.OPEN_HOST)
|
||||
?.actionPerformed(OpenHostActionEvent(this, it))
|
||||
}
|
||||
}
|
||||
|
||||
rename.addActionListener {
|
||||
startEditingAtPath(TreePath(model.getPathToRoot(lastHost)))
|
||||
}
|
||||
|
||||
expandAll.addActionListener {
|
||||
getSelectionNodes().forEach { expandNode(it, true) }
|
||||
}
|
||||
|
||||
|
||||
colspanAll.addActionListener {
|
||||
selectionModel.selectionPaths.map { it.lastPathComponent }
|
||||
.filterIsInstance<Host>()
|
||||
.filter { it.protocol == Protocol.Folder }
|
||||
.forEach { collapseNode(it) }
|
||||
}
|
||||
|
||||
copy.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val parent = model.getParent(lastHost) ?: return
|
||||
val node = copyNode(parent, lastHost)
|
||||
selectionPath = TreePath(model.getPathToRoot(node))
|
||||
}
|
||||
})
|
||||
|
||||
remove.addActionListener {
|
||||
if (OptionPane.showConfirmDialog(
|
||||
SwingUtilities.getWindowAncestor(this),
|
||||
"删除后无法恢复,你确定要删除吗?",
|
||||
I18n.getString("termora.remove"),
|
||||
JOptionPane.YES_NO_OPTION,
|
||||
JOptionPane.QUESTION_MESSAGE
|
||||
) == JOptionPane.YES_OPTION
|
||||
) {
|
||||
var lastParent: Host? = null
|
||||
while (!selectionModel.isSelectionEmpty) {
|
||||
val host = lastSelectedPathComponent ?: break
|
||||
if (host !is Host) {
|
||||
break
|
||||
} else {
|
||||
lastParent = model.getParent(host)
|
||||
}
|
||||
model.visit(host) { hostManager.removeHost(it.id) }
|
||||
}
|
||||
if (lastParent != null) {
|
||||
selectionPath = TreePath(model.getPathToRoot(lastParent))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newFolder.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
if (lastHost.protocol != Protocol.Folder) {
|
||||
return
|
||||
}
|
||||
|
||||
val host = Host(
|
||||
id = UUID.randomUUID().toSimpleString(),
|
||||
protocol = Protocol.Folder,
|
||||
name = I18n.getString("termora.welcome.contextmenu.new.folder.name"),
|
||||
sort = System.currentTimeMillis(),
|
||||
parentId = lastHost.id
|
||||
)
|
||||
|
||||
runCatchingHost(host)
|
||||
|
||||
expandNode(lastHost)
|
||||
selectionPath = TreePath(model.getPathToRoot(host))
|
||||
startEditingAtPath(selectionPath)
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
newHost.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
showAddHostDialog()
|
||||
}
|
||||
})
|
||||
|
||||
property.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val dialog = HostDialog(SwingUtilities.getWindowAncestor(this@HostTree), lastHost)
|
||||
dialog.isVisible = true
|
||||
val host = dialog.host ?: return
|
||||
runCatchingHost(host)
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化状态
|
||||
newFolder.isEnabled = lastHost.protocol == Protocol.Folder
|
||||
newHost.isEnabled = newFolder.isEnabled
|
||||
remove.isEnabled = !getSelectionNodes().any { it == model.root }
|
||||
copy.isEnabled = remove.isEnabled
|
||||
rename.isEnabled = remove.isEnabled
|
||||
property.isEnabled = lastHost.protocol != Protocol.Folder
|
||||
|
||||
popupMenu.addPopupMenuListener(object : PopupMenuListener {
|
||||
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
||||
this@HostTree.grabFocus()
|
||||
}
|
||||
|
||||
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {
|
||||
this@HostTree.requestFocusInWindow()
|
||||
}
|
||||
|
||||
override fun popupMenuCanceled(e: PopupMenuEvent) {
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
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) {
|
||||
expandPath(TreePath(model.getPathToRoot(node)))
|
||||
if (including) {
|
||||
model.getChildren(node).forEach { expandNode(it, true) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun copyNode(
|
||||
parent: Host,
|
||||
host: Host,
|
||||
idGenerator: () -> String = { UUID.randomUUID().toSimpleString() }
|
||||
): Host {
|
||||
val now = System.currentTimeMillis()
|
||||
val newHost = host.copy(
|
||||
name = "${host.name} ${I18n.getString("termora.welcome.contextmenu.copy")}",
|
||||
id = idGenerator.invoke(),
|
||||
parentId = parent.id,
|
||||
updateDate = now,
|
||||
createDate = now,
|
||||
sort = now
|
||||
)
|
||||
|
||||
runCatchingHost(newHost)
|
||||
|
||||
if (host.protocol == Protocol.Folder) {
|
||||
for (child in model.getChildren(host)) {
|
||||
copyNode(newHost, child, idGenerator)
|
||||
}
|
||||
if (isExpanded(TreePath(model.getPathToRoot(host)))) {
|
||||
expandNode(newHost)
|
||||
}
|
||||
}
|
||||
|
||||
return newHost
|
||||
|
||||
}
|
||||
|
||||
private fun runCatchingHost(host: Host) {
|
||||
hostManager.addHost(host)
|
||||
}
|
||||
|
||||
private fun collapseNode(node: Host) {
|
||||
model.getChildren(node).forEach { collapseNode(it) }
|
||||
collapsePath(TreePath(model.getPathToRoot(node)))
|
||||
}
|
||||
|
||||
private fun getSelectionNodes(): List<Host> {
|
||||
val selectionNodes = selectionModel.selectionPaths.map { it.lastPathComponent }
|
||||
.filterIsInstance<Host>()
|
||||
|
||||
if (selectionNodes.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val nodes = mutableListOf<Host>()
|
||||
val parents = mutableListOf<Host>()
|
||||
|
||||
for (node in selectionNodes) {
|
||||
if (node.protocol == Protocol.Folder) {
|
||||
parents.add(node)
|
||||
}
|
||||
nodes.add(node)
|
||||
}
|
||||
|
||||
while (parents.isNotEmpty()) {
|
||||
val p = parents.removeFirst()
|
||||
for (i in 0 until model.getChildCount(p)) {
|
||||
val child = model.getChild(p, i) as Host
|
||||
nodes.add(child)
|
||||
parents.add(child)
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
Database.instance.properties.putString(
|
||||
"HostTreeExpansionState",
|
||||
TreeUtils.saveExpansionState(this)
|
||||
)
|
||||
}
|
||||
|
||||
private abstract class HostTreeNodeTransferable(val hosts: List<Host>) :
|
||||
Transferable {
|
||||
|
||||
override fun getTransferDataFlavors(): Array<DataFlavor> {
|
||||
return arrayOf(getDataFlavor())
|
||||
}
|
||||
|
||||
override fun isDataFlavorSupported(flavor: DataFlavor): Boolean {
|
||||
return getDataFlavor() == flavor
|
||||
}
|
||||
|
||||
override fun getTransferData(flavor: DataFlavor): Any {
|
||||
return hosts
|
||||
}
|
||||
|
||||
abstract fun getDataFlavor(): DataFlavor
|
||||
}
|
||||
|
||||
private class MoveHostTransferable(hosts: List<Host>) : HostTreeNodeTransferable(hosts) {
|
||||
companion object {
|
||||
val dataFlavor =
|
||||
DataFlavor("${DataFlavor.javaJVMLocalObjectMimeType};class=${MoveHostTransferable::class.java.name}")
|
||||
}
|
||||
|
||||
override fun getDataFlavor(): DataFlavor {
|
||||
return dataFlavor
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
159
src/main/kotlin/app/termora/HostTreeModel.kt
Normal file
159
src/main/kotlin/app/termora/HostTreeModel.kt
Normal file
@@ -0,0 +1,159 @@
|
||||
package app.termora
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import javax.swing.event.TreeModelEvent
|
||||
import javax.swing.event.TreeModelListener
|
||||
import javax.swing.tree.TreeModel
|
||||
import javax.swing.tree.TreePath
|
||||
|
||||
class HostTreeModel : TreeModel {
|
||||
|
||||
val listeners = mutableListOf<TreeModelListener>()
|
||||
|
||||
private val hostManager get() = HostManager.instance
|
||||
private val hosts = mutableMapOf<String, Host>()
|
||||
private val myRoot by lazy {
|
||||
Host(
|
||||
id = "0",
|
||||
protocol = Protocol.Folder,
|
||||
name = I18n.getString("termora.welcome.my-hosts"),
|
||||
host = StringUtils.EMPTY,
|
||||
port = 0,
|
||||
remark = StringUtils.EMPTY,
|
||||
username = StringUtils.EMPTY
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
for (host in hostManager.hosts()) {
|
||||
hosts[host.id] = host
|
||||
}
|
||||
|
||||
hostManager.addHostListener(object : HostListener {
|
||||
override fun hostRemoved(id: String) {
|
||||
val host = hosts[id] ?: return
|
||||
removeNodeFromParent(host)
|
||||
}
|
||||
|
||||
override fun hostAdded(host: Host) {
|
||||
// 如果已经存在,那么是修改
|
||||
if (hosts.containsKey(host.id)) {
|
||||
val oldHost = hosts.getValue(host.id)
|
||||
// 父级结构变了
|
||||
if (oldHost.parentId != host.parentId) {
|
||||
hostRemoved(host.id)
|
||||
hostAdded(host)
|
||||
} else {
|
||||
hosts[host.id] = host
|
||||
val event = TreeModelEvent(this, getPathToRoot(host))
|
||||
listeners.forEach { it.treeStructureChanged(event) }
|
||||
}
|
||||
|
||||
} else {
|
||||
hosts[host.id] = host
|
||||
val parent = getParent(host) ?: return
|
||||
val path = TreePath(getPathToRoot(parent))
|
||||
val event = TreeModelEvent(this, path, intArrayOf(getIndexOfChild(parent, host)), arrayOf(host))
|
||||
listeners.forEach { it.treeNodesInserted(event) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun hostsChanged() {
|
||||
hosts.clear()
|
||||
for (host in hostManager.hosts()) {
|
||||
hosts[host.id] = host
|
||||
}
|
||||
val event = TreeModelEvent(this, getPathToRoot(root), null, null)
|
||||
listeners.forEach { it.treeStructureChanged(event) }
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
override fun getRoot(): Host {
|
||||
return myRoot
|
||||
}
|
||||
|
||||
override fun getChild(parent: Any?, index: Int): Any {
|
||||
return getChildren(parent)[index]
|
||||
}
|
||||
|
||||
override fun getChildCount(parent: Any?): Int {
|
||||
return getChildren(parent).size
|
||||
}
|
||||
|
||||
override fun isLeaf(node: Any?): Boolean {
|
||||
return getChildCount(node) == 0
|
||||
}
|
||||
|
||||
fun getParent(node: Host): Host? {
|
||||
if (node.parentId == root.id || root.id == node.id) {
|
||||
return root
|
||||
}
|
||||
return hosts.values.firstOrNull { it.id == node.parentId }
|
||||
}
|
||||
|
||||
override fun valueForPathChanged(path: TreePath?, newValue: Any?) {
|
||||
|
||||
}
|
||||
|
||||
override fun getIndexOfChild(parent: Any?, child: Any?): Int {
|
||||
return getChildren(parent).indexOf(child)
|
||||
}
|
||||
|
||||
override fun addTreeModelListener(listener: TreeModelListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
override fun removeTreeModelListener(listener: TreeModelListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅从结构中删除
|
||||
*/
|
||||
fun removeNodeFromParent(host: Host) {
|
||||
val parent = getParent(host) ?: return
|
||||
val index = getIndexOfChild(parent, host)
|
||||
val event = TreeModelEvent(this, TreePath(getPathToRoot(parent)), intArrayOf(index), arrayOf(host))
|
||||
hosts.remove(host.id)
|
||||
listeners.forEach { it.treeNodesRemoved(event) }
|
||||
}
|
||||
|
||||
fun visit(host: Host, visitor: (host: Host) -> Unit) {
|
||||
if (host.protocol == Protocol.Folder) {
|
||||
getChildren(host).forEach { visit(it, visitor) }
|
||||
visitor.invoke(host)
|
||||
} else {
|
||||
visitor.invoke(host)
|
||||
}
|
||||
}
|
||||
|
||||
fun getHost(id: String): Host? {
|
||||
return hosts[id]
|
||||
}
|
||||
|
||||
fun getPathToRoot(host: Host): Array<Host> {
|
||||
|
||||
if (host.id == root.id) {
|
||||
return arrayOf(root)
|
||||
}
|
||||
|
||||
val parents = mutableListOf(host)
|
||||
var pId = host.parentId
|
||||
while (pId != root.id) {
|
||||
val e = hosts[(pId)] ?: break
|
||||
parents.addFirst(e)
|
||||
pId = e.parentId
|
||||
}
|
||||
parents.addFirst(root)
|
||||
return parents.toTypedArray()
|
||||
}
|
||||
|
||||
fun getChildren(parent: Any?): List<Host> {
|
||||
val pId = if (parent is Host) parent.id else root.id
|
||||
return hosts.values.filter { it.parentId == pId }
|
||||
.sortedWith(compareBy<Host> { if (it.protocol == Protocol.Folder) 0 else 1 }.thenBy { it.sort })
|
||||
}
|
||||
}
|
||||
22
src/main/kotlin/app/termora/Hyperlink.kt
Normal file
22
src/main/kotlin/app/termora/Hyperlink.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon.ColorFilter
|
||||
import org.jdesktop.swingx.JXHyperlink
|
||||
import java.awt.Color
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.UIManager
|
||||
|
||||
class Hyperlink(action: AnAction, focusable: Boolean = true) : JXHyperlink(action) {
|
||||
init {
|
||||
val myIcon = FlatSVGIcon(Icons.externalLink.name)
|
||||
myIcon.colorFilter = object : ColorFilter() {
|
||||
override fun filter(color: Color?): Color {
|
||||
return UIManager.getColor("Hyperlink.linkColor")
|
||||
}
|
||||
}
|
||||
isFocusable = focusable
|
||||
icon = myIcon
|
||||
horizontalTextPosition = SwingConstants.LEFT
|
||||
}
|
||||
}
|
||||
57
src/main/kotlin/app/termora/I18n.kt
Normal file
57
src/main/kotlin/app/termora/I18n.kt
Normal file
@@ -0,0 +1,57 @@
|
||||
package app.termora
|
||||
|
||||
import org.apache.commons.lang3.LocaleUtils
|
||||
import org.apache.commons.text.StringSubstitutor
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.text.MessageFormat
|
||||
import java.util.*
|
||||
|
||||
object I18n {
|
||||
private val log = LoggerFactory.getLogger(I18n::class.java)
|
||||
private val bundle by lazy {
|
||||
val bundle = ResourceBundle.getBundle("i18n/messages", Locale.getDefault())
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("I18n: {}", bundle.baseBundleName ?: "null")
|
||||
}
|
||||
return@lazy bundle
|
||||
}
|
||||
|
||||
private val substitutor by lazy { StringSubstitutor { key -> getString(key) } }
|
||||
private val supportedLanguages = sortedMapOf(
|
||||
"en_US" to "English",
|
||||
"zh_CN" to "简体中文",
|
||||
"zh_TW" to "繁體中文",
|
||||
)
|
||||
|
||||
fun containsLanguage(locale: Locale): String? {
|
||||
for (key in supportedLanguages.keys) {
|
||||
val e = LocaleUtils.toLocale(key)
|
||||
if (LocaleUtils.toLocale(key) == locale ||
|
||||
(e.language.equals(locale.language, true) && e.country.equals(locale.country, true))
|
||||
) {
|
||||
return key
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getLanguages(): Map<String, String> {
|
||||
return supportedLanguages
|
||||
}
|
||||
|
||||
fun getString(key: String, vararg args: Any): String {
|
||||
try {
|
||||
val text = substitutor.replace(bundle.getString(key))
|
||||
if (args.isNotEmpty()) {
|
||||
return MessageFormat.format(text, *args)
|
||||
}
|
||||
return text
|
||||
} catch (e: MissingResourceException) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn(e.message, e)
|
||||
}
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
83
src/main/kotlin/app/termora/Icons.kt
Normal file
83
src/main/kotlin/app/termora/Icons.kt
Normal file
@@ -0,0 +1,83 @@
|
||||
package app.termora
|
||||
|
||||
object Icons {
|
||||
val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") }
|
||||
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_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 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 settings by lazy { DynamicIcon("icons/settings.svg", "icons/settings_dark.svg") }
|
||||
val pin by lazy { DynamicIcon("icons/pin.svg", "icons/pin_dark.svg") }
|
||||
val empty by lazy { DynamicIcon("icons/empty.svg") }
|
||||
val add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") }
|
||||
val networkPolicy by lazy { DynamicIcon("icons/networkPolicy.svg", "icons/networkPolicy_dark.svg") }
|
||||
val clusterRole by lazy { DynamicIcon("icons/clusterRole.svg", "icons/clusterRole_dark.svg") }
|
||||
val daemonSets by lazy { DynamicIcon("icons/daemonSets.svg", "icons/daemonSets_dark.svg") }
|
||||
val role by lazy { DynamicIcon("icons/role.svg", "icons/role_dark.svg") }
|
||||
val locked by lazy { DynamicIcon("icons/locked.svg", "icons/locked_dark.svg") }
|
||||
val warning by lazy { DynamicIcon("icons/warning.svg", "icons/warning_dark.svg") }
|
||||
val warningDialog by lazy { DynamicIcon("icons/warningDialog.svg", "icons/warningDialog_dark.svg") }
|
||||
val unlocked by lazy { DynamicIcon("icons/unlocked.svg", "icons/unlocked_dark.svg") }
|
||||
val i18n by lazy { DynamicIcon("icons/i18n.svg", "icons/i18n_dark.svg") }
|
||||
val rec by lazy { DynamicIcon("icons/rec.svg", "icons/rec_dark.svg") }
|
||||
val stop by lazy { DynamicIcon("icons/stop.svg", "icons/stop_dark.svg") }
|
||||
val find by lazy { DynamicIcon("icons/find.svg", "icons/find_dark.svg") }
|
||||
val keyboard by lazy { DynamicIcon("icons/keyboard.svg", "icons/keyboard_dark.svg") }
|
||||
val moreVertical by lazy { DynamicIcon("icons/moreVertical.svg", "icons/moreVertical_dark.svg") }
|
||||
val colors by lazy { DynamicIcon("icons/colors.svg", "icons/colors_dark.svg") }
|
||||
val chevronDown by lazy { DynamicIcon("icons/chevronDownLarge.svg", "icons/chevronDownLarge_dark.svg") }
|
||||
val homeFolder by lazy { DynamicIcon("icons/homeFolder.svg", "icons/homeFolder_dark.svg") }
|
||||
val openNewTab by lazy { DynamicIcon("icons/openNewTab.svg", "icons/openNewTab_dark.svg") }
|
||||
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") }
|
||||
val export by lazy { DynamicIcon("icons/export.svg", "icons/export_dark.svg") }
|
||||
val terminal by lazy { DynamicIcon("icons/terminal.svg", "icons/terminal_dark.svg") }
|
||||
val azure by lazy { DynamicIcon("icons/azure.svg", "icons/azure_dark.svg") }
|
||||
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
|
||||
val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") }
|
||||
val microsoft by lazy { DynamicIcon("icons/microsoft.svg", "icons/microsoft_dark.svg") }
|
||||
val tencent by lazy { DynamicIcon("icons/tencent.svg") }
|
||||
val google by lazy { DynamicIcon("icons/google-small.svg") }
|
||||
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }
|
||||
val yandexCloud by lazy { DynamicIcon("icons/yandexCloud.svg") }
|
||||
val aws by lazy { DynamicIcon("icons/aws.svg","icons/aws_dark.svg") }
|
||||
val huawei by lazy { DynamicIcon("icons/huawei.svg") }
|
||||
val baidu by lazy { DynamicIcon("icons/baiduyun.svg") }
|
||||
val tianyi by lazy { DynamicIcon("icons/tianyiyun.svg") }
|
||||
val digitalocean by lazy { DynamicIcon("icons/digitalocean.svg","icons/digitalocean_dark.svg") }
|
||||
val terminalUnread by lazy { DynamicIcon("icons/terminalUnread.svg", "icons/terminalUnread_dark.svg") }
|
||||
val dbPrimitive by lazy { DynamicIcon("icons/dbPrimitive.svg", "icons/dbPrimitive_dark.svg") }
|
||||
val linux by lazy { DynamicIcon("icons/linux.svg", "icons/linux_dark.svg") }
|
||||
val success by lazy { DynamicIcon("icons/success.svg", "icons/success_dark.svg") }
|
||||
val network by lazy { DynamicIcon("icons/network.svg", "icons/network_dark.svg") }
|
||||
val server by lazy { DynamicIcon("icons/server.svg", "icons/server_dark.svg") }
|
||||
val runAnything by lazy { DynamicIcon("icons/runAnything.svg", "icons/runAnything_dark.svg") }
|
||||
val uiForm by lazy { DynamicIcon("icons/uiForm.svg", "icons/uiForm_dark.svg") }
|
||||
val cloud by lazy { DynamicIcon("icons/cloud.svg", "icons/cloud_dark.svg") }
|
||||
val externalLink by lazy { DynamicIcon("icons/externalLink.svg", "icons/externalLink_dark.svg") }
|
||||
val user by lazy { DynamicIcon("icons/user.svg", "icons/user_dark.svg") }
|
||||
val infoOutline by lazy { DynamicIcon("icons/infoOutline.svg", "icons/infoOutline_dark.svg") }
|
||||
val lightning by lazy { DynamicIcon("icons/lightning.svg", "icons/lightning_dark.svg") }
|
||||
val split by lazy { DynamicIcon("icons/split.svg", "icons/split_dark.svg") }
|
||||
val setKey by lazy { DynamicIcon("icons/setKey.svg", "icons/setKey_dark.svg") }
|
||||
val greyKey by lazy { DynamicIcon("icons/greyKey.svg", "icons/greyKey_dark.svg") }
|
||||
val sortedSet by lazy { DynamicIcon("icons/sortedSet.svg", "icons/sortedSet_dark.svg") }
|
||||
val colorPicker by lazy { DynamicIcon("icons/colorPicker.svg", "icons/colorPicker_dark.svg") }
|
||||
val folder by lazy { DynamicIcon("icons/folder.svg", "icons/folder_dark.svg") }
|
||||
val help by lazy { DynamicIcon("icons/help.svg", "icons/help_dark.svg") }
|
||||
val expand by lazy { DynamicIcon("icons/expand.svg", "icons/expand_dark.svg") }
|
||||
val collapse by lazy { DynamicIcon("icons/collapse.svg", "icons/collapse_dark.svg") }
|
||||
val expandAll by lazy { DynamicIcon("icons/expandAll.svg", "icons/expandAll_dark.svg") }
|
||||
val collapseAll by lazy { DynamicIcon("icons/collapseAll.svg", "icons/collapseAll_dark.svg") }
|
||||
val web by lazy { DynamicIcon("icons/web.svg", "icons/web_dark.svg") }
|
||||
val download by lazy { DynamicIcon("icons/download.svg", "icons/download_dark.svg") }
|
||||
val upload by lazy { DynamicIcon("icons/upload.svg", "icons/upload_dark.svg") }
|
||||
val ideUpdate by lazy { DynamicIcon("icons/ideUpdate.svg", "icons/ideUpdate_dark.svg") }
|
||||
val listKey by lazy { DynamicIcon("icons/listKey.svg", "icons/listKey_dark.svg") }
|
||||
val forwardPorts by lazy { DynamicIcon("icons/forwardPorts.svg", "icons/forwardPorts_dark.svg") }
|
||||
val showWriteAccess by lazy { DynamicIcon("icons/showWriteAccess.svg", "icons/showWriteAccess_dark.svg") }
|
||||
|
||||
}
|
||||
74
src/main/kotlin/app/termora/InputDialog.kt
Normal file
74
src/main/kotlin/app/termora/InputDialog.kt
Normal file
@@ -0,0 +1,74 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.extras.components.FlatTextField
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.Window
|
||||
import java.awt.event.KeyAdapter
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.UIManager
|
||||
|
||||
class InputDialog(
|
||||
owner: Window,
|
||||
title: String,
|
||||
text: String = StringUtils.EMPTY,
|
||||
placeholderText: String = StringUtils.EMPTY
|
||||
) : DialogWrapper(owner) {
|
||||
private val textField = FlatTextField()
|
||||
private var text: String? = null
|
||||
|
||||
init {
|
||||
setSize(340, 60)
|
||||
setLocationRelativeTo(owner)
|
||||
|
||||
super.setTitle(title)
|
||||
|
||||
isResizable = false
|
||||
isModal = true
|
||||
controlsVisible = false
|
||||
titleBarHeight = UIManager.getInt("TabbedPane.tabHeight") * 0.8f
|
||||
|
||||
|
||||
textField.placeholderText = placeholderText
|
||||
textField.text = text
|
||||
textField.addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
if (e.keyCode == KeyEvent.VK_ENTER) {
|
||||
if (textField.text.isBlank()) {
|
||||
return
|
||||
}
|
||||
doOKAction()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
textField.background = UIManager.getColor("window")
|
||||
textField.border = BorderFactory.createEmptyBorder(0, 13, 0, 13)
|
||||
|
||||
return textField
|
||||
}
|
||||
|
||||
fun getText(): String? {
|
||||
isVisible = true
|
||||
return text
|
||||
}
|
||||
|
||||
override fun doCancelAction() {
|
||||
text = null
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
text = textField.text
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
override fun createSouthPanel(): JComponent? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
951
src/main/kotlin/app/termora/Laf.kt
Normal file
951
src/main/kotlin/app/termora/Laf.kt
Normal file
@@ -0,0 +1,951 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.terminal.ColorTheme
|
||||
import app.termora.terminal.TerminalColor
|
||||
import com.formdev.flatlaf.FlatDarkLaf
|
||||
import com.formdev.flatlaf.FlatLightLaf
|
||||
import com.formdev.flatlaf.FlatPropertiesLaf
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import java.util.*
|
||||
|
||||
|
||||
class LightLaf : FlatLightLaf(), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0
|
||||
TerminalColor.Normal.RED -> 13501701
|
||||
TerminalColor.Normal.GREEN -> 425239
|
||||
TerminalColor.Normal.YELLOW -> 11701248
|
||||
TerminalColor.Normal.BLUE -> 409563
|
||||
TerminalColor.Normal.MAGENTA -> 11733427
|
||||
TerminalColor.Normal.CYAN -> 167566
|
||||
TerminalColor.Normal.WHITE -> 9605778
|
||||
|
||||
TerminalColor.Bright.BLACK -> 0x4c4c4c
|
||||
TerminalColor.Bright.RED -> 0xff0000
|
||||
TerminalColor.Bright.GREEN -> 0x00ff00
|
||||
TerminalColor.Bright.YELLOW -> if (SystemInfo.isWindows) 0xC18301 else 0xffff00
|
||||
TerminalColor.Bright.BLUE -> 0x4682b4
|
||||
TerminalColor.Bright.MAGENTA -> 0xff00ff
|
||||
TerminalColor.Bright.CYAN -> 0x00ffff
|
||||
TerminalColor.Bright.WHITE -> 0xffffff
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DarkLaf : FlatDarkLaf(), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0
|
||||
TerminalColor.Normal.RED -> 15749711
|
||||
TerminalColor.Normal.GREEN -> 6067756
|
||||
TerminalColor.Normal.YELLOW -> 10914317
|
||||
TerminalColor.Normal.BLUE -> 3773396
|
||||
TerminalColor.Normal.MAGENTA -> 10973631
|
||||
TerminalColor.Normal.CYAN -> 41891
|
||||
TerminalColor.Normal.WHITE -> 8421504
|
||||
|
||||
|
||||
TerminalColor.Bright.BLACK -> 0x676767
|
||||
TerminalColor.Bright.RED -> 0xef766d
|
||||
TerminalColor.Bright.GREEN -> 0x8cf67a
|
||||
TerminalColor.Bright.YELLOW -> 0xfefb7e
|
||||
TerminalColor.Bright.BLUE -> 0x6a71f6
|
||||
TerminalColor.Bright.MAGENTA -> 0xf07ef8
|
||||
TerminalColor.Bright.CYAN -> 0x8ef9fd
|
||||
TerminalColor.Bright.WHITE -> 0xfeffff
|
||||
|
||||
// TerminalColor.Basic.BACKGROUND -> 1974050
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class iTerm2DarkLaf : FlatDarkLaf(), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
|
||||
TerminalColor.Basic.BACKGROUND -> 0
|
||||
TerminalColor.Basic.FOREGROUND -> 0xc7c7c7
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND -> 0xc6dcfc
|
||||
TerminalColor.Basic.SELECTION_FOREGROUND -> 0x000000
|
||||
TerminalColor.Basic.HYPERLINK -> 0x255ab4
|
||||
TerminalColor.Find.BACKGROUND -> 0xffff00
|
||||
TerminalColor.Find.FOREGROUND -> 0
|
||||
|
||||
TerminalColor.Cursor.BACKGROUND -> 0xc7c7c7
|
||||
|
||||
TerminalColor.Normal.BLACK -> 0
|
||||
TerminalColor.Normal.RED -> 0xb83019
|
||||
TerminalColor.Normal.GREEN -> 0x51bf37
|
||||
TerminalColor.Normal.YELLOW -> 0xc6c43d
|
||||
TerminalColor.Normal.BLUE -> 0x0c24bf
|
||||
TerminalColor.Normal.MAGENTA -> 0xb93ec1
|
||||
TerminalColor.Normal.CYAN -> 0x53c2c5
|
||||
TerminalColor.Normal.WHITE -> 0xc7c7c7
|
||||
|
||||
|
||||
TerminalColor.Bright.BLACK -> 0x676767
|
||||
TerminalColor.Bright.RED -> 0xef766d
|
||||
TerminalColor.Bright.GREEN -> 0x8cf67a
|
||||
TerminalColor.Bright.YELLOW -> 0xfefb7e
|
||||
TerminalColor.Bright.BLUE -> 0x6a71f6
|
||||
TerminalColor.Bright.MAGENTA -> 0xf07ef8
|
||||
TerminalColor.Bright.CYAN -> 0x8ef9fd
|
||||
TerminalColor.Bright.WHITE -> 0xfeffff
|
||||
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TermiusLightLaf : FlatPropertiesLaf("Termius Light", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "light",
|
||||
"@background" to "#d5dde0",
|
||||
"@windowText" to "#32364a",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
|
||||
return when (color) {
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0x32364a
|
||||
|
||||
TerminalColor.Basic.FOREGROUND -> 0x32364a
|
||||
|
||||
TerminalColor.Normal.BLACK -> 0x141729
|
||||
TerminalColor.Normal.RED -> 0xf24e50
|
||||
TerminalColor.Normal.GREEN -> 0x198c51
|
||||
TerminalColor.Normal.YELLOW -> 0xf8aa4b
|
||||
TerminalColor.Normal.BLUE -> 0x004878
|
||||
TerminalColor.Normal.MAGENTA -> 0x8f3c91
|
||||
TerminalColor.Normal.CYAN -> 0x2091f6
|
||||
TerminalColor.Normal.WHITE -> 0xeeeeee
|
||||
|
||||
TerminalColor.Bright.BLACK -> 0x3e4257
|
||||
TerminalColor.Bright.RED -> 0xff7375
|
||||
TerminalColor.Bright.GREEN -> 0x21b568
|
||||
TerminalColor.Bright.YELLOW -> 0xfdc47d
|
||||
TerminalColor.Bright.BLUE -> 0x1d6da2
|
||||
TerminalColor.Bright.MAGENTA -> 0xff7dc5
|
||||
TerminalColor.Bright.CYAN -> 0x44a7ff
|
||||
TerminalColor.Bright.WHITE -> 0xffffff
|
||||
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TermiusDarkLaf : FlatPropertiesLaf("Termius Dark", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "dark",
|
||||
"@background" to "#141729",
|
||||
"@windowText" to "#21b568",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0x21b568
|
||||
|
||||
TerminalColor.Basic.SELECTION_FOREGROUND ->0
|
||||
|
||||
TerminalColor.Basic.FOREGROUND -> 0x21b568
|
||||
|
||||
TerminalColor.Normal.BLACK -> 0x343851
|
||||
TerminalColor.Normal.RED -> 0xf24e50
|
||||
TerminalColor.Normal.GREEN -> 0x008463
|
||||
TerminalColor.Normal.YELLOW -> 0xeca855
|
||||
TerminalColor.Normal.BLUE -> 0x08639f
|
||||
TerminalColor.Normal.MAGENTA -> 0xc13282
|
||||
TerminalColor.Normal.CYAN -> 0x2091f6
|
||||
TerminalColor.Normal.WHITE -> 0xe2e3e8
|
||||
|
||||
TerminalColor.Bright.BLACK -> 0x8d91a5
|
||||
TerminalColor.Bright.RED -> 0xff7375
|
||||
TerminalColor.Bright.GREEN -> 0x3ed7be
|
||||
TerminalColor.Bright.YELLOW -> 0xfdc47d
|
||||
TerminalColor.Bright.BLUE -> 0x6ba0c3
|
||||
TerminalColor.Bright.MAGENTA -> 0xff7dc5
|
||||
TerminalColor.Bright.CYAN -> 0x44a7ff
|
||||
TerminalColor.Bright.WHITE -> 0xffffff
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NovelLaf : FlatPropertiesLaf("Novel", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "light",
|
||||
"@background" to "#dfdbc3",
|
||||
"@windowText" to "#3b2322",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x000000
|
||||
TerminalColor.Normal.RED -> 0xd30f0f
|
||||
TerminalColor.Normal.GREEN -> 0x00933b
|
||||
TerminalColor.Normal.YELLOW -> 0xd38b40
|
||||
TerminalColor.Normal.BLUE -> 0x00528e
|
||||
TerminalColor.Normal.MAGENTA -> 0xcc32cf
|
||||
TerminalColor.Normal.CYAN -> 0x26c3e6
|
||||
TerminalColor.Normal.WHITE -> 0xa6a6a6
|
||||
|
||||
TerminalColor.Bright.BLACK -> 0x5c5c5c
|
||||
TerminalColor.Bright.RED -> 0xe0692f
|
||||
TerminalColor.Bright.GREEN -> 0x00b400
|
||||
TerminalColor.Bright.YELLOW -> 0xfff284
|
||||
TerminalColor.Bright.BLUE -> 0x3ba6f3
|
||||
TerminalColor.Bright.MAGENTA -> 0xec88c2
|
||||
TerminalColor.Bright.CYAN -> 0x38daff
|
||||
TerminalColor.Bright.WHITE -> 0xf2f2f2
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0x73635a
|
||||
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AtomOneDarkLaf : FlatPropertiesLaf("Atom One Dark", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "dark",
|
||||
"@background" to "#1e2127",
|
||||
"@windowText" to "#abb2bf",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x000000
|
||||
TerminalColor.Normal.RED -> 0xca6169
|
||||
TerminalColor.Normal.GREEN -> 0x82a568
|
||||
TerminalColor.Normal.YELLOW -> 0xbf8c5d
|
||||
TerminalColor.Normal.BLUE -> 0x56a2e1
|
||||
TerminalColor.Normal.MAGENTA -> 0xb76ccd
|
||||
TerminalColor.Normal.CYAN -> 0x4e9aa3
|
||||
TerminalColor.Normal.WHITE -> 0xc5cbd6
|
||||
|
||||
TerminalColor.Bright.BLACK -> 0x5c6370
|
||||
TerminalColor.Bright.RED -> 0xe77c84
|
||||
TerminalColor.Bright.GREEN -> 0xb4e294
|
||||
TerminalColor.Bright.YELLOW -> 0xe9b17b
|
||||
TerminalColor.Bright.BLUE -> 0x7ec5ff
|
||||
TerminalColor.Bright.MAGENTA -> 0xdb8df2
|
||||
TerminalColor.Bright.CYAN -> 0x64cfdd
|
||||
TerminalColor.Bright.WHITE -> 0xffffff
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0xabb2bf
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AtomOneLightLaf : FlatPropertiesLaf("Atom One Light", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "light",
|
||||
"@background" to "#f9f9f9",
|
||||
"@windowText" to "#383a42",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x000000
|
||||
TerminalColor.Normal.RED -> 0xe45649
|
||||
TerminalColor.Normal.GREEN -> 0x4c9b4b
|
||||
TerminalColor.Normal.YELLOW -> 0xc99525
|
||||
TerminalColor.Normal.BLUE -> 0x4078f2
|
||||
TerminalColor.Normal.MAGENTA -> 0xa626a4
|
||||
TerminalColor.Normal.CYAN -> 0x0184bc
|
||||
TerminalColor.Normal.WHITE -> 0xb8b9bf
|
||||
|
||||
TerminalColor.Bright.BLACK -> 0x474747
|
||||
TerminalColor.Bright.RED -> 0xff7468
|
||||
TerminalColor.Bright.GREEN -> 0x74ca72
|
||||
TerminalColor.Bright.YELLOW -> 0xdba633
|
||||
TerminalColor.Bright.BLUE -> 0x6a99ff
|
||||
TerminalColor.Bright.MAGENTA -> 0xc142bf
|
||||
TerminalColor.Bright.CYAN -> 0x00b1fd
|
||||
TerminalColor.Bright.WHITE -> 0xffffff
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0x383a42
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class EverforestDarkLaf : FlatPropertiesLaf("Everforest Dark", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "dark",
|
||||
"@background" to "#282e32",
|
||||
"@windowText" to "#d3c6aa",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x42494e
|
||||
TerminalColor.Normal.RED -> 0xa1484a
|
||||
TerminalColor.Normal.GREEN -> 0x778e54
|
||||
TerminalColor.Normal.YELLOW -> 0xba9e68
|
||||
TerminalColor.Normal.BLUE -> 0x388084
|
||||
TerminalColor.Normal.MAGENTA -> 0x906378
|
||||
TerminalColor.Normal.CYAN -> 0x6ca37a
|
||||
TerminalColor.Normal.WHITE -> 0xc0dac6
|
||||
TerminalColor.Bright.BLACK -> 0x575656
|
||||
TerminalColor.Bright.RED -> 0xe67e80
|
||||
TerminalColor.Bright.GREEN -> 0xa7c080
|
||||
TerminalColor.Bright.YELLOW -> 0xdbbc7f
|
||||
TerminalColor.Bright.BLUE -> 0x7fbbb3
|
||||
TerminalColor.Bright.MAGENTA -> 0xd699b6
|
||||
TerminalColor.Bright.CYAN -> 0x83c092
|
||||
TerminalColor.Bright.WHITE -> 0xe8f4eb
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0xd3c6aa
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class EverforestLightLaf : FlatPropertiesLaf("Everforest Light", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "light",
|
||||
"@background" to "#fefbf1",
|
||||
"@windowText" to "#5c6a72",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x42494e
|
||||
TerminalColor.Normal.RED -> 0xd2413e
|
||||
TerminalColor.Normal.GREEN -> 0x919d45
|
||||
TerminalColor.Normal.YELLOW -> 0xd89902
|
||||
TerminalColor.Normal.BLUE -> 0x2b7ba7
|
||||
TerminalColor.Normal.MAGENTA -> 0xbc72a5
|
||||
TerminalColor.Normal.CYAN -> 0x50b08c
|
||||
TerminalColor.Normal.WHITE -> 0xc8d0c9
|
||||
TerminalColor.Bright.BLACK -> 0x575656
|
||||
TerminalColor.Bright.RED -> 0xe67e80
|
||||
TerminalColor.Bright.GREEN -> 0xa7c080
|
||||
TerminalColor.Bright.YELLOW -> 0xdbbc7f
|
||||
TerminalColor.Bright.BLUE -> 0x7fbbb3
|
||||
TerminalColor.Bright.MAGENTA -> 0xd699b6
|
||||
TerminalColor.Bright.CYAN -> 0x83c092
|
||||
TerminalColor.Bright.WHITE -> 0xd7e2d8
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0x5c6a72
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class NightOwlLaf : FlatPropertiesLaf("Night Owl", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "dark",
|
||||
"@background" to "#011627",
|
||||
"@windowText" to "#d6deeb",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x072945
|
||||
TerminalColor.Normal.RED -> 0xef5350
|
||||
TerminalColor.Normal.GREEN -> 0x22da6e
|
||||
TerminalColor.Normal.YELLOW -> 0xc5e478
|
||||
TerminalColor.Normal.BLUE -> 0x82aaff
|
||||
TerminalColor.Normal.MAGENTA -> 0xc792ea
|
||||
TerminalColor.Normal.CYAN -> 0x21c7a8
|
||||
TerminalColor.Normal.WHITE -> 0xe1f1ff
|
||||
TerminalColor.Bright.BLACK -> 0x575656
|
||||
TerminalColor.Bright.RED -> 0xff7472
|
||||
TerminalColor.Bright.GREEN -> 0x40fa8d
|
||||
TerminalColor.Bright.YELLOW -> 0xffeb95
|
||||
TerminalColor.Bright.BLUE -> 0xa0beff
|
||||
TerminalColor.Bright.MAGENTA -> 0xdaa4ff
|
||||
TerminalColor.Bright.CYAN -> 0x7fdbca
|
||||
TerminalColor.Bright.WHITE -> 0xffffff
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0x80a4c2
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LightOwlLaf : FlatPropertiesLaf("Light Owl", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "light",
|
||||
"@background" to "#fbfbfb",
|
||||
"@windowText" to "#403f53",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x403f53
|
||||
TerminalColor.Normal.RED -> 0xde3d3b
|
||||
TerminalColor.Normal.GREEN -> 0x08916a
|
||||
TerminalColor.Normal.YELLOW -> 0xe0af02
|
||||
TerminalColor.Normal.BLUE -> 0x288ed7
|
||||
TerminalColor.Normal.MAGENTA -> 0xd6438a
|
||||
TerminalColor.Normal.CYAN -> 0x2aa298
|
||||
TerminalColor.Normal.WHITE -> 0xe8e5e5
|
||||
TerminalColor.Bright.BLACK -> 0x57566d
|
||||
TerminalColor.Bright.RED -> 0xfa5d5b
|
||||
TerminalColor.Bright.GREEN -> 0x1abf90
|
||||
TerminalColor.Bright.YELLOW -> 0xf4c315
|
||||
TerminalColor.Bright.BLUE -> 0x3ca3ec
|
||||
TerminalColor.Bright.MAGENTA -> 0xf559a4
|
||||
TerminalColor.Bright.CYAN -> 0x39c6ba
|
||||
TerminalColor.Bright.WHITE -> 0xf6f6f6
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0x90a7b2
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AuraLaf : FlatPropertiesLaf("Aura", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "dark",
|
||||
"@background" to "#21202e",
|
||||
"@windowText" to "#edecee",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x1c1b22
|
||||
TerminalColor.Normal.RED -> 0xff6767
|
||||
TerminalColor.Normal.GREEN -> 0x4deeb8
|
||||
TerminalColor.Normal.YELLOW -> 0xf4be77
|
||||
TerminalColor.Normal.BLUE -> 0x5b72ee
|
||||
TerminalColor.Normal.MAGENTA -> 0xa277ff
|
||||
TerminalColor.Normal.CYAN -> 0x51fafa
|
||||
TerminalColor.Normal.WHITE -> 0xdddbfa
|
||||
TerminalColor.Bright.BLACK -> 0x4d4d4d
|
||||
TerminalColor.Bright.RED -> 0xffa285
|
||||
TerminalColor.Bright.GREEN -> 0x99ffdd
|
||||
TerminalColor.Bright.YELLOW -> 0xffd49d
|
||||
TerminalColor.Bright.BLUE -> 0x8296ff
|
||||
TerminalColor.Bright.MAGENTA -> 0xb592ff
|
||||
TerminalColor.Bright.CYAN -> 0x8cffff
|
||||
TerminalColor.Bright.WHITE -> 0xffffff
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0xedecee
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Cobalt2Laf : FlatPropertiesLaf("Cobalt2", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "dark",
|
||||
"@background" to "#132738",
|
||||
"@windowText" to "#ffffff",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x000000
|
||||
TerminalColor.Normal.RED -> 0xff0000
|
||||
TerminalColor.Normal.GREEN -> 0x38de21
|
||||
TerminalColor.Normal.YELLOW -> 0xffe50a
|
||||
TerminalColor.Normal.BLUE -> 0x1460d2
|
||||
TerminalColor.Normal.MAGENTA -> 0xff4387
|
||||
TerminalColor.Normal.CYAN -> 0x00bbbb
|
||||
TerminalColor.Normal.WHITE -> 0xcfcfcf
|
||||
TerminalColor.Bright.BLACK -> 0x555555
|
||||
TerminalColor.Bright.RED -> 0xff757a
|
||||
TerminalColor.Bright.GREEN -> 0x69fb79
|
||||
TerminalColor.Bright.YELLOW -> 0xfff285
|
||||
TerminalColor.Bright.BLUE -> 0x77adff
|
||||
TerminalColor.Bright.MAGENTA -> 0xff92cc
|
||||
TerminalColor.Bright.CYAN -> 0x6bffff
|
||||
TerminalColor.Bright.WHITE -> 0xffffff
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0xf0cc09
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class OctocatDarkLaf : FlatPropertiesLaf("Octocat Dark", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "dark",
|
||||
"@background" to "#101216",
|
||||
"@windowText" to "#8b949e",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x000000
|
||||
TerminalColor.Normal.RED -> 0xf78166
|
||||
TerminalColor.Normal.GREEN -> 0x56d364
|
||||
TerminalColor.Normal.YELLOW -> 0xe3b341
|
||||
TerminalColor.Normal.BLUE -> 0x6ca4f8
|
||||
TerminalColor.Normal.MAGENTA -> 0xdb61a2
|
||||
TerminalColor.Normal.CYAN -> 0x2b7489
|
||||
TerminalColor.Normal.WHITE -> 0xDADADA
|
||||
TerminalColor.Bright.BLACK -> 0x4d4d4d
|
||||
TerminalColor.Bright.RED -> 0xffb5a5
|
||||
TerminalColor.Bright.GREEN -> 0x69fb79
|
||||
TerminalColor.Bright.YELLOW -> 0xffcf5f
|
||||
TerminalColor.Bright.BLUE -> 0xb0d0ff
|
||||
TerminalColor.Bright.MAGENTA -> 0xff92cc
|
||||
TerminalColor.Bright.CYAN -> 0x54d8ff
|
||||
TerminalColor.Bright.WHITE -> 0xffffff
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0xc9d1d9
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class OctocatLightLaf : FlatPropertiesLaf("Octocat Light", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "light",
|
||||
"@background" to "#f4f4f4",
|
||||
"@windowText" to "#3e3e3e",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x000000
|
||||
TerminalColor.Normal.RED -> 0xff0000
|
||||
TerminalColor.Normal.GREEN -> 0x38de21
|
||||
TerminalColor.Normal.YELLOW -> 0xffe50a
|
||||
TerminalColor.Normal.BLUE -> 0x1460d2
|
||||
TerminalColor.Normal.MAGENTA -> 0xff4387
|
||||
TerminalColor.Normal.CYAN -> 0x00bbbb
|
||||
TerminalColor.Normal.WHITE -> 0xcfcfcf
|
||||
TerminalColor.Bright.BLACK -> 0x555555
|
||||
TerminalColor.Bright.RED -> 0xff757a
|
||||
TerminalColor.Bright.GREEN -> 0x69fb79
|
||||
TerminalColor.Bright.YELLOW -> 0xfff285
|
||||
TerminalColor.Bright.BLUE -> 0x77adff
|
||||
TerminalColor.Bright.MAGENTA -> 0xff92cc
|
||||
TerminalColor.Bright.CYAN -> 0x6bffff
|
||||
TerminalColor.Bright.WHITE -> 0xffffff
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0x3f3f3f
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AyuDarkLaf : FlatPropertiesLaf("Ayu Dark", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "dark",
|
||||
"@background" to "#0f1419",
|
||||
"@windowText" to "#e6e1cf",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x000000
|
||||
TerminalColor.Normal.RED -> 0xff3333
|
||||
TerminalColor.Normal.GREEN -> 0xb8cc52
|
||||
TerminalColor.Normal.YELLOW -> 0xdbb012
|
||||
TerminalColor.Normal.BLUE -> 0x36a3d9
|
||||
TerminalColor.Normal.MAGENTA -> 0xdf7a80
|
||||
TerminalColor.Normal.CYAN -> 0x6ceedf
|
||||
TerminalColor.Normal.WHITE -> 0xababab
|
||||
TerminalColor.Bright.BLACK -> 0x323232
|
||||
TerminalColor.Bright.RED -> 0xff8181
|
||||
TerminalColor.Bright.GREEN -> 0xeafe84
|
||||
TerminalColor.Bright.YELLOW -> 0xffe174
|
||||
TerminalColor.Bright.BLUE -> 0x68d5ff
|
||||
TerminalColor.Bright.MAGENTA -> 0xffa3aa
|
||||
TerminalColor.Bright.CYAN -> 0x94fff1
|
||||
TerminalColor.Bright.WHITE -> 0xffffff
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0xf29718
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AyuLightLaf : FlatPropertiesLaf("Ayu Light", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "light",
|
||||
"@background" to "#fafafa",
|
||||
"@windowText" to "#5c6773",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x000000
|
||||
TerminalColor.Normal.RED -> 0xff3333
|
||||
TerminalColor.Normal.GREEN -> 0x319900
|
||||
TerminalColor.Normal.YELLOW -> 0xf29718
|
||||
TerminalColor.Normal.BLUE -> 0x41a6d9
|
||||
TerminalColor.Normal.MAGENTA -> 0xe07ead
|
||||
TerminalColor.Normal.CYAN -> 0x1dd1b0
|
||||
TerminalColor.Normal.WHITE -> 0xdfdddd
|
||||
TerminalColor.Bright.BLACK -> 0x323232
|
||||
TerminalColor.Bright.RED -> 0xff5959
|
||||
TerminalColor.Bright.GREEN -> 0xb8e532
|
||||
TerminalColor.Bright.YELLOW -> 0xffc94a
|
||||
TerminalColor.Bright.BLUE -> 0x73d8ff
|
||||
TerminalColor.Bright.MAGENTA -> 0xffa3aa
|
||||
TerminalColor.Bright.CYAN -> 0x7ff1cb
|
||||
TerminalColor.Bright.WHITE -> 0xffffff
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0xff6a00
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class HomebrewLaf : FlatPropertiesLaf("Homebrew", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "dark",
|
||||
"@background" to "#000000",
|
||||
"@windowText" to "#00ff00",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x2e2e2e
|
||||
TerminalColor.Normal.RED -> 0xc93434
|
||||
TerminalColor.Normal.GREEN -> 0x348e48
|
||||
TerminalColor.Normal.YELLOW -> 0xe09e00
|
||||
TerminalColor.Normal.BLUE -> 0x0031e0
|
||||
TerminalColor.Normal.MAGENTA -> 0xe235ff
|
||||
TerminalColor.Normal.CYAN -> 0x3fc1dd
|
||||
TerminalColor.Normal.WHITE -> 0xd0cfcf
|
||||
TerminalColor.Bright.BLACK -> 0x5b5b5b
|
||||
TerminalColor.Bright.RED -> 0xff6767
|
||||
TerminalColor.Bright.GREEN -> 0x31ff31
|
||||
TerminalColor.Bright.YELLOW -> 0xffdca8
|
||||
TerminalColor.Bright.BLUE -> 0x4465da
|
||||
TerminalColor.Bright.MAGENTA -> 0xff5fc8
|
||||
TerminalColor.Bright.CYAN -> 0x8debff
|
||||
TerminalColor.Bright.WHITE -> 0xe6e6e6
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0x23ff18
|
||||
|
||||
TerminalColor.Basic.FOREGROUND -> 0x00ff00
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ProLaf : FlatPropertiesLaf("Pro", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "dark",
|
||||
"@background" to "#000000",
|
||||
"@windowText" to "#f2f2f2",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x2e2e2e
|
||||
TerminalColor.Normal.RED -> 0xc93434
|
||||
TerminalColor.Normal.GREEN -> 0x348e48
|
||||
TerminalColor.Normal.YELLOW -> 0xe09e00
|
||||
TerminalColor.Normal.BLUE -> 0x002bc7
|
||||
TerminalColor.Normal.MAGENTA -> 0xe235ff
|
||||
TerminalColor.Normal.CYAN -> 0x3fc1dd
|
||||
TerminalColor.Normal.WHITE -> 0xd0cfcf
|
||||
TerminalColor.Bright.BLACK -> 0x5b5b5b
|
||||
TerminalColor.Bright.RED -> 0xff6767
|
||||
TerminalColor.Bright.GREEN -> 0x31ff31
|
||||
TerminalColor.Bright.YELLOW -> 0xffdca8
|
||||
TerminalColor.Bright.BLUE -> 0x4465da
|
||||
TerminalColor.Bright.MAGENTA -> 0xff5fc8
|
||||
TerminalColor.Bright.CYAN -> 0x8debff
|
||||
TerminalColor.Bright.WHITE -> 0xe6e6e6
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0x4d4d4d
|
||||
|
||||
TerminalColor.Basic.FOREGROUND -> 0xf2f2f2
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class NordLightLaf : FlatPropertiesLaf("Nord Light", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "light",
|
||||
"@background" to "#e5e9f0",
|
||||
"@windowText" to "#414858",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x2c3344
|
||||
TerminalColor.Normal.RED -> 0xae545d
|
||||
TerminalColor.Normal.GREEN -> 0x8ca377
|
||||
TerminalColor.Normal.YELLOW -> 0xdabe84
|
||||
TerminalColor.Normal.BLUE -> 0x718fae
|
||||
TerminalColor.Normal.MAGENTA -> 0x95728e
|
||||
TerminalColor.Normal.CYAN -> 0x78acbb
|
||||
TerminalColor.Normal.WHITE -> 0xd8dee9
|
||||
TerminalColor.Bright.BLACK -> 0x4c556a
|
||||
TerminalColor.Bright.RED -> 0xd97982
|
||||
TerminalColor.Bright.GREEN -> 0xa3be8b
|
||||
TerminalColor.Bright.YELLOW -> 0xeacb8a
|
||||
TerminalColor.Bright.BLUE -> 0xa4c7e9
|
||||
TerminalColor.Bright.MAGENTA -> 0xb48dac
|
||||
TerminalColor.Bright.CYAN -> 0x8fbcbb
|
||||
TerminalColor.Bright.WHITE -> 0xeceff4
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0x88c0d0
|
||||
|
||||
TerminalColor.Basic.FOREGROUND -> 0x414858
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class NordDarkLaf : FlatPropertiesLaf("Nord Dark", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "dark",
|
||||
"@background" to "#2e3440",
|
||||
"@windowText" to "#d8dee9",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x3b4252
|
||||
TerminalColor.Normal.RED -> 0xae545d
|
||||
TerminalColor.Normal.GREEN -> 0x8ca377
|
||||
TerminalColor.Normal.YELLOW -> 0xdabe84
|
||||
TerminalColor.Normal.BLUE -> 0x718fae
|
||||
TerminalColor.Normal.MAGENTA -> 0x95728e
|
||||
TerminalColor.Normal.CYAN -> 0x78acbb
|
||||
TerminalColor.Normal.WHITE -> 0xd8dee9
|
||||
TerminalColor.Bright.BLACK -> 0x4c556a
|
||||
TerminalColor.Bright.RED -> 0xd97982
|
||||
TerminalColor.Bright.GREEN -> 0xa3be8b
|
||||
TerminalColor.Bright.YELLOW -> 0xeacb8a
|
||||
TerminalColor.Bright.BLUE -> 0xa4c7e9
|
||||
TerminalColor.Bright.MAGENTA -> 0xb48dac
|
||||
TerminalColor.Bright.CYAN -> 0x8fbcbb
|
||||
TerminalColor.Bright.WHITE -> 0xeceff4
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0xeceff4
|
||||
|
||||
TerminalColor.Basic.FOREGROUND -> 0xd8dee9
|
||||
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class GitHubLightLaf : FlatPropertiesLaf("GitHub Light", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "light",
|
||||
"@background" to "#f4f4f4",
|
||||
"@windowText" to "#3e3e3e",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x3e3e3e
|
||||
TerminalColor.Normal.RED -> 0x970b16
|
||||
TerminalColor.Normal.GREEN -> 0x07962a
|
||||
TerminalColor.Normal.YELLOW -> 0xf8eec7
|
||||
TerminalColor.Normal.BLUE -> 0x003e8a
|
||||
TerminalColor.Normal.MAGENTA -> 0xe94691
|
||||
TerminalColor.Normal.CYAN -> 0x89d1ec
|
||||
TerminalColor.Normal.WHITE -> 0x3e3e3e
|
||||
TerminalColor.Bright.BLACK -> 0x666666
|
||||
TerminalColor.Bright.RED -> 0xde0000
|
||||
TerminalColor.Bright.GREEN -> 0x87d5a2
|
||||
TerminalColor.Bright.YELLOW -> 0xf1d007
|
||||
TerminalColor.Bright.BLUE -> 0x2e6cba
|
||||
TerminalColor.Bright.MAGENTA -> 0xffa29f
|
||||
TerminalColor.Bright.CYAN -> 0x1cfafe
|
||||
TerminalColor.Bright.WHITE -> 0xffffff
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0x3f3f3f
|
||||
|
||||
TerminalColor.Basic.FOREGROUND -> 0x3e3e3e
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class GitHubDarkLaf : FlatPropertiesLaf("GitHub Dark", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "dark",
|
||||
"@background" to "#101216",
|
||||
"@windowText" to "#8b949e",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x000000
|
||||
TerminalColor.Normal.RED -> 0xf78166
|
||||
TerminalColor.Normal.GREEN -> 0x56d364
|
||||
TerminalColor.Normal.YELLOW -> 0xe3b341
|
||||
TerminalColor.Normal.BLUE -> 0x6ca4f8
|
||||
TerminalColor.Normal.MAGENTA -> 0xdb61a2
|
||||
TerminalColor.Normal.CYAN -> 0x2b7489
|
||||
TerminalColor.Normal.WHITE -> 0x8b949e
|
||||
TerminalColor.Bright.BLACK -> 0x4d4d4d
|
||||
TerminalColor.Bright.RED -> 0xf78166
|
||||
TerminalColor.Bright.GREEN -> 0x56d364
|
||||
TerminalColor.Bright.YELLOW -> 0xe3b341
|
||||
TerminalColor.Bright.BLUE -> 0x6ca4f8
|
||||
TerminalColor.Bright.MAGENTA -> 0xdb61a2
|
||||
TerminalColor.Bright.CYAN -> 0x2b7489
|
||||
TerminalColor.Bright.WHITE -> 0xffffff
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0xc9d1d9
|
||||
|
||||
TerminalColor.Basic.FOREGROUND -> 0x8b949e
|
||||
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ChalkLaf : FlatPropertiesLaf("Chalk", Properties().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"@baseTheme" to "dark",
|
||||
"@background" to "#2b2d2e",
|
||||
"@windowText" to "#d2d8d9",
|
||||
)
|
||||
)
|
||||
}), ColorTheme {
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
return when (color) {
|
||||
TerminalColor.Normal.BLACK -> 0x7d8b8f
|
||||
TerminalColor.Normal.RED -> 0xb23a52
|
||||
TerminalColor.Normal.GREEN -> 0x789b6a
|
||||
TerminalColor.Normal.YELLOW -> 0xb9ac4a
|
||||
TerminalColor.Normal.BLUE -> 0x2a7fac
|
||||
TerminalColor.Normal.MAGENTA -> 0xbd4f5a
|
||||
TerminalColor.Normal.CYAN -> 0x44a799
|
||||
TerminalColor.Normal.WHITE -> 0xd2d8d9
|
||||
TerminalColor.Bright.BLACK -> 0x888888
|
||||
TerminalColor.Bright.RED -> 0xf24840
|
||||
TerminalColor.Bright.GREEN -> 0x80c470
|
||||
TerminalColor.Bright.YELLOW -> 0xffeb62
|
||||
TerminalColor.Bright.BLUE -> 0x4196ff
|
||||
TerminalColor.Bright.MAGENTA -> 0xfc5275
|
||||
TerminalColor.Bright.CYAN -> 0x53cdbd
|
||||
TerminalColor.Bright.WHITE -> 0xd2d8d9
|
||||
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0x708284
|
||||
|
||||
TerminalColor.Basic.FOREGROUND -> 0xd2d8d9
|
||||
|
||||
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/main/kotlin/app/termora/LocalTerminalTab.kt
Normal file
20
src/main/kotlin/app/termora/LocalTerminalTab.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.terminal.PtyConnector
|
||||
import org.apache.commons.io.Charsets
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
class LocalTerminalTab(host: Host) : PtyHostTerminalTab(host) {
|
||||
|
||||
override suspend fun openPtyConnector(): PtyConnector {
|
||||
val winSize = terminalPanel.winSize()
|
||||
val ptyConnector = PtyConnectorFactory.instance.createPtyConnector(
|
||||
winSize.rows, winSize.cols,
|
||||
host.options.envs(),
|
||||
Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8),
|
||||
)
|
||||
|
||||
return ptyConnector
|
||||
}
|
||||
|
||||
}
|
||||
109
src/main/kotlin/app/termora/LogicCustomTitleBar.kt
Normal file
109
src/main/kotlin/app/termora/LogicCustomTitleBar.kt
Normal file
@@ -0,0 +1,109 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.jetbrains.JBR
|
||||
import com.jetbrains.WindowDecorations.CustomTitleBar
|
||||
import java.awt.Rectangle
|
||||
import java.awt.Window
|
||||
import javax.swing.RootPaneContainer
|
||||
|
||||
class LogicCustomTitleBar(private val titleBar: CustomTitleBar) : CustomTitleBar {
|
||||
companion object {
|
||||
fun createCustomTitleBar(rootPaneContainer: RootPaneContainer): CustomTitleBar {
|
||||
if (!JBR.isWindowDecorationsSupported()) {
|
||||
return LogicCustomTitleBar(object : CustomTitleBar {
|
||||
override fun getHeight(): Float {
|
||||
val bounds = rootPaneContainer.rootPane
|
||||
.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
||||
if (bounds is Rectangle) {
|
||||
return bounds.height.toFloat()
|
||||
}
|
||||
return 0f
|
||||
}
|
||||
|
||||
override fun setHeight(height: Float) {
|
||||
rootPaneContainer.rootPane.putClientProperty(
|
||||
FlatClientProperties.TITLE_BAR_HEIGHT,
|
||||
height.toInt()
|
||||
)
|
||||
}
|
||||
|
||||
override fun getProperties(): MutableMap<String, Any> {
|
||||
return mutableMapOf()
|
||||
}
|
||||
|
||||
override fun putProperties(m: MutableMap<String, *>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun putProperty(key: String?, value: Any?) {
|
||||
if (key == "controls.visible" && value is Boolean) {
|
||||
rootPaneContainer.rootPane.putClientProperty(
|
||||
FlatClientProperties.TITLE_BAR_SHOW_CLOSE,
|
||||
value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLeftInset(): Float {
|
||||
return 0f
|
||||
}
|
||||
|
||||
override fun getRightInset(): Float {
|
||||
val bounds = rootPaneContainer.rootPane
|
||||
.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
||||
if (bounds is Rectangle) {
|
||||
return bounds.width.toFloat()
|
||||
}
|
||||
return 0f
|
||||
}
|
||||
|
||||
override fun forceHitTest(client: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
override fun getContainingWindow(): Window {
|
||||
return rootPaneContainer as Window
|
||||
}
|
||||
})
|
||||
}
|
||||
return JBR.getWindowDecorations().createCustomTitleBar()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getHeight(): Float {
|
||||
return titleBar.height
|
||||
}
|
||||
|
||||
override fun setHeight(height: Float) {
|
||||
titleBar.height = height
|
||||
}
|
||||
|
||||
override fun getProperties(): MutableMap<String, Any> {
|
||||
return titleBar.properties
|
||||
}
|
||||
|
||||
override fun putProperties(m: MutableMap<String, *>?) {
|
||||
titleBar.putProperties(m)
|
||||
}
|
||||
|
||||
override fun putProperty(key: String?, value: Any?) {
|
||||
titleBar.putProperty(key, value)
|
||||
}
|
||||
|
||||
override fun getLeftInset(): Float {
|
||||
return titleBar.leftInset
|
||||
}
|
||||
|
||||
override fun getRightInset(): Float {
|
||||
return titleBar.rightInset
|
||||
}
|
||||
|
||||
override fun forceHitTest(client: Boolean) {
|
||||
titleBar.forceHitTest(client)
|
||||
}
|
||||
|
||||
override fun getContainingWindow(): Window {
|
||||
return titleBar.containingWindow
|
||||
}
|
||||
}
|
||||
6
src/main/kotlin/app/termora/Main.kt
Normal file
6
src/main/kotlin/app/termora/Main.kt
Normal file
@@ -0,0 +1,6 @@
|
||||
package app.termora
|
||||
|
||||
fun main() {
|
||||
ApplicationRunner().run()
|
||||
}
|
||||
|
||||
45
src/main/kotlin/app/termora/MultiplePtyConnector.kt
Normal file
45
src/main/kotlin/app/termora/MultiplePtyConnector.kt
Normal file
@@ -0,0 +1,45 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.terminal.PtyConnector
|
||||
import app.termora.terminal.PtyConnectorDelegate
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
|
||||
/**
|
||||
* 当开启转发时,会获取到所有的 [PtyConnector] 然后跳过中间层,直接找到最近的一个 [MultiplePtyConnector],如果找不到那就以最后一个匹配不到的为准 [getMultiplePtyConnector]。
|
||||
*/
|
||||
class MultiplePtyConnector(private val myConnector: PtyConnector) : PtyConnectorDelegate(myConnector) {
|
||||
|
||||
private val isMultiple get() = ActionManager.getInstance().isSelected(Actions.MULTIPLE)
|
||||
private val ptyConnectors get() = PtyConnectorFactory.instance.getPtyConnectors()
|
||||
|
||||
override fun write(buffer: ByteArray, offset: Int, len: Int) {
|
||||
if (isMultiple) {
|
||||
for (connector in ptyConnectors) {
|
||||
getMultiplePtyConnector(connector).write(buffer, offset, len)
|
||||
}
|
||||
} else {
|
||||
myConnector.write(buffer, offset, len)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun getMultiplePtyConnector(connector: PtyConnector): PtyConnector {
|
||||
if (connector is MultiplePtyConnector) {
|
||||
val c = connector.myConnector
|
||||
if (c is MultiplePtyConnector) {
|
||||
return getMultiplePtyConnector(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
if (connector is PtyConnectorDelegate) {
|
||||
val c = connector.ptyConnector
|
||||
if (c != null) {
|
||||
return getMultiplePtyConnector(c)
|
||||
}
|
||||
}
|
||||
|
||||
return connector
|
||||
}
|
||||
|
||||
}
|
||||
44
src/main/kotlin/app/termora/MultipleTerminalListener.kt
Normal file
44
src/main/kotlin/app/termora/MultipleTerminalListener.kt
Normal file
@@ -0,0 +1,44 @@
|
||||
package app.termora
|
||||
|
||||
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
|
||||
|
||||
class MultipleTerminalListener : TerminalPaintListener {
|
||||
override fun after(
|
||||
offset: Int,
|
||||
count: Int,
|
||||
g: Graphics,
|
||||
terminalPanel: TerminalPanel,
|
||||
terminalDisplay: TerminalDisplay,
|
||||
terminal: Terminal
|
||||
) {
|
||||
if (!ActionManager.getInstance().isSelected(Actions.MULTIPLE)) {
|
||||
return
|
||||
}
|
||||
|
||||
val oldFont = g.font
|
||||
val colorPalette = terminal.getTerminalModel().getColorPalette()
|
||||
val text = I18n.getString("termora.tools.multiple")
|
||||
val font = terminalDisplay.getDisplayFont(text, TextStyle.Default)
|
||||
val width = g.getFontMetrics(font).stringWidth(text)
|
||||
// 正在搜索那么需要下移
|
||||
val finding = terminal.getTerminalModel().getData(TerminalPanel.Finding, false)
|
||||
|
||||
g.font = font
|
||||
g.color = Color(colorPalette.getColor(TerminalColor.Normal.RED))
|
||||
g.drawString(
|
||||
text,
|
||||
terminalDisplay.width - width - terminalPanel.getAverageCharWidth() / 2,
|
||||
g.fontMetrics.ascent + if (finding)
|
||||
g.fontMetrics.height + g.fontMetrics.ascent / 2 else 0
|
||||
)
|
||||
g.font = oldFont
|
||||
}
|
||||
}
|
||||
11
src/main/kotlin/app/termora/MyTabbedPane.kt
Normal file
11
src/main/kotlin/app/termora/MyTabbedPane.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
|
||||
class MyTabbedPane : FlatTabbedPane() {
|
||||
override fun setSelectedIndex(index: Int) {
|
||||
val oldIndex = selectedIndex
|
||||
super.setSelectedIndex(index)
|
||||
firePropertyChange("selectedIndex", oldIndex,index)
|
||||
}
|
||||
}
|
||||
5
src/main/kotlin/app/termora/OpenHostActionEvent.kt
Normal file
5
src/main/kotlin/app/termora/OpenHostActionEvent.kt
Normal file
@@ -0,0 +1,5 @@
|
||||
package app.termora
|
||||
|
||||
import java.awt.event.ActionEvent
|
||||
|
||||
class OpenHostActionEvent(source: Any, val host: Host) : ActionEvent(source, ACTION_PERFORMED, String())
|
||||
165
src/main/kotlin/app/termora/OptionPane.kt
Normal file
165
src/main/kotlin/app/termora/OptionPane.kt
Normal file
@@ -0,0 +1,165 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatTextPane
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jetbrains.JBR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import org.jdesktop.swingx.JXLabel
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.Desktop
|
||||
import java.awt.Dimension
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import java.io.File
|
||||
import javax.swing.*
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
object OptionPane {
|
||||
fun showConfirmDialog(
|
||||
parentComponent: Component?,
|
||||
message: Any,
|
||||
title: String = UIManager.getString("OptionPane.messageDialogTitle"),
|
||||
optionType: Int = JOptionPane.YES_NO_OPTION,
|
||||
messageType: Int = JOptionPane.QUESTION_MESSAGE,
|
||||
icon: Icon? = null,
|
||||
options: Array<Any>? = null,
|
||||
initialValue: Any? = null,
|
||||
): Int {
|
||||
|
||||
val panel = if (message is JComponent) {
|
||||
message
|
||||
} else {
|
||||
val label = FlatTextPane()
|
||||
label.contentType = "text/html"
|
||||
label.text = "<html>$message</html>"
|
||||
label.isEditable = false
|
||||
label.background = null
|
||||
label.border = BorderFactory.createEmptyBorder()
|
||||
label
|
||||
}
|
||||
|
||||
val pane = object : JOptionPane(panel, messageType, optionType, icon, options, initialValue) {
|
||||
override fun selectInitialValue() {
|
||||
super.selectInitialValue()
|
||||
if (message is JComponent) {
|
||||
message.requestFocusInWindow()
|
||||
}
|
||||
}
|
||||
}
|
||||
val dialog = initDialog(pane.createDialog(parentComponent, title))
|
||||
dialog.addWindowListener(object : WindowAdapter() {
|
||||
override fun windowOpened(e: WindowEvent) {
|
||||
pane.selectInitialValue()
|
||||
}
|
||||
})
|
||||
dialog.isVisible = true
|
||||
dialog.dispose()
|
||||
val selectedValue = pane.value
|
||||
|
||||
|
||||
if (selectedValue == null) {
|
||||
return -1
|
||||
} else if (pane.options == null) {
|
||||
return if (selectedValue is Int) selectedValue else -1
|
||||
} else {
|
||||
var counter = 0
|
||||
|
||||
val maxCounter: Int = pane.options.size
|
||||
while (counter < maxCounter) {
|
||||
if (pane.options[counter] == selectedValue) {
|
||||
return counter
|
||||
}
|
||||
++counter
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
fun showMessageDialog(
|
||||
parentComponent: Component?,
|
||||
message: String,
|
||||
title: String = UIManager.getString("OptionPane.messageDialogTitle"),
|
||||
messageType: Int = JOptionPane.INFORMATION_MESSAGE,
|
||||
duration: Duration = 0.milliseconds,
|
||||
) {
|
||||
val label = JTextPane()
|
||||
label.contentType = "text/html"
|
||||
label.text = "<html>$message</html>"
|
||||
label.isEditable = false
|
||||
label.background = null
|
||||
label.border = BorderFactory.createEmptyBorder()
|
||||
val pane = JOptionPane(label, messageType, JOptionPane.DEFAULT_OPTION)
|
||||
val dialog = initDialog(pane.createDialog(parentComponent, title))
|
||||
if (duration.inWholeMilliseconds > 0) {
|
||||
dialog.addWindowListener(object : WindowAdapter() {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun windowOpened(e: WindowEvent) {
|
||||
GlobalScope.launch(Dispatchers.Swing) {
|
||||
delay(duration.inWholeMilliseconds)
|
||||
if (dialog.isVisible) {
|
||||
dialog.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
pane.selectInitialValue()
|
||||
dialog.isVisible = true
|
||||
dialog.dispose()
|
||||
}
|
||||
|
||||
fun openFileInFolder(
|
||||
parentComponent: Component,
|
||||
file: File,
|
||||
yMessage: String,
|
||||
nMessage: String? = null,
|
||||
) {
|
||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop()
|
||||
.isSupported(Desktop.Action.BROWSE_FILE_DIR)
|
||||
) {
|
||||
if (JOptionPane.YES_OPTION == showConfirmDialog(
|
||||
parentComponent,
|
||||
yMessage,
|
||||
optionType = JOptionPane.YES_NO_OPTION
|
||||
)
|
||||
) {
|
||||
Desktop.getDesktop().browseFileDirectory(file)
|
||||
}
|
||||
} else if (nMessage != null) {
|
||||
showMessageDialog(
|
||||
parentComponent,
|
||||
nMessage,
|
||||
messageType = JOptionPane.INFORMATION_MESSAGE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initDialog(dialog: JDialog): JDialog {
|
||||
|
||||
if (JBR.isWindowDecorationsSupported()) {
|
||||
|
||||
val windowDecorations = JBR.getWindowDecorations()
|
||||
val titleBar = windowDecorations.createCustomTitleBar()
|
||||
titleBar.putProperty("controls.visible", false)
|
||||
titleBar.height = UIManager.getInt("TabbedPane.tabHeight") - if (SystemInfo.isMacOS) 10f else 6f
|
||||
windowDecorations.setCustomTitleBar(dialog, titleBar)
|
||||
|
||||
val label = JLabel(dialog.title)
|
||||
label.putClientProperty(FlatClientProperties.STYLE, "font: bold")
|
||||
val box = Box.createHorizontalBox()
|
||||
box.add(Box.createHorizontalGlue())
|
||||
box.add(label)
|
||||
box.add(Box.createHorizontalGlue())
|
||||
box.preferredSize = Dimension(-1, titleBar.height.toInt())
|
||||
|
||||
dialog.contentPane.add(box, BorderLayout.NORTH)
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
136
src/main/kotlin/app/termora/OptionsPane.kt
Normal file
136
src/main/kotlin/app/termora/OptionsPane.kt
Normal file
@@ -0,0 +1,136 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import java.awt.*
|
||||
import javax.swing.*
|
||||
import javax.swing.border.Border
|
||||
|
||||
|
||||
open class OptionsPane : JPanel(BorderLayout()) {
|
||||
protected val formMargin = "7dlu"
|
||||
|
||||
protected val tabListModel = DefaultListModel<Option>()
|
||||
protected val tabList = object : JList<Option>(tabListModel) {
|
||||
override fun getBackground(): Color {
|
||||
return this@OptionsPane.background
|
||||
}
|
||||
}
|
||||
private val cardLayout = CardLayout()
|
||||
private val contentPanel = JPanel(cardLayout)
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
|
||||
tabList.fixedCellHeight = (UIManager.getInt("Tree.rowHeight") * 1.2).toInt()
|
||||
tabList.fixedCellWidth = 170
|
||||
tabList.selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
tabList.border = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor),
|
||||
BorderFactory.createEmptyBorder(6, 6, 0, 6)
|
||||
)
|
||||
tabList.cellRenderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
val option = value as Option
|
||||
val c = super.getListCellRendererComponent(list, option.getTitle(), index, isSelected, cellHasFocus)
|
||||
|
||||
icon = option.getIcon(isSelected)
|
||||
if (isSelected && tabList.hasFocus()) {
|
||||
if (!FlatLaf.isLafDark()) {
|
||||
if (icon is DynamicIcon) {
|
||||
icon = (icon as DynamicIcon).dark
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
add(tabList, BorderLayout.WEST)
|
||||
add(contentPanel, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
fun selectOption(option: Option) {
|
||||
val index = tabListModel.indexOf(option)
|
||||
if (index < 0) {
|
||||
return
|
||||
}
|
||||
setSelectedIndex(index)
|
||||
}
|
||||
|
||||
fun getSelectedOption(): Option? {
|
||||
val index = tabList.selectedIndex
|
||||
if (index < 0) return null
|
||||
return tabListModel.getElementAt(index)
|
||||
}
|
||||
|
||||
fun getSelectedIndex(): Int {
|
||||
return tabList.selectedIndex
|
||||
}
|
||||
|
||||
fun setSelectedIndex(index: Int) {
|
||||
tabList.selectedIndex = index
|
||||
}
|
||||
|
||||
fun selectOptionJComponent(c: JComponent) {
|
||||
for (element in tabListModel.elements()) {
|
||||
var p = c as Container?
|
||||
while (p != null) {
|
||||
if (p == element) {
|
||||
selectOption(element)
|
||||
return
|
||||
}
|
||||
p = p.parent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun addOption(option: Option) {
|
||||
for (element in tabListModel.elements()) {
|
||||
if (element.getTitle() == option.getTitle()) {
|
||||
throw UnsupportedOperationException("Title already exists")
|
||||
}
|
||||
}
|
||||
contentPanel.add(option.getJComponent(), option.getTitle())
|
||||
tabListModel.addElement(option)
|
||||
|
||||
if (tabList.selectedIndex < 0) {
|
||||
tabList.selectedIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
fun removeOption(option: Option) {
|
||||
contentPanel.remove(option.getJComponent())
|
||||
tabListModel.removeElement(option)
|
||||
}
|
||||
|
||||
fun setContentBorder(border: Border) {
|
||||
contentPanel.border = border
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
tabList.addListSelectionListener {
|
||||
if (tabList.selectedIndex >= 0) {
|
||||
cardLayout.show(contentPanel, tabListModel.get(tabList.selectedIndex).getTitle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Option {
|
||||
fun getIcon(isSelected: Boolean): Icon
|
||||
fun getTitle(): String
|
||||
fun getJComponent(): JComponent
|
||||
}
|
||||
}
|
||||
32
src/main/kotlin/app/termora/PropertyTerminalTab.kt
Normal file
32
src/main/kotlin/app/termora/PropertyTerminalTab.kt
Normal file
@@ -0,0 +1,32 @@
|
||||
package app.termora
|
||||
|
||||
import java.beans.PropertyChangeEvent
|
||||
import java.beans.PropertyChangeListener
|
||||
|
||||
abstract class PropertyTerminalTab : TerminalTab {
|
||||
protected val listeners = mutableListOf<PropertyChangeListener>()
|
||||
var hasFocus = false
|
||||
protected set
|
||||
|
||||
override fun addPropertyChangeListener(listener: PropertyChangeListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
override fun removePropertyChangeListener(listener: PropertyChangeListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
protected fun firePropertyChange(event: PropertyChangeEvent) {
|
||||
listeners.forEach { l -> l.propertyChange(event) }
|
||||
}
|
||||
|
||||
override fun onGrabFocus() {
|
||||
hasFocus = true
|
||||
}
|
||||
|
||||
override fun onLostFocus() {
|
||||
hasFocus = false
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
70
src/main/kotlin/app/termora/PtyConnectorFactory.kt
Normal file
70
src/main/kotlin/app/termora/PtyConnectorFactory.kt
Normal file
@@ -0,0 +1,70 @@
|
||||
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.SystemUtils
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.*
|
||||
|
||||
class PtyConnectorFactory {
|
||||
private val ptyConnectors = Collections.synchronizedList(mutableListOf<PtyConnector>())
|
||||
private val database get() = Database.instance
|
||||
|
||||
companion object {
|
||||
val instance by lazy { PtyConnectorFactory() }
|
||||
}
|
||||
|
||||
fun createPtyConnector(
|
||||
rows: Int = 24, cols: Int = 80,
|
||||
env: Map<String, String> = emptyMap(),
|
||||
charset: Charset = StandardCharsets.UTF_8
|
||||
): PtyConnector {
|
||||
val envs = mutableMapOf<String, String>()
|
||||
envs.putAll(System.getenv())
|
||||
envs["TERM"] = "xterm-256color"
|
||||
envs.putAll(env)
|
||||
|
||||
val command = database.terminal.localShell
|
||||
val ptyProcess = PtyProcessBuilder(arrayOf(command))
|
||||
.setEnvironment(envs)
|
||||
.setInitialRows(rows)
|
||||
.setInitialColumns(cols)
|
||||
.setConsole(false)
|
||||
.setDirectory(SystemUtils.USER_HOME)
|
||||
.setCygwin(false)
|
||||
.setUseWinConPty(SystemUtils.IS_OS_WINDOWS)
|
||||
.setRedirectErrorStream(false)
|
||||
.setWindowsAnsiColorEnabled(false)
|
||||
.setUnixOpenTtyToPreserveOutputAfterTermination(false)
|
||||
.setSpawnProcessUsingJdkOnMacIntel(true).start()
|
||||
|
||||
return decorate(PtyProcessConnector(ptyProcess, charset))
|
||||
}
|
||||
|
||||
fun decorate(ptyConnector: PtyConnector): PtyConnector {
|
||||
// 集成转发,如果PtyConnector支持转发那么应该在当前注释行前面代理
|
||||
val multiplePtyConnector = MultiplePtyConnector(ptyConnector)
|
||||
// 宏应该在转发前面执行,不然会导致重复录制
|
||||
val macroPtyConnector = MacroPtyConnector(multiplePtyConnector)
|
||||
// 集成自动删除
|
||||
val autoRemovePtyConnector = AutoRemovePtyConnector(macroPtyConnector)
|
||||
ptyConnectors.add(autoRemovePtyConnector)
|
||||
return autoRemovePtyConnector
|
||||
}
|
||||
|
||||
fun getPtyConnectors(): List<PtyConnector> {
|
||||
return ptyConnectors
|
||||
}
|
||||
|
||||
private inner class AutoRemovePtyConnector(connector: PtyConnector) : PtyConnectorDelegate(connector) {
|
||||
override fun close() {
|
||||
ptyConnectors.remove(this)
|
||||
super.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/main/kotlin/app/termora/PtyConnectorReader.kt
Normal file
27
src/main/kotlin/app/termora/PtyConnectorReader.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.terminal.PtyConnector
|
||||
import app.termora.terminal.Terminal
|
||||
import kotlinx.coroutines.delay
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class PtyConnectorReader(
|
||||
private val ptyConnector: PtyConnector,
|
||||
private val terminal: Terminal,
|
||||
) {
|
||||
|
||||
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)
|
||||
continue
|
||||
}
|
||||
val text = String(buffer, 0, i)
|
||||
SwingUtilities.invokeLater { terminal.write(text) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
121
src/main/kotlin/app/termora/PtyHostTerminalTab.kt
Normal file
121
src/main/kotlin/app/termora/PtyHostTerminalTab.kt
Normal file
@@ -0,0 +1,121 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.terminal.ControlCharacters
|
||||
import app.termora.terminal.PtyConnector
|
||||
import app.termora.terminal.PtyConnectorDelegate
|
||||
import app.termora.terminal.TerminalKeyEvent
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.JComponent
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
abstract class PtyHostTerminalTab(host: Host) : HostTerminalTab(host) {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
|
||||
}
|
||||
|
||||
|
||||
private var readerJob: Job? = null
|
||||
private val ptyConnectorDelegate = PtyConnectorDelegate()
|
||||
|
||||
protected val terminalPanel = TerminalPanelFactory.instance.createTerminalPanel(terminal, ptyConnectorDelegate)
|
||||
protected val ptyConnectorFactory get() = PtyConnectorFactory.instance
|
||||
|
||||
override fun start() {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
|
||||
try {
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
// clear terminal
|
||||
terminal.clearScreen()
|
||||
}
|
||||
|
||||
// 开启 PTY
|
||||
val ptyConnector = openPtyConnector()
|
||||
ptyConnectorDelegate.ptyConnector = ptyConnector
|
||||
|
||||
// 开启 reader
|
||||
startPtyConnectorReader()
|
||||
|
||||
// 启动命令
|
||||
if (host.options.startupCommand.isNotBlank()) {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
delay(250.milliseconds)
|
||||
withContext(Dispatchers.Swing) {
|
||||
ptyConnector.write(host.options.startupCommand)
|
||||
ptyConnector.write(terminal.getKeyEncoder().encode(TerminalKeyEvent(KeyEvent.VK_ENTER)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("Host: {} started", host.name)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
withContext(Dispatchers.Swing) {
|
||||
terminal.write("\r\n${ControlCharacters.ESC}[31m")
|
||||
terminal.write(ExceptionUtils.getRootCauseMessage(e))
|
||||
terminal.write("${ControlCharacters.ESC}[0m")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun canReconnect(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun reconnect() {
|
||||
stop()
|
||||
start()
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return terminalPanel
|
||||
}
|
||||
|
||||
open fun startPtyConnectorReader() {
|
||||
readerJob?.cancel()
|
||||
readerJob = coroutineScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
PtyConnectorReader(ptyConnectorDelegate, terminal).start()
|
||||
} catch (e: Exception) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun stop() {
|
||||
readerJob?.cancel()
|
||||
ptyConnectorDelegate.close()
|
||||
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("Host: {} stopped", host.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
stop()
|
||||
super.dispose()
|
||||
|
||||
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("Host: {} disposed", host.name)
|
||||
}
|
||||
}
|
||||
|
||||
open fun getPtyConnector(): PtyConnector {
|
||||
return ptyConnectorDelegate
|
||||
}
|
||||
|
||||
abstract suspend fun openPtyConnector(): PtyConnector
|
||||
}
|
||||
15
src/main/kotlin/app/termora/ResponseException.kt
Normal file
15
src/main/kotlin/app/termora/ResponseException.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
package app.termora
|
||||
|
||||
import okhttp3.Response
|
||||
|
||||
class ResponseException : RuntimeException {
|
||||
val code: Int
|
||||
val response: Response
|
||||
|
||||
constructor(code: Int, response: Response) : this(code, "Response code: $code", response)
|
||||
constructor(code: Int, message: String, response: Response) : super(message) {
|
||||
this.code = code
|
||||
this.response = response
|
||||
}
|
||||
|
||||
}
|
||||
229
src/main/kotlin/app/termora/SSHTerminalTab.kt
Normal file
229
src/main/kotlin/app/termora/SSHTerminalTab.kt
Normal file
@@ -0,0 +1,229 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
||||
import app.termora.terminal.ControlCharacters
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.PtyConnector
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.apache.commons.io.Charsets
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.sshd.client.SshClient
|
||||
import org.apache.sshd.client.channel.ChannelShell
|
||||
import org.apache.sshd.client.session.ClientSession
|
||||
import org.apache.sshd.common.SshConstants
|
||||
import org.apache.sshd.common.channel.Channel
|
||||
import org.apache.sshd.common.channel.ChannelListener
|
||||
import org.apache.sshd.common.session.Session
|
||||
import org.apache.sshd.common.session.SessionListener
|
||||
import org.apache.sshd.common.session.SessionListener.Event
|
||||
import org.apache.sshd.common.util.net.SshdSocketAddress
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.charset.StandardCharsets
|
||||
import javax.swing.JComponent
|
||||
|
||||
|
||||
class SSHTerminalTab(host: Host) : PtyHostTerminalTab(host) {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
|
||||
}
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
private var sshClient: SshClient? = null
|
||||
private var sshSession: ClientSession? = null
|
||||
private var sshChannelShell: ChannelShell? = null
|
||||
|
||||
init {
|
||||
terminalPanel.dropFiles = false
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return terminalPanel
|
||||
}
|
||||
|
||||
|
||||
override fun canReconnect(): Boolean {
|
||||
return !mutex.isLocked
|
||||
}
|
||||
|
||||
|
||||
override suspend fun openPtyConnector(): PtyConnector {
|
||||
if (mutex.tryLock()) {
|
||||
try {
|
||||
return doOpenPtyConnector()
|
||||
} finally {
|
||||
mutex.unlock()
|
||||
}
|
||||
}
|
||||
throw IllegalStateException("Opening PtyConnector")
|
||||
}
|
||||
|
||||
|
||||
private suspend fun doOpenPtyConnector(): PtyConnector {
|
||||
|
||||
// 连接提示
|
||||
withContext(Dispatchers.Swing) {
|
||||
// clear screen
|
||||
terminal.clearScreen()
|
||||
// hide cursor
|
||||
terminalModel.setData(DataKey.ShowCursor, false)
|
||||
// print
|
||||
terminal.write("SSH client is opening...\r\n")
|
||||
}
|
||||
|
||||
val client = SshClients.openClient(host).also { sshClient = it }
|
||||
val sessionListener = MySessionListener()
|
||||
val channelListener = MyChannelListener()
|
||||
|
||||
withContext(Dispatchers.Swing) { terminal.write("SSH client opened successfully.\r\n\r\n") }
|
||||
|
||||
client.addSessionListener(sessionListener)
|
||||
client.addChannelListener(channelListener)
|
||||
|
||||
val (session, channel) = try {
|
||||
val session = SshClients.openSession(host, client).also { sshSession = it }
|
||||
val channel = SshClients.openShell(
|
||||
host,
|
||||
terminalPanel.winSize(),
|
||||
session
|
||||
).also { sshChannelShell = it }
|
||||
Pair(session, channel)
|
||||
} finally {
|
||||
client.removeSessionListener(sessionListener)
|
||||
client.removeChannelListener(channelListener)
|
||||
}
|
||||
|
||||
// newline
|
||||
withContext(Dispatchers.Swing) {
|
||||
terminal.write("\r\n")
|
||||
}
|
||||
|
||||
|
||||
channel.addChannelListener(object : ChannelListener {
|
||||
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("${ControlCharacters.ESC}[0m")
|
||||
terminalModel.setData(DataKey.ShowCursor, false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 打开隧道
|
||||
openTunnelings(session, host)
|
||||
|
||||
// 隐藏提示
|
||||
withContext(Dispatchers.Swing) {
|
||||
// clear screen
|
||||
terminal.clearScreen()
|
||||
// show cursor
|
||||
terminalModel.setData(DataKey.ShowCursor, true)
|
||||
}
|
||||
|
||||
return ptyConnectorFactory.decorate(
|
||||
ZModemPtyConnectorAdaptor(
|
||||
terminal,
|
||||
terminalPanel,
|
||||
ChannelShellPtyConnector(
|
||||
channel,
|
||||
charset = Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun openTunnelings(session: ClientSession, host: Host) {
|
||||
if (host.tunnelings.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
for (tunneling in host.tunnelings) {
|
||||
if (tunneling.type == TunnelingType.Local) {
|
||||
session.startLocalPortForwarding(
|
||||
SshdSocketAddress(tunneling.sourceHost, tunneling.sourcePort),
|
||||
SshdSocketAddress(tunneling.destinationHost, tunneling.destinationPort)
|
||||
)
|
||||
} else if (tunneling.type == TunnelingType.Remote) {
|
||||
session.startRemotePortForwarding(
|
||||
SshdSocketAddress(tunneling.sourceHost, tunneling.sourcePort),
|
||||
SshdSocketAddress(tunneling.destinationHost, tunneling.destinationPort),
|
||||
)
|
||||
} else if (tunneling.type == TunnelingType.Dynamic) {
|
||||
session.startDynamicPortForwarding(
|
||||
SshdSocketAddress(
|
||||
tunneling.sourceHost,
|
||||
tunneling.sourcePort
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("SSH [{}] started {} port forwarding.", host.name, tunneling.name)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
terminal.write("Start [${tunneling.name}] port forwarding successfully.\r\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun stop() {
|
||||
if (mutex.tryLock()) {
|
||||
try {
|
||||
super.stop()
|
||||
|
||||
sshChannelShell?.close(true)
|
||||
sshSession?.disableSessionHeartbeat()
|
||||
sshSession?.disconnect(SshConstants.SSH2_DISCONNECT_BY_APPLICATION, StringUtils.EMPTY)
|
||||
sshSession?.close(true)
|
||||
sshClient?.close(true)
|
||||
|
||||
sshChannelShell = null
|
||||
sshSession = null
|
||||
sshClient = null
|
||||
} finally {
|
||||
mutex.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private inner class MySessionListener : SessionListener, Disposable {
|
||||
override fun sessionEvent(session: Session, event: Event) {
|
||||
coroutineScope.launch {
|
||||
when (event) {
|
||||
Event.KeyEstablished -> terminal.write("Session Key exchange successful.\r\n")
|
||||
Event.Authenticated -> terminal.write("Session authentication successful.\r\n\r\n")
|
||||
Event.KexCompleted -> terminal.write("Session KEX negotiation successful.\r\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun sessionEstablished(session: Session) {
|
||||
coroutineScope.launch { terminal.write("Session established.\r\n") }
|
||||
}
|
||||
|
||||
override fun sessionCreated(session: Session?) {
|
||||
coroutineScope.launch { terminal.write("Session created.\r\n") }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private inner class MyChannelListener : ChannelListener, Disposable {
|
||||
override fun channelOpenSuccess(channel: Channel) {
|
||||
coroutineScope.launch { terminal.write("Channel shell opened successfully.\r\n") }
|
||||
}
|
||||
|
||||
override fun channelInitialized(channel: Channel) {
|
||||
coroutineScope.launch { terminal.write("Channel shell initialization successful.\r\n") }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
66
src/main/kotlin/app/termora/SearchableHostTreeModel.kt
Normal file
66
src/main/kotlin/app/termora/SearchableHostTreeModel.kt
Normal file
@@ -0,0 +1,66 @@
|
||||
package app.termora
|
||||
|
||||
import javax.swing.event.TreeModelEvent
|
||||
import javax.swing.event.TreeModelListener
|
||||
import javax.swing.tree.TreeModel
|
||||
import javax.swing.tree.TreePath
|
||||
|
||||
class SearchableHostTreeModel(private val model: HostTreeModel) : TreeModel {
|
||||
private var text = String()
|
||||
|
||||
override fun getRoot(): Any {
|
||||
return model.root
|
||||
}
|
||||
|
||||
override fun getChild(parent: Any?, index: Int): Any {
|
||||
return getChildren(parent)[index]
|
||||
}
|
||||
|
||||
override fun getChildCount(parent: Any?): Int {
|
||||
return getChildren(parent).size
|
||||
}
|
||||
|
||||
override fun isLeaf(node: Any?): Boolean {
|
||||
return model.isLeaf(node)
|
||||
}
|
||||
|
||||
override fun valueForPathChanged(path: TreePath?, newValue: Any?) {
|
||||
return model.valueForPathChanged(path, newValue)
|
||||
}
|
||||
|
||||
override fun getIndexOfChild(parent: Any?, child: Any?): Int {
|
||||
return getChildren(parent).indexOf(child)
|
||||
}
|
||||
|
||||
override fun addTreeModelListener(l: TreeModelListener) {
|
||||
model.addTreeModelListener(l)
|
||||
}
|
||||
|
||||
override fun removeTreeModelListener(l: TreeModelListener) {
|
||||
model.removeTreeModelListener(l)
|
||||
}
|
||||
|
||||
|
||||
private fun getChildren(parent: Any?): List<Host> {
|
||||
val children = model.getChildren(parent)
|
||||
if (children.isEmpty()) return emptyList()
|
||||
return children.filter { e ->
|
||||
e.name.contains(text, true) || TreeUtils.children(model, e, true).filterIsInstance<Host>().any {
|
||||
it.name.contains(text, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun search(text: String) {
|
||||
this.text = text
|
||||
model.listeners.forEach {
|
||||
it.treeStructureChanged(
|
||||
TreeModelEvent(
|
||||
this, TreePath(root),
|
||||
null, null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
61
src/main/kotlin/app/termora/SettingsDialog.kt
Normal file
61
src/main/kotlin/app/termora/SettingsDialog.kt
Normal file
@@ -0,0 +1,61 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.db.Database
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.UIManager
|
||||
|
||||
class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
||||
private val optionsPane = SettingsOptionsPane()
|
||||
private val properties get() = Database.instance.properties
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||
isModal = true
|
||||
title = I18n.getString("termora.setting")
|
||||
setLocationRelativeTo(null)
|
||||
|
||||
init()
|
||||
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
Disposer.register(disposable, object : Disposable {
|
||||
override fun dispose() {
|
||||
properties.putString("Settings-SelectedOption", optionsPane.getSelectedIndex().toString())
|
||||
}
|
||||
})
|
||||
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowActivated(e: WindowEvent) {
|
||||
removeWindowListener(this)
|
||||
val index = properties.getString("Settings-SelectedOption")?.toIntOrNull() ?: return
|
||||
optionsPane.setSelectedIndex(index)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
optionsPane.background = UIManager.getColor("window")
|
||||
|
||||
val panel = JPanel(BorderLayout())
|
||||
panel.add(optionsPane, BorderLayout.CENTER)
|
||||
panel.background = UIManager.getColor("window")
|
||||
panel.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
override fun createSouthPanel(): JComponent? {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
1154
src/main/kotlin/app/termora/SettingsOptionsPane.kt
Normal file
1154
src/main/kotlin/app/termora/SettingsOptionsPane.kt
Normal file
File diff suppressed because it is too large
Load Diff
112
src/main/kotlin/app/termora/SshClients.kt
Normal file
112
src/main/kotlin/app/termora/SshClients.kt
Normal file
@@ -0,0 +1,112 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||
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.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.core.CoreModuleProperties
|
||||
import org.apache.sshd.server.forward.AcceptAllForwardingFilter
|
||||
import org.apache.sshd.server.forward.RejectAllForwardingFilter
|
||||
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 java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.time.Duration
|
||||
|
||||
object SshClients {
|
||||
private val timeout = Duration.ofSeconds(30)
|
||||
|
||||
/**
|
||||
* 打开一个 Shell
|
||||
*/
|
||||
fun openShell(
|
||||
host: Host,
|
||||
size: TerminalSize,
|
||||
session: ClientSession,
|
||||
): ChannelShell {
|
||||
|
||||
|
||||
val configuration = PtyChannelConfiguration()
|
||||
configuration.ptyColumns = size.cols
|
||||
configuration.ptyLines = size.rows
|
||||
configuration.ptyType = "xterm-256color"
|
||||
|
||||
val env = mutableMapOf<String, String>()
|
||||
env["TERM"] = configuration.ptyType
|
||||
env.putAll(host.options.envs())
|
||||
|
||||
val channel = session.createShellChannel(configuration, env)
|
||||
if (!channel.open().verify(timeout).await()) {
|
||||
throw SshException("Failed to open Shell")
|
||||
}
|
||||
|
||||
return channel
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开一个会话
|
||||
*/
|
||||
fun openSession(host: Host, client: SshClient): ClientSession {
|
||||
val session = client.connect(host.username, host.host, host.port)
|
||||
.verify(timeout).session
|
||||
if (host.authentication.type == AuthenticationType.Password) {
|
||||
session.addPasswordIdentity(host.authentication.password)
|
||||
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||
session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password)
|
||||
}
|
||||
if (!session.auth().verify(timeout).await(timeout)) {
|
||||
throw SshException("Authentication failed")
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开一个客户端
|
||||
*/
|
||||
fun openClient(host: Host): SshClient {
|
||||
val builder = ClientBuilder.builder()
|
||||
builder.globalRequestHandlers(listOf(KeepAliveHandler.INSTANCE))
|
||||
.factory { JGitSshClient() }
|
||||
|
||||
if (host.tunnelings.isEmpty()) {
|
||||
builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE)
|
||||
} else {
|
||||
builder.forwardingFilter(AcceptAllForwardingFilter.INSTANCE)
|
||||
}
|
||||
|
||||
val sshClient = builder.build() as JGitSshClient
|
||||
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, timeout)
|
||||
sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) }
|
||||
|
||||
if (host.proxy.type != ProxyType.No) {
|
||||
sshClient.setProxyDatabase {
|
||||
if (host.proxy.authenticationType == AuthenticationType.No) ProxyData(
|
||||
Proxy(
|
||||
Proxy.Type.SOCKS,
|
||||
InetSocketAddress(host.proxy.host, host.proxy.port)
|
||||
)
|
||||
)
|
||||
else
|
||||
ProxyData(
|
||||
Proxy(
|
||||
Proxy.Type.SOCKS,
|
||||
InetSocketAddress(host.proxy.host, host.proxy.port)
|
||||
),
|
||||
host.proxy.username,
|
||||
host.proxy.password.toCharArray(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sshClient.start()
|
||||
return sshClient
|
||||
}
|
||||
}
|
||||
106
src/main/kotlin/app/termora/TerminalFactory.kt
Normal file
106
src/main/kotlin/app/termora/TerminalFactory.kt
Normal file
@@ -0,0 +1,106 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.db.Database
|
||||
import app.termora.terminal.*
|
||||
import app.termora.terminal.panel.TerminalPanel
|
||||
import java.awt.Color
|
||||
import javax.swing.UIManager
|
||||
|
||||
class TerminalFactory {
|
||||
private val terminals = mutableListOf<Terminal>()
|
||||
|
||||
companion object {
|
||||
val instance by lazy { TerminalFactory() }
|
||||
}
|
||||
|
||||
fun createTerminal(): Terminal {
|
||||
val terminal = MyVisualTerminal()
|
||||
terminals.add(terminal)
|
||||
return terminal
|
||||
}
|
||||
|
||||
fun getTerminals(): List<Terminal> {
|
||||
return terminals
|
||||
}
|
||||
|
||||
private inner class MyVisualTerminal : VisualTerminal() {
|
||||
private val terminalModel by lazy { MyTerminalModel(this) }
|
||||
|
||||
override fun getTerminalModel(): TerminalModel {
|
||||
return terminalModel
|
||||
}
|
||||
}
|
||||
|
||||
private inner class MyTerminalModel(terminal: Terminal) : TerminalModelImpl(terminal) {
|
||||
private val colorPalette by lazy { MyColorPalette(terminal) }
|
||||
private val config get() = Database.instance.terminal
|
||||
|
||||
init {
|
||||
setData(DataKey.CursorStyle, config.cursor)
|
||||
setData(TerminalPanel.Debug, config.debug)
|
||||
}
|
||||
|
||||
override fun getColorPalette(): ColorPalette {
|
||||
return colorPalette
|
||||
}
|
||||
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : Any> getData(key: DataKey<T>): T {
|
||||
if (key == TerminalPanel.SelectCopy) {
|
||||
return config.selectCopy as T
|
||||
}
|
||||
return super.getData(key)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : Any> getData(key: DataKey<T>, defaultValue: T): T {
|
||||
if (key == TerminalPanel.SelectCopy) {
|
||||
return config.selectCopy as T
|
||||
}
|
||||
return super.getData(key, defaultValue)
|
||||
}
|
||||
|
||||
override fun getMaxRows(): Int {
|
||||
return config.maxRows
|
||||
}
|
||||
}
|
||||
|
||||
class FlatLafColorTheme : ColorTheme {
|
||||
private fun Color.toRGB(): Int {
|
||||
return 65536 * red + 256 * green + blue
|
||||
}
|
||||
|
||||
override fun getColor(color: TerminalColor): Int {
|
||||
val laf = UIManager.getLookAndFeel()
|
||||
if (laf is ColorTheme) {
|
||||
val c = laf.getColor(color)
|
||||
if (c != Int.MAX_VALUE) return c
|
||||
}
|
||||
|
||||
return when (color) {
|
||||
TerminalColor.Basic.FOREGROUND -> UIManager.getColor("windowText").toRGB()
|
||||
TerminalColor.Basic.BACKGROUND -> UIManager.getColor("window").toRGB()
|
||||
TerminalColor.Basic.SELECTION_FOREGROUND -> UIManager.getColor("textHighlightText").toRGB()
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND -> UIManager.getColor("textHighlight").toRGB()
|
||||
TerminalColor.Cursor.BACKGROUND -> getColor(TerminalColor.Basic.FOREGROUND)
|
||||
TerminalColor.Find.BACKGROUND -> UIManager.getColor("Component.warning.focusedBorderColor").toRGB()
|
||||
TerminalColor.Find.FOREGROUND -> UIManager.getColor("windowText").toRGB()
|
||||
TerminalColor.Basic.HYPERLINK -> UIManager.getColor("Hyperlink.linkColor")?.toRGB() ?: getColor(
|
||||
TerminalColor.Basic.SELECTION_FOREGROUND
|
||||
)
|
||||
|
||||
else -> DefaultColorTheme.instance.getColor(color)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private inner class MyColorPalette(terminal: Terminal) : ColorPaletteImpl(terminal) {
|
||||
private val colorTheme by lazy { FlatLafColorTheme() }
|
||||
override fun getTheme(): ColorTheme {
|
||||
return colorTheme
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/main/kotlin/app/termora/TerminalPanelFactory.kt
Normal file
48
src/main/kotlin/app/termora/TerminalPanelFactory.kt
Normal file
@@ -0,0 +1,48 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.highlight.KeywordHighlightPaintListener
|
||||
import app.termora.terminal.PtyConnector
|
||||
import app.termora.terminal.Terminal
|
||||
import app.termora.terminal.panel.TerminalHyperlinkPaintListener
|
||||
import app.termora.terminal.panel.TerminalPanel
|
||||
import java.awt.event.ComponentEvent
|
||||
import java.awt.event.ComponentListener
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
class TerminalPanelFactory {
|
||||
private val terminalPanels = mutableListOf<TerminalPanel>()
|
||||
|
||||
companion object {
|
||||
val instance by lazy { TerminalPanelFactory() }
|
||||
}
|
||||
|
||||
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
||||
val terminalPanel = TerminalPanel(terminal, ptyConnector)
|
||||
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
||||
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.instance)
|
||||
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.instance)
|
||||
terminalPanels.add(terminalPanel)
|
||||
return terminalPanel
|
||||
}
|
||||
|
||||
fun getTerminalPanels(): List<TerminalPanel> {
|
||||
return terminalPanels
|
||||
}
|
||||
|
||||
fun repaintAll() {
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
terminalPanels.forEach { it.repaintImmediate() }
|
||||
} else {
|
||||
SwingUtilities.invokeLater { repaintAll() }
|
||||
}
|
||||
}
|
||||
|
||||
fun fireResize() {
|
||||
getTerminalPanels().forEach { c ->
|
||||
c.getListeners(ComponentListener::class.java).forEach {
|
||||
it.componentResized(ComponentEvent(c, ComponentEvent.COMPONENT_RESIZED))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
41
src/main/kotlin/app/termora/TerminalTab.kt
Normal file
41
src/main/kotlin/app/termora/TerminalTab.kt
Normal file
@@ -0,0 +1,41 @@
|
||||
package app.termora
|
||||
|
||||
import java.beans.PropertyChangeListener
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComponent
|
||||
|
||||
interface TerminalTab : Disposable {
|
||||
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
fun getTitle(): String
|
||||
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
fun getIcon(): Icon
|
||||
|
||||
fun addPropertyChangeListener(listener: PropertyChangeListener)
|
||||
fun removePropertyChangeListener(listener: PropertyChangeListener)
|
||||
|
||||
/**
|
||||
* 显示组件
|
||||
*/
|
||||
fun getJComponent(): JComponent
|
||||
|
||||
/**
|
||||
* 重连
|
||||
*/
|
||||
fun reconnect() {}
|
||||
|
||||
/**
|
||||
* 是否可以重连
|
||||
*/
|
||||
fun canReconnect(): Boolean = true
|
||||
|
||||
fun onLostFocus() {}
|
||||
fun onGrabFocus() {}
|
||||
|
||||
|
||||
}
|
||||
45
src/main/kotlin/app/termora/TerminalTabDialog.kt
Normal file
45
src/main/kotlin/app/termora/TerminalTabDialog.kt
Normal file
@@ -0,0 +1,45 @@
|
||||
package app.termora
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
|
||||
class TerminalTabDialog(
|
||||
owner: Window,
|
||||
size: Dimension,
|
||||
private val terminalTab: TerminalTab
|
||||
) : DialogWrapper(null), Disposable {
|
||||
|
||||
init {
|
||||
title = terminalTab.getTitle()
|
||||
isModal = false
|
||||
isAlwaysOnTop = false
|
||||
iconImages = owner.iconImages
|
||||
escapeDispose = false
|
||||
|
||||
super.setSize(size)
|
||||
|
||||
init()
|
||||
setLocationRelativeTo(null)
|
||||
}
|
||||
|
||||
override fun createSouthPanel(): JComponent? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val panel = JPanel(BorderLayout())
|
||||
panel.add(terminalTab.getJComponent(), BorderLayout.CENTER)
|
||||
panel.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||
return panel
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
Disposer.dispose(this)
|
||||
super<DialogWrapper>.dispose()
|
||||
}
|
||||
|
||||
}
|
||||
404
src/main/kotlin/app/termora/TerminalTabbed.kt
Normal file
404
src/main/kotlin/app/termora/TerminalTabbed.kt
Normal file
@@ -0,0 +1,404 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
|
||||
import app.termora.findeverywhere.FindEverywhere
|
||||
import app.termora.findeverywhere.FindEverywhereProvider
|
||||
import app.termora.findeverywhere.FindEverywhereResult
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.action.ActionContainerFactory
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.KeyEvent
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.beans.PropertyChangeEvent
|
||||
import java.beans.PropertyChangeListener
|
||||
import javax.swing.*
|
||||
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
||||
import kotlin.math.min
|
||||
|
||||
class TerminalTabbed(
|
||||
private val toolbar: JToolBar,
|
||||
private val tabbedPane: FlatTabbedPane,
|
||||
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager {
|
||||
private val tabs = mutableListOf<TerminalTab>()
|
||||
|
||||
private val iconListener = PropertyChangeListener { e ->
|
||||
val source = e.source
|
||||
if (e.propertyName == "icon" && source is TerminalTab) {
|
||||
val index = tabs.indexOf(source)
|
||||
if (index >= 0) {
|
||||
tabbedPane.setIconAt(index, source.getIcon())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
tabbedPane.tabLayoutPolicy = SCROLL_TAB_LAYOUT
|
||||
tabbedPane.isTabsClosable = true
|
||||
tabbedPane.tabType = FlatTabbedPane.TabType.card
|
||||
|
||||
tabbedPane.styleMap = mapOf(
|
||||
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground")
|
||||
)
|
||||
|
||||
val actionManager = ActionManager.getInstance()
|
||||
val actionContainerFactory = ActionContainerFactory(actionManager)
|
||||
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
|
||||
updateBtn.isVisible = updateBtn.isEnabled
|
||||
updateBtn.addChangeListener { updateBtn.isVisible = updateBtn.isEnabled }
|
||||
|
||||
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
|
||||
override fun actionPerformed(e: ActionEvent?) {
|
||||
actionManager.getAction(Actions.FIND_EVERYWHERE)?.actionPerformed(e)
|
||||
}
|
||||
|
||||
override fun isEnabled(): Boolean {
|
||||
return actionManager.getAction(Actions.FIND_EVERYWHERE)?.isEnabled ?: false
|
||||
}
|
||||
}))
|
||||
toolbar.add(Box.createHorizontalStrut(UIManager.getInt("TabbedPane.tabHeight")))
|
||||
toolbar.add(Box.createHorizontalGlue())
|
||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.MACRO)))
|
||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.KEYWORD_HIGHLIGHT_EVERYWHERE)))
|
||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.KEY_MANAGER)))
|
||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.MULTIPLE)))
|
||||
toolbar.add(updateBtn)
|
||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.FIND_EVERYWHERE)))
|
||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.SETTING)))
|
||||
|
||||
|
||||
tabbedPane.trailingComponent = toolbar
|
||||
|
||||
add(tabbedPane, BorderLayout.CENTER)
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun initEvents() {
|
||||
// 关闭 tab
|
||||
tabbedPane.setTabCloseCallback { _, i -> removeTabAt(i, true) }
|
||||
|
||||
// 选中变动
|
||||
tabbedPane.addPropertyChangeListener("selectedIndex", object : PropertyChangeListener {
|
||||
override fun propertyChange(evt: PropertyChangeEvent) {
|
||||
val oldIndex = evt.oldValue as Int
|
||||
val newIndex = evt.newValue as Int
|
||||
if (oldIndex >= 0 && tabs.size > newIndex) {
|
||||
tabs[oldIndex].onLostFocus()
|
||||
}
|
||||
if (newIndex >= 0 && tabs.size > newIndex) {
|
||||
tabs[newIndex].onGrabFocus()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 选择变动
|
||||
tabbedPane.addChangeListener {
|
||||
if (tabbedPane.selectedIndex >= 0) {
|
||||
val c = tabbedPane.getComponentAt(tabbedPane.selectedIndex)
|
||||
c.requestFocusInWindow()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 快捷键
|
||||
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) {
|
||||
if (!SwingUtilities.isRightMouseButton(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
val index = tabbedPane.indexAtLocation(e.x, e.y)
|
||||
if (index < 0) return
|
||||
|
||||
showContextMenu(index, e)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// 点击
|
||||
tabbedPane.addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (SwingUtilities.isLeftMouseButton(e)) {
|
||||
val index = tabbedPane.indexAtLocation(e.x, e.y)
|
||||
if (index >= 0) {
|
||||
tabbedPane.getComponentAt(index).requestFocusInWindow()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 注册全局搜索
|
||||
FindEverywhere.registerProvider(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider {
|
||||
override fun find(pattern: String): List<FindEverywhereResult> {
|
||||
val results = mutableListOf<FindEverywhereResult>()
|
||||
for (i in 0 until tabbedPane.tabCount) {
|
||||
if (tabbedPane.getComponentAt(i) is WelcomePanel) {
|
||||
continue
|
||||
}
|
||||
results.add(
|
||||
SwitchFindEverywhereResult(
|
||||
tabbedPane.getTitleAt(i),
|
||||
tabbedPane.getIconAt(i),
|
||||
tabbedPane.getComponentAt(i)
|
||||
)
|
||||
)
|
||||
}
|
||||
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
|
||||
}
|
||||
openHost(e.host)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private fun removeTabAt(index: Int, disposable: Boolean = true) {
|
||||
if (tabbedPane.isTabClosable(index)) {
|
||||
val tab = tabs[index]
|
||||
tab.onLostFocus()
|
||||
tab.removePropertyChangeListener(iconListener)
|
||||
|
||||
// remove tab
|
||||
tabbedPane.removeTabAt(index)
|
||||
|
||||
// remove ele
|
||||
tabs.removeAt(index)
|
||||
|
||||
// 新的获取到焦点
|
||||
tabs[tabbedPane.selectedIndex].onGrabFocus()
|
||||
|
||||
if (disposable) {
|
||||
Disposer.dispose(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 popupMenu = FlatPopupMenu()
|
||||
|
||||
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
|
||||
rename.addActionListener {
|
||||
val index = tabbedPane.selectedIndex
|
||||
if (index > 0) {
|
||||
val dialog = InputDialog(
|
||||
SwingUtilities.getWindowAncestor(this),
|
||||
title = rename.text,
|
||||
text = tabbedPane.getTitleAt(index),
|
||||
)
|
||||
val text = dialog.getText()
|
||||
if (!text.isNullOrBlank()) {
|
||||
tabbedPane.setTitleAt(index, text)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val clone = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.clone"))
|
||||
clone.addActionListener {
|
||||
val index = tabbedPane.selectedIndex
|
||||
if (index > 0) {
|
||||
val tab = tabs[index]
|
||||
if (tab is HostTerminalTab) {
|
||||
ActionManager.getInstance()
|
||||
.getAction(Actions.OPEN_HOST)
|
||||
.actionPerformed(OpenHostActionEvent(this, tab.host))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val openInNewWindow = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.open-in-new-window"))
|
||||
openInNewWindow.addActionListener {
|
||||
val index = tabbedPane.selectedIndex
|
||||
if (index > 0) {
|
||||
val tab = tabs[index]
|
||||
removeTabAt(index, false)
|
||||
val dialog = TerminalTabDialog(
|
||||
owner = SwingUtilities.getWindowAncestor(this),
|
||||
terminalTab = tab,
|
||||
size = Dimension(min(size.width, 1280), min(size.height, 800))
|
||||
)
|
||||
Disposer.register(dialog, tab)
|
||||
Disposer.register(this, dialog)
|
||||
dialog.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
popupMenu.addSeparator()
|
||||
|
||||
val close = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close"))
|
||||
close.addActionListener {
|
||||
tabbedPane.tabCloseCallback?.accept(tabbedPane, tabIndex)
|
||||
}
|
||||
|
||||
popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close-other-tabs")).addActionListener {
|
||||
for (i in tabbedPane.tabCount - 1 downTo tabIndex + 1) {
|
||||
tabbedPane.tabCloseCallback?.accept(tabbedPane, i)
|
||||
}
|
||||
for (i in 1 until tabIndex) {
|
||||
tabbedPane.tabCloseCallback?.accept(tabbedPane, tabIndex - i)
|
||||
}
|
||||
}
|
||||
|
||||
popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close-all-tabs")).addActionListener {
|
||||
for (i in 0 until tabbedPane.tabCount) {
|
||||
tabbedPane.tabCloseCallback?.accept(tabbedPane, tabbedPane.tabCount - 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
close.isEnabled = c !is WelcomePanel
|
||||
rename.isEnabled = close.isEnabled
|
||||
clone.isEnabled = close.isEnabled
|
||||
openInNewWindow.isEnabled = close.isEnabled
|
||||
|
||||
|
||||
if (close.isEnabled) {
|
||||
popupMenu.addSeparator()
|
||||
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
|
||||
reconnect.addActionListener {
|
||||
val index = tabbedPane.selectedIndex
|
||||
if (index > 0) {
|
||||
tabs[index].reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
reconnect.isEnabled = tabs[tabIndex].canReconnect()
|
||||
}
|
||||
|
||||
popupMenu.show(this, e.x, e.y)
|
||||
}
|
||||
|
||||
|
||||
fun addTab(tab: TerminalTab) {
|
||||
tabbedPane.addTab(
|
||||
tab.getTitle(),
|
||||
tab.getIcon(),
|
||||
tab.getJComponent()
|
||||
)
|
||||
|
||||
// 监听 icons 变化
|
||||
tab.addPropertyChangeListener(iconListener)
|
||||
|
||||
tabs.add(tab)
|
||||
tabbedPane.selectedIndex = tabbedPane.tabCount - 1
|
||||
Disposer.register(this, tab)
|
||||
}
|
||||
|
||||
private inner class SwitchFindEverywhereResult(
|
||||
private val title: String,
|
||||
private val icon: Icon?,
|
||||
private val c: Component
|
||||
) : FindEverywhereResult {
|
||||
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
tabbedPane.selectedComponent = c
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
if (isSelected) {
|
||||
if (!FlatLaf.isLafDark()) {
|
||||
if (icon is DynamicIcon) {
|
||||
return icon.dark
|
||||
}
|
||||
}
|
||||
}
|
||||
return icon ?: super.getIcon(isSelected)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun dispose() {
|
||||
}
|
||||
|
||||
override fun addTerminalTab(tab: TerminalTab) {
|
||||
addTab(tab)
|
||||
}
|
||||
|
||||
override fun getSelectedTerminalTab(): TerminalTab? {
|
||||
val index = tabbedPane.selectedIndex
|
||||
if (index == -1) {
|
||||
return null
|
||||
}
|
||||
|
||||
return tabs[index]
|
||||
}
|
||||
|
||||
override fun getTerminalTabs(): List<TerminalTab> {
|
||||
return tabs
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
7
src/main/kotlin/app/termora/TerminalTabbedManager.kt
Normal file
7
src/main/kotlin/app/termora/TerminalTabbedManager.kt
Normal file
@@ -0,0 +1,7 @@
|
||||
package app.termora
|
||||
|
||||
interface TerminalTabbedManager {
|
||||
fun addTerminalTab(tab: TerminalTab)
|
||||
fun getSelectedTerminalTab(): TerminalTab?
|
||||
fun getTerminalTabs(): List<TerminalTab>
|
||||
}
|
||||
455
src/main/kotlin/app/termora/TermoraFrame.kt
Normal file
455
src/main/kotlin/app/termora/TermoraFrame.kt
Normal file
@@ -0,0 +1,455 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.findeverywhere.FindEverywhere
|
||||
import app.termora.highlight.KeywordHighlightDialog
|
||||
import app.termora.keymgr.KeyManagerDialog
|
||||
import app.termora.macro.MacroAction
|
||||
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 javax.imageio.ImageIO
|
||||
import javax.swing.*
|
||||
import javax.swing.SwingUtilities.isEventDispatchThread
|
||||
import javax.swing.event.HyperlinkEvent
|
||||
import kotlin.concurrent.fixedRateTimer
|
||||
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() {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(TermoraFrame::class.java)
|
||||
}
|
||||
|
||||
private val toolbar = JToolBar()
|
||||
private val tabbedPane = MyTabbedPane()
|
||||
private lateinit var terminalTabbed: TerminalTabbed
|
||||
private val disposable = Disposer.newDisposable()
|
||||
private val isWindowDecorationsSupported by lazy { JBR.isWindowDecorationsSupported() }
|
||||
private val titleBar = LogicCustomTitleBar.createCustomTitleBar(this)
|
||||
private val updaterManager get() = UpdaterManager.instance
|
||||
|
||||
private val preferencesHandler = object : Runnable {
|
||||
override fun run() {
|
||||
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow ?: this@TermoraFrame
|
||||
if (owner != this@TermoraFrame) {
|
||||
return
|
||||
}
|
||||
|
||||
val that = this
|
||||
FlatDesktop.setPreferencesHandler {}
|
||||
val dialog = SettingsDialog(owner)
|
||||
dialog.addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
FlatDesktop.setPreferencesHandler(that)
|
||||
}
|
||||
})
|
||||
dialog.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
initActions()
|
||||
initView()
|
||||
initEvents()
|
||||
initDesktopHandler()
|
||||
scheduleUpdate()
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
// 监听窗口大小变动,然后修改边距避开控制按钮
|
||||
addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentResized(e: ComponentEvent) {
|
||||
if (SystemInfo.isMacOS) {
|
||||
val left = titleBar.leftInset.toInt()
|
||||
if (tabbedPane.tabAreaInsets.left != left) {
|
||||
tabbedPane.tabAreaInsets = Insets(0, left, 0, 0)
|
||||
}
|
||||
} else if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
|
||||
val right = titleBar.rightInset.toInt()
|
||||
|
||||
for (i in 0 until toolbar.componentCount) {
|
||||
val c = toolbar.getComponent(i)
|
||||
if (c.name == "spacing") {
|
||||
if (c.width == right) {
|
||||
return
|
||||
}
|
||||
toolbar.remove(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (right > 0) {
|
||||
val spacing = Box.createHorizontalStrut(right)
|
||||
spacing.name = "spacing"
|
||||
toolbar.add(spacing)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
forceHitTest()
|
||||
|
||||
// macos 需要判断是否全部删除
|
||||
// 当 Tab 为 0 的时候,需要加一个边距,避开控制栏
|
||||
if (SystemInfo.isMacOS && isWindowDecorationsSupported) {
|
||||
tabbedPane.addChangeListener {
|
||||
tabbedPane.leadingComponent = if (tabbedPane.tabCount == 0) {
|
||||
Box.createHorizontalStrut(titleBar.leftInset.toInt())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// 监听主题变化 需要动态修改控制栏颜色
|
||||
if (SystemInfo.isWindows && isWindowDecorationsSupported) {
|
||||
ThemeManager.instance.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_EVERYWHERE, object : AnAction(
|
||||
I18n.getString("termora.highlight"),
|
||||
Icons.edit
|
||||
) {
|
||||
override fun actionPerformed(evt: ActionEvent) {
|
||||
KeywordHighlightDialog(this@TermoraFrame).isVisible = true
|
||||
}
|
||||
})
|
||||
|
||||
// app update
|
||||
ActionManager.getInstance().addAction(Actions.APP_UPDATE, object :
|
||||
AnAction(
|
||||
StringUtils.EMPTY,
|
||||
Icons.ideUpdate
|
||||
) {
|
||||
init {
|
||||
isEnabled = false
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: ActionEvent) {
|
||||
showUpdateDialog()
|
||||
}
|
||||
})
|
||||
|
||||
// macro
|
||||
ActionManager.getInstance().addAction(Actions.MACRO, MacroAction())
|
||||
|
||||
// FIND_EVERYWHERE
|
||||
ActionManager.getInstance().addAction(Actions.FIND_EVERYWHERE, object : AnAction(
|
||||
I18n.getString("termora.find-everywhere"),
|
||||
Icons.find
|
||||
) {
|
||||
override fun actionPerformed(evt: ActionEvent) {
|
||||
if (this.isEnabled) {
|
||||
val focusWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
|
||||
val frame = this@TermoraFrame
|
||||
if (focusWindow == frame) {
|
||||
FindEverywhere(frame).isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Key manager
|
||||
ActionManager.getInstance().addAction(Actions.KEY_MANAGER, object : AnAction(
|
||||
I18n.getString("termora.keymgr.title"),
|
||||
Icons.greyKey
|
||||
) {
|
||||
override fun actionPerformed(evt: ActionEvent) {
|
||||
if (this.isEnabled) {
|
||||
KeyManagerDialog(this@TermoraFrame).isVisible = true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
if (isWindowDecorationsSupported) {
|
||||
titleBar.height = UIManager.getInt("TabbedPane.tabHeight").toFloat()
|
||||
titleBar.putProperty("controls.dark", FlatLaf.isLafDark())
|
||||
JBR.getWindowDecorations().setCustomTitleBar(this, titleBar)
|
||||
}
|
||||
|
||||
if (SystemInfo.isLinux) {
|
||||
rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, UIManager.getInt("TabbedPane.tabHeight"))
|
||||
}
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
val sizes = listOf(16, 20, 24, 28, 32, 48, 64)
|
||||
val loader = TermoraFrame::class.java.classLoader
|
||||
val images = sizes.mapNotNull { e ->
|
||||
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
|
||||
}
|
||||
iconImages = images
|
||||
}
|
||||
|
||||
minimumSize = Dimension(640, 400)
|
||||
terminalTabbed = TerminalTabbed(toolbar, tabbedPane).apply {
|
||||
Application.registerService(TerminalTabbedManager::class, this)
|
||||
}
|
||||
terminalTabbed.addTab(WelcomePanel())
|
||||
|
||||
// macOS 要避开左边的控制栏
|
||||
if (SystemInfo.isMacOS) {
|
||||
val left = max(titleBar.leftInset.toInt(), 76)
|
||||
if (tabbedPane.tabCount == 0) {
|
||||
tabbedPane.leadingComponent = Box.createHorizontalStrut(left)
|
||||
} else {
|
||||
tabbedPane.tabAreaInsets = Insets(0, left, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
Disposer.register(disposable, terminalTabbed)
|
||||
add(terminalTabbed)
|
||||
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
private fun hit(e: MouseEvent) {
|
||||
if (e.source == tabbedPane) {
|
||||
val index = tabbedPane.indexAtLocation(e.x, e.y)
|
||||
if (index >= 0) {
|
||||
if (e.id == MouseEvent.MOUSE_CLICKED) {
|
||||
tabbedPane.getComponentAt(index)?.requestFocusInWindow()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
titleBar.forceHitTest(false)
|
||||
}
|
||||
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
hit(e)
|
||||
}
|
||||
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (e.source == toolbar) {
|
||||
if (!isWindowDecorationsSupported && SwingUtilities.isLeftMouseButton(e)) {
|
||||
if (JBR.isWindowMoveSupported()) {
|
||||
JBR.getWindowMove().startMovingTogetherWithMouse(this@TermoraFrame, e.button)
|
||||
}
|
||||
}
|
||||
}
|
||||
hit(e)
|
||||
}
|
||||
|
||||
override fun mouseReleased(e: MouseEvent) {
|
||||
hit(e)
|
||||
}
|
||||
|
||||
override fun mouseEntered(e: MouseEvent) {
|
||||
hit(e)
|
||||
}
|
||||
|
||||
override fun mouseDragged(e: MouseEvent) {
|
||||
|
||||
hit(e)
|
||||
}
|
||||
|
||||
override fun mouseMoved(e: MouseEvent) {
|
||||
hit(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
terminalTabbed.addMouseListener(mouseAdapter)
|
||||
terminalTabbed.addMouseMotionListener(mouseAdapter)
|
||||
|
||||
tabbedPane.addMouseListener(mouseAdapter)
|
||||
tabbedPane.addMouseMotionListener(mouseAdapter)
|
||||
|
||||
toolbar.addMouseListener(mouseAdapter)
|
||||
toolbar.addMouseMotionListener(mouseAdapter)
|
||||
}
|
||||
|
||||
private fun initDesktopHandler() {
|
||||
if (SystemInfo.isMacOS) {
|
||||
FlatDesktop.setPreferencesHandler {
|
||||
preferencesHandler.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
194
src/main/kotlin/app/termora/TextField.kt
Normal file
194
src/main/kotlin/app/termora/TextField.kt
Normal file
@@ -0,0 +1,194 @@
|
||||
package app.termora
|
||||
|
||||
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.text.ParseException
|
||||
import javax.swing.DefaultListCellRenderer
|
||||
import javax.swing.JComboBox
|
||||
import javax.swing.JList
|
||||
import javax.swing.SpinnerNumberModel
|
||||
import javax.swing.text.AttributeSet
|
||||
import javax.swing.text.DefaultFormatterFactory
|
||||
import javax.swing.text.PlainDocument
|
||||
|
||||
|
||||
open class OutlineTextField(var maxLength: Int = Int.MAX_VALUE) : FlatTextField() {
|
||||
init {
|
||||
this.addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
outline = null
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
document = object : PlainDocument() {
|
||||
override fun insertString(offs: Int, str: String?, a: AttributeSet?) {
|
||||
if (str != null && str.length + length <= maxLength) {
|
||||
super.insertString(offs, str, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OutlineTextArea : FlatTextArea() {
|
||||
init {
|
||||
border = FlatTextBorder()
|
||||
|
||||
addFocusListener(object : FocusAdapter() {
|
||||
override fun focusLost(e: FocusEvent) {
|
||||
border = FlatTextBorder()
|
||||
}
|
||||
|
||||
override fun focusGained(e: FocusEvent) {
|
||||
border = FlatTextBorder()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class FixedLengthTextArea(var maxLength: Int = Int.MAX_VALUE) : FlatTextArea() {
|
||||
init {
|
||||
document = object : PlainDocument() {
|
||||
override fun insertString(offs: Int, str: String?, a: AttributeSet?) {
|
||||
if (str != null && str.length + length <= maxLength) {
|
||||
super.insertString(offs, str, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class OutlinePasswordField(
|
||||
var maxLength: Int = Int.MAX_VALUE,
|
||||
var allowSpace: Boolean = false
|
||||
) : FlatPasswordField() {
|
||||
init {
|
||||
addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
outline = null
|
||||
}
|
||||
})
|
||||
|
||||
document = object : PlainDocument() {
|
||||
override fun insertString(offs: Int, str: String?, a: AttributeSet?) {
|
||||
if (str != null && str.length + length <= maxLength) {
|
||||
val text = if (allowSpace) str else str.replace(StringUtils.SPACE, StringUtils.EMPTY)
|
||||
if (text.isNotEmpty()) {
|
||||
super.insertString(offs, text, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
styleMap = mapOf(
|
||||
"showRevealButton" to true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
open class OutlineFormattedTextField : FlatFormattedTextField() {
|
||||
init {
|
||||
this.addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
outline = null
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
open class EmailFormattedTextField(var maxLength: Int = Int.MAX_VALUE) : OutlineFormattedTextField() {
|
||||
init {
|
||||
formatterFactory = DefaultFormatterFactory(object : AbstractFormatter() {
|
||||
private val regex = Regex(
|
||||
("^(?=.{1,64}@)[A-Za-z0-9\\+_-]+(\\.[A-Za-z0-9\\+_-]+)*@"
|
||||
+ "[^-][A-Za-z0-9\\+-]+(\\.[A-Za-z0-9\\+-]+)*(\\.[A-Za-z]{2,})$")
|
||||
)
|
||||
|
||||
override fun stringToValue(text: String?): Any {
|
||||
if (text.isNullOrBlank()) return String()
|
||||
if (!regex.matches(text)) throw ParseException("Not an email", 0)
|
||||
return text
|
||||
}
|
||||
|
||||
override fun valueToString(value: Any?): String {
|
||||
return value?.toString() ?: String()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
document = object : PlainDocument() {
|
||||
override fun insertString(offs: Int, str: String?, a: AttributeSet?) {
|
||||
if (str != null && str.length + length <= maxLength) {
|
||||
super.insertString(offs, str, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
abstract class NumberSpinner(
|
||||
value: Int,
|
||||
minimum: Int,
|
||||
maximum: Int,
|
||||
) : FlatSpinner() {
|
||||
init {
|
||||
val snm = SpinnerNumberModel()
|
||||
snm.minimum = minimum
|
||||
snm.maximum = maximum
|
||||
snm.value = value
|
||||
model = snm
|
||||
}
|
||||
|
||||
override fun getModel(): SpinnerNumberModel {
|
||||
return super.getModel() as SpinnerNumberModel
|
||||
}
|
||||
}
|
||||
|
||||
class PortSpinner(value: Int = 22) : NumberSpinner(value, 0, UShort.MAX_VALUE.toInt()) {
|
||||
init {
|
||||
setEditor(NumberEditor(this, "#"))
|
||||
}
|
||||
}
|
||||
|
||||
class IntSpinner(value: Int, minimum: Int = Int.MIN_VALUE, maximum: Int = Int.MAX_VALUE) :
|
||||
NumberSpinner(value, minimum, maximum) {
|
||||
init {
|
||||
setEditor(NumberEditor(this, "#"))
|
||||
}
|
||||
}
|
||||
|
||||
class YesOrNoComboBox : JComboBox<Boolean>() {
|
||||
init {
|
||||
renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
if (value == true) I18n.getString("termora.yes") else I18n.getString("termora.no"),
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
addItem(true)
|
||||
addItem(false)
|
||||
}
|
||||
}
|
||||
139
src/main/kotlin/app/termora/ThemeManager.kt
Normal file
139
src/main/kotlin/app/termora/ThemeManager.kt
Normal file
@@ -0,0 +1,139 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.db.Database
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import com.formdev.flatlaf.extras.FlatAnimatedLafChange
|
||||
import com.jthemedetecor.OsThemeDetector
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
import java.util.function.Consumer
|
||||
import javax.swing.PopupFactory
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.UIManager
|
||||
import javax.swing.event.EventListenerList
|
||||
|
||||
interface ThemeChangeListener : EventListener {
|
||||
fun onChanged()
|
||||
}
|
||||
|
||||
class ThemeManager private constructor() {
|
||||
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(ThemeManager::class.java)
|
||||
val instance by lazy { ThemeManager() }
|
||||
}
|
||||
|
||||
val themes = mapOf(
|
||||
"Light" to LightLaf::class.java.name,
|
||||
"Dark" to DarkLaf::class.java.name,
|
||||
"iTerm2 Dark" to iTerm2DarkLaf::class.java.name,
|
||||
"Termius Dark" to TermiusDarkLaf::class.java.name,
|
||||
"Termius Light" to TermiusLightLaf::class.java.name,
|
||||
"Atom One Dark" to AtomOneDarkLaf::class.java.name,
|
||||
"Atom One Light" to AtomOneLightLaf::class.java.name,
|
||||
"Everforest Dark" to EverforestDarkLaf::class.java.name,
|
||||
"Everforest Light" to EverforestLightLaf::class.java.name,
|
||||
"Octocat Dark" to OctocatDarkLaf::class.java.name,
|
||||
"Octocat Light" to OctocatLightLaf::class.java.name,
|
||||
"Night Owl" to NightOwlLaf::class.java.name,
|
||||
"Light Owl" to LightOwlLaf::class.java.name,
|
||||
"Nord Dark" to NordDarkLaf::class.java.name,
|
||||
"Nord Light" to NordLightLaf::class.java.name,
|
||||
"GitHub Dark" to GitHubDarkLaf::class.java.name,
|
||||
"GitHub Light" to GitHubLightLaf::class.java.name,
|
||||
"Novel" to NovelLaf::class.java.name,
|
||||
"Aura" to AuraLaf::class.java.name,
|
||||
"Cobalt2" to Cobalt2Laf::class.java.name,
|
||||
"Ayu Dark" to AyuDarkLaf::class.java.name,
|
||||
"Ayu Light" to AyuLightLaf::class.java.name,
|
||||
"Homebrew" to HomebrewLaf::class.java.name,
|
||||
"Pro" to ProLaf::class.java.name,
|
||||
"Chalk" to ChalkLaf::class.java.name,
|
||||
)
|
||||
|
||||
private var listenerList = EventListenerList()
|
||||
|
||||
/**
|
||||
* 当前的主题
|
||||
*/
|
||||
val theme: String
|
||||
get() {
|
||||
val themeClass = UIManager.getLookAndFeel().javaClass.name
|
||||
for (e in themes.entries) {
|
||||
if (e.value == themeClass) {
|
||||
return e.key
|
||||
}
|
||||
}
|
||||
return themeClass
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
OsThemeDetector.getDetector().registerListener(object : Consumer<Boolean> {
|
||||
override fun accept(isDark: Boolean) {
|
||||
if (!Database.instance.appearance.followSystem) {
|
||||
return
|
||||
}
|
||||
|
||||
if (FlatLaf.isLafDark() && isDark) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isDark) {
|
||||
SwingUtilities.invokeLater { change("Dark") }
|
||||
} else {
|
||||
SwingUtilities.invokeLater { change("Light") }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun change(classname: String, immediate: Boolean = false) {
|
||||
val themeClassname = themes.getOrDefault(classname, classname)
|
||||
|
||||
if (UIManager.getLookAndFeel().javaClass.name == themeClassname) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (immediate) {
|
||||
immediateChange(themeClassname)
|
||||
} else {
|
||||
FlatAnimatedLafChange.showSnapshot()
|
||||
immediateChange(themeClassname)
|
||||
FlatLaf.updateUI()
|
||||
FlatAnimatedLafChange.hideSnapshotWithAnimation()
|
||||
}
|
||||
|
||||
listenerList.getListeners(ThemeChangeListener::class.java).forEach { it.onChanged() }
|
||||
}
|
||||
|
||||
private fun immediateChange(classname: String) {
|
||||
try {
|
||||
|
||||
val oldPopupFactory = PopupFactory.getSharedInstance()
|
||||
UIManager.setLookAndFeel(classname)
|
||||
PopupFactory.setSharedInstance(oldPopupFactory)
|
||||
|
||||
} catch (ex: Exception) {
|
||||
log.error(ex.message, ex)
|
||||
}
|
||||
}
|
||||
|
||||
fun addThemeChangeListener(listener: ThemeChangeListener) {
|
||||
listenerList.add(ThemeChangeListener::class.java, listener)
|
||||
}
|
||||
|
||||
fun removeThemeChangeListener(listener: ThemeChangeListener) {
|
||||
listenerList.remove(ThemeChangeListener::class.java, listener)
|
||||
}
|
||||
|
||||
}
|
||||
77
src/main/kotlin/app/termora/TreeUtils.kt
Normal file
77
src/main/kotlin/app/termora/TreeUtils.kt
Normal file
@@ -0,0 +1,77 @@
|
||||
package app.termora
|
||||
|
||||
import javax.swing.JTree
|
||||
import javax.swing.tree.TreeModel
|
||||
import javax.swing.tree.TreeNode
|
||||
|
||||
object TreeUtils {
|
||||
/**
|
||||
* 获取子节点
|
||||
*/
|
||||
fun children(
|
||||
model: TreeModel,
|
||||
parent: Any,
|
||||
including: Boolean = true
|
||||
): List<Any> {
|
||||
|
||||
val nodes = mutableListOf<Any>()
|
||||
val parents = mutableListOf(parent)
|
||||
|
||||
while (parents.isNotEmpty()) {
|
||||
val p = parents.removeFirst()
|
||||
for (i in 0 until model.getChildCount(p)) {
|
||||
val child = model.getChild(p, i) ?: continue
|
||||
nodes.add(child)
|
||||
if (including) {
|
||||
parents.add(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
fun parents(node: TreeNode): List<Any> {
|
||||
val parents = mutableListOf<Any>()
|
||||
var p = node.parent
|
||||
while (p != null) {
|
||||
parents.add(p)
|
||||
p = p.parent
|
||||
}
|
||||
return parents
|
||||
}
|
||||
|
||||
fun saveExpansionState(tree: JTree): String {
|
||||
val rows = mutableListOf<Int>()
|
||||
for (i in 0 until tree.rowCount) {
|
||||
if (tree.isExpanded(i)) {
|
||||
rows.add(i)
|
||||
}
|
||||
}
|
||||
return rows.joinToString(",")
|
||||
}
|
||||
|
||||
fun loadExpansionState(tree: JTree, state: String) {
|
||||
if (state.isBlank()) {
|
||||
return
|
||||
}
|
||||
|
||||
state.split(",")
|
||||
.mapNotNull { it.toIntOrNull() }
|
||||
.forEach {
|
||||
tree.expandRow(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun expandAll(tree: JTree) {
|
||||
var j = tree.rowCount
|
||||
var i = 0
|
||||
while (i < j) {
|
||||
tree.expandRow(i)
|
||||
i += 1
|
||||
j = tree.rowCount
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
149
src/main/kotlin/app/termora/UpdaterManager.kt
Normal file
149
src/main/kotlin/app/termora/UpdaterManager.kt
Normal file
@@ -0,0 +1,149 @@
|
||||
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
|
||||
import org.commonmark.node.BulletList
|
||||
import org.commonmark.node.Heading
|
||||
import org.commonmark.node.Paragraph
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.AttributeProvider
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
|
||||
class UpdaterManager private constructor() {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(UpdaterManager::class.java)
|
||||
val instance by lazy { UpdaterManager() }
|
||||
}
|
||||
|
||||
data class Asset(
|
||||
val name: String,
|
||||
val url: String,
|
||||
val downloadUrl: String,
|
||||
val size: Long
|
||||
)
|
||||
|
||||
data class LatestVersion(
|
||||
// tag name
|
||||
val version: String,
|
||||
val prerelease: Boolean,
|
||||
val draft: Boolean,
|
||||
val name: String,
|
||||
val createdDate: Date,
|
||||
val publishedDate: Date,
|
||||
val body: String,
|
||||
val htmlBody: String,
|
||||
val assets: List<Asset>
|
||||
) {
|
||||
companion object {
|
||||
val self = LatestVersion(
|
||||
version = Application.getVersion(),
|
||||
prerelease = false,
|
||||
draft = false,
|
||||
name = StringUtils.EMPTY,
|
||||
createdDate = Date(),
|
||||
publishedDate = Date(),
|
||||
body = StringUtils.EMPTY,
|
||||
htmlBody = StringUtils.EMPTY,
|
||||
assets = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
val isSelf get() = this == self
|
||||
}
|
||||
|
||||
private val properties get() = Database.instance.properties
|
||||
var lastVersion = LatestVersion.self
|
||||
|
||||
fun fetchLatestVersion(): LatestVersion {
|
||||
try {
|
||||
val request = Request.Builder().get()
|
||||
.url("https://api.github.com/repos/TermoraDev/termora/releases/latest")
|
||||
.build()
|
||||
val response = Application.httpClient.newCall(request).execute()
|
||||
if (!response.isSuccessful) {
|
||||
return LatestVersion.self
|
||||
}
|
||||
|
||||
val text = response.use { resp -> resp.body?.use { it.string() } }
|
||||
if (text.isNullOrBlank()) {
|
||||
return LatestVersion.self
|
||||
}
|
||||
|
||||
val json = ohMyJson.parseToJsonElement(text).jsonObject
|
||||
val version = json.getValue("tag_name").jsonPrimitive.content
|
||||
val prerelease = json.getValue("prerelease").jsonPrimitive.boolean
|
||||
val draft = json.getValue("draft").jsonPrimitive.boolean
|
||||
val name = json.getValue("name").jsonPrimitive.content
|
||||
val createdDate = Date.from(Instant.parse(json.getValue("created_at").jsonPrimitive.content))
|
||||
val publishedDate =
|
||||
Date.from(Instant.parse(json.getValue("published_at").jsonPrimitive.content))
|
||||
val body = json.getValue("body").jsonPrimitive.content
|
||||
val assets = json.getValue("assets").jsonArray.map { it.jsonObject }
|
||||
.map {
|
||||
Asset(
|
||||
name = it.getValue("name").jsonPrimitive.content,
|
||||
url = it.getValue("url").jsonPrimitive.content,
|
||||
downloadUrl = it.getValue("browser_download_url").jsonPrimitive.content,
|
||||
size = it.getValue("size").jsonPrimitive.long,
|
||||
)
|
||||
}
|
||||
|
||||
val parser = Parser.builder().build()
|
||||
val document = parser.parse("# ${name.trim()}\n${body.trim()}")
|
||||
val renderer = HtmlRenderer.builder()
|
||||
.attributeProviderFactory {
|
||||
AttributeProvider { node, _, attributes ->
|
||||
if (attributes != null) {
|
||||
if (node is Heading) {
|
||||
attributes["style"] = "margin: 5px 0;"
|
||||
} else if (node is BulletList) {
|
||||
attributes["style"] = "margin: 0 20px;"
|
||||
}else if(node is Paragraph){
|
||||
attributes["style"] = "margin: 0;"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
lastVersion = LatestVersion(
|
||||
version = version,
|
||||
prerelease = prerelease,
|
||||
draft = draft,
|
||||
name = name,
|
||||
body = body,
|
||||
htmlBody = renderer.render(document),
|
||||
createdDate = createdDate,
|
||||
publishedDate = publishedDate,
|
||||
assets = assets
|
||||
)
|
||||
|
||||
return lastVersion
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error("Failed to get latest version", e)
|
||||
}
|
||||
}
|
||||
return LatestVersion.self
|
||||
}
|
||||
|
||||
|
||||
fun isIgnored(version: String): Boolean {
|
||||
return properties.getString("ignored.version.$version", "false").toBoolean()
|
||||
}
|
||||
|
||||
fun ignore(version: String) {
|
||||
properties.putString("ignored.version.$version", "true")
|
||||
}
|
||||
|
||||
private fun doGetLatestVersion() {
|
||||
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user