first commit
This commit is contained in:
commit
fc9e7b7598
16
.github/copilot-instructions.md
vendored
Normal file
16
.github/copilot-instructions.md
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Workspace Instructions
|
||||||
|
- Project: Java 11 SSH Terminal
|
||||||
|
- Language: Java 11
|
||||||
|
- UI Framework: Swing
|
||||||
|
- SSH Library: JSch (mwiede fork)
|
||||||
|
- Security: AES Encryption with Master Password
|
||||||
|
|
||||||
|
## Steps Completed
|
||||||
|
- [x] Initialize project structure and pom.xml
|
||||||
|
- [x] Implement Encryption Service
|
||||||
|
- [x] Implement Connection Model and Storage
|
||||||
|
- [x] Implement SSH Terminal UI component
|
||||||
|
- [x] Implement Main UI with SplitPane and Tabs
|
||||||
|
- [x] Add Connection Management (Add/Remove)
|
||||||
|
- [x] Compile and Test
|
||||||
|
- [x] README and Documentation
|
||||||
1
.master_key
Normal file
1
.master_key
Normal file
@ -0,0 +1 @@
|
|||||||
|
ByBYvcAXjSA51UXD76DrxA==
|
||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"java.configuration.updateBuildConfiguration": "automatic"
|
||||||
|
}
|
||||||
26
README.md
Normal file
26
README.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Java SSH Terminal
|
||||||
|
|
||||||
|
A basic SSH terminal application built with Java 11 and Swing.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Connection management in the left side panel.
|
||||||
|
- Multiple SSH terminals in a tabbed view.
|
||||||
|
- Master password protection for stored credentials (AES encrypted).
|
||||||
|
- Toolbar for quick actions (Add connection, Disconnect).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
- Java 11
|
||||||
|
- Maven
|
||||||
|
|
||||||
|
## How to Run
|
||||||
|
1. Build the project:
|
||||||
|
```bash
|
||||||
|
mvn clean package
|
||||||
|
```
|
||||||
|
2. Run the application:
|
||||||
|
```bash
|
||||||
|
java -jar target/java-ssh-terminal-1.0-SNAPSHOT.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
## Note on Terminal Emulation
|
||||||
|
This is a base application. The terminal component uses a basic `JTextArea` which does not support full ANSI escape sequences (colors, cursor positioning). For production use, consider integrating a library like `JediTerm`.
|
||||||
1
connections.json
Normal file
1
connections.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
[{"name":"server01","host":"server01","port":64022,"username":"kamma","encryptedPassword":"cT7BoqH9BONVY740RjdkFQ\u003d\u003d"},{"name":"kamma.cz","host":"kamma.cz","port":22,"username":"root","encryptedPassword":"DmErsKpA1Jf4az0cc2lrgA\u003d\u003d"}]
|
||||||
78
pom.xml
Normal file
78
pom.xml
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>cz.kamma.term</groupId>
|
||||||
|
<artifactId>java-ssh-terminal</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>11</maven.compiler.source>
|
||||||
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- SSH Library -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.mwiede</groupId>
|
||||||
|
<artifactId>jsch</artifactId>
|
||||||
|
<version>0.2.17</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JSON for storing connections -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.code.gson</groupId>
|
||||||
|
<artifactId>gson</artifactId>
|
||||||
|
<version>2.10.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Better Terminal UI (Optional but good) -->
|
||||||
|
<!-- We will use a standard JTextArea/JTextPane with ANSI support for simplicity or a lightweight lib -->
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.8.1</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>3.2.0</version>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<mainClass>cz.kamma.term.App</mainClass>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.2.4</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||||
|
<transformers>
|
||||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<mainClass>cz.kamma.term.App</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
1
settings.json
Normal file
1
settings.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"fontName":"Monospaced","fontSize":12,"backgroundColor":-16777216,"foregroundColor":-16711936}
|
||||||
18
src/main/java/cz/kamma/term/App.java
Normal file
18
src/main/java/cz/kamma/term/App.java
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package cz.kamma.term;
|
||||||
|
|
||||||
|
import cz.kamma.term.ui.MainFrame;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
|
||||||
|
public class App {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
try {
|
||||||
|
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
new MainFrame().setVisible(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main/java/cz/kamma/term/model/AppSettings.java
Normal file
20
src/main/java/cz/kamma/term/model/AppSettings.java
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package cz.kamma.term.model;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
|
||||||
|
public class AppSettings {
|
||||||
|
private String fontName = "Monospaced";
|
||||||
|
private int fontSize = 14;
|
||||||
|
private int backgroundColor = Color.BLACK.getRGB();
|
||||||
|
private int foregroundColor = Color.GREEN.getRGB();
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getFontName() { return fontName; }
|
||||||
|
public void setFontName(String fontName) { this.fontName = fontName; }
|
||||||
|
public int getFontSize() { return fontSize; }
|
||||||
|
public void setFontSize(int fontSize) { this.fontSize = fontSize; }
|
||||||
|
public int getBackgroundColor() { return backgroundColor; }
|
||||||
|
public void setBackgroundColor(int backgroundColor) { this.backgroundColor = backgroundColor; }
|
||||||
|
public int getForegroundColor() { return foregroundColor; }
|
||||||
|
public void setForegroundColor(int foregroundColor) { this.foregroundColor = foregroundColor; }
|
||||||
|
}
|
||||||
34
src/main/java/cz/kamma/term/model/ConnectionInfo.java
Normal file
34
src/main/java/cz/kamma/term/model/ConnectionInfo.java
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package cz.kamma.term.model;
|
||||||
|
|
||||||
|
public class ConnectionInfo {
|
||||||
|
private String name;
|
||||||
|
private String host;
|
||||||
|
private int port = 22;
|
||||||
|
private String username;
|
||||||
|
private String encryptedPassword;
|
||||||
|
|
||||||
|
public ConnectionInfo(String name, String host, int port, String username, String encryptedPassword) {
|
||||||
|
this.name = name;
|
||||||
|
this.host = host;
|
||||||
|
this.port = port;
|
||||||
|
this.username = username;
|
||||||
|
this.encryptedPassword = encryptedPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
public String getHost() { return host; }
|
||||||
|
public void setHost(String host) { this.host = host; }
|
||||||
|
public int getPort() { return port; }
|
||||||
|
public void setPort(int port) { this.port = port; }
|
||||||
|
public String getUsername() { return username; }
|
||||||
|
public void setUsername(String username) { this.username = username; }
|
||||||
|
public String getEncryptedPassword() { return encryptedPassword; }
|
||||||
|
public void setEncryptedPassword(String encryptedPassword) { this.encryptedPassword = encryptedPassword; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return name + " (" + host + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/main/java/cz/kamma/term/service/ConnectionManager.java
Normal file
55
src/main/java/cz/kamma/term/service/ConnectionManager.java
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package cz.kamma.term.service;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import cz.kamma.term.model.ConnectionInfo;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ConnectionManager {
|
||||||
|
private static final String FILE_NAME = "connections.json";
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
private List<ConnectionInfo> connections = new ArrayList<>();
|
||||||
|
|
||||||
|
public ConnectionManager() {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ConnectionInfo> getConnections() {
|
||||||
|
return connections;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addConnection(ConnectionInfo connection) {
|
||||||
|
connections.add(connection);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeConnection(ConnectionInfo connection) {
|
||||||
|
connections.remove(connection);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
try (Writer writer = new FileWriter(FILE_NAME)) {
|
||||||
|
gson.toJson(connections, writer);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void load() {
|
||||||
|
File file = new File(FILE_NAME);
|
||||||
|
if (file.exists()) {
|
||||||
|
try (Reader reader = new FileReader(file)) {
|
||||||
|
Type listType = new TypeToken<ArrayList<ConnectionInfo>>(){}.getType();
|
||||||
|
connections = gson.fromJson(reader, listType);
|
||||||
|
if (connections == null) connections = new ArrayList<>();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/main/java/cz/kamma/term/service/EncryptionService.java
Normal file
37
src/main/java/cz/kamma/term/service/EncryptionService.java
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package cz.kamma.term.service;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.SecretKeyFactory;
|
||||||
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.security.spec.KeySpec;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
public class EncryptionService {
|
||||||
|
private static final String ALGORITHM = "AES";
|
||||||
|
private static final String SALT = "some_random_salt_123"; // Should be stored properly, but for base app this is okay
|
||||||
|
|
||||||
|
public static String encrypt(String password, String masterPassword) throws Exception {
|
||||||
|
SecretKey key = deriveKey(masterPassword);
|
||||||
|
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||||
|
byte[] encryptedBytes = cipher.doFinal(password.getBytes());
|
||||||
|
return Base64.getEncoder().encodeToString(encryptedBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String decrypt(String encryptedPassword, String masterPassword) throws Exception {
|
||||||
|
SecretKey key = deriveKey(masterPassword);
|
||||||
|
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, key);
|
||||||
|
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedPassword));
|
||||||
|
return new String(decryptedBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SecretKey deriveKey(String masterPassword) throws Exception {
|
||||||
|
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||||
|
KeySpec spec = new PBEKeySpec(masterPassword.toCharArray(), SALT.getBytes(), 65536, 128);
|
||||||
|
SecretKey tmp = factory.generateSecret(spec);
|
||||||
|
return new SecretKeySpec(tmp.getEncoded(), ALGORITHM);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/main/java/cz/kamma/term/service/HistoryManager.java
Normal file
36
src/main/java/cz/kamma/term/service/HistoryManager.java
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package cz.kamma.term.service;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class HistoryManager {
|
||||||
|
private List<String> history = new ArrayList<>();
|
||||||
|
|
||||||
|
public HistoryManager() {
|
||||||
|
// Don't load local history - will be loaded from server
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addRemoteCommand(String command) {
|
||||||
|
if (command == null || command.trim().isEmpty()) return;
|
||||||
|
|
||||||
|
// Remove duplicate if it was the last command
|
||||||
|
if (!history.isEmpty() && history.get(history.size() - 1).equals(command)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
history.add(command);
|
||||||
|
// Don't save locally - server handles it via .history
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadRemoteHistory(List<String> remoteCommands) {
|
||||||
|
if (remoteCommands != null) {
|
||||||
|
history.clear();
|
||||||
|
history.addAll(remoteCommands);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getHistory() {
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/main/java/cz/kamma/term/service/SettingsManager.java
Normal file
42
src/main/java/cz/kamma/term/service/SettingsManager.java
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package cz.kamma.term.service;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import cz.kamma.term.model.AppSettings;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
|
||||||
|
public class SettingsManager {
|
||||||
|
private static final String FILE_NAME = "settings.json";
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
private AppSettings settings;
|
||||||
|
|
||||||
|
public SettingsManager() {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppSettings getSettings() {
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
try (Writer writer = new FileWriter(FILE_NAME)) {
|
||||||
|
gson.toJson(settings, writer);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void load() {
|
||||||
|
File file = new File(FILE_NAME);
|
||||||
|
if (file.exists()) {
|
||||||
|
try (Reader reader = new FileReader(file)) {
|
||||||
|
settings = gson.fromJson(reader, AppSettings.class);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (settings == null) {
|
||||||
|
settings = new AppSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
313
src/main/java/cz/kamma/term/ui/MainFrame.java
Normal file
313
src/main/java/cz/kamma/term/ui/MainFrame.java
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
package cz.kamma.term.ui;
|
||||||
|
|
||||||
|
import cz.kamma.term.model.ConnectionInfo;
|
||||||
|
import cz.kamma.term.model.AppSettings;
|
||||||
|
import cz.kamma.term.service.ConnectionManager;
|
||||||
|
import cz.kamma.term.service.EncryptionService;
|
||||||
|
import cz.kamma.term.service.SettingsManager;
|
||||||
|
import cz.kamma.term.util.SystemIdUtil;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
|
||||||
|
public class MainFrame extends JFrame {
|
||||||
|
private static final String MASTER_PASS_FILE = ".master_key";
|
||||||
|
private ConnectionManager connectionManager;
|
||||||
|
private SettingsManager settingsManager;
|
||||||
|
private JTabbedPane tabbedPane;
|
||||||
|
private DefaultListModel<ConnectionInfo> listModel;
|
||||||
|
private JList<ConnectionInfo> connectionList;
|
||||||
|
private String masterPassword;
|
||||||
|
|
||||||
|
public MainFrame() {
|
||||||
|
setTitle("Java SSH Terminal");
|
||||||
|
setSize(1000, 700);
|
||||||
|
setDefaultCloseOperation(EXIT_ON_CLOSE);
|
||||||
|
setLocationRelativeTo(null);
|
||||||
|
|
||||||
|
connectionManager = new ConnectionManager();
|
||||||
|
settingsManager = new SettingsManager();
|
||||||
|
|
||||||
|
if (!tryLoadMasterPassword()) {
|
||||||
|
showMasterPasswordDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
initUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean tryLoadMasterPassword() {
|
||||||
|
File file = new File(MASTER_PASS_FILE);
|
||||||
|
if (file.exists()) {
|
||||||
|
try {
|
||||||
|
String encryptedMaster = Files.readString(file.toPath()).trim();
|
||||||
|
masterPassword = EncryptionService.decrypt(encryptedMaster, SystemIdUtil.getMachineId());
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
file.delete(); // Corrupted or wrong machine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showMasterPasswordDialog() {
|
||||||
|
JPasswordField passF = new JPasswordField();
|
||||||
|
JCheckBox rememberCb = new JCheckBox("Remember on this computer");
|
||||||
|
Object[] message = {
|
||||||
|
"Enter Master Password:", passF,
|
||||||
|
rememberCb
|
||||||
|
};
|
||||||
|
|
||||||
|
int option = JOptionPane.showConfirmDialog(this, message, "Security", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
|
||||||
|
|
||||||
|
if (option == JOptionPane.OK_OPTION) {
|
||||||
|
masterPassword = new String(passF.getPassword());
|
||||||
|
if (masterPassword.isEmpty()) {
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
if (rememberCb.isSelected()) {
|
||||||
|
saveMasterPassword();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveMasterPassword() {
|
||||||
|
try {
|
||||||
|
String encryptedMaster = EncryptionService.encrypt(masterPassword, SystemIdUtil.getMachineId());
|
||||||
|
Files.writeString(new File(MASTER_PASS_FILE).toPath(), encryptedMaster);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initUI() {
|
||||||
|
// Toolbar
|
||||||
|
JToolBar toolBar = new JToolBar();
|
||||||
|
JButton addButton = new JButton("Add Connection");
|
||||||
|
JButton settingsButton = new JButton("Settings");
|
||||||
|
toolBar.add(addButton);
|
||||||
|
toolBar.add(settingsButton);
|
||||||
|
add(toolBar, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
// Left Panel (Connections)
|
||||||
|
listModel = new DefaultListModel<>();
|
||||||
|
connectionManager.getConnections().forEach(listModel::addElement);
|
||||||
|
connectionList = new JList<>(listModel);
|
||||||
|
connectionList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||||
|
|
||||||
|
JScrollPane leftScroll = new JScrollPane(connectionList);
|
||||||
|
leftScroll.setPreferredSize(new Dimension(200, 0));
|
||||||
|
|
||||||
|
// Main Panel (Tabs)
|
||||||
|
tabbedPane = new JTabbedPane();
|
||||||
|
|
||||||
|
JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftScroll, tabbedPane);
|
||||||
|
add(splitPane, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
// Listeners
|
||||||
|
addButton.addActionListener(e -> showAddConnectionDialog(null));
|
||||||
|
settingsButton.addActionListener(e -> showSettingsDialog());
|
||||||
|
|
||||||
|
connectionList.addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mousePressed(MouseEvent e) {
|
||||||
|
checkPopup(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent e) {
|
||||||
|
checkPopup(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkPopup(MouseEvent e) {
|
||||||
|
if (e.isPopupTrigger()) {
|
||||||
|
int index = connectionList.locationToIndex(e.getPoint());
|
||||||
|
if (index != -1) {
|
||||||
|
connectionList.setSelectedIndex(index);
|
||||||
|
showContextMenu(e.getComponent(), e.getX(), e.getY(), connectionList.getSelectedValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent e) {
|
||||||
|
if (e.getClickCount() == 2) {
|
||||||
|
ConnectionInfo selected = connectionList.getSelectedValue();
|
||||||
|
if (selected != null) {
|
||||||
|
openTerminal(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openTerminal(ConnectionInfo info) {
|
||||||
|
TerminalPanel terminal = new TerminalPanel(info, masterPassword, settingsManager.getSettings());
|
||||||
|
tabbedPane.addTab(info.getName(), terminal);
|
||||||
|
int index = tabbedPane.indexOfComponent(terminal);
|
||||||
|
|
||||||
|
// Custom Tab Component with (X) button
|
||||||
|
JPanel pnlTab = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0));
|
||||||
|
pnlTab.setOpaque(false);
|
||||||
|
JLabel lblTitle = new JLabel(info.getName());
|
||||||
|
JButton btnClose = new JButton("x");
|
||||||
|
|
||||||
|
// Style the close button
|
||||||
|
btnClose.setMargin(new Insets(0, 2, 0, 2));
|
||||||
|
btnClose.setBorderPainted(false);
|
||||||
|
btnClose.setContentAreaFilled(false);
|
||||||
|
btnClose.setFocusable(false);
|
||||||
|
btnClose.setFont(new Font("Monospaced", Font.BOLD, 12));
|
||||||
|
|
||||||
|
btnClose.addActionListener(e -> {
|
||||||
|
terminal.disconnect();
|
||||||
|
tabbedPane.remove(terminal);
|
||||||
|
});
|
||||||
|
|
||||||
|
pnlTab.add(lblTitle);
|
||||||
|
pnlTab.add(btnClose);
|
||||||
|
|
||||||
|
tabbedPane.setTabComponentAt(index, pnlTab);
|
||||||
|
tabbedPane.setSelectedIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showContextMenu(Component invoker, int x, int y, ConnectionInfo selected) {
|
||||||
|
JPopupMenu menu = new JPopupMenu();
|
||||||
|
|
||||||
|
JMenuItem connectItem = new JMenuItem("Connect in new tab");
|
||||||
|
JMenuItem editItem = new JMenuItem("Edit");
|
||||||
|
JMenuItem deleteItem = new JMenuItem("Delete");
|
||||||
|
|
||||||
|
connectItem.addActionListener(e -> openTerminal(selected));
|
||||||
|
editItem.addActionListener(e -> showAddConnectionDialog(selected));
|
||||||
|
deleteItem.addActionListener(e -> {
|
||||||
|
int confirm = JOptionPane.showConfirmDialog(this, "Delete " + selected.getName() + "?", "Confirm", JOptionPane.YES_NO_OPTION);
|
||||||
|
if (confirm == JOptionPane.YES_OPTION) {
|
||||||
|
connectionManager.removeConnection(selected);
|
||||||
|
listModel.removeElement(selected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
menu.add(connectItem);
|
||||||
|
menu.addSeparator();
|
||||||
|
menu.add(editItem);
|
||||||
|
menu.add(deleteItem);
|
||||||
|
menu.show(invoker, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showAddConnectionDialog(ConnectionInfo existing) {
|
||||||
|
JTextField nameF = new JTextField(existing != null ? existing.getName() : "");
|
||||||
|
JTextField hostF = new JTextField(existing != null ? existing.getHost() : "");
|
||||||
|
JTextField portF = new JTextField(existing != null ? String.valueOf(existing.getPort()) : "22");
|
||||||
|
JTextField userF = new JTextField(existing != null ? existing.getUsername() : "");
|
||||||
|
JPasswordField passF = new JPasswordField();
|
||||||
|
|
||||||
|
Object[] message = {
|
||||||
|
"Name:", nameF,
|
||||||
|
"Host:", hostF,
|
||||||
|
"Port:", portF,
|
||||||
|
"User:", userF,
|
||||||
|
"Pass (leave empty to keep existing):", passF
|
||||||
|
};
|
||||||
|
|
||||||
|
int option = JOptionPane.showConfirmDialog(null, message,
|
||||||
|
existing == null ? "Add New Connection" : "Edit Connection", JOptionPane.OK_CANCEL_OPTION);
|
||||||
|
|
||||||
|
if (option == JOptionPane.OK_OPTION) {
|
||||||
|
try {
|
||||||
|
String encryptedPass;
|
||||||
|
if (passF.getPassword().length > 0) {
|
||||||
|
encryptedPass = EncryptionService.encrypt(new String(passF.getPassword()), masterPassword);
|
||||||
|
} else if (existing != null) {
|
||||||
|
encryptedPass = existing.getEncryptedPassword();
|
||||||
|
} else {
|
||||||
|
throw new Exception("Password is required for new connection.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing != null) {
|
||||||
|
existing.setName(nameF.getText());
|
||||||
|
existing.setHost(hostF.getText());
|
||||||
|
existing.setPort(Integer.parseInt(portF.getText()));
|
||||||
|
existing.setUsername(userF.getText());
|
||||||
|
existing.setEncryptedPassword(encryptedPass);
|
||||||
|
connectionManager.save();
|
||||||
|
connectionList.repaint();
|
||||||
|
} else {
|
||||||
|
ConnectionInfo info = new ConnectionInfo(
|
||||||
|
nameF.getText(),
|
||||||
|
hostF.getText(),
|
||||||
|
Integer.parseInt(portF.getText()),
|
||||||
|
userF.getText(),
|
||||||
|
encryptedPass
|
||||||
|
);
|
||||||
|
connectionManager.addConnection(info);
|
||||||
|
listModel.addElement(info);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Error: " + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSettingsDialog() {
|
||||||
|
AppSettings settings = settingsManager.getSettings();
|
||||||
|
|
||||||
|
String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
|
||||||
|
JComboBox<String> fontCmbo = new JComboBox<>(fonts);
|
||||||
|
fontCmbo.setSelectedItem(settings.getFontName());
|
||||||
|
|
||||||
|
JSpinner sizeSpin = new JSpinner(new SpinnerNumberModel(settings.getFontSize(), 8, 72, 1));
|
||||||
|
|
||||||
|
JButton bgBtn = new JButton("Choose Background");
|
||||||
|
bgBtn.setBackground(new Color(settings.getBackgroundColor()));
|
||||||
|
final int[] bgArr = {settings.getBackgroundColor()};
|
||||||
|
bgBtn.addActionListener(e -> {
|
||||||
|
Color c = JColorChooser.showDialog(this, "Select Background", new Color(bgArr[0]));
|
||||||
|
if (c != null) {
|
||||||
|
bgArr[0] = c.getRGB();
|
||||||
|
bgBtn.setBackground(c);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
JButton fgBtn = new JButton("Choose Foreground");
|
||||||
|
fgBtn.setBackground(new Color(settings.getForegroundColor()));
|
||||||
|
final int[] fgArr = {settings.getForegroundColor()};
|
||||||
|
fgBtn.addActionListener(e -> {
|
||||||
|
Color c = JColorChooser.showDialog(this, "Select Foreground", new Color(fgArr[0]));
|
||||||
|
if (c != null) {
|
||||||
|
fgArr[0] = c.getRGB();
|
||||||
|
fgBtn.setBackground(c);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object[] message = {
|
||||||
|
"Font:", fontCmbo,
|
||||||
|
"Size:", sizeSpin,
|
||||||
|
"Background:", bgBtn,
|
||||||
|
"Foreground:", fgBtn
|
||||||
|
};
|
||||||
|
|
||||||
|
int option = JOptionPane.showConfirmDialog(this, message, "Settings", JOptionPane.OK_CANCEL_OPTION);
|
||||||
|
if (option == JOptionPane.OK_OPTION) {
|
||||||
|
settings.setFontName((String) fontCmbo.getSelectedItem());
|
||||||
|
settings.setFontSize((Integer) sizeSpin.getValue());
|
||||||
|
settings.setBackgroundColor(bgArr[0]);
|
||||||
|
settings.setForegroundColor(fgArr[0]);
|
||||||
|
|
||||||
|
settingsManager.save();
|
||||||
|
|
||||||
|
// Apply to all tabs
|
||||||
|
for (int i = 0; i < tabbedPane.getTabCount(); i++) {
|
||||||
|
Component comp = tabbedPane.getComponentAt(i);
|
||||||
|
if (comp instanceof TerminalPanel) {
|
||||||
|
((TerminalPanel) comp).updateSettings(settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
347
src/main/java/cz/kamma/term/ui/TerminalPanel.java
Normal file
347
src/main/java/cz/kamma/term/ui/TerminalPanel.java
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
package cz.kamma.term.ui;
|
||||||
|
|
||||||
|
import com.jcraft.jsch.*;
|
||||||
|
import cz.kamma.term.model.ConnectionInfo;
|
||||||
|
import cz.kamma.term.model.AppSettings;
|
||||||
|
import cz.kamma.term.service.EncryptionService;
|
||||||
|
import cz.kamma.term.service.HistoryManager;
|
||||||
|
import cz.kamma.term.util.AnsiHelper;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.text.DefaultCaret;
|
||||||
|
import javax.swing.text.NavigationFilter;
|
||||||
|
import javax.swing.text.Position;
|
||||||
|
import javax.swing.text.StyledDocument;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.KeyAdapter;
|
||||||
|
import java.awt.event.KeyEvent;
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TerminalPanel extends JPanel {
|
||||||
|
private JTextPane textPane;
|
||||||
|
private Session session;
|
||||||
|
private ChannelShell channel;
|
||||||
|
private PrintStream out;
|
||||||
|
private String masterPassword;
|
||||||
|
private ConnectionInfo connectionInfo;
|
||||||
|
private AppSettings settings;
|
||||||
|
private HistoryManager historyManager;
|
||||||
|
private StringBuilder commandLineBuffer = new StringBuilder();
|
||||||
|
private int historyIndex = -1;
|
||||||
|
private int commandStartPosition = 0;
|
||||||
|
|
||||||
|
public TerminalPanel(ConnectionInfo info, String masterPassword, AppSettings settings) {
|
||||||
|
this.connectionInfo = info;
|
||||||
|
this.masterPassword = masterPassword;
|
||||||
|
this.settings = settings;
|
||||||
|
this.historyManager = new HistoryManager(); // Initialize without loading any history
|
||||||
|
setLayout(new BorderLayout());
|
||||||
|
|
||||||
|
textPane = new JTextPane();
|
||||||
|
textPane.setFocusTraversalKeysEnabled(false);
|
||||||
|
applySettings();
|
||||||
|
textPane.setCaretColor(Color.WHITE);
|
||||||
|
|
||||||
|
// Auto-scroll to bottom
|
||||||
|
DefaultCaret caret = (DefaultCaret) textPane.getCaret();
|
||||||
|
caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
|
||||||
|
|
||||||
|
// Prevent moving caret into protected prompt area
|
||||||
|
textPane.setNavigationFilter(new NavigationFilter() {
|
||||||
|
@Override
|
||||||
|
public void setDot(FilterBypass fb, int dot, Position.Bias bias) {
|
||||||
|
if (dot < commandStartPosition) dot = commandStartPosition;
|
||||||
|
fb.setDot(dot, bias);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void moveDot(FilterBypass fb, int dot, Position.Bias bias) {
|
||||||
|
if (dot < commandStartPosition) dot = commandStartPosition;
|
||||||
|
fb.moveDot(dot, bias);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
JScrollPane scrollPane = new JScrollPane(textPane);
|
||||||
|
add(scrollPane, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
textPane.addKeyListener(new KeyAdapter() {
|
||||||
|
@Override
|
||||||
|
public void keyPressed(KeyEvent e) {
|
||||||
|
if (out == null) return;
|
||||||
|
|
||||||
|
// UP arrow - previous command in history
|
||||||
|
if (e.getKeyCode() == KeyEvent.VK_UP) {
|
||||||
|
if (historyManager.getHistory().isEmpty()) {
|
||||||
|
e.consume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (historyIndex < historyManager.getHistory().size() - 1) {
|
||||||
|
historyIndex++;
|
||||||
|
String command = historyManager.getHistory().get(historyManager.getHistory().size() - 1 - historyIndex);
|
||||||
|
displayCommand(command);
|
||||||
|
}
|
||||||
|
e.consume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOWN arrow - next command in history
|
||||||
|
if (e.getKeyCode() == KeyEvent.VK_DOWN) {
|
||||||
|
if (historyIndex > 0) {
|
||||||
|
historyIndex--;
|
||||||
|
String command = historyManager.getHistory().get(historyManager.getHistory().size() - 1 - historyIndex);
|
||||||
|
displayCommand(command);
|
||||||
|
} else if (historyIndex == 0) {
|
||||||
|
historyIndex = -1;
|
||||||
|
clearCurrentCommand();
|
||||||
|
}
|
||||||
|
e.consume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String seq = null;
|
||||||
|
switch (e.getKeyCode()) {
|
||||||
|
case KeyEvent.VK_TAB:
|
||||||
|
out.print("\t");
|
||||||
|
out.flush();
|
||||||
|
e.consume();
|
||||||
|
return;
|
||||||
|
case KeyEvent.VK_RIGHT: seq = "\u001B[C"; break;
|
||||||
|
case KeyEvent.VK_LEFT: seq = "\u001B[D"; break;
|
||||||
|
case KeyEvent.VK_HOME: seq = "\u001B[1~"; break;
|
||||||
|
case KeyEvent.VK_END: seq = "\u001B[4~"; break;
|
||||||
|
case KeyEvent.VK_DELETE:
|
||||||
|
// Protect command prompt - only allow delete if there's command content
|
||||||
|
if (canDeleteCharacter()) {
|
||||||
|
seq = "\u001B[3~";
|
||||||
|
}
|
||||||
|
e.consume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seq != null) {
|
||||||
|
out.print(seq);
|
||||||
|
out.flush();
|
||||||
|
e.consume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void keyTyped(KeyEvent e) {
|
||||||
|
if (out != null) {
|
||||||
|
char c = e.getKeyChar();
|
||||||
|
|
||||||
|
// Handle backspace - protect command prompt
|
||||||
|
if (c == '\b') {
|
||||||
|
if (canDeleteCharacter()) {
|
||||||
|
out.print(c);
|
||||||
|
out.flush();
|
||||||
|
if (commandLineBuffer.length() > 0) {
|
||||||
|
commandLineBuffer.setLength(commandLineBuffer.length() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Don't send backspace if it would delete the prompt
|
||||||
|
e.consume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send all other characters to server
|
||||||
|
out.print(c);
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
if (c == '\r' || c == '\n') {
|
||||||
|
// Command sent to server, server will save it to .history
|
||||||
|
// Add to local memory only for history browsing
|
||||||
|
String cmd = commandLineBuffer.toString();
|
||||||
|
if (!cmd.trim().isEmpty()) {
|
||||||
|
historyManager.addRemoteCommand(cmd);
|
||||||
|
}
|
||||||
|
commandLineBuffer.setLength(0);
|
||||||
|
historyIndex = -1;
|
||||||
|
} else if (!Character.isISOControl(c)) {
|
||||||
|
commandLineBuffer.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.consume();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connect() {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
JSch jsch = new JSch();
|
||||||
|
String password = EncryptionService.decrypt(connectionInfo.getEncryptedPassword(), masterPassword);
|
||||||
|
|
||||||
|
session = jsch.getSession(connectionInfo.getUsername(), connectionInfo.getHost(), connectionInfo.getPort());
|
||||||
|
session.setPassword(password);
|
||||||
|
session.setConfig("StrictHostKeyChecking", "no");
|
||||||
|
session.connect();
|
||||||
|
|
||||||
|
channel = (ChannelShell) session.openChannel("shell");
|
||||||
|
channel.setPtyType("xterm");
|
||||||
|
|
||||||
|
// Set up input from our pipe
|
||||||
|
PipedInputStream pin = new PipedInputStream(65536);
|
||||||
|
PipedOutputStream pout = new PipedOutputStream(pin);
|
||||||
|
out = new PrintStream(pout, true);
|
||||||
|
channel.setInputStream(pin);
|
||||||
|
|
||||||
|
// Connect first, then read output from channel
|
||||||
|
channel.connect();
|
||||||
|
|
||||||
|
// Get the output stream from the channel and read it
|
||||||
|
InputStream channelInput = channel.getInputStream();
|
||||||
|
|
||||||
|
// Read from channel in a separate thread to ensure we get all output
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int len;
|
||||||
|
while ((len = channelInput.read(buffer)) > 0) {
|
||||||
|
String output = new String(buffer, 0, len);
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
AnsiHelper.appendAnsi(textPane, output, commandStartPosition);
|
||||||
|
// Update prompt boundary only when not typing/navigating history
|
||||||
|
if (commandLineBuffer.length() == 0) {
|
||||||
|
commandStartPosition = textPane.getDocument().getLength();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Connection closed
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
|
||||||
|
// Load command history from remote server in a separate thread
|
||||||
|
// Delay to ensure shell initializes first
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(500); // Shorter delay - 500ms should be enough
|
||||||
|
loadRemoteHistory();
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
}).start();
|
||||||
|
|
||||||
|
// Focus on textPane after successful connection
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
textPane.requestFocusInWindow();
|
||||||
|
textPane.setCaretPosition(textPane.getDocument().getLength());
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
AnsiHelper.appendAnsi(textPane, "\nConnection failed: " + e.getMessage(), 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadRemoteHistory() {
|
||||||
|
try {
|
||||||
|
ChannelSftp sftpChannel = (ChannelSftp) session.openChannel("sftp");
|
||||||
|
sftpChannel.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to get home directory first
|
||||||
|
String homeDir = sftpChannel.pwd();
|
||||||
|
|
||||||
|
// Try .bash_history first (bash shell), then .history (zsh/generic)
|
||||||
|
InputStream is = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
is = sftpChannel.get(homeDir + "/.bash_history");
|
||||||
|
} catch (com.jcraft.jsch.SftpException e1) {
|
||||||
|
try {
|
||||||
|
is = sftpChannel.get(homeDir + "/.history");
|
||||||
|
} catch (com.jcraft.jsch.SftpException e2) {
|
||||||
|
throw e1; // throw original exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String historyContent = new String(is.readAllBytes());
|
||||||
|
is.close();
|
||||||
|
|
||||||
|
List<String> commands = new ArrayList<>();
|
||||||
|
for (String line : historyContent.split("\n")) {
|
||||||
|
String trimmed = line.trim();
|
||||||
|
if (!trimmed.isEmpty()) {
|
||||||
|
commands.add(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
historyManager.loadRemoteHistory(commands);
|
||||||
|
} catch (com.jcraft.jsch.SftpException sftpEx) {
|
||||||
|
if (sftpEx.id == 2) {
|
||||||
|
// File not found - that's okay, history is empty
|
||||||
|
historyManager.loadRemoteHistory(new ArrayList<>());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Ignore errors loading history
|
||||||
|
}
|
||||||
|
|
||||||
|
sftpChannel.disconnect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// SFTP not available, continue without history
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disconnect() {
|
||||||
|
if (channel != null && channel.isConnected()) channel.disconnect();
|
||||||
|
if (session != null && session.isConnected()) session.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateSettings(AppSettings newSettings) {
|
||||||
|
this.settings = newSettings;
|
||||||
|
applySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applySettings() {
|
||||||
|
textPane.setBackground(new Color(settings.getBackgroundColor()));
|
||||||
|
textPane.setForeground(new Color(settings.getForegroundColor()));
|
||||||
|
textPane.setFont(new Font(settings.getFontName(), Font.PLAIN, settings.getFontSize()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void displayCommand(String command) {
|
||||||
|
try {
|
||||||
|
StyledDocument doc = (StyledDocument) textPane.getDocument();
|
||||||
|
int docLength = doc.getLength();
|
||||||
|
|
||||||
|
// Remove everything after the prompt boundary
|
||||||
|
if (docLength > commandStartPosition) {
|
||||||
|
doc.remove(commandStartPosition, docLength - commandStartPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put the history command into the buffer so it can be edited
|
||||||
|
commandLineBuffer.setLength(0);
|
||||||
|
commandLineBuffer.append(command);
|
||||||
|
|
||||||
|
// Add command from history to display
|
||||||
|
AnsiHelper.appendAnsi(textPane, command, commandStartPosition);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearCurrentCommand() {
|
||||||
|
try {
|
||||||
|
StyledDocument doc = (StyledDocument) textPane.getDocument();
|
||||||
|
int docLength = doc.getLength();
|
||||||
|
|
||||||
|
// Remove everything after the prompt boundary
|
||||||
|
if (docLength > commandStartPosition) {
|
||||||
|
doc.remove(commandStartPosition, docLength - commandStartPosition);
|
||||||
|
}
|
||||||
|
commandLineBuffer.setLength(0);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canDeleteCharacter() {
|
||||||
|
// Allow deletion only if the caret is beyond the protected prompt boundary
|
||||||
|
// Standard backspace deletes character at caret - 1.
|
||||||
|
return textPane.getCaretPosition() > commandStartPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
311
src/main/java/cz/kamma/term/util/AnsiHelper.java
Normal file
311
src/main/java/cz/kamma/term/util/AnsiHelper.java
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
package cz.kamma.term.util;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.text.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class AnsiHelper {
|
||||||
|
// Comprehensive ANSI escape sequence pattern
|
||||||
|
// Matches: ESC [ params command, ESC ] OSC sequence, and other escape sequences
|
||||||
|
private static final Pattern ANSI_PATTERN = Pattern.compile(
|
||||||
|
"\u001B" + // ESC character
|
||||||
|
"(?:" +
|
||||||
|
"\\[([?0-9;]*)([@-~])|" + // CSI sequences: ESC [ params char
|
||||||
|
"\\].*?(?:\u0007|\u001B\\\\)|" + // OSC sequences
|
||||||
|
"[()][B0UK]" + // Charset selection
|
||||||
|
")"
|
||||||
|
);
|
||||||
|
|
||||||
|
public static void appendAnsi(JTextPane textPane, String text, int protectedLength) {
|
||||||
|
if (text == null || text.isEmpty()) return;
|
||||||
|
|
||||||
|
StyledDocument doc = textPane.getStyledDocument();
|
||||||
|
MutableAttributeSet currentAttrs = new SimpleAttributeSet();
|
||||||
|
StyleConstants.setForeground(currentAttrs, new Color(0, 200, 0)); // Default green
|
||||||
|
StyleConstants.setBackground(currentAttrs, Color.BLACK);
|
||||||
|
|
||||||
|
Matcher matcher = ANSI_PATTERN.matcher(text);
|
||||||
|
int lastEnd = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (matcher.find()) {
|
||||||
|
// Insert text before the sequence
|
||||||
|
if (matcher.start() > lastEnd) {
|
||||||
|
processPlainText(doc, text.substring(lastEnd, matcher.start()), currentAttrs, protectedLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
String params = matcher.group(1);
|
||||||
|
String command = matcher.group(2);
|
||||||
|
|
||||||
|
if (command != null && !command.isEmpty()) {
|
||||||
|
processCsiSequence(command, params, currentAttrs, doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastEnd = matcher.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert remaining text
|
||||||
|
if (lastEnd < text.length()) {
|
||||||
|
processPlainText(doc, text.substring(lastEnd), currentAttrs, protectedLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
textPane.setCaretPosition(doc.getLength());
|
||||||
|
} catch (BadLocationException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void processPlainText(StyledDocument doc, String text, AttributeSet attrs, int protectedLength) throws BadLocationException {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
char c = text.charAt(i);
|
||||||
|
|
||||||
|
if (c == '\r') {
|
||||||
|
// Check if next char is \n (standard Windows/SSH line ending)
|
||||||
|
if (i + 1 < text.length() && text.charAt(i + 1) == '\n') {
|
||||||
|
// \r\n sequence - treat as single newline
|
||||||
|
if (sb.length() > 0) {
|
||||||
|
sb.append('\n');
|
||||||
|
} else {
|
||||||
|
doc.insertString(doc.getLength(), "\n", attrs);
|
||||||
|
}
|
||||||
|
i++; // Skip the \n
|
||||||
|
} else {
|
||||||
|
// Standalone \r - carriage return (move to beginning of line)
|
||||||
|
if (sb.length() > 0) {
|
||||||
|
doc.insertString(doc.getLength(), sb.toString(), attrs);
|
||||||
|
sb.setLength(0);
|
||||||
|
}
|
||||||
|
String content = doc.getText(0, doc.getLength());
|
||||||
|
int lastNewline = content.lastIndexOf('\n');
|
||||||
|
int startToDelete = lastNewline == -1 ? 0 : lastNewline + 1;
|
||||||
|
|
||||||
|
// Respect protected boundary
|
||||||
|
if (startToDelete < protectedLength) {
|
||||||
|
startToDelete = protectedLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
int endToDelete = doc.getLength();
|
||||||
|
if (endToDelete > startToDelete) {
|
||||||
|
doc.remove(startToDelete, endToDelete - startToDelete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (c == '\b') {
|
||||||
|
// Backspace: delete previous character
|
||||||
|
if (sb.length() > 0) {
|
||||||
|
doc.insertString(doc.getLength(), sb.toString(), attrs);
|
||||||
|
sb.setLength(0);
|
||||||
|
}
|
||||||
|
// NEVER delete before or at protectedLength
|
||||||
|
if (doc.getLength() > protectedLength) {
|
||||||
|
doc.remove(doc.getLength() - 1, 1);
|
||||||
|
}
|
||||||
|
} else if (c == '\t') {
|
||||||
|
// Tab: convert to spaces (4 spaces)
|
||||||
|
sb.append(" ");
|
||||||
|
} else if (!Character.isISOControl(c)) {
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sb.length() > 0) {
|
||||||
|
doc.insertString(doc.getLength(), sb.toString(), attrs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void processCsiSequence(String command, String params, MutableAttributeSet attrs, StyledDocument doc) {
|
||||||
|
if (params == null) params = "";
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "m": // SGR - Select Graphic Rendition (colors, styles)
|
||||||
|
processSgr(params, attrs);
|
||||||
|
break;
|
||||||
|
case "K": // EL - Erase in Line
|
||||||
|
eraseInLine(doc, params);
|
||||||
|
break;
|
||||||
|
case "J": // ED - Erase in Display
|
||||||
|
eraseInDisplay(doc, params);
|
||||||
|
break;
|
||||||
|
case "H":
|
||||||
|
case "f": // CUP/HVP - Cursor Position
|
||||||
|
// Ignore - JTextPane doesn't support cursor repositioning
|
||||||
|
break;
|
||||||
|
case "A": // CUU - Cursor Up
|
||||||
|
case "B": // CUD - Cursor Down
|
||||||
|
case "C": // CUF - Cursor Forward (Right)
|
||||||
|
case "D": // CUB - Cursor Backward (Left)
|
||||||
|
case "E": // CNL - Cursor Next Line
|
||||||
|
case "F": // CPL - Cursor Previous Line
|
||||||
|
case "G": // CHA - Cursor Horizontal Absolute
|
||||||
|
// Ignore cursor movement - JTextPane handles cursor differently
|
||||||
|
break;
|
||||||
|
case "s": // SCP - Save Cursor Position
|
||||||
|
case "u": // RCP - Restore Cursor Position
|
||||||
|
// Ignore - not needed for output display
|
||||||
|
break;
|
||||||
|
case "h": // SM - Set Mode
|
||||||
|
case "l": // RM - Reset Mode
|
||||||
|
// Handle mode settings (like cursor visibility, alt screen)
|
||||||
|
if (params.contains("25")) {
|
||||||
|
// ?25h = show cursor, ?25l = hide cursor - we'll just display normally
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "c": // DA - Device Attributes
|
||||||
|
case "n": // DSR - Device Status Report
|
||||||
|
// Device info requests - ignore
|
||||||
|
break;
|
||||||
|
case "1" :
|
||||||
|
case "2" :
|
||||||
|
case "3" :
|
||||||
|
case "4" :
|
||||||
|
// These look like incomplete sequences, ignore
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void processSgr(String params, MutableAttributeSet attrs) {
|
||||||
|
if (params == null || params.isEmpty()) {
|
||||||
|
resetAttributes(attrs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] codes = params.split(";");
|
||||||
|
for (String code : codes) {
|
||||||
|
if (code.isEmpty()) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
int c = Integer.parseInt(code);
|
||||||
|
|
||||||
|
if (c == 0) {
|
||||||
|
// Reset all attributes
|
||||||
|
resetAttributes(attrs);
|
||||||
|
} else if (c == 1) {
|
||||||
|
// Bold
|
||||||
|
StyleConstants.setBold(attrs, true);
|
||||||
|
} else if (c == 2) {
|
||||||
|
// Dim (reduced intensity)
|
||||||
|
StyleConstants.setBold(attrs, false);
|
||||||
|
} else if (c == 3) {
|
||||||
|
// Italic
|
||||||
|
StyleConstants.setItalic(attrs, true);
|
||||||
|
} else if (c == 4) {
|
||||||
|
// Underline
|
||||||
|
StyleConstants.setUnderline(attrs, true);
|
||||||
|
} else if (c == 5) {
|
||||||
|
// Blink (we'll just show it normally)
|
||||||
|
StyleConstants.setBold(attrs, true);
|
||||||
|
} else if (c == 7) {
|
||||||
|
// Reverse video
|
||||||
|
Color fg = (Color) attrs.getAttribute(StyleConstants.Foreground);
|
||||||
|
Color bg = (Color) attrs.getAttribute(StyleConstants.Background);
|
||||||
|
if (fg != null && bg != null) {
|
||||||
|
StyleConstants.setForeground(attrs, bg);
|
||||||
|
StyleConstants.setBackground(attrs, fg);
|
||||||
|
}
|
||||||
|
} else if (c == 22) {
|
||||||
|
// Normal intensity
|
||||||
|
StyleConstants.setBold(attrs, false);
|
||||||
|
} else if (c == 23) {
|
||||||
|
// No italic
|
||||||
|
StyleConstants.setItalic(attrs, false);
|
||||||
|
} else if (c == 24) {
|
||||||
|
// No underline
|
||||||
|
StyleConstants.setUnderline(attrs, false);
|
||||||
|
} else if (c >= 30 && c <= 37) {
|
||||||
|
// Foreground color (standard)
|
||||||
|
StyleConstants.setForeground(attrs, getAnsiColor(c - 30, false));
|
||||||
|
} else if (c == 39) {
|
||||||
|
// Default foreground
|
||||||
|
StyleConstants.setForeground(attrs, new Color(0, 200, 0));
|
||||||
|
} else if (c >= 40 && c <= 47) {
|
||||||
|
// Background color (standard)
|
||||||
|
StyleConstants.setBackground(attrs, getAnsiColor(c - 40, false));
|
||||||
|
} else if (c == 49) {
|
||||||
|
// Default background
|
||||||
|
StyleConstants.setBackground(attrs, Color.BLACK);
|
||||||
|
} else if (c >= 90 && c <= 97) {
|
||||||
|
// Foreground color (bright)
|
||||||
|
StyleConstants.setForeground(attrs, getAnsiColor(c - 90, true));
|
||||||
|
} else if (c >= 100 && c <= 107) {
|
||||||
|
// Background color (bright)
|
||||||
|
StyleConstants.setBackground(attrs, getAnsiColor(c - 100, true));
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// Ignore invalid codes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void resetAttributes(MutableAttributeSet attrs) {
|
||||||
|
StyleConstants.setForeground(attrs, new Color(0, 200, 0)); // Default green
|
||||||
|
StyleConstants.setBackground(attrs, Color.BLACK);
|
||||||
|
StyleConstants.setBold(attrs, false);
|
||||||
|
StyleConstants.setItalic(attrs, false);
|
||||||
|
StyleConstants.setUnderline(attrs, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void eraseInLine(StyledDocument doc, String param) {
|
||||||
|
try {
|
||||||
|
// EL 0 = erase from cursor to end of line (default)
|
||||||
|
// EL 1 = erase from start of line to cursor
|
||||||
|
// EL 2 = erase entire line
|
||||||
|
|
||||||
|
String content = doc.getText(0, doc.getLength());
|
||||||
|
int lastNewline = content.lastIndexOf('\n');
|
||||||
|
int lineStart = lastNewline == -1 ? 0 : lastNewline + 1;
|
||||||
|
int lineEnd = doc.getLength();
|
||||||
|
|
||||||
|
if ("2".equals(param)) {
|
||||||
|
// Erase entire line
|
||||||
|
doc.remove(lineStart, lineEnd - lineStart);
|
||||||
|
} else {
|
||||||
|
// Default: erase from cursor to end of line
|
||||||
|
doc.remove(lineStart, lineEnd - lineStart);
|
||||||
|
}
|
||||||
|
} catch (BadLocationException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void eraseInDisplay(StyledDocument doc, String param) {
|
||||||
|
try {
|
||||||
|
// ED 0 = erase from cursor to end of display (default)
|
||||||
|
// ED 1 = erase from start of display to cursor
|
||||||
|
// ED 2 = erase entire display
|
||||||
|
// ED 3 = erase scrollback buffer
|
||||||
|
|
||||||
|
if ("2".equals(param) || "3".equals(param)) {
|
||||||
|
// For terminal apps: don't clear entire display,
|
||||||
|
// just leave the content as is
|
||||||
|
// Clearing would remove important command output
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color getAnsiColor(int index, boolean bright) {
|
||||||
|
if (bright) {
|
||||||
|
switch (index) {
|
||||||
|
case 0: return new Color(128, 128, 128); // Bright Black (Gray)
|
||||||
|
case 1: return new Color(255, 85, 85); // Bright Red
|
||||||
|
case 2: return new Color(85, 255, 85); // Bright Green
|
||||||
|
case 3: return new Color(255, 255, 85); // Bright Yellow
|
||||||
|
case 4: return new Color(85, 85, 255); // Bright Blue
|
||||||
|
case 5: return new Color(255, 85, 255); // Bright Magenta
|
||||||
|
case 6: return new Color(85, 255, 255); // Bright Cyan
|
||||||
|
case 7: return new Color(255, 255, 255); // Bright White
|
||||||
|
default: return Color.WHITE;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (index) {
|
||||||
|
case 0: return new Color(0, 0, 0); // Black
|
||||||
|
case 1: return new Color(170, 0, 0); // Red
|
||||||
|
case 2: return new Color(0, 170, 0); // Green
|
||||||
|
case 3: return new Color(170, 85, 0); // Yellow
|
||||||
|
case 4: return new Color(0, 0, 170); // Blue
|
||||||
|
case 5: return new Color(170, 0, 170); // Magenta
|
||||||
|
case 6: return new Color(0, 170, 170); // Cyan
|
||||||
|
case 7: return new Color(170, 170, 170); // White
|
||||||
|
default: return new Color(0, 200, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/main/java/cz/kamma/term/util/SystemIdUtil.java
Normal file
30
src/main/java/cz/kamma/term/util/SystemIdUtil.java
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package cz.kamma.term.util;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
|
||||||
|
public class SystemIdUtil {
|
||||||
|
public static String getMachineId() {
|
||||||
|
try {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(System.getProperty("os.name"));
|
||||||
|
sb.append(System.getProperty("user.name"));
|
||||||
|
|
||||||
|
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
|
||||||
|
while (networkInterfaces.hasMoreElements()) {
|
||||||
|
NetworkInterface ni = networkInterfaces.nextElement();
|
||||||
|
byte[] hardwareAddress = ni.getHardwareAddress();
|
||||||
|
if (hardwareAddress != null) {
|
||||||
|
for (byte b : hardwareAddress) {
|
||||||
|
sb.append(String.format("%02X", b));
|
||||||
|
}
|
||||||
|
break; // Just use the first MAC address found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "fallback-machine-id-12345";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
target/classes/cz/kamma/term/App.class
Normal file
BIN
target/classes/cz/kamma/term/App.class
Normal file
Binary file not shown.
BIN
target/classes/cz/kamma/term/model/AppSettings.class
Normal file
BIN
target/classes/cz/kamma/term/model/AppSettings.class
Normal file
Binary file not shown.
BIN
target/classes/cz/kamma/term/model/ConnectionInfo.class
Normal file
BIN
target/classes/cz/kamma/term/model/ConnectionInfo.class
Normal file
Binary file not shown.
BIN
target/classes/cz/kamma/term/service/ConnectionManager$1.class
Normal file
BIN
target/classes/cz/kamma/term/service/ConnectionManager$1.class
Normal file
Binary file not shown.
BIN
target/classes/cz/kamma/term/service/ConnectionManager.class
Normal file
BIN
target/classes/cz/kamma/term/service/ConnectionManager.class
Normal file
Binary file not shown.
BIN
target/classes/cz/kamma/term/service/EncryptionService.class
Normal file
BIN
target/classes/cz/kamma/term/service/EncryptionService.class
Normal file
Binary file not shown.
BIN
target/classes/cz/kamma/term/service/HistoryManager.class
Normal file
BIN
target/classes/cz/kamma/term/service/HistoryManager.class
Normal file
Binary file not shown.
BIN
target/classes/cz/kamma/term/service/SettingsManager.class
Normal file
BIN
target/classes/cz/kamma/term/service/SettingsManager.class
Normal file
Binary file not shown.
BIN
target/classes/cz/kamma/term/ui/MainFrame$1.class
Normal file
BIN
target/classes/cz/kamma/term/ui/MainFrame$1.class
Normal file
Binary file not shown.
BIN
target/classes/cz/kamma/term/ui/MainFrame.class
Normal file
BIN
target/classes/cz/kamma/term/ui/MainFrame.class
Normal file
Binary file not shown.
BIN
target/classes/cz/kamma/term/ui/TerminalPanel$1.class
Normal file
BIN
target/classes/cz/kamma/term/ui/TerminalPanel$1.class
Normal file
Binary file not shown.
BIN
target/classes/cz/kamma/term/ui/TerminalPanel$2.class
Normal file
BIN
target/classes/cz/kamma/term/ui/TerminalPanel$2.class
Normal file
Binary file not shown.
BIN
target/classes/cz/kamma/term/ui/TerminalPanel.class
Normal file
BIN
target/classes/cz/kamma/term/ui/TerminalPanel.class
Normal file
Binary file not shown.
BIN
target/classes/cz/kamma/term/util/AnsiHelper.class
Normal file
BIN
target/classes/cz/kamma/term/util/AnsiHelper.class
Normal file
Binary file not shown.
BIN
target/classes/cz/kamma/term/util/SystemIdUtil.class
Normal file
BIN
target/classes/cz/kamma/term/util/SystemIdUtil.class
Normal file
Binary file not shown.
@ -0,0 +1,15 @@
|
|||||||
|
cz/kamma/term/service/ConnectionManager.class
|
||||||
|
cz/kamma/term/service/HistoryManager.class
|
||||||
|
cz/kamma/term/ui/MainFrame$1.class
|
||||||
|
cz/kamma/term/model/AppSettings.class
|
||||||
|
cz/kamma/term/util/SystemIdUtil.class
|
||||||
|
cz/kamma/term/service/EncryptionService.class
|
||||||
|
cz/kamma/term/model/ConnectionInfo.class
|
||||||
|
cz/kamma/term/ui/TerminalPanel$2.class
|
||||||
|
cz/kamma/term/ui/TerminalPanel.class
|
||||||
|
cz/kamma/term/service/SettingsManager.class
|
||||||
|
cz/kamma/term/ui/TerminalPanel$1.class
|
||||||
|
cz/kamma/term/service/ConnectionManager$1.class
|
||||||
|
cz/kamma/term/ui/MainFrame.class
|
||||||
|
cz/kamma/term/util/AnsiHelper.class
|
||||||
|
cz/kamma/term/App.class
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
/home/kamma/projects/terminal/src/main/java/cz/kamma/term/util/SystemIdUtil.java
|
||||||
|
/home/kamma/projects/terminal/src/main/java/cz/kamma/term/ui/MainFrame.java
|
||||||
|
/home/kamma/projects/terminal/src/main/java/cz/kamma/term/service/HistoryManager.java
|
||||||
|
/home/kamma/projects/terminal/src/main/java/cz/kamma/term/service/EncryptionService.java
|
||||||
|
/home/kamma/projects/terminal/src/main/java/cz/kamma/term/service/SettingsManager.java
|
||||||
|
/home/kamma/projects/terminal/src/main/java/cz/kamma/term/service/ConnectionManager.java
|
||||||
|
/home/kamma/projects/terminal/src/main/java/cz/kamma/term/model/AppSettings.java
|
||||||
|
/home/kamma/projects/terminal/src/main/java/cz/kamma/term/ui/TerminalPanel.java
|
||||||
|
/home/kamma/projects/terminal/src/main/java/cz/kamma/term/model/ConnectionInfo.java
|
||||||
|
/home/kamma/projects/terminal/src/main/java/cz/kamma/term/util/AnsiHelper.java
|
||||||
|
/home/kamma/projects/terminal/src/main/java/cz/kamma/term/App.java
|
||||||
Loading…
x
Reference in New Issue
Block a user