From fc9e7b7598ae5f29b4aa5c860a5336cc5d9187b3 Mon Sep 17 00:00:00 2001 From: rdavidek Date: Sun, 25 Jan 2026 19:06:08 +0100 Subject: [PATCH] first commit --- .github/copilot-instructions.md | 16 + .master_key | 1 + .vscode/settings.json | 3 + README.md | 26 ++ connections.json | 1 + pom.xml | 78 ++++ settings.json | 1 + src/main/java/cz/kamma/term/App.java | 18 + .../java/cz/kamma/term/model/AppSettings.java | 20 + .../cz/kamma/term/model/ConnectionInfo.java | 34 ++ .../kamma/term/service/ConnectionManager.java | 55 +++ .../kamma/term/service/EncryptionService.java | 37 ++ .../cz/kamma/term/service/HistoryManager.java | 36 ++ .../kamma/term/service/SettingsManager.java | 42 +++ src/main/java/cz/kamma/term/ui/MainFrame.java | 313 ++++++++++++++++ .../java/cz/kamma/term/ui/TerminalPanel.java | 347 ++++++++++++++++++ .../java/cz/kamma/term/util/AnsiHelper.java | 311 ++++++++++++++++ .../java/cz/kamma/term/util/SystemIdUtil.java | 30 ++ target/classes/cz/kamma/term/App.class | Bin 0 -> 1409 bytes .../cz/kamma/term/model/AppSettings.class | Bin 0 -> 1343 bytes .../cz/kamma/term/model/ConnectionInfo.class | Bin 0 -> 2073 bytes .../term/service/ConnectionManager$1.class | Bin 0 -> 758 bytes .../term/service/ConnectionManager.class | Bin 0 -> 2705 bytes .../term/service/EncryptionService.class | Bin 0 -> 2351 bytes .../kamma/term/service/HistoryManager.class | Bin 0 -> 1427 bytes .../kamma/term/service/SettingsManager.class | Bin 0 -> 1912 bytes .../cz/kamma/term/ui/MainFrame$1.class | Bin 0 -> 1873 bytes .../classes/cz/kamma/term/ui/MainFrame.class | Bin 0 -> 14693 bytes .../cz/kamma/term/ui/TerminalPanel$1.class | Bin 0 -> 1373 bytes .../cz/kamma/term/ui/TerminalPanel$2.class | Bin 0 -> 2924 bytes .../cz/kamma/term/ui/TerminalPanel.class | Bin 0 -> 9879 bytes .../cz/kamma/term/util/AnsiHelper.class | Bin 0 -> 7899 bytes .../cz/kamma/term/util/SystemIdUtil.class | Bin 0 -> 1691 bytes .../compile/default-compile/createdFiles.lst | 15 + .../compile/default-compile/inputFiles.lst | 11 + 35 files changed, 1395 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 .master_key create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 connections.json create mode 100644 pom.xml create mode 100644 settings.json create mode 100644 src/main/java/cz/kamma/term/App.java create mode 100644 src/main/java/cz/kamma/term/model/AppSettings.java create mode 100644 src/main/java/cz/kamma/term/model/ConnectionInfo.java create mode 100644 src/main/java/cz/kamma/term/service/ConnectionManager.java create mode 100644 src/main/java/cz/kamma/term/service/EncryptionService.java create mode 100644 src/main/java/cz/kamma/term/service/HistoryManager.java create mode 100644 src/main/java/cz/kamma/term/service/SettingsManager.java create mode 100644 src/main/java/cz/kamma/term/ui/MainFrame.java create mode 100644 src/main/java/cz/kamma/term/ui/TerminalPanel.java create mode 100644 src/main/java/cz/kamma/term/util/AnsiHelper.java create mode 100644 src/main/java/cz/kamma/term/util/SystemIdUtil.java create mode 100644 target/classes/cz/kamma/term/App.class create mode 100644 target/classes/cz/kamma/term/model/AppSettings.class create mode 100644 target/classes/cz/kamma/term/model/ConnectionInfo.class create mode 100644 target/classes/cz/kamma/term/service/ConnectionManager$1.class create mode 100644 target/classes/cz/kamma/term/service/ConnectionManager.class create mode 100644 target/classes/cz/kamma/term/service/EncryptionService.class create mode 100644 target/classes/cz/kamma/term/service/HistoryManager.class create mode 100644 target/classes/cz/kamma/term/service/SettingsManager.class create mode 100644 target/classes/cz/kamma/term/ui/MainFrame$1.class create mode 100644 target/classes/cz/kamma/term/ui/MainFrame.class create mode 100644 target/classes/cz/kamma/term/ui/TerminalPanel$1.class create mode 100644 target/classes/cz/kamma/term/ui/TerminalPanel$2.class create mode 100644 target/classes/cz/kamma/term/ui/TerminalPanel.class create mode 100644 target/classes/cz/kamma/term/util/AnsiHelper.class create mode 100644 target/classes/cz/kamma/term/util/SystemIdUtil.class create mode 100644 target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst create mode 100644 target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..79608c8 --- /dev/null +++ b/.github/copilot-instructions.md @@ -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 diff --git a/.master_key b/.master_key new file mode 100644 index 0000000..828713f --- /dev/null +++ b/.master_key @@ -0,0 +1 @@ +ByBYvcAXjSA51UXD76DrxA== \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e0f15db --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "automatic" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a7e66a9 --- /dev/null +++ b/README.md @@ -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`. diff --git a/connections.json b/connections.json new file mode 100644 index 0000000..db1b25c --- /dev/null +++ b/connections.json @@ -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"}] \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..4c95f76 --- /dev/null +++ b/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + cz.kamma.term + java-ssh-terminal + 1.0-SNAPSHOT + + + 11 + 11 + UTF-8 + + + + + + com.github.mwiede + jsch + 0.2.17 + + + + + com.google.code.gson + gson + 2.10.1 + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + cz.kamma.term.App + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + false + + + cz.kamma.term.App + + + + + + + + + diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..0d49e58 --- /dev/null +++ b/settings.json @@ -0,0 +1 @@ +{"fontName":"Monospaced","fontSize":12,"backgroundColor":-16777216,"foregroundColor":-16711936} \ No newline at end of file diff --git a/src/main/java/cz/kamma/term/App.java b/src/main/java/cz/kamma/term/App.java new file mode 100644 index 0000000..d098fee --- /dev/null +++ b/src/main/java/cz/kamma/term/App.java @@ -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); + }); + } +} diff --git a/src/main/java/cz/kamma/term/model/AppSettings.java b/src/main/java/cz/kamma/term/model/AppSettings.java new file mode 100644 index 0000000..c03fab5 --- /dev/null +++ b/src/main/java/cz/kamma/term/model/AppSettings.java @@ -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; } +} diff --git a/src/main/java/cz/kamma/term/model/ConnectionInfo.java b/src/main/java/cz/kamma/term/model/ConnectionInfo.java new file mode 100644 index 0000000..1852396 --- /dev/null +++ b/src/main/java/cz/kamma/term/model/ConnectionInfo.java @@ -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 + ")"; + } +} diff --git a/src/main/java/cz/kamma/term/service/ConnectionManager.java b/src/main/java/cz/kamma/term/service/ConnectionManager.java new file mode 100644 index 0000000..d6c171f --- /dev/null +++ b/src/main/java/cz/kamma/term/service/ConnectionManager.java @@ -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 connections = new ArrayList<>(); + + public ConnectionManager() { + load(); + } + + public List 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>(){}.getType(); + connections = gson.fromJson(reader, listType); + if (connections == null) connections = new ArrayList<>(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/main/java/cz/kamma/term/service/EncryptionService.java b/src/main/java/cz/kamma/term/service/EncryptionService.java new file mode 100644 index 0000000..a7b1e3d --- /dev/null +++ b/src/main/java/cz/kamma/term/service/EncryptionService.java @@ -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); + } +} diff --git a/src/main/java/cz/kamma/term/service/HistoryManager.java b/src/main/java/cz/kamma/term/service/HistoryManager.java new file mode 100644 index 0000000..6375645 --- /dev/null +++ b/src/main/java/cz/kamma/term/service/HistoryManager.java @@ -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 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 remoteCommands) { + if (remoteCommands != null) { + history.clear(); + history.addAll(remoteCommands); + } + } + + public List getHistory() { + return history; + } +} diff --git a/src/main/java/cz/kamma/term/service/SettingsManager.java b/src/main/java/cz/kamma/term/service/SettingsManager.java new file mode 100644 index 0000000..57d4ee2 --- /dev/null +++ b/src/main/java/cz/kamma/term/service/SettingsManager.java @@ -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(); + } + } +} diff --git a/src/main/java/cz/kamma/term/ui/MainFrame.java b/src/main/java/cz/kamma/term/ui/MainFrame.java new file mode 100644 index 0000000..56009cd --- /dev/null +++ b/src/main/java/cz/kamma/term/ui/MainFrame.java @@ -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 listModel; + private JList 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 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); + } + } + } + } +} diff --git a/src/main/java/cz/kamma/term/ui/TerminalPanel.java b/src/main/java/cz/kamma/term/ui/TerminalPanel.java new file mode 100644 index 0000000..a5554cd --- /dev/null +++ b/src/main/java/cz/kamma/term/ui/TerminalPanel.java @@ -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 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; + } +} diff --git a/src/main/java/cz/kamma/term/util/AnsiHelper.java b/src/main/java/cz/kamma/term/util/AnsiHelper.java new file mode 100644 index 0000000..466c21a --- /dev/null +++ b/src/main/java/cz/kamma/term/util/AnsiHelper.java @@ -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); + } + } + } +} diff --git a/src/main/java/cz/kamma/term/util/SystemIdUtil.java b/src/main/java/cz/kamma/term/util/SystemIdUtil.java new file mode 100644 index 0000000..e37915e --- /dev/null +++ b/src/main/java/cz/kamma/term/util/SystemIdUtil.java @@ -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 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"; + } + } +} diff --git a/target/classes/cz/kamma/term/App.class b/target/classes/cz/kamma/term/App.class new file mode 100644 index 0000000000000000000000000000000000000000..1544eadda562cc932af71b6064bee64793c0e559 GIT binary patch literal 1409 zcma)6+foxj5Iqw@7M6vCyMQ1dXoAY(6&1Vy5?O53LGXus<^?FM z@&hct$+9O&!BS9^58Jyv)93W*?%DbM_v9CVX}mKLLxX|1g+?S8Qrmo&+Ya|O?GGE< zqNo@Wv(l65B|}3xQ|KU6lZ9rOySM= z{3eMxukE^$j#N^F3@y^z^>;*$D-kgCrZ3~m6ijrW(?FMnZuBs8M;X_bSGdPFHGFVW zsQiAYgq!pIojI?R6~b9?co?p7S1@#))08xMrab{S0lPP!T9)iVCU&G6e(I z89L7@U)(E-vXZ`MVgN%HZs>L<%S5m8iWhg*0$vmbMyMCX@AeLNU2apou3eG#3YT6s zAbBQ+(V)$kg>l?sFiA{7hLT#!5Kn*BdD9H-=PBmOW!hcvOB9sMNl&a++zk<|=}9nj z?ekz$gD=@;Xmz-|QQ~8oV{D3n zk%aCD&uOD5QERm|E4*A+W#;`#P!w6IbvIF2le)olEa!b+g(~3X6`{8LQkX&_nE=i0 zHfAk6sCq9=yuxb(a~9^Yz%Y6?oZ11ib5&(T!N2mN@`HUkdv4VG6CHIORLi);y^l<%OV>7|C_axS7W39UY&V?J5F9ILG87cJx>JH!!JSuS%&co zJ-Mtr14|4;SBKKYQ0oOoXb3U77k~yZeT|UKkZ04I(YtZt2<(79VwfPSw*dop$Xc}k z+{HcenlV`gY4CI%+ne)_;ulWAy$& z)BA}-4F1IMrz4EU4{`ILx}PAn0aCB!^%ELsA=^f)7EH9#SCa0A)M+*oaXiE$ihE3t mf!MNvfjAL7AvZ?apVC{`_JY=);W<5TXvL(JMZCo_`u+jfoLfBr literal 0 HcmV?d00001 diff --git a/target/classes/cz/kamma/term/model/AppSettings.class b/target/classes/cz/kamma/term/model/AppSettings.class new file mode 100644 index 0000000000000000000000000000000000000000..9b786c2b6026c97271a01731a1645aa36b518212 GIT binary patch literal 1343 zcmaiyZEw<07>4h|c+i$f-+dZPy z)Y?ayZSZ=y8Ft4~SX#aDY4whDg2oNy2ZWM|4UkaM$pKQzS1f$TwE=Q8yW!5(zqmGc=$j=A z&@W6z5@JXrj%Bhg(5~zmq6mdn0-LxyY^Ku4%_7P-BtFw51U6$9E*lp|M#cBAH7tHk zYCd69*(As@idQ}7R%boMu$rAY3vQ}nb?7q*k^1C=NCraO$Ac+|70SpDk-7kaFM=T+ z214Zg5a~dON5K#|Kg9e65c~=l;&C9vx*sAF2=OEsV&Y|Z*RgN`1V2HBcbC|%SgOrOjG6FL% z~^JRRM_>px~)21 zT?YYK`Z{O?e`tYJPlx13G}w6BMIJLG3}jac%r*}$+%TaZY}Z(8*2SFF+R>3=!MuwF z7G(D#6|yM{kGUwZu%LogxZg9X{X|C5YaO;{DYzvUCvZ|=u`72KF}0+5?wcLjMMefD z5?C#d-##v>jEe-C-(}huHyxK$HYpo*%A7#H=?7|kue+tfr*f-f^P^j`vnfNL&jVq~ z?D&Mg()|DI*94qxW!7!7i{->voakSL`Glw#k6o-$C1^*wgB{79*uqj@G~zGuIaO}3 z(NR)(HmDYz$nfA)q;HQ#izZMcr37)-=(sloavORtY^ldSCui>4PP@#lL@oS4YZIAJ z_I^`lN4KLaZVRNva#>XJ8T^#P9n>7$<@WfQvUt#de(*}~Dlfh(?wH52WpubN@Yir^ z28yx&H5s%!Dq3l3y}Q?|jTd-G@{Qrs{hmrte=(#V&tPr1Yy%f{+39TfMp1#?2F+Vo z?@;fn$iXiH=f+}6LUr&!;M9Ms()h=5;WQ201dUai-Za6qo8TF>EBy=XN+W7lno3$- zP}*^d-apXk4e4iSJzxC@QJq=n!>L~EW2$_@9>cVxv_R>*Ql#=yT~;g-MM6=lA`Pe#5$l?X8C*OzvK=J4>MwOs zA+g{C_$b76=%Q*DjYppQ(D6CGzW3(#>Kec{Ubj%gOan=VS=1TUitqeXRF&XCx{7=0 z&Q&4#z#1cqpsd*wMwHTZwixQ+METAKI+Ra&s+*#TRs(5eONpo($|rGR)`7$o+A3B;8OPY050*wh;PI zI2GgH$s|_9uraMfXRFK5-jm_Pj=l&dDoL+K1g!&A8WF~hgudqgEwYz?6X$}DgVKD* zIdPFIA9{aXqM}0dAC1^G$F|$2k0Cv=foBZw zrtDmX7qMZ9wqnH4+?PIlwmvXqyEGYYpoRC+;|)zylLm{m+i{+vM|%zrDF2YWKG_=C zbbaF&*!m?JKPjqVp8oX-mZMGn(me`~us{*vCu|Il$)6?4ACdkE1vM-ZIND`AC7WOc R&+&4SS-~r^DbYqrT6dA?#v=d# literal 0 HcmV?d00001 diff --git a/target/classes/cz/kamma/term/service/ConnectionManager.class b/target/classes/cz/kamma/term/service/ConnectionManager.class new file mode 100644 index 0000000000000000000000000000000000000000..f003480eca747b1864f28d9cc48ebe7a4495abfc GIT binary patch literal 2705 zcma)8TT|Oc7(HtY0-~hNyu? zGzoNUSeuraw~9G)VQoWZJb|VuyJ&ki1R7%TWev>&M>9^r%sEajFU?%pDVn$V(4nEB z1+6eN95K*_c7aQo7v>YIP_Rr-x&^Z=-Ay|q%^9ayB(&`mQ&!Q+Nmn4Ma0Ggj2SFz^ zbP9C))GD5xH>X|KdY-h)o{o;-(4z)=&@0fIsbZJuO|oXAwkLG*J7(axay6QFtZXYz z;G~9A22SIQK--)9>9dv13iQMRpgLU=@%swwoPqN`SlQZ?3asD2MTOMl%7U{gHCz(t z@=*KhzsDiXu{&Rq0dyO>MFmzBz+225P$5uS6$S8eD3 zdUb@SOC?#%T5Eaf1B~N}h6w}jD&B#^q!!Q;Xw2lDGHGAk6WsTC>9OlPRR}sJ@t%R} zNC+IEHj9-~$(7}@q)Nx?X;dwEriSi#pQT1Fz`!LmD$ujVG2%G=Ig3{zPRorZZLzQGyKBP zC+(t4R|;#=T~bM=C^#7_zihd-num)~@3GBb$M>Nh+EsW!8>Oyw8ya`*DPASZUDy7z z_|_h%8mJw>j<-K44)i^~ETGqlU!Z@VPX}7DVFXR3{=lZEem|-9x@sxYFF_@Hg4n!S zcl>t5ue6j^3dL>CGjKsx9;=K08#(EBHC0pfr-faVWiKUF4lqo{JZ}hhCU3E`lyQ!YHp1(w0e$!aUqav^XA#8ErS2Jj z%?0SXn!ELKKzVbxh;nkLdn^g9sFuVxb^w#*LxzBY%(0 z?;Ci+rvgdRO8MHMK!$!oUj!>8ceX~ZjoyHi)NRv_lRj>5U=!lf8hI3mC{R2U`NAQ{ zXNY{3$mjkCnfG4E`A;&6lGEfa23EUbI~Yuoz3zLkjgg^kjBTMi6}(&0!z0_cHvH=r znv-0*@sid$^*P0>F9F)jxjGVrL%MqD{%9J@CMbm3SL^QN&7>Cd>`C9$E1ar>(rN+M?Zfcl{Qd#~k z1fGfsVye{G*u}K(>5OvWqwn!x7q?f3c5o-Pi_|LfY+)Ps(t}C{(Pj2B^1(3U%wlEo|Z`y_KPEDhoG4-E>yg zGd!mXfr8rKxUFGY!-Kg{zNjysTG>-jt18%l7sQI-Ykb3Zlqe~D%aQJnH~{_y;e%;N literal 0 HcmV?d00001 diff --git a/target/classes/cz/kamma/term/service/EncryptionService.class b/target/classes/cz/kamma/term/service/EncryptionService.class new file mode 100644 index 0000000000000000000000000000000000000000..69f4c4c777d9cfd729a6beac2edf1a866300e598 GIT binary patch literal 2351 zcma)-YggMw5Qg8y1{os=7(%#{0Bvk8Q3HggAx&x<2rlN*m^!6yONG1vfh|!<5J)e{ zr+(+e7Q1n?o=Mp1{5gs_ZyG%&R8 zs)wplQjKk8b!(Sv4nxCZ-O!yU454@;A4LR>5~4DiAT!+6UMqWQxvVM~_1 zOO)4mA>~ev;5`ZN%eag9g}VbsFDWV2<}=d_QF2EaH7SD(eQ_^SN`=XYKTA=JVobug zj0xOh=yJ1M!;qncyAIdQ3+7HibM;uF7{LcJ?qky9nB)J(;m;DsG#*Hpkx5|>YjFf_ z8j7w%hR#|?B~sUeM?#yk4D|vrAHl~A9c!tjxrNCWy0f!bR<+z>a`M4U6i@MqgrpEi zilM(sAc3G3RLwChQsGps9j{TLzi3v%934TGV|$-#%35lk#F3-F0(e2jGc1xA9W%Y7 zT1m@NX+I%_bOfIgy=|8HrllH1v%G1mC1>;gZ+s?d5SHWeYW~Mv%a{@5W8x^(402iDt7W}>3}LZoJ(d2IGm;` zV7QUh4Zdcf)Sk~0kQeL%m@$3(VogJNOFp>SAU5Yd&QC*1E#qh9(Jg$<~ zC>g5%ebu&KnO2dGtE}488rOn>(4OdkUDxQ02pTlMmEEki@f(#_x&o0G{T@?SAhk#5 zk2LPKTw4P53Aa6ZjXanU-G}m4HCt9gQc3uyZ{PsZ|HA1g1 z(60wy;~T1=P4}spp1-Yl&LB*4^n#f)95OD1}K8l zQzKqto3;^)e@2L00V88)kd|Z3XSlI45gtFm&56+ybe~}GQTPnQ-Qg1?ekHp)2;ZIV znpV2D(GD@Xwxbsv?q1WLKkN{)_>4ez;p;JoUF?yECUm2OGVR<;ECx&pXrC-aSl`iz z9nk&a)5R<4>Rq}sCA%PMqgB4)Ka|)JOS0!Z8sH(YyFHKyexkfi)BOz^{{a%TJ9Pj6 literal 0 HcmV?d00001 diff --git a/target/classes/cz/kamma/term/service/HistoryManager.class b/target/classes/cz/kamma/term/service/HistoryManager.class new file mode 100644 index 0000000000000000000000000000000000000000..3ce70f9ba28c5b923b4a8f789139d5ff57fcbad8 GIT binary patch literal 1427 zcma)5T~8B16g|@q3uP%#pil)7M4&}kzrR2f5QL;?A_gOGL%XR9+b(XmB=|pk&|e{m zLeRwcTTP%zpi_hDzU`PZo+oz-cFj*BfoL;YU?k9LAcZ!8;o^I9 zQ&uX{^ewMq)+}$^E?VXy;jXu{EUU6?c>;;IL4fRqf0`E!Ej2O*W@*L|1EUzDZ`QlIbZQFPAT0#uw1m8YaRrUaQb|!< zGccj55HC8G^i<@!fg8BVWJJt3jzBg{rrn=&9fvit-Rfj+O`v(jE?1>r_o#aKgq2eb z8bW)fC;7%Xw?uSX!LC}*>y>rOdnMN$M$!ehD4kX5+3FkyV?IkIFjDxR!ZLe1Rll?< zu5Zn`Y=dg)gc+fb+$!r+JVc~VD-~#6@nvyyS#E_&IviJqsl#?+TQ*vw+eal%d5r?7 zK6CtKJ6ngZ8$EgdOwUx#BwOH!HhtK7x+I#H7M%*7nT)Ac+`3n^7HnmDXP7h`jS1D8 z801hzIIMyz$BN%uJVkIDG446FT<Y3)hIHD=! zd3cN`gsCU{{3?lAO6&=AToq6ovYH`lOtYr>YH*-p=tC>|$v4ozm_1_5Vn%yBOLX9k wirfx0(Ba3GN#)Nq0SpK0{sUvRN9U;qFB literal 0 HcmV?d00001 diff --git a/target/classes/cz/kamma/term/service/SettingsManager.class b/target/classes/cz/kamma/term/service/SettingsManager.class new file mode 100644 index 0000000000000000000000000000000000000000..570eec19a7f597b9dab2fc6aeecae9f6b0422b69 GIT binary patch literal 1912 zcmah}+fv(B6kW#`1VWH-zvRL#Tx>3CNTCf*-4X%>YC{`SQqnezuoXwdwmgy`nf^(? zfY)ZGftfPX_N_DhhW>z=Z`-D_yEsUL=aUF(-23K z!1Y7p*w9NxrKoQo9GZDgpy`2CvAncEbSkl{pjn_hZzRp2~f@lj` zp($w7(2i>Yf*7}qc%Lu;?`3OEVG^CjCg6-)_qUHad7>DaF&f{IBbHB4iM+>JsZcT}x9 zrt6vof&NAsbqo4bFjqsP0vp?_Z}Mi!uP7X$TXeVpS$!twH-5(KMn0Eo&Q@)%~sI z>?*@)Fb_+Bvai?`*E1^Ku2DKNS)D5^IqH|OD(2Qv`M`8`WTvFGoi|FmhGWTfxEb?a zT2yTEO4EO=6KjbjpMdhHiFA9xT4We>`Xu!843_3eh5 z;8~kr1zDpSa&B_`Og7?#g45LtRtB}KL&-`ZhMKdFoV+Pl>>afL!pA=+%L*fW(GfPJ zCMFdpzC}7L?D5rTM2h*9IqQcsgwGh zq@=^ebM)ISeV3fRz^)&`V#xX_ zapur;L5PTgRs~}a3MMaPD-YIO`CIWnARa21jZq0UVxmxfF+t2YG4{_f???K+3}N9r zq|b4GZ~6?WY*L0k)0sZSC%+*54qY>+cyxlEtzd3lip>thD8Q@~Aj?f%>hvqxnawn_ zxlR))$siluJjvW+zggnkfk#-cOES+~f^w-C;>j>?kMRB|ErxmT4sVXKuEr?e1m&2- zb0mC;k_=!FNqmVt2KG3VNLJ#0D3MChp5ZG7EX7)YazW5QoOy-)Lxg(8e7)pWO){k> NSqF{3rf~${`~@|Yn>7Fc literal 0 HcmV?d00001 diff --git a/target/classes/cz/kamma/term/ui/MainFrame$1.class b/target/classes/cz/kamma/term/ui/MainFrame$1.class new file mode 100644 index 0000000000000000000000000000000000000000..adbe05b96adb26fe29a3489f2dfd72d311678aee GIT binary patch literal 1873 zcma)6TW=dh6#m9`Y{!|Vu3JKzq-{t{>s-ht+zO-#v?fVi>@+lPXv<9-dq}of?^@n< z6ICFUi_`}$QXl&S5Bve7Vk90APdxCKkPzRj9ovZ#)RlH;_RN{@eCL}v=kI@i{{z4! zeAXX%XRXW=L?(+sO=iwj^%6`*R_!nq;O8+JVpfKo628xEr+hD%y=h{ zz(&tejKz?aco7!_PH5`;umq0VZpqZ~if+y+t6hfdlA{STp3^`XiE&H_Bt7M?DVr=+ zxwR#d$u)JFl6VQz0+O_{Zl&rv)KFlcn_Al*Br+p0iq_9%9=dH(qZ=FdNNp~LIf2prT36h%vW;w8;+*rDo8alpODy1R zYT<3UJ6!hFu3u12U0}XTiw^Hx?!XN_47hc3u1H+fi5%04*j8yf>((7#M}1df5lhs{ zttzL;im)8h7Py#sHgJLS2R*whis310mKigzyyd8sdSye^ispt*ODfM(x7W>@rT<&T zh%P{u!-4qSsOed%nk*=P%Pp^(x^h`(+#@Tx?pABc^Hfa`fBDS@saKH3$8QvZUmw@`9cX&tJ>>aE{{T&(zvLj#@xNFh-A-G)ex`Sr43xc77nx;477cS z7`~)dU$KpTjSKh&OL!EJ8=>C-W^fa0fdtF#l$Tp67qyZQ|8R@Itv2Habk~f>82_D~ zpKx{`hr-lf=+l;|2C^MC14IJ8<;j1?sNZ7{Kd?PL?qVXbjt>GRV{Im5T};9dqln-( cVM4fryBs6z2Or|&z;YU&a7>V?K2JXX0Y!+YQvd(} literal 0 HcmV?d00001 diff --git a/target/classes/cz/kamma/term/ui/MainFrame.class b/target/classes/cz/kamma/term/ui/MainFrame.class new file mode 100644 index 0000000000000000000000000000000000000000..29f9ab6eecfd008e0e6c2a51705fbc725d131b59 GIT binary patch literal 14693 zcmcIqd0QZs7T4jR7y((&5T3c(?(z@5W)Y{tCT9xm2?t5=CGYQ)M@wsH~yUV#} zU+%q6e75^OB05UF?ScTf&i; z1J9SVQY#{Mpk@a9x)eY~eP9&>>8R3_m+$rBYyHGPCnk zd*=$1DyfQT6zI&J03!CYZyuyM!mS#Urqc{?E0x&V91nN4gp-i)vT!oFIiBd$Dsqy{ zJItiRMWMzgyW^X4CoPJEqw)1J=?IgKlu3o6Un`pon$2WteTc+s8zWK6M@KUi&*;&c ztlMC1_0ue>6I~rCt+^&0E65aDTOvu3{4APp(s8r^U8(r8aH^YWY(=H5Vllw%0?b&Jt6^RgErFl@b*Y6*9AM`Bh} zr=M2PN`qFJw3@!OU-fK=btJa-ih71@g@u`QTDp(v=wVE97$Y6pCF*^WNo(k2rjfSD z2K6E^biP4fb{Vf3(6FV$(nyIMzhY8Yi?-8>S}Dt*4mg!;t6$>6Zj4w_(A_R>tP6^n zGpWAY>e#R_zQspSh4STAkJZy|C4%u-Fx4GN20LKSeb8S&SyX4xh8$@vZ9fGz>?25O zY(osQg0>z9-JR=v1pnX*Ssi_eNNTG=8{sW-wqL60Sr(32e(I&9Nh#`s?TbyoMRY|H zJ+?709XqU-$zqy~&77>vX0;-!SPc@txUvs}&BwU}q`e)wfLgwk-8Wk|D^aBV|DX ze)^6{-_=nh8Qx^&8oemyxh8#2w9>F_{WIu%I0$VX8KZ)Sj@euj3;n)HKcEXi(`GQ( zaxHP~u$Gv`nfmTuWHTJ+TJIvXSXIM)Yne-zeWK=IAQYw@RcWhXZ2zNjY zFM-+Sh)=Skop7gB!>WaYcWy*7=z2%{Yh_>|+_9Bt4sVV3L5FXE;9KK~RJVxfCfVX< z#G7G(XYtX|If-?*GEK^++li{p@VhN>*r=awvBS*mCfz|lV=9WeLre!~m@ZH9m%|4{V;zQ-EGirlLqJ>n3L8ZJ!~V4RU;D+ed&KAHcY^gKgI;tpPp!F&B0W|ti4FYp0{vcQ1pM?I{lTO^(#!Bm z*kxJ5>ar3EtJBU5vs1LpYMr9Jn!o>waC^JeY487vt+8*I^d|iY&McJ~L#)W<$KWz> z<WxO=bH&TQMehj4?=sa?G}-!0sRdQ zD6F+k=c~?3U?ZxR~6vJ)+vR3wnFqNpt+n z>;pMm#B@?VIrj8Hx78HuiVqX1zi<=8WiDpZ;1MQ|l<1TFDScIaJ(f>-B7+xaMykCwqP7=-j-im3%C zEl_1dC2`u$btcb|2__~q#yk3w;r6Iy@UcuIGRQV!p^xXoPqoBj@nkR3kxoC)W^<$%t_x5N^+?+X7%Xet7>W?0$upnS5ey$gw(`L}v{S<$LF4@!r1P7Aw~0 z=jF)JcvXIpAY)CasCVQoq2n#{+jhse({ zsuQ=CGPzHDnz*aQabzv%+ida{M$IC9%aESx^8-{fP5f1pPvz4vvMC(xvzCIwG91|0 z#+R+(K0cjE&73LFzHaiFvV(Z{l{kut!72LqEJ=OQf&{;XrfiMo*=Wj+Xnq$>nU3bU zOjD(H3RXldlrTX{>Y`vO9^7DAy+M7-fTNX4%sda8vHW@_obb_751h@B=14$PdBK14cs>M;1f4=hiQnzLp<0`4N$x z9~#kOC6hRA43NQlOn!_XM_A|yZ?J$04mMXuQr&3T$E0~Bkt>j)go*n8!sI8VV$>%W z&L++=9zV^`82n3W0VyFYQd&(Br2g&B3NU%ClcK%x{CU*496ZrNiq0$`Mw|StUZncB0oRR zFN#*Y1b^hXiD2af);%_nxMT1e zQ0TmhB7zg-gu$pwaw@P;uO4XalXI*VU4a3uKb!ov_)+nEvZItmetwgu`}uW#PssWU z(_~xO-B92_(l8MHef(F*x4t_bPg=o+IPh4Xi1)=h4gQ;3vx-#e<57tad-+3?{~
cl&Zk*7YOH|r0n{o9 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 literal 0 HcmV?d00001 diff --git a/target/classes/cz/kamma/term/ui/TerminalPanel.class b/target/classes/cz/kamma/term/ui/TerminalPanel.class new file mode 100644 index 0000000000000000000000000000000000000000..9c3c3232d8569481443c8355e6c46cc9d31513c8 GIT binary patch literal 9879 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 literal 0 HcmV?d00001 diff --git a/target/classes/cz/kamma/term/util/AnsiHelper.class b/target/classes/cz/kamma/term/util/AnsiHelper.class new file mode 100644 index 0000000000000000000000000000000000000000..30066a9a5f7b7539c6298bef1b3c24a3a727be7f GIT binary patch literal 7899 zcmb7J3w%`7ng5@8-8++;#$U1dzzv@K|}MlgR}}CNptnqG)|! z)o$Beb*pW+uu}0kE!$c_4WJfZEp=PBYuE19wz}>1wQlR`Zmn&lBK?2&-h>IkuFj8> zbH4AqzW4W?%eUWu;y8fw)#4C5@M`cG@FSpLUJ<`4-js@`cQmc(y24826$0~<>12L^ zg15e5V+cBe8bSt2U<_H_na?KEI~25JZfQ?%{u+Dz<`BwIuE8`=fl7t29KX9McQuzb zU9{fXonISITM89BtbAwwnv~VOB$MdtvC_PEQhme9;^ld+ZW_9%Rl^7_9KI^q)0?su z=SfmmU*77p@*z|So7Di)7s%emv)`pEiQZv!O zBurNDx2;^C@9Bv*<*jT_ zQ(r!rYFeDmC6`;NUMs6$EMDE&aoO6%>(?*6c(p>rxyW8dW~|*!YvXzDOt*&6gc%yn zGcXgg6sm0z4qr#*_r&vwopSapM+S~5weO5)JFP4GtaQR^b+qbG<6N`K*~Dw+pjpHD z2Ih)!4ZFr&r{LS3On1xb76YxAPgzkFS=tOuv_njEfq{isq!7&Yb>&>c97*lEciZ;u z7%VSsK%0SfETO1-vzdgI%dJhtlWDO|g^lOX(XKQy+$;546Z3grCK$c*h*9Hn2v- z?zKb>I@TGuSl~d)O7F<;6x4dr!15Ao#3l`YW#AH-{{UAia{3?x!Akd#GHfxh72Bv6 zT6udsYvtEwa>;x$lcot!SQ|B5rZ8ogf40TDS7s7%S-5m}!s?Yl2$v#mpi6>Wc`rj1 z;|=|0eKwx3G|*`sYb(-XTVFEOZ81P3;1!N`7}$vR`*3q}4{g;!g{zRrCu<-F9%m%eIV(%DoH#nCzT?Aaafo4wb`rSCz}48T zpyx9KhmCM!spF1=mY=}220kVd4pPF7+=Zm;4cs8^qfr~;2EzUx12^I03MHv{F5i*v zwsx=C?#Q4+w%lUiR(yi&XRV&hRSfBVakq^+KFLMn`4h@MEi(EWg~oH3><7y#gxhet zhC2-W9X_K_Wk(-dQ4&|;35F<?G2lC+@$~^{&NOCF9xXZjOTW? zXSyvNU!eSZWNx2MQ|DyCRypj8+_6H(0d^PZxR2d(9S^YEuHzwg zi*(gi7n`SvpRzn=PU11q1(tZ!3h{(9pSq!e}k#pg1H9aqV*j0I{G3 zylCK`@m=cL%Eoh6M|x#al613k>X0+e3J=3A);aJc1K*Pa%iIH(By+u~_%%9SR`AT6 zBWn0J1OJX6P(=yqkl~k#s3~S+Xbxdv(SRQs_z`|gf6iJo-$75{&W3Z4dTw@gyhc$n z3+VWXfuG_(C7JZ5l61(a#n-!1Y}hiK#SnguUugKHfnVXjSj(KT&yIASd6Wv#d*fLV zBu^b;T?3~wX^p{u8~8PTL$8yZo=Gu;L_Z`On?=fF@IMCLlmo<>JM!^VG9lRiHSm9e zHHh6nzs;to%^)YWYOq;8er!61q+qc;BY$OkqQ9^F*bT$32);j9tDdjHvI}?8c5Wm6WD@ZnkkE-s--d zE-OnuS)UB11Wg5oM3cb@R$)pvRM4)%HZAX1zf@Nx3Nvf!7q)EOQom*4jK+Db+oBEi zTNX9mu&v>`t=pzY7jjO!u4e1jhU>P}H*DL|HeoLR5{g}$n^9kQ*@-i z5dMZ2RjmwW60vCJ;)V_<>!p@tJs@c=wK1Me3hW|%`JG9+z_^uvvHIh&AtzHwML0;) zfO8Ml=5CQ%lozFZVYH-0$9s5BvB9|J?C{zXmn^7A=2TnH14rBS(794HNz!G~c@k6) z*>r{;ibEFJSv-^H3&rYo5*P34U`f)QNhnMhve~}ztVl&WO7vy3d})Z0l8?IKTh3wE zq;YteJW#)qZ%K|zh3`_kyjT_Qb+tn6GZ;t!uoLKN>uPaBDd44YU7uNErK~*R64nl9 zAVtmi;|_u2h7&mGU!7K-lzDPpgX9${Y@p_##s^KD412cowdU#KA78?@FTZr0s5(cC z(^NC#ijYjcq|(-8Y;)?jErE2hCbw921iG@x9Xq*1ZKgu!C%DjlK889oec6Py%yxnD zGw+b;5>GIJ@!Z2FslsS^yl@oam~T=jWfE#4#AFVC^a#`;4m_%Ye>u&+K~>4WhOVB<|_h34~(((DuT2DB6$G)lZ`8k|PMW_#(b1G4c{`1zzwT!KkCC ziTDmKPUyfF`Jk@RGI%)g(0;KGJqGW~wRF z??UWRbzB!vQ@N)J!4jM%L7Ij#4Z706BL5E>=J3Qxr%*>6|GkS~c%_C*10;uTId0sEZ!h|k-PYocCf*D+k@ zh3HR?yRlgh>z)J9yai}s-FJC&FdVGC4}sd|ptHO=6b^-h55SCugEHzqhMT=8U?<7e z4`AW{d#z1s?E^U9CCu0DbB~z9BZ3Env&z5j$EV0yRXA`If4dJQ5uaGzu1&()`!Bh7 zgom>xe&-n8?KCRj0(#W~gsH?@ET)OIQ?W~_k7axwE~k-Rgl?=t3aj~?Sc6+=hIjFy zcsDxn6+RgbV-p_7W_%r6@C`onzQ-Nk$7T32;;g8;@D5G(PuQ;f*rBTV@*0a>ssSl= z9(vR~EKm#ihPn_}s!Ncw4Yilrzl->K+feVO%{0)kScYSYigFa>O(Rr{qY!G<7(7Lj z53s73rKS<*7t65?752zAW8xasbO}uyi9vfAZPuwKH3L4|WMeQ-!O)OU@1aeV`Ny5Y z1hL$9!XfoX)OhM@Do&#OB>WMQ4jQQ#<`EuRF;f_4d8%sFYF zaNC{irOBCtEjH~azEwc1F?JN+DPWTH|589r0iy~CGbG0<|Fs|A--}=Yfdaf|;!+F0 zHv?Be!)~vRYrUoTnAgPh-Y{@a?N48Ql_8H{Yv3B5N61*NJP5ec3^c`OoSwG@l!td$CQiF-#5 zUW#W4R=eOE1sofL=LVHIc3JAhIWEcIIf^NHB(ERVa(H+NG+&NX8>+kG#p+3mU?rV!EX&T_B6 zMYEIQ4N`l}Rr}O_<#kiwaaJq9mzZ?EjB=(D6JKRD_ApK2YnaRACY9O}7H}(AQLSUO zbP1kda(fCpn1*`rG(F}SetUb4-tas=g>SMJJAp6Y1w2F>d5l(a9N%GDe37H?GA+J@ zSD7Gxj+gNpLcis=l9T*eqT`1&&mXCAc$E?RHMIyoQETy2wFy5{Ork1-U#c7ND|H)Q zSNrgW+K*qWdsvS>h&OF>Uk;7;P3GRoOq7rD-8+k;AQq}E{0cRmvyCdNX7i4KSgP#| zWSS#!27WYEcc?k4nFW|Q!$OQI@tvkH0`vzd+tm5|r|3QJp_-`o_+LP!@|)`^D=Vvw z)S9&?F}h7t-jiUa^wS;qJ;!km1{SBbpBg<&|4~P;Q88=z^M?F+OG1G2Bp5BU@{@;2 zDOKZbeo*)wtKfG~hu_;gJI{ovHV5*`;Fm7vO68&R%ifJcinmqM?9y#Pt;Z?c0=2B$ zd~UhsuY8tOB$Gi^H0%re!@hxnjRo8g*tbe72(*OtgVlQ-B@KS-C}YVR=Hw)bDn|R7 z&5~ycb-PZ5_y#Dksa`TP;YPOGZ3FkpyVIri z;2D4~(u2h_=7vGYD|5x0ia?nv0v)IbG4RlRg!elY{YFXWe8l-!XU}FpS*pfky_&$f zb|UNRN&NIb8F#8F{O(92!ZL3LRC( Uqo@~eS4AHH=+k@-NCa5)XQ*{4>Hq)$ literal 0 HcmV?d00001 diff --git a/target/classes/cz/kamma/term/util/SystemIdUtil.class b/target/classes/cz/kamma/term/util/SystemIdUtil.class new file mode 100644 index 0000000000000000000000000000000000000000..f3e478c423248185fa05a5e088e7dac17782be1f GIT binary patch literal 1691 zcma)6&sSST6#ga&nGjx~ga(>Y1=M1aHlzhge}tl>Qi}v=O`)Z9Gb97NlDx#1mr}Gn z$A$ldTQ_XFs2hrU>~ZD7|G@u57mD9YQV9`{=H$JZcW3UM?|$E%`^%r-zXosxkAm>P zE8#QH2EV|`$Lfhn7L>J-TwHssbB=(2(zHxxO28Y9ElUIh`VKc`9ow`v(p9sN*LDyR z-T*oTWT`S@siF?z1Uec6J1YqnXy&sn|bF?m;~U zP9aQqs$AAqULf(md>c?x0rUy9S1a0X0FB^`M8AQvcuOEsLt$wrxuBixlD#!+Ioe)V zIW2ICh+C}{xFar8kDNnDh;WMC9y0-cBKr*|ACE1#%BRWGg!^hDFmO1R5B?J_1Lt{AwA zYe(g+)dkgAFWE)KD!O)zI;%~})~oH`#7-HQ#0`OfQ>s^7Yx;F-O5y_|JxpTeX-=12 z5dxT|MC+O2D90^L0$KEM>9A zCoSNyOo9G7OUD%>Jy|s^3VT*v&k93Cn)p3(QR&ysct;AGNc-yhTT7F>1#=8HMf;mq zuJsI3?a=n z=KeeoZX@ISBYq`03XW~@R}kOUmS^~Om%$cti*KX80UzTwzuGa28??|_6u2)y+m}B; zuDn9~Ts#!qhp~sy{dlOiG26%K%=z!|w!qi8%gK8JKOr)IVGq%T#Lq}2uKOas*As2t z34hohZu