37 Commits

Author SHA1 Message Date
rudi a43969da08 Fix aid keys
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m11s
Release j3270 / Build & Publish Release (push) Successful in 1m10s
2026-08-31 00:11:30 +00:00
rudi e18d2f436f Adjust clearing 2026-08-31 00:00:13 +00:00
rudi 27976dd31f Add missing menus back 2026-08-30 23:46:29 +00:00
rudi a3c4b95379 Fix ADMDRAW targetting 2026-08-30 23:33:33 +00:00
rudi 3a576b79b4 Phase 6
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m4s
2026-08-28 13:57:09 -04:00
rudi 40ebd40fe2 Phase 5 2026-08-28 13:36:02 -04:00
rudi f310d39fc1 Phase 4 2026-08-28 13:26:40 -04:00
rudi a44ea00d3a Phase 3 2026-08-28 13:11:28 -04:00
rudi 037ad1f941 Phase 2 2026-08-28 13:00:12 -04:00
rudi 79258c0c44 Phase 1 2026-08-28 12:38:56 -04:00
rudi ff5dfda343 IND testing
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m5s
2026-08-27 22:42:40 -04:00
rudi 8654b72d8c IND testing
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m5s
2026-08-27 22:08:01 -04:00
rudi 2e9b6d325a GDDM Tweaking
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 41s
2026-08-27 18:14:39 +00:00
rudi 540a8dfd1d GDDM Tweaking 2026-08-27 17:05:23 +00:00
rudi ebc0b5ae6f Fix IND$FILE
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 45s
2026-08-26 00:04:30 +00:00
rudi bcfd4ba2e0 Antialiasing fixes
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 40s
2026-08-25 23:31:14 +00:00
rudi 2584f4289f Admdraw appears 2026-08-25 22:50:12 +00:00
rudi 3526e682a1 Admops fix
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m1s
2026-08-25 22:22:20 +00:00
rudi 7eebd2107c Refactor
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 44s
2026-08-25 16:05:12 +00:00
rudi 36b93df986 Antialiasing and fill fixes
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m8s
2026-08-25 15:48:36 +00:00
rudi dbe0cdaa89 Keep par0dy with a3270
Build and Test j3270 / Build JAR & Run Tests (push) Failing after 1m3s
2026-08-25 01:12:50 +00:00
rudi b5a4978b24 Keep par0dy with a3270
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m1s
2026-08-24 20:46:45 +00:00
rudi d2bdb9bb0a Lightpen on
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m8s
2026-08-24 16:55:38 +00:00
rudi e28b7dc30a Lightpen on
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 37s
2026-08-24 16:45:55 +00:00
rudi d9df1052a1 Lightpen on 2026-08-24 16:35:24 +00:00
rudi 0837de8db3 ADMOPSLA and ADMCHART are happier
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m7s
2026-08-23 01:27:45 +00:00
rudi 42eef519a6 Repo clean
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m4s
2026-08-22 13:17:31 +00:00
rudi b57d8f960d How had I not tested on Linux before
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 45s
2026-08-21 21:00:10 -04:00
rudi 09600260b5 Add some cleaning
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 37s
2026-08-21 18:56:37 -04:00
rudi 00c7b0770c Missed a folder!
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 46s
2026-08-21 16:35:18 -04:00
rudi 1212b2cceb Add tn3270(no e) and fix animations
Release j3270 / Build & Publish Release (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m10s
2026-08-21 16:32:48 -04:00
rudi bf5e4410e6 Additional logging to debug a3270
Release j3270 / Build & Publish Release (push) Successful in 41s
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m6s
2026-08-21 12:51:31 -04:00
rudi 7e658a6c20 Update readme
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m4s
2026-08-21 11:57:14 -04:00
rudi c32521fd4b Cleaner rendering
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m6s
2026-08-21 11:51:37 -04:00
rudi d7d2f8bad5 TLS and Graphics, what more?
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 43s
Release j3270 / Build & Publish Release (push) Successful in 1m7s
2026-08-21 02:51:24 -04:00
rudi f0fc1b6e70 Fix YMS bug
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 43s
Release j3270 / Build & Publish Release (push) Successful in 43s
2026-08-20 19:58:52 -04:00
rudi 10a31476c2 Update README.md
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m8s
2026-08-20 23:31:46 +00:00
196 changed files with 26905 additions and 3488 deletions
+6
View File
@@ -1,3 +1,9 @@
j3270.log.*
*.txt
*.py
*.pcap
build/*
*.log
.DS_Store
*.jar
build
+26 -34
View File
@@ -1,9 +1,9 @@
# j3270
[![Build & Test](https://git.hugfreevikings.wtf/rudi/j3270/actions/workflows/build.yaml/badge.svg)](https://git.hugfreevikings.wtf/rudi/j3270/actions)
[![Latest Release](https://git.hugfreevikings.wtf/rudi/j3270/badges/release.svg)](https://git.hugfreevikings.wtf/rudi/j3270/releases)
[![Releases](https://img.shields.io/badge/Releases-Gitea-blue.svg)](https://git.hugfreevikings.wtf/rudi/j3270/releases)
[![Java](https://img.shields.io/badge/Java-11%20%7C%2017%20%7C%2021-blue.svg)](https://adoptium.net)
[![License](https://img.shields.io/badge/License-MIT%20%2F%20BSD-green.svg)](LICENSE)
[![License](https://img.shields.io/badge/License-Unlicense-blue.svg)](LICENSE)
An **x3270-aligned IBM 3270 mainframe terminal emulator** written in pure Java. Designed for high compatibility with IBM z/OS, z/VM, CMS, and TSO systems over TN3270 and TN3270E.
@@ -24,6 +24,19 @@ Requires **Java 11 or higher** (Java 11, 17, 21+):
# Launch interactive GUI
java -jar j3270.jar
# Connect to TLS/SSL mainframe with custom port
java -jar j3270.jar -s mainframe.example.com 992
# Or using standard x3270 L: prefix
java -jar j3270.jar L:mainframe.example.com:992
# Connect via Plain TN3270 (Non-E)
java -jar j3270.jar -P mainframe.example.com 23
# Or using standard x3270 P: prefix
java -jar j3270.jar P:mainframe.example.com:23
# Connect with unverified/self-signed certificate verification bypass
java -jar j3270.jar --tls --insecure mainframe.example.com 992
# Launch with custom configuration file
java -jar j3270.jar -c config.ini
@@ -35,7 +48,11 @@ java -jar j3270.jar -c config.ini
## ✨ Features
- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, and SSL/TLS encryption.
- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, plain TN3270 fallback, and SSL/TLS encryption.
- **SSL/TLS Security**:
- Encrypted TN3270 over TLS connections on standard port `992` or custom ports.
- Interactive certificate verification prompt for self-signed or untrusted certificates with fingerprint, subject, issuer, and validity inspection.
- Optional unverified/insecure mode for testing and headless scripting.
- **IND$FILE File Transfer**: Full support for both **CUT** and **DFT (DDM)** high-speed structured field transfers with ASCII/binary translation, CRLF handling, and recfm/lrecl formatting for TSO, VM/CMS, and CICS.
- **z/VM & Line-Mode Support**: Proper SSCP-LU and unformatted line handling (`CP TERM CONMODE 3270` supported).
- **APL & Graphic Escape**: Comprehensive box-drawing and math symbol character rendering.
@@ -59,7 +76,7 @@ The project includes self-contained, portable build scripts:
# Build executable JAR in 2 seconds:
sh ./build_all.sh
# Run all 25 automated unit tests in ~130ms:
# Run all 57 automated unit tests in ~500ms:
sh ./test_all.sh
```
@@ -73,38 +90,13 @@ The resulting standalone JAR is created at `build/j3270.jar`.
---
## 🏷️ How to Tag & Publish Releases
Releases can be published automatically to Gitea via Git tags:
```bash
# 1. Create a version tag
git tag -a v0.1.0 -m "Release v0.1.0"
# 2. Push the tag to Gitea
git push origin v0.1.0
```
Pushing any `v*` tag triggers the automated release workflow (`.gitea/workflows/release.yaml`), which runs tests, builds the JAR, and publishes a new Gitea Release with `j3270.jar` attached automatically.
---
## 🔍 Topics & Discoverability
Suggested keywords/topics for repository indexing:
`tn3270` · `tn3270e` · `ibm-3270` · `mainframe` · `terminal-emulator` · `x3270` · `ind-file` · `file-transfer` · `ebcdic` · `z-vm` · `z-os` · `cms` · `tso` · `cics` · `java` · `desktop-app`
---
## 🤝 Contributing
Contributions and issue reports are welcome! Please feel free to open a bug report or feature request via the Gitea issue tracker.
---
## 📜 Acknowledgements
- [x3270](https://x3270.miraheze.org/wiki/Main_Page) — Reference C implementation and protocol specifications
- [Antigravity](https://antigravity.google)
- [Antigravity](https://antigravity.google/)
- Claude Opus 4.6
- Gemini 3.1 Pro
- Gemini 3.7 Flash
- Gemma4 12B and 26B
- GPT-OSS 120B
- Qwen3 4B and 32B
-10
View File
@@ -1,10 +0,0 @@
# Fixed Bugs
- Complete failure when using `cp term conmode 3270` under VM:
Fixed in InputProcessor by sending raw line-mode EBCDIC character data in SSCP-LU mode instead of 3270 AID headers and 1920-byte buffer dumps, and correctly processing subsequent 3270 stream transitions upon CONMODE 3270.
- Unable to start a 2nd transfer after first completed ("A transfer is already in progress"):
Fixed in FTDft by signaling completion upon handling `TR_CLOSE_REQ` / host completion messages, and adding state reset in FileTransfer.
- IND$FILE CMS and TSO:
Fixed command formatting options handling (empty parenthesis removal for CMS binary/default modes and option spacing) and added Query Reply filtering for DFT/DDM mode.
+5 -3
View File
@@ -1,5 +1,5 @@
allprojects {
group = 'org.j3270'
group = 'haus.nightmare.j3270'
version = '0.1.0'
repositories {
@@ -10,8 +10,10 @@ allprojects {
subprojects {
apply plugin: 'java'
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
test {
useJUnitPlatform()
-6
View File
@@ -1,6 +0,0 @@
Manifest-Version: 1.0
Main-Class: org.pubvm.j3270.J3270App
Implementation-Title: j3270
Implementation-Version: 0.1.0
Created-By: j3270 build_all.sh
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+58 -1
View File
@@ -77,7 +77,7 @@ fi
# 4. Package executable JAR
echo "Packaging j3270.jar..."
printf "Manifest-Version: 1.0\nMain-Class: org.pubvm.j3270.J3270App\nImplementation-Title: j3270\nImplementation-Version: 0.1.0\nCreated-By: j3270 build_all.sh\n\n" > "$BUILD_DIR/MANIFEST.MF"
printf "Manifest-Version: 1.0\nMain-Class: haus.nightmare.j3270.J3270App\nImplementation-Title: j3270\nImplementation-Version: 0.1.0\nCreated-By: j3270 build_all.sh\n\n" > "$BUILD_DIR/MANIFEST.MF"
"$JAR_BIN" cvfm "$BUILD_DIR/j3270.jar" "$BUILD_DIR/MANIFEST.MF" \
-C "$BUILD_DIR/lib3270j" . \
@@ -87,3 +87,60 @@ echo "=== Build Complete ==="
echo "Executable JAR created: $BUILD_DIR/j3270.jar"
ls -lh "$BUILD_DIR/j3270.jar"
# 5. Compile and run tests if requested
if [ "$1" = "test" ] || [ "$1" = "check" ]; then
echo "=== Running Tests ==="
TEST_BUILD_DIR="$BUILD_DIR/test-classes"
mkdir -p "$TEST_BUILD_DIR"
JUNIT_CP=$(find "$HOME/.gradle/caches/modules-2/files-2.1/" -name "*.jar" 2>/dev/null | grep -E "junit|opentest|apiguardian" | tr "\n" ":" || true)
FULL_CP="$BUILD_DIR/lib3270j:$BUILD_DIR/j3270:$JUNIT_CP"
find "$SCRIPT_DIR/lib3270j/src/test/java" -name "*.java" > "$BUILD_DIR/test_sources.txt" 2>/dev/null || true
if [ -d "$SCRIPT_DIR/j3270/src/test/java" ]; then
find "$SCRIPT_DIR/j3270/src/test/java" -name "*.java" >> "$BUILD_DIR/test_sources.txt" 2>/dev/null || true
fi
if [ -s "$BUILD_DIR/test_sources.txt" ] && [ -n "$JUNIT_CP" ]; then
echo "Compiling tests..."
"$JAVAC_BIN" -cp "$FULL_CP" -d "$TEST_BUILD_DIR" @"$BUILD_DIR/test_sources.txt"
rm -f "$BUILD_DIR/test_sources.txt"
echo "Executing tests with JUnit Platform..."
cat << 'EOF' > "$BUILD_DIR/TestRunner.java"
import org.junit.platform.launcher.*;
import org.junit.platform.launcher.core.*;
import org.junit.platform.launcher.listeners.*;
import org.junit.platform.engine.discovery.*;
import static org.junit.platform.engine.discovery.DiscoverySelectors.*;
import java.io.PrintWriter;
public class TestRunner {
public static void main(String[] args) {
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(
selectPackage("haus.nightmare.lib3270j")
)
.build();
Launcher launcher = LauncherFactory.create();
SummaryGeneratingListener listener = new SummaryGeneratingListener();
launcher.registerTestExecutionListeners(listener);
launcher.execute(request);
TestExecutionSummary summary = listener.getSummary();
summary.printTo(new PrintWriter(System.out));
summary.printFailuresTo(new PrintWriter(System.err));
if (summary.getTotalFailureCount() > 0) {
System.exit(1);
}
}
}
EOF
"$JAVAC_BIN" -cp "$FULL_CP:$TEST_BUILD_DIR" -d "$BUILD_DIR" "$BUILD_DIR/TestRunner.java"
JAVA_BIN_EXEC="${JAVA_HOME:+$JAVA_HOME/bin/java}"
JAVA_BIN_EXEC="${JAVA_BIN_EXEC:-java}"
"$JAVA_BIN_EXEC" -cp "$FULL_CP:$TEST_BUILD_DIR:$BUILD_DIR" TestRunner
fi
fi
Vendored
+1 -1
View File
@@ -1,2 +1,2 @@
#!/bin/sh
exec "$(dirname "$0")/build_all.sh"
exec "$(dirname "$0")/build_all.sh" "$@"
+2 -2
View File
@@ -7,12 +7,12 @@ dependencies {
}
application {
mainClass = 'org.pubvm.j3270.J3270App'
mainClass = 'haus.nightmare.j3270.J3270App'
}
jar {
manifest {
attributes 'Main-Class': 'org.pubvm.j3270.J3270App',
attributes 'Main-Class': 'haus.nightmare.j3270.J3270App',
'Implementation-Title': 'j3270',
'Implementation-Version': archiveVersion
}
@@ -0,0 +1,918 @@
package haus.nightmare.j3270;
import haus.nightmare.lib3270j.*;
import haus.nightmare.lib3270j.ecl.ECLConstants;
import haus.nightmare.lib3270j.graphics.GraphicsMode;
import haus.nightmare.lib3270j.listener.ConnectionListener;
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.j3270.ui.*;
import haus.nightmare.j3270.ft.FileTransfer;
import haus.nightmare.j3270.ft.FileTransferDialog;
import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.*;
import java.awt.print.PrinterJob;
import java.io.IOException;
import java.util.logging.*;
/**
* j3270 — Java TN3270 Terminal Emulator.
* Main application class. Creates the main window with terminal display,
* status bar, menu bar, and manages the Telnet3270Client lifecycle.
*/
public class J3270App extends JFrame implements ConnectionListener, ScreenUpdateListener {
private static final Logger log = Logger.getLogger(J3270App.class.getName());
private Telnet3270Client client;
private TerminalPanel terminalPanel;
private StatusBar statusBar;
private Timer refreshTimer;
// Last connection details for reconnect
private String lastHost = "";
private int lastPort = 23;
private FileTransfer fileTransfer;
private final java.util.concurrent.atomic.AtomicBoolean screenUpdatePending = new java.util.concurrent.atomic.AtomicBoolean(false);
// Dialog instances
private FindDialog findDialog;
private ScriptDialog scriptDialog;
private FieldInspectorDialog fieldInspectorDialog;
private PrinterSessionDialog printerSessionDialog;
public J3270App() {
super("j3270 — Java TN3270 Terminal Emulator");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBackground(Color.BLACK);
buildUI();
buildMenuBar();
pack();
setLocationRelativeTo(null);
setMinimumSize(new Dimension(640, 400));
// Status refresh timer — also ensures focus stays on terminal
refreshTimer = new Timer(100, e -> {
if (client != null) {
statusBar.updateStatus();
if (isActive() && !terminalPanel.hasFocus()) {
terminalPanel.requestFocusInWindow();
}
}
});
refreshTimer.start();
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
disconnect();
if (refreshTimer != null)
refreshTimer.stop();
if (printerSessionDialog != null) {
printerSessionDialog.dispose();
}
terminalPanel.dispose();
}
@Override
public void windowActivated(WindowEvent e) {
terminalPanel.requestFocusInWindow();
}
});
}
private void buildUI() {
terminalPanel = new TerminalPanel();
statusBar = new StatusBar();
getContentPane().setLayout(new BorderLayout());
getContentPane().setBackground(Color.BLACK);
getContentPane().add(terminalPanel, BorderLayout.CENTER);
getContentPane().add(statusBar, BorderLayout.SOUTH);
}
private void buildMenuBar() {
JMenuBar menuBar = new JMenuBar();
menuBar.setBackground(new Color(30, 30, 30));
menuBar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(50, 50, 50)));
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
// 1. File menu
JMenu fileMenu = createMenu("File");
fileMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
fileMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect));
fileMenu.addSeparator();
fileMenu.add(createMenuItem("Save Screen As...", -1, () -> ScreenExporter.showExportDialog(this, client, terminalPanel)));
fileMenu.add(createMenuItem("3287 Printer Session...", -1, this::showPrinterSessionDialog));
fileMenu.add(createMenuItem("Print Screen...", KeyEvent.VK_P, this::printScreen));
fileMenu.addSeparator();
fileMenu.add(createMenuItem("Settings...", -1, this::showSettingsDialog));
fileMenu.addSeparator();
fileMenu.add(createMenuItem("Quit", KeyEvent.VK_Q, () -> {
disconnect();
System.exit(0);
}));
menuBar.add(fileMenu);
// 2. Edit menu
JMenu editMenu = createMenu("Edit");
editMenu.add(createMenuItem("Copy", KeyEvent.VK_C, () -> terminalPanel.copySelection()));
editMenu.add(createMenuItem("Paste", KeyEvent.VK_V, () -> terminalPanel.pasteClipboard()));
editMenu.add(createMenuItem("Paste with Line Wrap...", -1, this::showPasteLineWrapDialog));
editMenu.addSeparator();
editMenu.add(createMenuItem("Select All", KeyEvent.VK_A, () -> terminalPanel.selectAll()));
editMenu.add(createMenuItem("Clear Selection", -1, () -> terminalPanel.clearSelection()));
editMenu.addSeparator();
editMenu.add(createMenuItem("Find on Screen...", KeyEvent.VK_F, this::showFindDialog));
editMenu.add(createMenuItem("Find Next", KeyEvent.VK_G, this::findNext));
editMenu.add(createMenuItem("Find Previous", KeyEvent.VK_G, this::findPrevious, true));
menuBar.add(editMenu);
// 3. View menu
JMenu viewMenu = createMenu("View");
viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2)));
viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2)));
viewMenu.add(createMenuItem("Reset Font", KeyEvent.VK_0, () -> {
terminalPanel.setFontSize(16);
terminalPanel.guardedPack();
}));
viewMenu.addSeparator();
// CodePage Submenu
JMenu cpMenu = createMenu("Code Page");
String[] codePages = {
"037 - US / Canada / Brazil",
"1047 - IBM Open Systems / z/OS Unix",
"500 - International Latin-1",
"273 - Germany / Austria",
"277 - Denmark / Norway",
"278 - Sweden / Finland",
"280 - Italy",
"284 - Spain / Latin America",
"285 - United Kingdom",
"297 - France",
"870 - Eastern Europe / Latin-2",
"871 - Iceland",
"875 - Greece (Greek)",
"1026 - Turkey (Turkish)",
"1140 - US / Canada (Euro \u20AC)",
"1141 - Germany / Austria (Euro \u20AC)",
"1148 - International (Euro \u20AC)",
"930 - Japanese Katakana Mixed DBCS",
"939 - Japanese Latin Mixed DBCS",
"935 - Simplified Chinese Mixed DBCS",
"937 - Traditional Chinese Mixed DBCS",
"933 - Korean Mixed DBCS"
};
for (String cpItem : codePages) {
String cpId = cpItem.split(" -")[0].trim();
JMenuItem cpMi = new JMenuItem(cpItem);
cpMi.setBackground(new Color(40, 40, 40));
cpMi.setForeground(new Color(200, 200, 200));
cpMi.addActionListener(e -> changeCodePage(cpId));
cpMenu.add(cpMi);
}
viewMenu.add(cpMenu);
// Graphics Mode Submenu
JMenu gfxMenu = createMenu("Graphics Mode");
for (GraphicsMode gm : GraphicsMode.values()) {
JMenuItem gmMi = new JMenuItem(gm.name());
gmMi.setBackground(new Color(40, 40, 40));
gmMi.setForeground(new Color(200, 200, 200));
gmMi.addActionListener(e -> changeGraphicsMode(gm));
gfxMenu.add(gmMi);
}
viewMenu.add(gfxMenu);
viewMenu.addSeparator();
viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog));
menuBar.add(viewMenu);
// 4. Actions menu
JMenu actionsMenu = createMenu("Actions");
actionsMenu.add(createMenuItem("Send Enter", KeyEvent.VK_ENTER, () -> {
if (client != null && client.getConnectionState().isFullSession()) {
client.sendEnter();
terminalPanel.repaint();
}
}));
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_K, () -> {
if (client != null)
client.sendClear();
terminalPanel.repaint();
}));
actionsMenu.add(createMenuItem("Reset", KeyEvent.VK_R, () -> {
if (client != null)
client.reset();
terminalPanel.repaint();
}));
actionsMenu.add(createMenuItem("Erase Input", KeyEvent.VK_E, () -> {
if (client != null && client.getConnectionState().isFullSession()) {
client.eraseInput();
terminalPanel.repaint();
}
}, true));
actionsMenu.add(createMenuItem("Attention", KeyEvent.VK_A, () -> {
if (client != null && client.isConnected()) client.attn();
}, true));
actionsMenu.add(createMenuItem("System Request", KeyEvent.VK_S, () -> {
if (client != null && client.isConnected()) {
client.sysReq();
terminalPanel.repaint();
}
}, true));
actionsMenu.add(createMenuItem("Cursor Select", KeyEvent.VK_Q, () -> {
if (client != null && client.getConnectionState().isFullSession()) {
client.cursorSelect();
terminalPanel.repaint();
}
}, true));
actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("Toggle Light Pen (Alt+L)", KeyEvent.VK_L, () -> {
terminalPanel.toggleLightPen();
}, true));
actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog));
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog));
menuBar.add(actionsMenu);
// 5. Help menu
JMenu helpMenu = createMenu("Help");
helpMenu.add(createMenuItem("Key Mappings", -1, this::showKeyMappings));
helpMenu.add(createMenuItem("Mnemonic Keystroke Reference", -1, this::showMnemonicHelp));
helpMenu.addSeparator();
helpMenu.add(createMenuItem("About", -1, this::showAbout));
menuBar.add(helpMenu);
setJMenuBar(menuBar);
}
private JMenu createMenu(String name) {
JMenu menu = new JMenu(name);
menu.setForeground(new Color(200, 200, 200));
return menu;
}
private JMenuItem createMenuItem(String name, int mnemonic, Runnable action, boolean useAlt) {
JMenuItem item = new JMenuItem(name);
item.setBackground(new Color(40, 40, 40));
item.setForeground(new Color(200, 200, 200));
if (mnemonic > 0) {
int modifier = useAlt ? KeyEvent.ALT_DOWN_MASK : Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
item.setAccelerator(KeyStroke.getKeyStroke(mnemonic, modifier));
}
item.addActionListener(e -> action.run());
return item;
}
private JMenuItem createMenuItem(String name, int mnemonic, Runnable action) {
return createMenuItem(name, mnemonic, action, false);
}
// ========== Dialog Openers ==========
private void showFindDialog() {
if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before using Find.", "Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
if (findDialog == null) {
findDialog = new FindDialog(this, client, terminalPanel);
}
findDialog.setVisible(true);
}
private void findNext() {
if (client == null) return;
String last = FindDialog.getLastSearchText();
if (last == null || last.isEmpty()) {
showFindDialog();
} else {
ScreenBuffer sb = client.getScreenBuffer();
int curAddr = sb.getCursorAddress();
int cols = sb.getDisplayCols();
int curRow = curAddr / cols;
int curCol = (curAddr % cols + 1) % cols;
if (curCol == 0) curRow = (curRow + 1) % sb.getDisplayRows();
int found = client.getPS().searchString(last, curRow, curCol, ECLConstants.SEARCH_FORWARD, !FindDialog.getLastMatchCase());
if (found >= 0) {
sb.setCursorAddress(found);
terminalPanel.setSearchHighlight(found, last.length());
terminalPanel.repaint();
} else {
Toolkit.getDefaultToolkit().beep();
}
}
}
private void findPrevious() {
if (client == null) return;
String last = FindDialog.getLastSearchText();
if (last == null || last.isEmpty()) {
showFindDialog();
} else {
ScreenBuffer sb = client.getScreenBuffer();
int curAddr = sb.getCursorAddress();
int cols = sb.getDisplayCols();
int curRow = curAddr / cols;
int curCol = (curAddr % cols - 1 + cols) % cols;
if (curCol == cols - 1) curRow = (curRow - 1 + sb.getDisplayRows()) % sb.getDisplayRows();
int found = client.getPS().searchString(last, curRow, curCol, ECLConstants.SEARCH_BACKWARD, !FindDialog.getLastMatchCase());
if (found >= 0) {
sb.setCursorAddress(found);
terminalPanel.setSearchHighlight(found, last.length());
terminalPanel.repaint();
} else {
Toolkit.getDefaultToolkit().beep();
}
}
}
private void showScriptDialog() {
if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before running scripts.", "Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
if (scriptDialog == null) {
scriptDialog = new ScriptDialog(this, client, terminalPanel);
}
scriptDialog.setVisible(true);
}
private void showFieldInspectorDialog() {
if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before inspecting fields.", "Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
if (fieldInspectorDialog == null) {
fieldInspectorDialog = new FieldInspectorDialog(this, client, terminalPanel);
} else {
fieldInspectorDialog.refreshFields();
}
fieldInspectorDialog.setVisible(true);
}
private void showPrinterSessionDialog() {
if (printerSessionDialog == null) {
String host = (client != null) ? client.getConfig().getHost() : lastHost;
int port = (client != null) ? client.getConfig().getPort() : lastPort;
boolean tls = (client != null) ? client.getConfig().isUseTls() : false;
printerSessionDialog = new PrinterSessionDialog(this, host, port, tls);
}
printerSessionDialog.setVisible(true);
}
private void showPasteLineWrapDialog() {
if (client == null || !client.getConnectionState().isFullSession()) {
JOptionPane.showMessageDialog(this, "Connect to a 3270 session before pasting.", "Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
try {
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard().getData(DataFlavor.stringFlavor);
if (text == null || text.isEmpty()) {
JOptionPane.showMessageDialog(this, "Clipboard is empty.", "Clipboard", JOptionPane.INFORMATION_MESSAGE);
return;
}
int cols = client.getScreenBuffer().getDisplayCols();
String endColStr = JOptionPane.showInputDialog(this, "Enter right margin column (1-" + cols + ") for wrapping [0 = no right margin]:", "80");
if (endColStr != null) {
int endCol = 0;
try {
endCol = Integer.parseInt(endColStr.trim());
} catch (NumberFormatException ignored) {}
terminalPanel.pasteLineWrap(text, endCol, true);
}
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Could not read clipboard: " + ex.getMessage(), "Paste Error", JOptionPane.ERROR_MESSAGE);
}
}
private void changeCodePage(String cpId) {
if (client != null) {
client.setCodePage(cpId);
client.getScreenBuffer().translateToUnicode();
terminalPanel.refreshScreen();
statusBar.updateStatus();
}
haus.nightmare.j3270.config.Settings.setCodePage(cpId);
}
private void changeGraphicsMode(GraphicsMode mode) {
if (client != null && client.getDataStreamProcessor() != null) {
client.getDataStreamProcessor().getQueryReplyBuilder().setGraphicsMode(mode);
terminalPanel.repaint();
}
haus.nightmare.j3270.config.Settings.setGraphicsMode(mode);
}
private void printScreen() {
if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before printing screen.", "Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
PrinterJob job = PrinterJob.getPrinterJob();
job.setJobName("j3270 Screen Capture");
job.setPrintable(terminalPanel);
if (job.printDialog()) {
try {
job.print();
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Print error: " + ex.getMessage(), "Print Error", JOptionPane.ERROR_MESSAGE);
}
}
}
private void showFileTransferDialog() {
if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before using File Transfer.",
"Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
if (fileTransfer == null) {
fileTransfer = new FileTransfer(client, new FileTransfer.FileTransferCallback() {
private ProgressMonitor monitor;
@Override
public void onTransferStarted() {
monitor = new ProgressMonitor(J3270App.this, "Starting IND$FILE transfer...",
"Waiting for host...", 0, 100);
monitor.setMillisToDecideToPopup(0);
monitor.setMillisToPopup(0);
Timer cancelCheckTimer = new Timer(500, e -> {
if (monitor != null && monitor.isCanceled() && fileTransfer != null) {
fileTransfer.cancel();
((Timer)e.getSource()).stop();
}
});
cancelCheckTimer.start();
}
@Override
public void onTransferRunning() {
if (monitor != null) {
monitor.setNote("Transferring data...");
}
}
@Override
public void onBytesTransferred(long bytes) {
if (monitor != null) {
monitor.setNote(bytes + " bytes transferred");
}
}
@Override
public void onTransferComplete(String message) {
if (monitor != null) monitor.close();
monitor = null;
JOptionPane.showMessageDialog(J3270App.this, message, "Transfer Complete", JOptionPane.INFORMATION_MESSAGE);
}
@Override
public void onTransferAborted(String error) {
if (monitor != null) monitor.close();
monitor = null;
JOptionPane.showMessageDialog(J3270App.this, "Transfer Failed: " + error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
}
});
}
FileTransferDialog dialog = new FileTransferDialog(this, fileTransfer);
dialog.setVisible(true);
}
// ========== Connection management ==========
private void showConnectDialog() {
ConnectDialog dialog = new ConnectDialog(this);
String initialHost = lastHost != null && !lastHost.isEmpty() ? lastHost : haus.nightmare.j3270.config.Settings.getAutoConnectHost();
int initialPort = lastPort > 0 ? lastPort : haus.nightmare.j3270.config.Settings.getAutoConnectPort();
dialog.setInitialHost(initialHost);
dialog.setInitialPort(initialPort);
dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls());
dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert());
dialog.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
dialog.setVisible(true);
if (dialog.isConfirmed()) {
ConnectionConfig config = dialog.getConnectionConfig();
lastHost = config.getHost();
lastPort = config.getPort();
connect(config);
}
}
private void showSettingsDialog() {
haus.nightmare.j3270.ui.SettingsDialog dialog = new haus.nightmare.j3270.ui.SettingsDialog(this);
dialog.setVisible(true);
}
public TerminalPanel getTerminalPanel() {
return terminalPanel;
}
void connect(ConnectionConfig config) {
lastHost = config.getHost();
lastPort = config.getPort();
haus.nightmare.j3270.config.Settings.setAutoConnectHost(config.getHost());
haus.nightmare.j3270.config.Settings.setAutoConnectPort(config.getPort());
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage());
disconnect();
if (config.getCertificateVerifier() == null) {
config.setCertificateVerifier((chain, authType, exception) ->
haus.nightmare.j3270.ui.UntrustedCertificateDialog.showPrompt(this, config.getHost(), config.getPort(), chain, exception)
);
}
client = new Telnet3270Client(config);
client.addConnectionListener(this);
client.addScreenUpdateListener(this);
terminalPanel.setClient(client);
statusBar.setClient(client, terminalPanel);
String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
terminalPanel.guardedPack();
new Thread(() -> {
try {
client.connect();
} catch (IOException e) {
log.log(Level.WARNING, "Connection failed", e);
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(this,
"Connection failed: " + e.getMessage(),
"Connection Error", JOptionPane.ERROR_MESSAGE);
});
}
}, "Connect-Thread").start();
terminalPanel.requestFocusInWindow();
}
private void disconnect() {
if (client != null) {
if (fileTransfer != null) {
fileTransfer.cancel();
fileTransfer = null;
}
client.disconnect();
client = null;
terminalPanel.setClient(null);
statusBar.setClient(null, terminalPanel);
terminalPanel.repaint();
setTitle("j3270 — Java TN3270 Terminal Emulator");
}
}
private void adjustFontSize(int delta) {
int current = terminalPanel.getFontSize();
int newSize = Math.max(8, Math.min(72, current + delta));
terminalPanel.setFontSize(newSize);
terminalPanel.guardedPack();
}
// ========== ConnectionListener ==========
@Override
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
log.info("Connection state: " + oldState + " -> " + newState);
SwingUtilities.invokeLater(() -> {
statusBar.updateStatus();
terminalPanel.repaint();
if (newState.isFullSession() && !oldState.isFullSession()) {
terminalPanel.guardedPack();
terminalPanel.requestFocusInWindow();
}
});
}
@Override
public void onConnectionError(String message) {
log.warning("Connection error: " + message);
SwingUtilities.invokeLater(statusBar::updateStatus);
}
@Override
public void onTN3270ENegotiated(String deviceType, String deviceName) {
log.info("TN3270E negotiated: type=" + deviceType + " name=" + deviceName);
SwingUtilities.invokeLater(() -> {
if (deviceName != null) {
setTitle("j3270 — " + lastHost + ":" + lastPort + " [" + deviceName + "]");
}
statusBar.updateStatus();
});
}
// ========== ScreenUpdateListener ==========
@Override
public void onScreenUpdated() {
if (screenUpdatePending.compareAndSet(false, true)) {
SwingUtilities.invokeLater(() -> {
screenUpdatePending.set(false);
if (fileTransfer != null) {
fileTransfer.onScreenUpdated();
}
if (client != null && (fileTransfer == null || !fileTransfer.isTransferActive())) {
client.getInputProcessor().setKeyboardLocked(false);
}
terminalPanel.repaint();
statusBar.updateStatus();
});
}
}
@Override
public void onSoundAlarm() {
Toolkit.getDefaultToolkit().beep();
}
@Override
public void onScreenSizeChanged(int rows, int cols) {
SwingUtilities.invokeLater(() -> {
terminalPanel.guardedPack();
statusBar.updateStatus();
});
}
// ========== Help dialogs ==========
private void showKeyMappings() {
String text = "Key Mappings:\n\n" +
"Enter — Send Enter (AID)\n" +
"F1-F12 — PF1-PF12\n" +
"Shift+F1-F12 — PF13-PF24\n" +
"Alt+1/2/3 — PA1/PA2/PA3\n" +
"Tab — Tab to next field\n" +
"Shift+Tab — Back-tab\n" +
"Arrow keys — Cursor movement\n" +
"Home — Cursor to home\n" +
"End — Erase to end of field\n" +
"Delete — Delete character\n" +
"Backspace — Backspace\n" +
"Insert — Toggle insert mode\n" +
"Escape — Reset\n" +
"PageUp/Down — PF7/PF8\n" +
"Alt+C / Ctrl+K — Clear\n" +
"Alt+E — Erase Input\n" +
"Alt+A — Attention\n" +
"Alt+S — System Request\n" +
"Alt+Q — Cursor Select\n" +
"Alt+L — Toggle Light Pen\n" +
"Cmd/Ctrl+F — Find on Screen\n" +
"Cmd/Ctrl+G / F3— Find Next\n" +
"Cmd/Ctrl+T — File Transfer\n" +
"Cmd/Ctrl+D — Disconnect\n" +
"Cmd/Ctrl+Q — Quit\n" +
"Cmd/Ctrl+=/-/0 — Font size +/-/reset";
JTextArea area = new JTextArea(text);
area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
area.setBackground(new Color(30, 30, 30));
area.setForeground(new Color(200, 200, 200));
JScrollPane sp = new JScrollPane(area);
sp.setPreferredSize(new Dimension(380, 430));
JOptionPane.showMessageDialog(this, sp, "Key Mappings", JOptionPane.INFORMATION_MESSAGE);
}
private void showMnemonicHelp() {
String text = "IBM Host On-Demand ECL Mnemonic Tokens:\n\n" +
"[enter] — Send Enter key\n" +
"[tab] — Tab to next field\n" +
"[backtab] — Back-tab to previous field\n" +
"[clear] — Clear screen\n" +
"[reset] — Reset keyboard lock\n" +
"[eraseeof] — Erase to End-of-Field\n" +
"[eraseinput] — Erase all unprotected fields\n" +
"[newline] — Advance to next line\n" +
"[dup] — Insert Duplicate order (0x1C)\n" +
"[fm] — Insert Field Mark order (0x1E)\n" +
"[attn] — Send Attention signal\n" +
"[sysreq] — System Request\n" +
"[cursel] — Cursor Select\n" +
"[pf1] - [pf24] — PF1 through PF24 keys\n" +
"[pa1] - [pa3] — PA1 through PA3 keys\n" +
"[up]/[down] — Cursor up / down\n" +
"[left]/[right] — Cursor left / right\n" +
"[home] — Cursor to home\n\n" +
"Example Script:\n" +
"LOGON APPLID(TSO)[enter]USER[tab]PASSWORD[enter][pf3]";
JTextArea area = new JTextArea(text);
area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
area.setBackground(new Color(30, 30, 30));
area.setForeground(new Color(200, 200, 200));
JScrollPane sp = new JScrollPane(area);
sp.setPreferredSize(new Dimension(440, 430));
JOptionPane.showMessageDialog(this, sp, "Mnemonic Keystroke Reference", JOptionPane.INFORMATION_MESSAGE);
}
private void showAbout() {
JOptionPane.showMessageDialog(this,
"j3270 — Java TN3270 Terminal Emulator\n\n" +
"A pure Java reimplementation of x3270 & IBM Host On-Demand ECL.\n" +
"lib3270j v0.2.0\n\n" +
"Features:\n" +
"• TN3270 & TN3270E (RFC 2355, SSCP-LU, NVT)\n" +
"• Models 2-5 & Dynamic Geometry\n" +
"• IBM 3287/3286 Printer Session Emulation (SCS & LU3)\n" +
"• IND$FILE CUT & DFT File Transfer + Host Catalog Query\n" +
"• GDDM/GOCA Vector Graphics & Programmed Symbols\n" +
"• 22+ Pluggable EBCDIC Codepages & DBCS Support\n" +
"• ECL Presentation Space Search, Inspector & Scripting",
"About j3270", JOptionPane.INFORMATION_MESSAGE);
}
// ========== Main ==========
public static void main(String[] args) {
boolean debug = false;
boolean cliTls = false;
boolean cliNoVerifyCert = false;
Boolean cliTn3270e = null;
GraphicsMode cliGraphicsMode = null;
String configFile = null;
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
for (int i = 0; i < args.length; i++) {
if ("--debug".equals(args[i]) || "-d".equals(args[i])) {
debug = true;
} else if ("--tls".equals(args[i]) || "--ssl".equals(args[i]) || "-s".equals(args[i])) {
cliTls = true;
} else if ("--no-verify-cert".equals(args[i]) || "--insecure".equals(args[i]) || "-k".equals(args[i])) {
cliNoVerifyCert = true;
} else if ("--no-tn3270e".equals(args[i]) || "--plain-tn3270".equals(args[i]) || "--plain".equals(args[i]) || "-P".equals(args[i]) || "-p".equals(args[i]) || "--non-e".equals(args[i])) {
cliTn3270e = false;
} else if ("--tn3270e".equals(args[i])) {
cliTn3270e = true;
} else if (args[i].startsWith("--graphics=")) {
cliGraphicsMode = GraphicsMode.fromString(args[i].substring(11));
} else if (("--graphics".equals(args[i]) || "-g".equals(args[i])) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliGraphicsMode = GraphicsMode.fromString(args[++i]);
} else if ("--no-graphics".equals(args[i])) {
cliGraphicsMode = GraphicsMode.NONE;
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) {
configFile = args[++i];
} else {
remainingArgs.add(args[i]);
}
}
Level logLevel = debug ? Level.ALL : Level.INFO;
Logger globalRoot = Logger.getLogger("");
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
globalRoot.removeHandler(h);
}
globalRoot.setLevel(Level.ALL);
java.util.logging.Filter appFilter = record -> {
String name = record.getLoggerName();
return name != null && (name.startsWith("haus.nightmare.lib3270j") || name.startsWith("haus.nightmare.j3270"));
};
ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(logLevel);
consoleHandler.setFormatter(new SimpleFormatter());
consoleHandler.setFilter(appFilter);
globalRoot.addHandler(consoleHandler);
Logger.getLogger("haus.nightmare").setLevel(Level.ALL);
Logger.getLogger("haus.nightmare.j3270").setLevel(Level.ALL);
Logger.getLogger("haus.nightmare.lib3270j").setLevel(Level.ALL);
try {
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) {
@Override
public synchronized void publish(java.util.logging.LogRecord record) {
super.publish(record);
flush();
}
};
fileHandler.setLevel(Level.ALL);
fileHandler.setFormatter(new SimpleFormatter());
fileHandler.setFilter(appFilter);
globalRoot.addHandler(fileHandler);
log.info("Logging protocol trace to j3270.log (debug=" + debug + ", level=" + logLevel + ")");
} catch (Exception e) {
System.err.println("Could not create j3270.log: " + e.getMessage());
}
if (configFile != null) {
try {
haus.nightmare.j3270.config.Settings.loadFromIniFile(configFile);
} catch (Exception e) {
System.err.println("Failed to load config file: " + configFile);
System.err.println(e.getMessage());
System.exit(1);
}
}
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
log.fine("Could not set system look and feel");
}
System.setProperty("apple.laf.useScreenMenuBar", "true");
System.setProperty("apple.awt.application.name", "j3270");
final boolean finalTls = cliTls;
final boolean finalNoVerify = cliNoVerifyCert;
final Boolean finalTn3270e = cliTn3270e;
final GraphicsMode finalGraphicsMode = cliGraphicsMode;
SwingUtilities.invokeLater(() -> {
J3270App app = new J3270App();
app.setVisible(true);
if (!remainingArgs.isEmpty()) {
String hostArg = remainingArgs.get(0);
int port = finalTls ? 992 : 23;
if (remainingArgs.size() >= 2) {
try {
port = Integer.parseInt(remainingArgs.get(1));
} catch (NumberFormatException ignored) {}
}
TerminalModel model = TerminalModel.IBM_3279_4;
if (remainingArgs.size() >= 3) {
try {
int modelNum = Integer.parseInt(remainingArgs.get(2));
model = TerminalModel.forModel(modelNum, true);
} catch (Exception ignored) {}
}
ConnectionConfig config = ConnectionConfig.parseHostString(hostArg, port, model);
if (finalTls) {
config.setUseTls(true);
}
if (finalNoVerify) {
config.setTlsVerifyCert(false);
}
if (finalTn3270e != null) {
config.setTn3270eEnabled(finalTn3270e);
}
if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode);
} else {
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
}
app.connect(config);
} else {
haus.nightmare.j3270.config.Settings.StartupBehavior behavior = haus.nightmare.j3270.config.Settings
.getStartupBehavior();
switch (behavior) {
case SHOW_CONNECT:
SwingUtilities.invokeLater(app::showConnectDialog);
break;
case AUTO_CONNECT:
String host = haus.nightmare.j3270.config.Settings.getAutoConnectHost();
int port = haus.nightmare.j3270.config.Settings.getAutoConnectPort();
boolean tls = haus.nightmare.j3270.config.Settings.getAutoConnectTls();
boolean verify = haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert();
boolean tn3270e = haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e();
if (host != null && !host.isEmpty()) {
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
config.setTlsVerifyCert(verify);
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode);
} else {
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
}
app.connect(config);
} else {
SwingUtilities.invokeLater(app::showConnectDialog);
}
break;
case DO_NOTHING:
default:
break;
}
}
});
}
}
@@ -1,4 +1,4 @@
package org.pubvm.j3270.config;
package haus.nightmare.j3270.config;
import java.util.prefs.Preferences;
import java.awt.Color;
@@ -62,6 +62,47 @@ public class Settings {
prefs.putInt("autoConnectPort", port);
}
public static boolean getAutoConnectTls() {
return prefs.getBoolean("autoConnectTls", false);
}
public static void setAutoConnectTls(boolean tls) {
prefs.putBoolean("autoConnectTls", tls);
}
public static boolean getAutoConnectVerifyCert() {
return prefs.getBoolean("autoConnectVerifyCert", true);
}
public static void setAutoConnectVerifyCert(boolean verify) {
prefs.putBoolean("autoConnectVerifyCert", verify);
}
public static boolean getAutoConnectTn3270e() {
return prefs.getBoolean("autoConnectTn3270e", true);
}
public static void setAutoConnectTn3270e(boolean tn3270e) {
prefs.putBoolean("autoConnectTn3270e", tn3270e);
}
public static haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() {
String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name());
return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr);
}
public static void setGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode mode) {
prefs.put("graphicsMode", (mode != null ? mode : haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH).name());
}
public static String getCodePage() {
return prefs.get("codePage", "037");
}
public static void setCodePage(String codePage) {
prefs.put("codePage", (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037");
}
public static Color getColorOverride(int index, Color defaultColor) {
String hex = prefs.get("color_" + index, null);
try {
@@ -194,15 +235,43 @@ public class Settings {
break;
case "behavior":
case "connection":
switch (key) {
case "startupBehavior":
setStartupBehavior(StartupBehavior.valueOf(value.toUpperCase()));
break;
case "autoConnectHost": setAutoConnectHost(value); break;
case "autoConnectPort": setAutoConnectPort(Integer.parseInt(value)); break;
case "autoConnectHost":
case "host":
setAutoConnectHost(value);
break;
case "autoConnectPort":
case "port":
setAutoConnectPort(Integer.parseInt(value));
break;
case "autoConnectTls":
case "tls":
case "ssl":
case "useTls":
setAutoConnectTls(Boolean.parseBoolean(value));
break;
case "autoConnectVerifyCert":
case "verifyCert":
case "tlsVerifyCert":
setAutoConnectVerifyCert(Boolean.parseBoolean(value));
break;
case "autoConnectTn3270e":
case "tn3270e":
case "enableTn3270e":
setAutoConnectTn3270e(Boolean.parseBoolean(value));
break;
case "codePage":
case "codepage":
case "charset":
setCodePage(value);
break;
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
default:
log.warning("Unknown behavior key: " + key);
log.warning("Unknown behavior/connection key: " + key);
}
break;
@@ -222,6 +291,14 @@ public class Settings {
setKeyBinding(key, value);
break;
case "graphics":
if ("graphicsMode".equalsIgnoreCase(key) || "mode".equalsIgnoreCase(key)) {
setGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(value));
} else {
log.warning("Unknown graphics key: " + key);
}
break;
default:
// Allow bare keys outside any section treat as raw prefs
log.fine("Setting raw preference: " + key + " = " + value);
@@ -256,10 +333,19 @@ public class Settings {
if (acHost != null && !acHost.isEmpty()) {
w.println("autoConnectHost = " + acHost);
w.println("autoConnectPort = " + getAutoConnectPort());
w.println("autoConnectTls = " + getAutoConnectTls());
w.println("autoConnectVerifyCert = " + getAutoConnectVerifyCert());
w.println("autoConnectTn3270e = " + getAutoConnectTn3270e());
}
w.println("codePage = " + getCodePage());
w.println("blockSelectMode = " + getBlockSelectMode());
w.println();
// [graphics]
w.println("[graphics]");
w.println("graphicsMode = " + getGraphicsMode().name());
w.println();
// [colors]
w.println("[colors]");
// Host colors 0-15
@@ -1,10 +1,10 @@
package org.pubvm.j3270.ft;
package haus.nightmare.j3270.ft;
import org.lib3270j.Telnet3270Client;
import org.lib3270j.ft.FTConfig;
import org.lib3270j.ft.FTConstants.FTState;
import org.lib3270j.ft.FTCut;
import org.lib3270j.ft.FTDft;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ft.FTConfig;
import haus.nightmare.lib3270j.ft.FTConstants.FTState;
import haus.nightmare.lib3270j.ft.FTCut;
import haus.nightmare.lib3270j.ft.FTDft;
import java.io.File;
import java.io.IOException;
@@ -53,6 +53,10 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
this.callback = callback;
}
public Telnet3270Client getClient() {
return client;
}
/**
* Start a new file transfer with the given configuration.
* @return null if started successfully, error message otherwise.
@@ -121,6 +125,21 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
if (state == FTState.RUNNING || state == FTState.AWAIT_ACK) {
log.info("User cancelled transfer");
setState(FTState.ABORT_WAIT); // Signal handlers to abort at next chance
// Start a safety timeout in case the host does not send further packets to trigger abort
cancelTimeout();
timeoutTimer = new Timer("FTCancelTimeout", true);
timeoutTimer.schedule(new TimerTask() {
@Override
public void run() {
SwingUtilities.invokeLater(() -> {
if (state != FTState.NONE) {
log.warning("Transfer cancel timeout — force resetting state");
completeTransfer("Transfer cancelled by user.");
callback.onTransferAborted("Cancelled by user.");
}
});
}
}, 3000);
} else if (state != FTState.NONE) {
log.info("Forcing cancel from state " + state);
completeTransfer("Transfer cancelled.");
@@ -1,6 +1,8 @@
package org.pubvm.j3270.ft;
package haus.nightmare.j3270.ft;
import org.lib3270j.ft.FTConfig;
import haus.nightmare.j3270.ui.HostDirectoryDialog;
import haus.nightmare.lib3270j.ft.FTConfig;
import haus.nightmare.lib3270j.ft.FTConstants;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
@@ -18,6 +20,8 @@ public class FileTransferDialog extends JDialog {
private JTextField localFileField;
private JButton browseLocalButton;
private JTextField hostFileField;
private JButton browseHostButton;
private JComboBox<Integer> mtuCombo;
// Options
private JRadioButton asciiRadio;
@@ -40,7 +44,7 @@ public class FileTransferDialog extends JDialog {
private JButton cancelButton;
public FileTransferDialog(Frame owner, FileTransfer coordinator) {
super(owner, "File Transfer (IND$FILE)", false); // non-modal to see progress
super(owner, "File Transfer (IND$FILE)", false);
this.owner = owner;
this.coordinator = coordinator;
@@ -76,7 +80,6 @@ public class FileTransferDialog extends JDialog {
dirPanel.add(Box.createRigidArea(new Dimension(15, 0)));
dirPanel.add(sendRadio);
// Disable append/overwrite when sending
sendRadio.addActionListener(e -> updateOptionStates());
receiveRadio.addActionListener(e -> updateOptionStates());
@@ -94,7 +97,13 @@ public class FileTransferDialog extends JDialog {
// Host File
hostFileField = new JTextField(20);
formPanel.add(createRow("Host File:", hostFileField));
browseHostButton = new JButton("Browse Host...");
browseHostButton.addActionListener(e -> browseHostDirectory());
JPanel hostPanel = new JPanel(new BorderLayout(5, 0));
hostPanel.setOpaque(false);
hostPanel.add(hostFileField, BorderLayout.CENTER);
hostPanel.add(browseHostButton, BorderLayout.EAST);
formPanel.add(createRow("Host File:", hostPanel));
// Mode
asciiRadio = new JRadioButton("ASCII");
@@ -114,6 +123,11 @@ public class FileTransferDialog extends JDialog {
formPanel.add(createRow("Transfer Mode:", modePanel));
// MTU Buffer Size
mtuCombo = new JComboBox<>(new Integer[]{2048, 4096, 8192, 16384, 32768});
mtuCombo.setSelectedItem(FTConstants.DFT_BUF);
formPanel.add(createRow("DFT Buffer Size:", mtuCombo));
// Options (checkboxes)
crCheck = new JCheckBox("Add/Remove CR");
crCheck.setSelected(true);
@@ -198,10 +212,7 @@ public class FileTransferDialog extends JDialog {
setContentPane(mainPanel);
// Theme styling for components
applyTheme(mainPanel);
// Initialize state
updateOptionStates();
}
@@ -236,14 +247,29 @@ public class FileTransferDialog extends JDialog {
JFileChooser chooser = new JFileChooser();
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
localFileField.setText(chooser.getSelectedFile().getAbsolutePath());
// Auto-fill host file if empty
if (hostFileField.getText().trim().isEmpty()) {
hostFileField.setText(chooser.getSelectedFile().getName());
}
}
}
private void browseHostDirectory() {
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
String initialQuery = hostFileField.getText().trim();
HostDirectoryDialog dialog = new HostDirectoryDialog(this, coordinator.getClient(), hostType, initialQuery);
dialog.setVisible(true);
if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) {
hostFileField.setText(dialog.getSelectedHostFile());
if (localFileField.getText().trim().isEmpty()) {
String cleanName = dialog.getSelectedHostFile().replace("'", "").replace("\"", "");
int lastDot = cleanName.lastIndexOf('.');
if (lastDot > 0) cleanName = cleanName.substring(lastDot + 1);
localFileField.setText(cleanName);
}
}
}
private void startTransfer() {
if (localFileField.getText().trim().isEmpty() || hostFileField.getText().trim().isEmpty()) {
JOptionPane.showMessageDialog(this, "Local and Host filenames are required.",
@@ -262,6 +288,11 @@ public class FileTransferDialog extends JDialog {
config.setAppend(appendCheck.isSelected());
config.setOverwrite(overwriteCheck.isSelected());
Integer mtu = (Integer) mtuCombo.getSelectedItem();
if (mtu != null) {
config.setDftBufferSize(mtu);
}
config.setRecfm(recfmField.getText().trim());
config.setLrecl(lreclField.getText().trim());
config.setBlksize(blksizeField.getText().trim());
@@ -272,7 +303,6 @@ public class FileTransferDialog extends JDialog {
if (error != null) {
JOptionPane.showMessageDialog(this, error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
} else {
// Success, close dialog (progress will be shown separately)
dispose();
}
}
@@ -0,0 +1,337 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel;
import javax.swing.*;
import java.awt.*;
/**
* Connection dialog for entering host, port, model, and LU name.
*/
public class ConnectDialog extends JDialog {
private JTextField hostField;
private JTextField portField;
private JComboBox<TerminalModel> modelCombo;
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsCombo;
private JComboBox<String> codePageCombo;
private JTextField luField;
private JCheckBox tlsCheckBox;
private JCheckBox verifyCertCheckBox;
private JCheckBox tn3270eCheckBox;
private boolean confirmed;
private ConnectionConfig result;
public ConnectDialog(Frame parent) {
super(parent, "Connect to Host", true);
buildUI();
pack();
setLocationRelativeTo(parent);
setResizable(false);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new GridBagLayout());
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
mainPanel.setBackground(new Color(30, 30, 30));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 4, 4, 4);
gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 14);
Color fg = new Color(200, 200, 200);
// Host
gbc.gridx = 0;
gbc.gridy = 0;
JLabel hostLabel = new JLabel("Host:");
hostLabel.setForeground(fg);
hostLabel.setFont(labelFont);
mainPanel.add(hostLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
hostField = createDarkField(20);
mainPanel.add(hostField, gbc);
// Port
gbc.gridx = 0;
gbc.gridy = 1;
gbc.weightx = 0;
JLabel portLabel = new JLabel("Port:");
portLabel.setForeground(fg);
portLabel.setFont(labelFont);
mainPanel.add(portLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
portField = createDarkField(6);
portField.setText("23");
mainPanel.add(portField, gbc);
// Model
gbc.gridx = 0;
gbc.gridy = 2;
gbc.weightx = 0;
JLabel modelLabel = new JLabel("Model:");
modelLabel.setForeground(fg);
modelLabel.setFont(labelFont);
mainPanel.add(modelLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
modelCombo = new JComboBox<>(TerminalModel.values());
modelCombo.setSelectedItem(TerminalModel.IBM_3279_4);
modelCombo.setBackground(new Color(45, 45, 45));
modelCombo.setForeground(fg);
modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
mainPanel.add(modelCombo, gbc);
// LU Name
gbc.gridx = 0;
gbc.gridy = 3;
gbc.weightx = 0;
JLabel luLabel = new JLabel("LU Name:");
luLabel.setForeground(fg);
luLabel.setFont(labelFont);
mainPanel.add(luLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
luField = createDarkField(12);
mainPanel.add(luField, gbc);
// Graphics Mode
gbc.gridx = 0;
gbc.gridy = 4;
gbc.weightx = 0;
JLabel graphicsLabel = new JLabel("Graphics:");
graphicsLabel.setForeground(fg);
graphicsLabel.setFont(labelFont);
mainPanel.add(graphicsLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
graphicsCombo.setBackground(new Color(45, 45, 45));
graphicsCombo.setForeground(fg);
graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
mainPanel.add(graphicsCombo, gbc);
// Code Page
gbc.gridx = 0;
gbc.gridy = 5;
gbc.weightx = 0;
JLabel cpLabel = new JLabel("Code Page:");
cpLabel.setForeground(fg);
cpLabel.setFont(labelFont);
mainPanel.add(cpLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
String[] commonCodePages = {
"037 - US / Canada / Brazil",
"1047 - IBM Open Systems / z/OS Unix",
"500 - International Latin-1",
"273 - Germany / Austria",
"277 - Denmark / Norway",
"278 - Sweden / Finland",
"280 - Italy",
"284 - Spain / Latin America",
"285 - United Kingdom",
"297 - France",
"870 - Eastern Europe / Latin-2",
"871 - Iceland",
"875 - Greece (Greek)",
"1026 - Turkey (Turkish)",
"1140 - US / Canada (Euro \u20AC)",
"1141 - Germany / Austria (Euro \u20AC)",
"1148 - International (Euro \u20AC)",
"930 - Japanese Katakana Mixed DBCS",
"939 - Japanese Latin Mixed DBCS",
"935 - Simplified Chinese Mixed DBCS",
"937 - Traditional Chinese Mixed DBCS",
"933 - Korean Mixed DBCS"
};
codePageCombo = new JComboBox<>(commonCodePages);
codePageCombo.setEditable(true);
String currentCp = haus.nightmare.j3270.config.Settings.getCodePage();
for (String item : commonCodePages) {
if (item.startsWith(currentCp + " ") || item.equals(currentCp)) {
codePageCombo.setSelectedItem(item);
break;
}
}
codePageCombo.setBackground(new Color(45, 45, 45));
codePageCombo.setForeground(fg);
codePageCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
mainPanel.add(codePageCombo, gbc);
// TLS / SSL Checkbox
gbc.gridx = 1;
gbc.gridy = 6;
gbc.weightx = 1.0;
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
tlsCheckBox.setBackground(new Color(30, 30, 30));
tlsCheckBox.setForeground(fg);
tlsCheckBox.setFont(labelFont);
tlsCheckBox.setFocusPainted(false);
tlsCheckBox.addActionListener(e -> {
boolean isTls = tlsCheckBox.isSelected();
verifyCertCheckBox.setEnabled(isTls);
String currentPort = portField.getText().trim();
if (isTls && "23".equals(currentPort)) {
portField.setText("992");
} else if (!isTls && "992".equals(currentPort)) {
portField.setText("23");
}
});
mainPanel.add(tlsCheckBox, gbc);
// Verify Certificate Checkbox
gbc.gridx = 1;
gbc.gridy = 7;
verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
verifyCertCheckBox.setBackground(new Color(30, 30, 30));
verifyCertCheckBox.setForeground(new Color(160, 160, 160));
verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
verifyCertCheckBox.setSelected(true);
verifyCertCheckBox.setEnabled(false);
verifyCertCheckBox.setFocusPainted(false);
mainPanel.add(verifyCertCheckBox, gbc);
// TN3270E Checkbox
gbc.gridx = 1;
gbc.gridy = 8;
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
tn3270eCheckBox.setBackground(new Color(30, 30, 30));
tn3270eCheckBox.setForeground(fg);
tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
tn3270eCheckBox.setFocusPainted(false);
mainPanel.add(tn3270eCheckBox, gbc);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
buttonPanel.setBackground(new Color(30, 30, 30));
JButton connectBtn = new JButton("Connect");
connectBtn.setBackground(new Color(50, 120, 50));
connectBtn.setForeground(Color.WHITE);
connectBtn.setFont(labelFont);
connectBtn.addActionListener(e -> onConnect());
JButton cancelBtn = new JButton("Cancel");
cancelBtn.setBackground(new Color(60, 60, 60));
cancelBtn.setForeground(fg);
cancelBtn.setFont(labelFont);
cancelBtn.addActionListener(e -> {
confirmed = false;
dispose();
});
buttonPanel.add(cancelBtn);
buttonPanel.add(connectBtn);
gbc.gridx = 0;
gbc.gridy = 9;
gbc.gridwidth = 2;
mainPanel.add(buttonPanel, gbc);
setContentPane(mainPanel);
// Enter key triggers connect
getRootPane().setDefaultButton(connectBtn);
}
private JTextField createDarkField(int cols) {
JTextField field = new JTextField(cols);
field.setBackground(new Color(45, 45, 45));
field.setForeground(new Color(200, 200, 200));
field.setCaretColor(new Color(200, 200, 200));
field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
field.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(60, 60, 60)),
BorderFactory.createEmptyBorder(4, 6, 4, 6)));
return field;
}
private void onConnect() {
String host = hostField.getText().trim();
if (host.isEmpty()) {
hostField.requestFocus();
return;
}
int port;
try {
port = Integer.parseInt(portField.getText().trim());
} catch (NumberFormatException e) {
portField.requestFocus();
return;
}
result = new ConnectionConfig(host, port, (TerminalModel) modelCombo.getSelectedItem());
String lu = luField.getText().trim();
if (!lu.isEmpty()) {
result.setLuName(lu);
}
result.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
Object cpSelection = codePageCombo.getSelectedItem();
if (cpSelection != null) {
String cpStr = cpSelection.toString().trim();
int dash = cpStr.indexOf(" -");
if (dash > 0) {
cpStr = cpStr.substring(0, dash).trim();
}
result.setCodePage(cpStr);
haus.nightmare.j3270.config.Settings.setCodePage(cpStr);
}
result.setUseTls(tlsCheckBox.isSelected());
result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
result.setTn3270eEnabled(tn3270eCheckBox.isSelected());
confirmed = true;
dispose();
}
public boolean isConfirmed() {
return confirmed;
}
public ConnectionConfig getConnectionConfig() {
return result;
}
/** Set initial values. */
public void setInitialHost(String host) {
hostField.setText(host);
}
public void setInitialPort(int port) {
portField.setText(String.valueOf(port));
}
public void setInitialTls(boolean tls) {
tlsCheckBox.setSelected(tls);
verifyCertCheckBox.setEnabled(tls);
}
public void setInitialVerifyCert(boolean verify) {
verifyCertCheckBox.setSelected(verify);
}
public void setInitialTn3270e(boolean tn3270e) {
tn3270eCheckBox.setSelected(tn3270e);
}
public void setInitialCodePage(String cp) {
if (cp != null && codePageCombo != null) {
for (int i = 0; i < codePageCombo.getItemCount(); i++) {
String item = codePageCombo.getItemAt(i);
if (item.startsWith(cp + " ") || item.equals(cp)) {
codePageCombo.setSelectedIndex(i);
return;
}
}
codePageCombo.setSelectedItem(cp);
}
}
}
@@ -0,0 +1,233 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ecl.ECLField;
import haus.nightmare.lib3270j.ecl.ECLFieldList;
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.List;
/**
* Live Field Inspector dialog displaying all fields in the 3270 Presentation Space (ECLFieldList).
*/
public class FieldInspectorDialog extends JDialog implements ScreenUpdateListener {
private final Telnet3270Client client;
private final TerminalPanel terminalPanel;
private DefaultTableModel tableModel;
private JTable table;
private JLabel countLabel;
private static final Color DARK_BG = new Color(35, 35, 35);
private static final Color DARK_FIELD_BG = new Color(45, 45, 45);
private static final Color DARK_FG = new Color(220, 220, 220);
private static final Color DARK_BORDER = new Color(65, 65, 65);
private static final Color DARK_SELECTION = new Color(75, 110, 175);
public FieldInspectorDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
super(parent, "3270 Presentation Space Field Inspector", false);
this.client = client;
this.terminalPanel = terminalPanel;
buildUI();
setSize(850, 420);
setLocationRelativeTo(parent);
if (client != null) {
client.addScreenUpdateListener(this);
refreshFields();
}
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
if (client != null) {
// Screen update listener removed on close
}
}
});
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(8, 8));
mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
mainPanel.setBackground(DARK_BG);
// Top info bar
JPanel topPanel = new JPanel(new BorderLayout());
topPanel.setOpaque(false);
countLabel = new JLabel("0 fields detected on screen");
countLabel.setForeground(DARK_FG);
countLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
topPanel.add(countLabel, BorderLayout.WEST);
JButton refreshBtn = new JButton("Refresh");
refreshBtn.setBackground(new Color(55, 55, 55));
refreshBtn.setForeground(DARK_FG);
refreshBtn.addActionListener(e -> refreshFields());
topPanel.add(refreshBtn, BorderLayout.EAST);
mainPanel.add(topPanel, BorderLayout.NORTH);
// Table
String[] columns = {
"#", "Pos", "Start", "End", "Len", "Prot", "MDT", "Num", "Hi-Int", "Hidden", "Pen-Sel", "Content Text"
};
tableModel = new DefaultTableModel(columns, 0) {
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
};
table = new JTable(tableModel);
table.setBackground(DARK_FIELD_BG);
table.setForeground(DARK_FG);
table.setSelectionBackground(DARK_SELECTION);
table.setSelectionForeground(Color.WHITE);
table.setGridColor(DARK_BORDER);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20);
JTableHeader header = table.getTableHeader();
header.setBackground(new Color(50, 50, 50));
header.setForeground(DARK_FG);
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
// Column widths
int[] widths = {35, 45, 60, 60, 40, 45, 45, 45, 50, 55, 60, 260};
for (int i = 0; i < widths.length && i < table.getColumnCount(); i++) {
table.getColumnModel().getColumn(i).setPreferredWidth(widths[i]);
}
// Center align boolean columns
DefaultTableCellRenderer centerRenderer = new DefaultTableCellRenderer();
centerRenderer.setHorizontalAlignment(SwingConstants.CENTER);
for (int i = 5; i <= 10; i++) {
table.getColumnModel().getColumn(i).setCellRenderer(centerRenderer);
}
table.getSelectionModel().addListSelectionListener(e -> {
if (!e.getValueIsAdjusting() && table.getSelectedRow() >= 0) {
selectFieldOnTerminal(table.getSelectedRow());
}
});
JScrollPane scrollPane = new JScrollPane(table);
scrollPane.setBorder(new LineBorder(DARK_BORDER));
scrollPane.getViewport().setBackground(DARK_FIELD_BG);
mainPanel.add(scrollPane, BorderLayout.CENTER);
// Bottom panel
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
bottomPanel.setOpaque(false);
JButton jumpBtn = new JButton("Jump to Selected Field");
jumpBtn.setBackground(new Color(50, 100, 160));
jumpBtn.setForeground(Color.WHITE);
jumpBtn.addActionListener(e -> {
int row = table.getSelectedRow();
if (row >= 0) {
selectFieldOnTerminal(row);
}
});
JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(55, 55, 55));
closeBtn.setForeground(DARK_FG);
closeBtn.addActionListener(e -> dispose());
bottomPanel.add(jumpBtn);
bottomPanel.add(closeBtn);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
}
public synchronized void refreshFields() {
if (client == null || !client.getConnectionState().isFullSession()) {
tableModel.setRowCount(0);
countLabel.setText("Not connected or screen unformatted");
return;
}
ECLFieldList fieldList = client.getFieldList();
List<ECLField> fields = fieldList.getFields();
int selectedRow = table.getSelectedRow();
tableModel.setRowCount(0);
int cols = client.getScreenBuffer().getDisplayCols();
for (int i = 0; i < fields.size(); i++) {
ECLField f = fields.get(i);
int startRow = f.getStartRow() + 1;
int startCol = f.getStartCol() + 1;
int endRow = f.getEndRow() + 1;
int endCol = f.getEndCol() + 1;
String text = f.getText();
if (f.isHidden()) {
text = "••••••••";
}
tableModel.addRow(new Object[]{
(i + 1),
f.getStart(),
String.format("%02d/%02d", startRow, startCol),
String.format("%02d/%02d", endRow, endCol),
f.getLength(),
f.isProtected() ? "YES" : "-",
f.isModified() ? "YES" : "-",
f.isNumeric() ? "YES" : "-",
f.isHighIntensity() ? "YES" : "-",
f.isHidden() ? "YES" : "-",
f.isPenSelectable() ? "YES" : "-",
text
});
}
countLabel.setText(fields.size() + " fields detected on screen (" + client.getScreenBuffer().getDisplayRows() + "x" + cols + ")");
if (selectedRow >= 0 && selectedRow < tableModel.getRowCount()) {
table.setRowSelectionInterval(selectedRow, selectedRow);
}
}
private void selectFieldOnTerminal(int rowIndex) {
if (client == null || rowIndex < 0) return;
List<ECLField> fields = client.getFieldList().getFields();
if (rowIndex < fields.size()) {
ECLField f = fields.get(rowIndex);
client.getScreenBuffer().setCursorAddress(f.getDataStart());
if (terminalPanel != null) {
terminalPanel.setSelectionRange(f.getDataStart(), f.getEnd());
terminalPanel.repaint();
}
}
}
// ========== ScreenUpdateListener ==========
@Override
public void onScreenUpdated() {
SwingUtilities.invokeLater(this::refreshFields);
}
@Override
public void onScreenSizeChanged(int rows, int cols) {
SwingUtilities.invokeLater(this::refreshFields);
}
@Override
public void onSoundAlarm() {}
}
@@ -0,0 +1,234 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ecl.ECLConstants;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
/**
* Find dialog for searching text in the 3270 Presentation Space using ECLPS.
*/
public class FindDialog extends JDialog {
private final Telnet3270Client client;
private final TerminalPanel terminalPanel;
private JTextField searchField;
private JCheckBox matchCaseCheck;
private JRadioButton forwardRadio;
private JRadioButton backwardRadio;
private JLabel statusLabel;
private static String lastSearchText = "";
private static boolean lastMatchCase = false;
private static boolean lastForward = true;
public FindDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
super(parent, "Find on Screen", false);
this.client = client;
this.terminalPanel = terminalPanel;
buildUI();
pack();
setLocationRelativeTo(parent);
setResizable(false);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
mainPanel.setBackground(new Color(35, 35, 35));
// Form
JPanel formPanel = new JPanel(new GridBagLayout());
formPanel.setOpaque(false);
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 4, 4, 4);
gbc.fill = GridBagConstraints.HORIZONTAL;
Color fg = new Color(220, 220, 220);
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
// Search text
gbc.gridx = 0;
gbc.gridy = 0;
JLabel findLabel = new JLabel("Find what:");
findLabel.setForeground(fg);
findLabel.setFont(labelFont);
formPanel.add(findLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
searchField = new JTextField(20);
searchField.setText(lastSearchText);
searchField.setBackground(new Color(50, 50, 50));
searchField.setForeground(fg);
searchField.setCaretColor(fg);
searchField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
searchField.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(70, 70, 70)),
BorderFactory.createEmptyBorder(3, 6, 3, 6)));
formPanel.add(searchField, gbc);
// Options
gbc.gridx = 1;
gbc.gridy = 1;
matchCaseCheck = new JCheckBox("Match case", lastMatchCase);
matchCaseCheck.setForeground(fg);
matchCaseCheck.setOpaque(false);
matchCaseCheck.setFont(labelFont);
matchCaseCheck.setFocusPainted(false);
formPanel.add(matchCaseCheck, gbc);
// Direction
gbc.gridx = 1;
gbc.gridy = 2;
JPanel dirPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
dirPanel.setOpaque(false);
JLabel dirLabel = new JLabel("Direction: ");
dirLabel.setForeground(fg);
dirLabel.setFont(labelFont);
forwardRadio = new JRadioButton("Down", lastForward);
backwardRadio = new JRadioButton("Up", !lastForward);
forwardRadio.setForeground(fg);
backwardRadio.setForeground(fg);
forwardRadio.setOpaque(false);
backwardRadio.setOpaque(false);
forwardRadio.setFocusPainted(false);
backwardRadio.setFocusPainted(false);
ButtonGroup bg = new ButtonGroup();
bg.add(forwardRadio);
bg.add(backwardRadio);
dirPanel.add(dirLabel);
dirPanel.add(forwardRadio);
dirPanel.add(Box.createHorizontalStrut(10));
dirPanel.add(backwardRadio);
formPanel.add(dirPanel, gbc);
// Status
gbc.gridx = 0;
gbc.gridy = 3;
gbc.gridwidth = 2;
statusLabel = new JLabel(" ");
statusLabel.setForeground(new Color(255, 120, 120));
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
formPanel.add(statusLabel, gbc);
mainPanel.add(formPanel, BorderLayout.CENTER);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
buttonPanel.setOpaque(false);
JButton findNextBtn = new JButton("Find Next");
findNextBtn.setBackground(new Color(60, 63, 65));
findNextBtn.setForeground(fg);
findNextBtn.setFont(labelFont);
findNextBtn.addActionListener(e -> findNext());
JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(60, 63, 65));
closeBtn.setForeground(fg);
closeBtn.setFont(labelFont);
closeBtn.addActionListener(e -> dispose());
buttonPanel.add(findNextBtn);
buttonPanel.add(closeBtn);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
getRootPane().setDefaultButton(findNextBtn);
searchField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
findNext();
} else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
dispose();
}
}
});
}
public void findNext() {
find(forwardRadio.isSelected());
}
public void findPrevious() {
find(!forwardRadio.isSelected());
}
private void find(boolean forward) {
String target = searchField.getText();
if (target == null || target.isEmpty()) {
statusLabel.setText("Please enter search text.");
return;
}
if (client == null || !client.getConnectionState().isFullSession()) {
statusLabel.setText("Terminal is not connected.");
return;
}
lastSearchText = target;
lastMatchCase = matchCaseCheck.isSelected();
lastForward = forwardRadio.isSelected();
ScreenBuffer sb = client.getScreenBuffer();
int curAddr = sb.getCursorAddress();
int cols = sb.getDisplayCols();
int curRow = curAddr / cols;
int curCol = curAddr % cols;
// Offset search start position past current character so repeat search moves to next occurrence
int startRow = curRow;
int startCol = curCol;
if (forward) {
startCol = (curCol + 1) % cols;
if (startCol == 0) startRow = (curRow + 1) % sb.getDisplayRows();
} else {
startCol = (curCol - 1 + cols) % cols;
if (startCol == cols - 1) startRow = (curRow - 1 + sb.getDisplayRows()) % sb.getDisplayRows();
}
int dir = forward ? ECLConstants.SEARCH_FORWARD : ECLConstants.SEARCH_BACKWARD;
int foundPos = client.getPS().searchString(target, startRow, startCol, dir, !matchCaseCheck.isSelected());
if (foundPos >= 0) {
statusLabel.setText("Found at position " + (foundPos + 1) + " (" + (foundPos / cols + 1) + "/" + (foundPos % cols + 1) + ")");
statusLabel.setForeground(new Color(100, 255, 100));
sb.setCursorAddress(foundPos);
if (terminalPanel != null) {
terminalPanel.setSearchHighlight(foundPos, target.length());
terminalPanel.repaint();
}
} else {
statusLabel.setText("Text not found: \"" + target + "\"");
statusLabel.setForeground(new Color(255, 120, 120));
if (terminalPanel != null) {
terminalPanel.clearSearchHighlight();
terminalPanel.repaint();
}
}
}
public static String getLastSearchText() {
return lastSearchText;
}
public static boolean getLastMatchCase() {
return lastMatchCase;
}
public static boolean getLastForward() {
return lastForward;
}
}
@@ -0,0 +1,289 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ft.FTConfig;
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
/**
* Host Directory and Dataset Catalog browser dialog for TSO and VM/CMS environments.
*/
public class HostDirectoryDialog extends JDialog {
private final Telnet3270Client client;
private final FTConfig.HostType initialHostType;
private JComboBox<FTConfig.HostType> hostTypeCombo;
private JTextField queryField;
private JTable table;
private DefaultTableModel tableModel;
private JLabel statusLabel;
private String selectedHostFile = null;
private boolean confirmed = false;
private final List<HostDirectoryEntry> currentEntries = new ArrayList<>();
public HostDirectoryDialog(Dialog parent, Telnet3270Client client, FTConfig.HostType hostType, String initialQuery) {
super(parent, "Host Catalog & Directory Browser", true);
this.client = client;
this.initialHostType = hostType;
buildUI(initialQuery);
setSize(700, 420);
setLocationRelativeTo(parent);
}
private void buildUI(String initialQuery) {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 12, 12, 12));
mainPanel.setBackground(new Color(35, 35, 35));
Color fg = new Color(220, 220, 220);
Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
// Top Query bar
JPanel topPanel = new JPanel(new GridBagLayout());
topPanel.setOpaque(false);
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(3, 4, 3, 4);
gbc.fill = GridBagConstraints.HORIZONTAL;
JLabel envLabel = new JLabel("System:");
envLabel.setForeground(fg);
envLabel.setFont(font);
topPanel.add(envLabel, gbc);
gbc.gridx = 1;
hostTypeCombo = new JComboBox<>(new FTConfig.HostType[]{FTConfig.HostType.TSO, FTConfig.HostType.CMS});
if (initialHostType == FTConfig.HostType.CMS) hostTypeCombo.setSelectedItem(FTConfig.HostType.CMS);
else hostTypeCombo.setSelectedItem(FTConfig.HostType.TSO);
hostTypeCombo.setBackground(new Color(50, 50, 50));
hostTypeCombo.setForeground(fg);
topPanel.add(hostTypeCombo, gbc);
gbc.gridx = 2;
JLabel queryLabel = new JLabel("Query Pattern / Text:");
queryLabel.setForeground(fg);
queryLabel.setFont(font);
topPanel.add(queryLabel, gbc);
gbc.gridx = 3;
gbc.weightx = 1.0;
queryField = new JTextField(initialQuery != null ? initialQuery : "", 16);
queryField.setBackground(new Color(50, 50, 50));
queryField.setForeground(fg);
queryField.setCaretColor(fg);
queryField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
queryField.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(new Color(70, 70, 70)),
new EmptyBorder(3, 5, 3, 5)));
topPanel.add(queryField, gbc);
gbc.gridx = 4;
gbc.weightx = 0;
JButton parseBtn = new JButton("Query / Parse");
parseBtn.setBackground(new Color(55, 55, 55));
parseBtn.setForeground(fg);
parseBtn.setFont(font);
parseBtn.addActionListener(e -> runQuery());
topPanel.add(parseBtn, gbc);
mainPanel.add(topPanel, BorderLayout.NORTH);
// Table
tableModel = new DefaultTableModel();
table = new JTable(tableModel);
table.setBackground(new Color(45, 45, 45));
table.setForeground(fg);
table.setSelectionBackground(new Color(75, 110, 175));
table.setSelectionForeground(Color.WHITE);
table.setGridColor(new Color(65, 65, 65));
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20);
JTableHeader header = table.getTableHeader();
header.setBackground(new Color(50, 50, 50));
header.setForeground(fg);
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
table.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2 && table.getSelectedRow() >= 0) {
onConfirmSelection();
}
}
});
JScrollPane scrollPane = new JScrollPane(table);
scrollPane.setBorder(new LineBorder(new Color(65, 65, 65)));
scrollPane.getViewport().setBackground(new Color(45, 45, 45));
mainPanel.add(scrollPane, BorderLayout.CENTER);
// Bottom
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
bottomPanel.setOpaque(false);
statusLabel = new JLabel("Enter a dataset pattern or parse active screen.");
statusLabel.setForeground(new Color(170, 170, 170));
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
bottomPanel.add(statusLabel, BorderLayout.WEST);
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
btnPanel.setOpaque(false);
JButton pasteScreenBtn = new JButton("Parse Current Screen");
pasteScreenBtn.setBackground(new Color(50, 50, 50));
pasteScreenBtn.setForeground(fg);
pasteScreenBtn.setFont(font);
pasteScreenBtn.addActionListener(e -> parseCurrentScreen());
JButton selectBtn = new JButton("Select Dataset");
selectBtn.setBackground(new Color(50, 120, 50));
selectBtn.setForeground(Color.WHITE);
selectBtn.setFont(font);
selectBtn.addActionListener(e -> onConfirmSelection());
JButton cancelBtn = new JButton("Cancel");
cancelBtn.setBackground(new Color(60, 60, 60));
cancelBtn.setForeground(fg);
cancelBtn.setFont(font);
cancelBtn.addActionListener(e -> {
confirmed = false;
dispose();
});
btnPanel.add(pasteScreenBtn);
btnPanel.add(Box.createHorizontalStrut(10));
btnPanel.add(selectBtn);
btnPanel.add(cancelBtn);
bottomPanel.add(btnPanel, BorderLayout.EAST);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
// Initial setup
setupTableColumns();
if (initialQuery != null && !initialQuery.isEmpty()) {
runQuery();
} else {
parseCurrentScreen();
}
}
private void setupTableColumns() {
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
tableModel.setRowCount(0);
if (hostType == FTConfig.HostType.CMS) {
tableModel.setColumnIdentifiers(new Object[]{
"Filename", "Filetype", "Filemode", "Format", "Lrecl", "Records", "Date / Time"
});
} else {
tableModel.setColumnIdentifiers(new Object[]{
"Dataset Name", "Volume", "Recfm", "Lrecl", "Blksize", "Tracks", "Extents"
});
}
}
private void runQuery() {
setupTableColumns();
currentEntries.clear();
String query = queryField.getText().trim();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
if (client != null && client.getXfer() != null) {
if (hostType == FTConfig.HostType.CMS) {
List<CMSDirectoryEntry> cmsEntries = client.getXfer().getCmsDirectory(query);
currentEntries.addAll(cmsEntries);
for (CMSDirectoryEntry entry : cmsEntries) {
tableModel.addRow(new Object[]{
entry.getFilename(), entry.getFiletype(), entry.getFilemode(),
entry.getRecfm(), entry.getLrecl(), entry.getNumRecords(), entry.getDate() + " " + entry.getTime()
});
}
} else {
List<TSODirectoryEntry> tsoEntries = client.getXfer().getTsoDirectory(query);
currentEntries.addAll(tsoEntries);
for (TSODirectoryEntry entry : tsoEntries) {
tableModel.addRow(new Object[]{
entry.getDatasetName(), entry.getVolume(), entry.getRecfm(),
entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents()
});
}
}
}
statusLabel.setText(currentEntries.size() + " entries found.");
}
private void parseCurrentScreen() {
if (client == null || !client.getConnectionState().isFullSession()) {
statusLabel.setText("No active session to parse.");
return;
}
setupTableColumns();
currentEntries.clear();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
String screenText = client.getPS().getString(0, client.getPS().getSize());
if (hostType == FTConfig.HostType.CMS) {
List<CMSDirectoryEntry> cmsEntries = client.getXfer().getCmsDirectory(screenText);
currentEntries.addAll(cmsEntries);
for (CMSDirectoryEntry entry : cmsEntries) {
tableModel.addRow(new Object[]{
entry.getFilename(), entry.getFiletype(), entry.getFilemode(),
entry.getRecfm(), entry.getLrecl(), entry.getNumRecords(), entry.getDate() + " " + entry.getTime()
});
}
} else {
List<TSODirectoryEntry> tsoEntries = client.getXfer().getTsoDirectory(screenText);
currentEntries.addAll(tsoEntries);
for (TSODirectoryEntry entry : tsoEntries) {
tableModel.addRow(new Object[]{
entry.getDatasetName(), entry.getVolume(), entry.getRecfm(),
entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents()
});
}
}
statusLabel.setText("Parsed " + currentEntries.size() + " entries from presentation space.");
}
private void onConfirmSelection() {
int row = table.getSelectedRow();
if (row < 0 || row >= currentEntries.size()) {
JOptionPane.showMessageDialog(this, "Please select an entry from the table.",
"No Selection", JOptionPane.WARNING_MESSAGE);
return;
}
HostDirectoryEntry entry = currentEntries.get(row);
selectedHostFile = entry.getName();
confirmed = true;
dispose();
}
public boolean isConfirmed() {
return confirmed;
}
public String getSelectedHostFile() {
return selectedHostFile;
}
}
@@ -0,0 +1,478 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.printer.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.print.PrinterJob;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* IBM 3287 / 3286 TN3270E Printer Session Manager and Spool Viewer.
* Integrates directly with lib3270j.printer.Telnet3270EP and PD3270.
*/
public class PrinterSessionDialog extends JDialog implements PrintSessionListener {
private final Frame parent;
private PrinterConfig config;
private Telnet3270EP printerSession;
// Configuration Fields
private JTextField hostField;
private JTextField portField;
private JTextField printerLuField;
private JTextField displayLuField;
private JComboBox<String> codePageCombo;
private JCheckBox tlsCheck;
private JCheckBox verifyCertCheck;
private JComboBox<PrinterConfig.DestinationType> destinationCombo;
private JTextField targetField;
// Status Fields
private JLabel statusLabel;
private JLabel sessionTypeLabel;
private JLabel pagesLabel;
private JLabel bytesLabel;
private JButton connectBtn;
private JButton disconnectBtn;
// Spool Display
private JTextArea spoolArea;
private static final Color DARK_BG = new Color(35, 35, 35);
private static final Color DARK_FG = new Color(220, 220, 220);
private static final Color DARK_FIELD_BG = new Color(45, 45, 45);
public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) {
super(parent, "IBM 3287 Printer Session Manager", false);
this.parent = parent;
config = new PrinterConfig(defaultHost, defaultPort);
config.setUseTls(defaultTls);
config.setDestinationType(PrinterConfig.DestinationType.MEMORY);
buildUI();
setSize(780, 560);
setLocationRelativeTo(parent);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
mainPanel.setBackground(DARK_BG);
// Top: Configuration Panel
JPanel topPanel = new JPanel(new GridBagLayout());
topPanel.setOpaque(false);
topPanel.setBorder(BorderFactory.createTitledBorder(
new LineBorder(new Color(65, 65, 65)), "Printer Session Configuration"));
((TitledBorder) topPanel.getBorder()).setTitleColor(DARK_FG);
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(3, 4, 3, 4);
gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
// Host / Port
gbc.gridx = 0; gbc.gridy = 0;
JLabel hLbl = new JLabel("Host:");
hLbl.setForeground(DARK_FG); hLbl.setFont(labelFont);
topPanel.add(hLbl, gbc);
gbc.gridx = 1; gbc.weightx = 1.0;
hostField = createDarkField(config.getHost() != null ? config.getHost() : "localhost", 14);
topPanel.add(hostField, gbc);
gbc.gridx = 2; gbc.weightx = 0;
JLabel pLbl = new JLabel("Port:");
pLbl.setForeground(DARK_FG); pLbl.setFont(labelFont);
topPanel.add(pLbl, gbc);
gbc.gridx = 3; gbc.weightx = 0.5;
portField = createDarkField(String.valueOf(config.getPort()), 5);
topPanel.add(portField, gbc);
// Printer LU / Display LU
gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 0;
JLabel pluLbl = new JLabel("Printer LU:");
pluLbl.setForeground(DARK_FG); pluLbl.setFont(labelFont);
topPanel.add(pluLbl, gbc);
gbc.gridx = 1; gbc.weightx = 1.0;
printerLuField = createDarkField("", 10);
topPanel.add(printerLuField, gbc);
gbc.gridx = 2; gbc.weightx = 0;
JLabel assocLbl = new JLabel("Assoc LU:");
assocLbl.setForeground(DARK_FG); assocLbl.setFont(labelFont);
topPanel.add(assocLbl, gbc);
gbc.gridx = 3; gbc.weightx = 0.5;
displayLuField = createDarkField("", 10);
topPanel.add(displayLuField, gbc);
// CodePage & TLS
gbc.gridx = 0; gbc.gridy = 2; gbc.weightx = 0;
JLabel cpLbl = new JLabel("CodePage:");
cpLbl.setForeground(DARK_FG); cpLbl.setFont(labelFont);
topPanel.add(cpLbl, gbc);
gbc.gridx = 1; gbc.weightx = 1.0;
codePageCombo = new JComboBox<>(new String[]{"037", "1047", "500", "273", "277", "278", "280", "284", "285", "297", "870", "1140"});
codePageCombo.setBackground(DARK_FIELD_BG);
codePageCombo.setForeground(DARK_FG);
topPanel.add(codePageCombo, gbc);
gbc.gridx = 2; gbc.gridwidth = 2;
JPanel tlsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
tlsPanel.setOpaque(false);
tlsCheck = new JCheckBox("TLS/SSL", config.isUseTls());
tlsCheck.setOpaque(false); tlsCheck.setForeground(DARK_FG); tlsCheck.setFocusPainted(false);
verifyCertCheck = new JCheckBox("Verify Cert", config.isTlsVerifyCert());
verifyCertCheck.setOpaque(false); verifyCertCheck.setForeground(DARK_FG); verifyCertCheck.setFocusPainted(false);
tlsPanel.add(tlsCheck);
tlsPanel.add(verifyCertCheck);
topPanel.add(tlsPanel, gbc);
// Destination Type & Target
gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 1;
JLabel destLbl = new JLabel("Destination:");
destLbl.setForeground(DARK_FG); destLbl.setFont(labelFont);
topPanel.add(destLbl, gbc);
gbc.gridx = 1;
destinationCombo = new JComboBox<>(PrinterConfig.DestinationType.values());
destinationCombo.setSelectedItem(PrinterConfig.DestinationType.MEMORY);
destinationCombo.setBackground(DARK_FIELD_BG);
destinationCombo.setForeground(DARK_FG);
topPanel.add(destinationCombo, gbc);
gbc.gridx = 2;
JLabel tgtLbl = new JLabel("Target Path:");
tgtLbl.setForeground(DARK_FG); tgtLbl.setFont(labelFont);
topPanel.add(tgtLbl, gbc);
gbc.gridx = 3;
targetField = createDarkField("printer_output.txt", 12);
topPanel.add(targetField, gbc);
// Connect / Disconnect Buttons
gbc.gridx = 0; gbc.gridy = 4; gbc.gridwidth = 4;
JPanel connBtnPan = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
connBtnPan.setOpaque(false);
connectBtn = new JButton("Start Printer Session");
connectBtn.setBackground(new Color(50, 120, 50));
connectBtn.setForeground(Color.WHITE);
connectBtn.addActionListener(e -> startPrinterSession());
disconnectBtn = new JButton("Stop Session");
disconnectBtn.setBackground(new Color(120, 50, 50));
disconnectBtn.setForeground(Color.WHITE);
disconnectBtn.setEnabled(false);
disconnectBtn.addActionListener(e -> stopPrinterSession());
connBtnPan.add(connectBtn);
connBtnPan.add(disconnectBtn);
topPanel.add(connBtnPan, gbc);
mainPanel.add(topPanel, BorderLayout.NORTH);
// Center: Spool Area & Status
JPanel centerPanel = new JPanel(new BorderLayout(6, 6));
centerPanel.setOpaque(false);
centerPanel.setBorder(BorderFactory.createTitledBorder(
new LineBorder(new Color(65, 65, 65)), "Printer Spool & Status"));
((TitledBorder) centerPanel.getBorder()).setTitleColor(DARK_FG);
// Status Header
JPanel statusHeader = new JPanel(new GridLayout(1, 4, 10, 0));
statusHeader.setOpaque(false);
statusHeader.setBorder(new EmptyBorder(0, 4, 4, 4));
statusLabel = new JLabel("Status: Disconnected");
statusLabel.setForeground(new Color(180, 180, 180));
statusLabel.setFont(labelFont);
sessionTypeLabel = new JLabel("Session: -");
sessionTypeLabel.setForeground(DARK_FG);
sessionTypeLabel.setFont(labelFont);
pagesLabel = new JLabel("Pages: 0");
pagesLabel.setForeground(DARK_FG);
pagesLabel.setFont(labelFont);
bytesLabel = new JLabel("Bytes: 0");
bytesLabel.setForeground(DARK_FG);
bytesLabel.setFont(labelFont);
statusHeader.add(statusLabel);
statusHeader.add(sessionTypeLabel);
statusHeader.add(pagesLabel);
statusHeader.add(bytesLabel);
centerPanel.add(statusHeader, BorderLayout.NORTH);
// Spool text area
spoolArea = new JTextArea();
spoolArea.setBackground(new Color(25, 25, 25));
spoolArea.setForeground(new Color(100, 255, 100));
spoolArea.setCaretColor(Color.WHITE);
spoolArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
spoolArea.setEditable(false);
JScrollPane spoolScroll = new JScrollPane(spoolArea);
spoolScroll.setBorder(new LineBorder(new Color(60, 60, 60)));
centerPanel.add(spoolScroll, BorderLayout.CENTER);
mainPanel.add(centerPanel, BorderLayout.CENTER);
// Bottom: Spool controls
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
bottomPanel.setOpaque(false);
JButton clearSpoolBtn = new JButton("Clear Spool");
clearSpoolBtn.setBackground(new Color(50, 50, 50));
clearSpoolBtn.setForeground(DARK_FG);
clearSpoolBtn.addActionListener(e -> {
spoolArea.setText("");
if (printerSession != null && printerSession.getPD() != null) {
printerSession.getPD().resetCapture();
}
pagesLabel.setText("Pages: 0");
bytesLabel.setText("Bytes: 0");
});
JButton saveSpoolBtn = new JButton("Save Spool As...");
saveSpoolBtn.setBackground(new Color(50, 50, 50));
saveSpoolBtn.setForeground(DARK_FG);
saveSpoolBtn.addActionListener(e -> saveSpool());
JButton printSpoolBtn = new JButton("Print Spool...");
printSpoolBtn.setBackground(new Color(50, 100, 160));
printSpoolBtn.setForeground(Color.WHITE);
printSpoolBtn.addActionListener(e -> printSpool());
JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(60, 60, 60));
closeBtn.setForeground(DARK_FG);
closeBtn.addActionListener(e -> dispose());
bottomPanel.add(clearSpoolBtn);
bottomPanel.add(saveSpoolBtn);
bottomPanel.add(printSpoolBtn);
bottomPanel.add(Box.createHorizontalStrut(10));
bottomPanel.add(closeBtn);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
}
private JTextField createDarkField(String text, int cols) {
JTextField tf = new JTextField(text, cols);
tf.setBackground(DARK_FIELD_BG);
tf.setForeground(DARK_FG);
tf.setCaretColor(DARK_FG);
tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
tf.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(new Color(65, 65, 65)),
new EmptyBorder(2, 4, 2, 4)));
return tf;
}
private void startPrinterSession() {
String host = hostField.getText().trim();
if (host.isEmpty()) {
hostField.requestFocus();
return;
}
int port = 23;
try {
port = Integer.parseInt(portField.getText().trim());
} catch (NumberFormatException ignored) {}
config.setHost(host);
config.setPort(port);
config.setUseTls(tlsCheck.isSelected());
config.setTlsVerifyCert(verifyCertCheck.isSelected());
config.setCodePage((String) codePageCombo.getSelectedItem());
String plu = printerLuField.getText().trim();
config.setPrinterLuName(plu.isEmpty() ? null : plu);
String dlu = displayLuField.getText().trim();
config.setAssociatedDisplayLuName(dlu.isEmpty() ? null : dlu);
config.setDestinationType((PrinterConfig.DestinationType) destinationCombo.getSelectedItem());
config.setDestinationTarget(targetField.getText().trim());
if (printerSession != null) {
printerSession.close();
}
printerSession = new Telnet3270EP(config);
printerSession.addPrintListener(this);
connectBtn.setEnabled(false);
disconnectBtn.setEnabled(true);
statusLabel.setText("Status: Connecting...");
statusLabel.setForeground(new Color(255, 200, 80));
new Thread(() -> {
boolean success = printerSession.open();
if (!success) {
SwingUtilities.invokeLater(() -> {
connectBtn.setEnabled(true);
disconnectBtn.setEnabled(false);
statusLabel.setText("Status: Connection Failed");
statusLabel.setForeground(new Color(255, 80, 80));
});
}
}, "PrinterSession-Thread").start();
}
private void stopPrinterSession() {
if (printerSession != null) {
printerSession.close();
printerSession = null;
}
connectBtn.setEnabled(true);
disconnectBtn.setEnabled(false);
statusLabel.setText("Status: Disconnected");
statusLabel.setForeground(new Color(180, 180, 180));
sessionTypeLabel.setText("Session: -");
}
private void saveSpool() {
JFileChooser fc = new JFileChooser();
fc.setDialogTitle("Save Printer Spool Output");
fc.setSelectedFile(new File("spool_output.txt"));
if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
try (FileOutputStream fos = new FileOutputStream(fc.getSelectedFile())) {
fos.write(spoolArea.getText().getBytes(StandardCharsets.UTF_8));
JOptionPane.showMessageDialog(this, "Spool saved successfully to " + fc.getSelectedFile().getName(),
"Saved", JOptionPane.INFORMATION_MESSAGE);
} catch (IOException e) {
JOptionPane.showMessageDialog(this, "Save error: " + e.getMessage(),
"Error", JOptionPane.ERROR_MESSAGE);
}
}
}
private void printSpool() {
String text = spoolArea.getText();
if (text.isEmpty()) {
JOptionPane.showMessageDialog(this, "Spool buffer is empty.", "Nothing to Print", JOptionPane.INFORMATION_MESSAGE);
return;
}
PrinterJob job = PrinterJob.getPrinterJob();
job.setJobName("j3270 3287 Print Spool");
if (job.printDialog()) {
try {
spoolArea.print();
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Print error: " + ex.getMessage(), "Print Error", JOptionPane.ERROR_MESSAGE);
}
}
}
// ========== PrintSessionListener ==========
@Override
public void onPrintJobStarted(PrintSessionEvent event) {
SwingUtilities.invokeLater(() -> {
statusLabel.setText("Status: Printing...");
statusLabel.setForeground(new Color(50, 205, 50));
});
}
@Override
public void onPrintJobData(PrintSessionEvent event) {
SwingUtilities.invokeLater(() -> {
if (printerSession != null && printerSession.getPD() != null) {
spoolArea.setText(printerSession.getPD().getCapturedText());
spoolArea.setCaretPosition(spoolArea.getDocument().getLength());
pagesLabel.setText("Pages: " + printerSession.getPD().getPageCount());
bytesLabel.setText("Bytes: " + printerSession.getPD().getByteCount());
}
});
}
@Override
public void onPrintJobPageComplete(PrintSessionEvent event) {
SwingUtilities.invokeLater(() -> {
if (printerSession != null && printerSession.getPD() != null) {
spoolArea.setText(printerSession.getPD().getCapturedText());
pagesLabel.setText("Pages: " + printerSession.getPD().getPageCount());
bytesLabel.setText("Bytes: " + printerSession.getPD().getByteCount());
}
});
}
@Override
public void onPrintJobComplete(PrintSessionEvent event) {
SwingUtilities.invokeLater(() -> {
if (printerSession != null && printerSession.getPD() != null) {
spoolArea.setText(printerSession.getPD().getCapturedText());
pagesLabel.setText("Pages: " + printerSession.getPD().getPageCount());
bytesLabel.setText("Bytes: " + printerSession.getPD().getByteCount());
}
statusLabel.setText("Status: In Session (Idle)");
statusLabel.setForeground(new Color(50, 205, 50));
});
}
@Override
public void onPrinterStatusChanged(PrintSessionEvent event) {
SwingUtilities.invokeLater(() -> {
int code = event.getStatusCode();
if (code == PrinterConstants.STATUS_CONNECTED || code == PrinterConstants.STATUS_PRINTER_READY || code == PrinterConstants.STATUS_PRINTING || code == PrinterConstants.STATUS_JOB_COMPLETE || code == PrinterConstants.STATUS_PAGE_COMPLETE) {
String lu = printerSession != null ? printerSession.getAssignedLuName() : null;
statusLabel.setText("Status: Connected" + (lu != null ? " (" + lu + ")" : ""));
statusLabel.setForeground(new Color(50, 205, 50));
connectBtn.setEnabled(false);
disconnectBtn.setEnabled(true);
if (printerSession != null) {
short luType = printerSession.getActiveLuType();
if (luType == PrinterConstants.LU_TYPE_1_SCS) {
sessionTypeLabel.setText("Session: LU1 (SCS)");
} else if (luType == PrinterConstants.LU_TYPE_3_DS) {
sessionTypeLabel.setText("Session: LU3 (3270 DS)");
}
}
} else if (code == PrinterConstants.STATUS_DISCONNECTED) {
statusLabel.setText("Status: Disconnected");
statusLabel.setForeground(new Color(180, 180, 180));
connectBtn.setEnabled(true);
disconnectBtn.setEnabled(false);
sessionTypeLabel.setText("Session: -");
} else {
statusLabel.setText("Status: " + event.getMessage());
}
});
}
@Override
public void onPrinterError(PrintSessionEvent event) {
SwingUtilities.invokeLater(() -> {
statusLabel.setText("Status: Error (" + event.getMessage() + ")");
statusLabel.setForeground(new Color(255, 80, 80));
});
}
@Override
public void dispose() {
stopPrinterSession();
super.dispose();
}
}
@@ -0,0 +1,253 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* Utility for exporting the 3270 Presentation Space to Plain Text, HTML, or PNG image.
*/
public class ScreenExporter {
public static void showExportDialog(Frame parent, Telnet3270Client client, TerminalPanel panel) {
if (client == null) {
JOptionPane.showMessageDialog(parent, "Connect to a host before exporting the screen.",
"Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
JFileChooser fc = new JFileChooser();
fc.setDialogTitle("Save Screen As...");
fc.addChoosableFileFilter(new FileNameExtensionFilter("PNG Image (*.png)", "png"));
fc.addChoosableFileFilter(new FileNameExtensionFilter("HTML Document (*.html)", "html", "htm"));
FileNameExtensionFilter txtFilter = new FileNameExtensionFilter("Plain Text (*.txt)", "txt");
fc.addChoosableFileFilter(txtFilter);
fc.setFileFilter(txtFilter);
fc.setSelectedFile(new File("screen_capture.txt"));
int result = fc.showSaveDialog(parent);
if (result == JFileChooser.APPROVE_OPTION) {
File file = fc.getSelectedFile();
String name = file.getName().toLowerCase();
try {
if (name.endsWith(".png") || (fc.getFileFilter() != null && fc.getFileFilter().getDescription().contains("PNG"))) {
if (!name.endsWith(".png")) file = new File(file.getAbsolutePath() + ".png");
exportToPng(panel, file);
} else if (name.endsWith(".html") || name.endsWith(".htm") || (fc.getFileFilter() != null && fc.getFileFilter().getDescription().contains("HTML"))) {
if (!name.endsWith(".html") && !name.endsWith(".htm")) file = new File(file.getAbsolutePath() + ".html");
exportToHtml(client, file);
} else {
if (!name.endsWith(".txt")) file = new File(file.getAbsolutePath() + ".txt");
exportToText(client, file);
}
JOptionPane.showMessageDialog(parent, "Screen saved successfully to:\n" + file.getAbsolutePath(),
"Export Successful", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
JOptionPane.showMessageDialog(parent, "Failed to export screen: " + ex.getMessage(),
"Export Error", JOptionPane.ERROR_MESSAGE);
}
}
}
public static void exportToText(Telnet3270Client client, File file) throws IOException {
ScreenBuffer sb = client.getScreenBuffer();
int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols();
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
for (int r = 0; r < rows; r++) {
StringBuilder line = new StringBuilder();
for (int c = 0; c < cols; c++) {
ExtendedAttribute ea = sb.getCell(r * cols + c);
char ch = ea.ucs4;
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
line.append(' ');
} else {
line.append(ch);
}
}
// Strip trailing spaces for clean text output
int lastNonSpace = line.length() - 1;
while (lastNonSpace >= 0 && line.charAt(lastNonSpace) == ' ') {
lastNonSpace--;
}
writer.write(line.substring(0, lastNonSpace + 1));
writer.newLine();
}
}
}
public static void exportToHtml(Telnet3270Client client, File file) throws IOException {
ScreenBuffer sb = client.getScreenBuffer();
int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols();
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
writer.write("<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>3270 Screen Capture</title>\n");
writer.write("<style>\n");
writer.write("body { background-color: #0a0a0a; color: #32cd32; font-family: 'Courier New', Courier, monospace; font-size: 14px; margin: 20px; }\n");
writer.write(".screen { background-color: #000; padding: 15px; border-radius: 4px; display: inline-block; box-shadow: 0 0 10px rgba(0,0,0,0.8); line-height: 1.2; }\n");
writer.write(".c-blue { color: #5078ff; }\n");
writer.write(".c-red { color: #ff3232; }\n");
writer.write(".c-pink { color: #ff82b4; }\n");
writer.write(".c-green { color: #32cd32; }\n");
writer.write(".c-turq { color: #40e0d0; }\n");
writer.write(".c-yellow { color: #ffff50; }\n");
writer.write(".c-white { color: #ffffff; }\n");
writer.write(".c-black { color: #000000; }\n");
writer.write(".c-orange { color: #ffa500; }\n");
writer.write(".c-purple { color: #b482ff; }\n");
writer.write(".c-palegreen { color: #90ee90; }\n");
writer.write(".c-paleturq { color: #afeeee; }\n");
writer.write(".c-grey { color: #aaaaaa; }\n");
writer.write(".bold { font-weight: bold; }\n");
writer.write(".underline { text-decoration: underline; }\n");
writer.write("</style>\n</head>\n<body>\n<div class=\"screen\"><pre>");
byte currentFA = 0;
ExtendedAttribute currentFieldEa = null;
for (int r = 0; r < rows; r++) {
String currentStyle = null;
StringBuilder currentRun = new StringBuilder();
for (int c = 0; c < cols; c++) {
int baddr = r * cols + c;
ExtendedAttribute ea = sb.getCell(baddr);
if (ea.isFieldAttribute()) {
currentFA = ea.fa;
currentFieldEa = ea;
if (currentRun.length() > 0) {
writeHtmlSpan(writer, currentStyle, currentRun.toString());
currentRun.setLength(0);
currentStyle = null;
}
writer.write(' ');
continue;
}
if (faIsZero(currentFA & 0xFF)) {
if (currentRun.length() > 0) {
writeHtmlSpan(writer, currentStyle, currentRun.toString());
currentRun.setLength(0);
currentStyle = null;
}
writer.write(' ');
continue;
}
char ch = ea.ucs4;
if (ch == 0 || ch == 0xFF) {
if (currentRun.length() > 0) {
writeHtmlSpan(writer, currentStyle, currentRun.toString());
currentRun.setLength(0);
currentStyle = null;
}
writer.write(' ');
continue;
}
String colorClass = getHtmlColorClass(ea, currentFieldEa, currentFA);
boolean bold = faIsHigh(currentFA & 0xFF) || (ea.gr & GR_INTENSIFY) != 0;
boolean underline = (ea.gr & GR_UNDERLINE) != 0;
StringBuilder css = new StringBuilder();
if (!colorClass.isEmpty()) css.append(colorClass).append(" ");
if (bold) css.append("bold ");
if (underline) css.append("underline ");
String style = css.toString().trim();
if (currentStyle == null || !currentStyle.equals(style)) {
if (currentRun.length() > 0) {
writeHtmlSpan(writer, currentStyle, currentRun.toString());
currentRun.setLength(0);
}
currentStyle = style;
}
currentRun.append(escapeHtml(ch));
}
if (currentRun.length() > 0) {
writeHtmlSpan(writer, currentStyle, currentRun.toString());
}
writer.newLine();
}
writer.write("</pre></div>\n</body>\n</html>");
}
}
private static void writeHtmlSpan(BufferedWriter writer, String style, String content) throws IOException {
if (style != null && !style.isEmpty()) {
writer.write("<span class=\"" + style + "\">" + content + "</span>");
} else {
writer.write(content);
}
}
private static String getHtmlColorClass(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
int fg = ea.fg != 0 ? (ea.fg & 0xFF) : (currentFieldEa != null && currentFieldEa.fg != 0 ? (currentFieldEa.fg & 0xFF) : 0);
if (fg >= 0xf0 && fg <= 0xff) {
switch (fg - 0xf0) {
case 1: return "c-blue";
case 2: return "c-red";
case 3: return "c-pink";
case 4: return "c-green";
case 5: return "c-turq";
case 6: return "c-yellow";
case 7: return "c-white";
case 8: return "c-black";
case 9: return "c-blue";
case 10: return "c-orange";
case 11: return "c-purple";
case 12: return "c-palegreen";
case 13: return "c-paleturq";
case 14: return "c-grey";
case 15: return "c-white";
}
}
if (faIsProtected(currentFA & 0xFF)) {
return faIsHigh(currentFA & 0xFF) ? "c-white" : "c-blue";
}
return faIsHigh(currentFA & 0xFF) ? "c-red" : "c-green";
}
private static String escapeHtml(char ch) {
switch (ch) {
case '<': return "&lt;";
case '>': return "&gt;";
case '&': return "&amp;";
case '"': return "&quot;";
default: return String.valueOf(ch);
}
}
public static void exportToPng(TerminalPanel panel, File file) throws IOException {
int w = panel.getWidth();
int h = panel.getHeight();
if (w <= 0 || h <= 0) {
Dimension pref = panel.getPreferredSize();
w = Math.max(pref.width, 800);
h = Math.max(pref.height, 500);
}
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = image.createGraphics();
panel.paint(g2);
g2.dispose();
ImageIO.write(image, "PNG", file);
}
}
@@ -0,0 +1,291 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.Telnet3270Client;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* Dialog for composing and executing Host On-Demand ECL mnemonic keystroke scripts.
*/
public class ScriptDialog extends JDialog {
private final Telnet3270Client client;
private final TerminalPanel terminalPanel;
private JTextArea scriptArea;
private JLabel statusLabel;
public ScriptDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
super(parent, "Run Keystrokes / ECL Script", false);
this.client = client;
this.terminalPanel = terminalPanel;
buildUI();
setSize(600, 480);
setLocationRelativeTo(parent);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
mainPanel.setBackground(new Color(35, 35, 35));
Color fg = new Color(220, 220, 220);
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
// Header
JPanel topPanel = new JPanel(new BorderLayout(5, 5));
topPanel.setOpaque(false);
JLabel descLabel = new JLabel("<html>Compose keystrokes and ECL bracketed mnemonics to stream to the mainframe.<br>"
+ "Example: <code>TSO[enter]USER[tab]PASSWORD[enter]</code> or <code>[pf3][clear]</code></html>");
descLabel.setForeground(new Color(180, 180, 180));
descLabel.setFont(labelFont);
topPanel.add(descLabel, BorderLayout.CENTER);
mainPanel.add(topPanel, BorderLayout.NORTH);
// Script area
scriptArea = new JTextArea();
scriptArea.setBackground(new Color(25, 25, 25));
scriptArea.setForeground(new Color(50, 205, 50));
scriptArea.setCaretColor(Color.WHITE);
scriptArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
scriptArea.setLineWrap(true);
scriptArea.setWrapStyleWord(false);
JScrollPane scrollPane = new JScrollPane(scriptArea);
scrollPane.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(new Color(60, 60, 60)),
new EmptyBorder(2, 2, 2, 2)));
// Mnemonic helper buttons
JPanel tokenPanel = new JPanel(new WrapLayout(FlowLayout.LEFT, 4, 4));
tokenPanel.setOpaque(false);
tokenPanel.setBorder(BorderFactory.createTitledBorder(
new LineBorder(new Color(60, 60, 60)), "Insert Keystroke Token"));
((TitledBorder) tokenPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
String[] tokens = {
"[enter]", "[tab]", "[backtab]", "[clear]", "[reset]",
"[eraseeof]", "[eraseinput]", "[newline]", "[dup]", "[fm]",
"[attn]", "[sysreq]", "[cursel]",
"[pf1]", "[pf2]", "[pf3]", "[pf4]", "[pf5]", "[pf6]",
"[pf7]", "[pf8]", "[pf9]", "[pf10]", "[pf11]", "[pf12]",
"[pf13]", "[pf14]", "[pf15]", "[pf16]", "[pf17]", "[pf18]",
"[pf19]", "[pf20]", "[pf21]", "[pf22]", "[pf23]", "[pf24]",
"[pa1]", "[pa2]", "[pa3]", "[up]", "[down]", "[left]", "[right]", "[home]"
};
for (String token : tokens) {
JButton btn = new JButton(token);
btn.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11));
btn.setBackground(new Color(50, 50, 50));
btn.setForeground(fg);
btn.setFocusable(false);
btn.setMargin(new Insets(2, 4, 2, 4));
btn.addActionListener(e -> insertToken(token));
tokenPanel.add(btn);
}
JPanel centerPanel = new JPanel(new BorderLayout(6, 6));
centerPanel.setOpaque(false);
centerPanel.add(scrollPane, BorderLayout.CENTER);
centerPanel.add(tokenPanel, BorderLayout.SOUTH);
mainPanel.add(centerPanel, BorderLayout.CENTER);
// Bottom
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
bottomPanel.setOpaque(false);
statusLabel = new JLabel("Ready");
statusLabel.setForeground(new Color(160, 160, 160));
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
bottomPanel.add(statusLabel, BorderLayout.WEST);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
buttonPanel.setOpaque(false);
JButton loadBtn = new JButton("Load Script...");
loadBtn.setBackground(new Color(50, 50, 50));
loadBtn.setForeground(fg);
loadBtn.setFont(labelFont);
loadBtn.addActionListener(e -> loadScript());
JButton saveBtn = new JButton("Save Script...");
saveBtn.setBackground(new Color(50, 50, 50));
saveBtn.setForeground(fg);
saveBtn.setFont(labelFont);
saveBtn.addActionListener(e -> saveScript());
JButton runBtn = new JButton("Execute");
runBtn.setBackground(new Color(50, 120, 50));
runBtn.setForeground(Color.WHITE);
runBtn.setFont(labelFont);
runBtn.addActionListener(e -> executeScript());
JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(60, 60, 60));
closeBtn.setForeground(fg);
closeBtn.setFont(labelFont);
closeBtn.addActionListener(e -> dispose());
buttonPanel.add(loadBtn);
buttonPanel.add(saveBtn);
buttonPanel.add(Box.createHorizontalStrut(10));
buttonPanel.add(runBtn);
buttonPanel.add(closeBtn);
bottomPanel.add(buttonPanel, BorderLayout.EAST);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
}
private void insertToken(String token) {
scriptArea.replaceSelection(token);
scriptArea.requestFocusInWindow();
}
private void executeScript() {
String script = scriptArea.getText();
if (script == null || script.trim().isEmpty()) {
statusLabel.setText("Script is empty.");
return;
}
if (client == null || !client.getConnectionState().isFullSession()) {
statusLabel.setText("Not connected to host.");
JOptionPane.showMessageDialog(this, "Connect to a 3270 session before executing scripts.",
"Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
statusLabel.setText("Executing keystrokes...");
new Thread(() -> {
try {
// Split multi-line scripts or process sequentially
String cleaned = script.replace("\r\n", "\n").replace("\r", "\n");
String[] lines = cleaned.split("\n");
for (int i = 0; i < lines.length; i++) {
String line = lines[i].trim();
if (!line.isEmpty() && !line.startsWith("#") && !line.startsWith("//")) {
client.sendKeys(line);
Thread.sleep(100);
}
}
SwingUtilities.invokeLater(() -> {
statusLabel.setText("Script execution completed successfully.");
if (terminalPanel != null) {
terminalPanel.repaint();
}
});
} catch (Exception ex) {
SwingUtilities.invokeLater(() -> {
statusLabel.setText("Error executing script: " + ex.getMessage());
JOptionPane.showMessageDialog(this, "Execution error: " + ex.getMessage(),
"Script Error", JOptionPane.ERROR_MESSAGE);
});
}
}, "ScriptRunner").start();
}
private void loadScript() {
JFileChooser fc = new JFileChooser();
fc.setDialogTitle("Open Script File");
if (fc.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
try {
byte[] bytes = java.nio.file.Files.readAllBytes(fc.getSelectedFile().toPath());
scriptArea.setText(new String(bytes, StandardCharsets.UTF_8));
statusLabel.setText("Loaded script: " + fc.getSelectedFile().getName());
} catch (IOException e) {
JOptionPane.showMessageDialog(this, "Could not open file: " + e.getMessage(),
"Open Error", JOptionPane.ERROR_MESSAGE);
}
}
}
private void saveScript() {
JFileChooser fc = new JFileChooser();
fc.setDialogTitle("Save Script File");
if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
try {
java.nio.file.Files.write(fc.getSelectedFile().toPath(),
scriptArea.getText().getBytes(StandardCharsets.UTF_8));
statusLabel.setText("Saved script: " + fc.getSelectedFile().getName());
} catch (IOException e) {
JOptionPane.showMessageDialog(this, "Could not save file: " + e.getMessage(),
"Save Error", JOptionPane.ERROR_MESSAGE);
}
}
}
/**
* FlowLayout subclass that fully supports word wrapping inside JScrollPanes / dialogs.
*/
private static class WrapLayout extends FlowLayout {
public WrapLayout(int align, int hgap, int vgap) {
super(align, hgap, vgap);
}
@Override
public Dimension preferredLayoutSize(Container target) {
return layoutSize(target, true);
}
@Override
public Dimension minimumLayoutSize(Container target) {
Dimension minimum = layoutSize(target, false);
minimum.width -= (getHgap() + 1);
return minimum;
}
private Dimension layoutSize(Container target, boolean preferred) {
synchronized (target.getTreeLock()) {
int targetWidth = target.getWidth();
if (targetWidth == 0) targetWidth = Integer.MAX_VALUE;
int hgap = getHgap();
int vgap = getVgap();
Insets insets = target.getInsets();
int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2);
int maxWidth = targetWidth - horizontalInsetsAndGap;
Dimension dim = new Dimension(0, 0);
int rowWidth = 0;
int rowHeight = 0;
int nmembers = target.getComponentCount();
for (int i = 0; i < nmembers; i++) {
Component m = target.getComponent(i);
if (m.isVisible()) {
Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize();
if (rowWidth + d.width > maxWidth) {
addRow(dim, rowWidth, rowHeight);
rowWidth = 0;
rowHeight = 0;
}
if (rowWidth != 0) rowWidth += hgap;
rowWidth += d.width;
rowHeight = Math.max(rowHeight, d.height);
}
}
addRow(dim, rowWidth, rowHeight);
dim.width += horizontalInsetsAndGap;
dim.height += insets.top + insets.bottom + vgap * 2;
return dim;
}
}
private void addRow(Dimension dim, int rowWidth, int rowHeight) {
dim.width = Math.max(dim.width, rowWidth);
if (dim.height > 0) dim.height += getVgap();
dim.height += rowHeight;
}
}
}
@@ -1,9 +1,9 @@
package org.pubvm.j3270.ui;
package haus.nightmare.j3270.ui;
import javax.swing.*;
import org.pubvm.j3270.J3270App;
import org.pubvm.j3270.config.Settings;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import java.awt.*;
import java.awt.event.*;
@@ -31,6 +31,7 @@ public class SettingsDialog extends JDialog {
// Appearance tab
private JComboBox<String> fontBox;
private JSpinner fontSizeSpinner;
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsModeBox;
// Behavior tab
private JComboBox<Settings.StartupBehavior> startupBehaviorBox;
@@ -264,8 +265,21 @@ public class SettingsDialog extends JDialog {
gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(fontSizeSpinner, gbc);
// Fill remaining space
// Graphics Mode
gbc.gridx = 0;
gbc.gridy = 2;
gbc.fill = GridBagConstraints.NONE;
JLabel graphicsLabel = new JLabel("Graphics Mode:");
panel.add(graphicsLabel, gbc);
graphicsModeBox = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
graphicsModeBox.setSelectedItem(Settings.getGraphicsMode());
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(graphicsModeBox, gbc);
// Fill remaining space
gbc.gridy = 3;
gbc.weighty = 1.0;
panel.add(Box.createGlue(), gbc);
@@ -356,7 +370,7 @@ public class SettingsDialog extends JDialog {
hostPanel.setBorder(BorderFactory.createTitledBorder("Host Colors"));
for (int i = 0; i < 16; i++) {
Color init = org.pubvm.j3270.config.Settings.getColorOverride(i, TerminalPanel.DEFAULT_HOST_COLORS[i]);
Color init = haus.nightmare.j3270.config.Settings.getColorOverride(i, TerminalPanel.DEFAULT_HOST_COLORS[i]);
tempHostColors[i] = init;
JPanel cb = new JPanel();
cb.setName("colorSwatch");
@@ -388,7 +402,7 @@ public class SettingsDialog extends JDialog {
for (int i = 0; i < monoKeys.length; i++) {
String mk = monoKeys[i];
Color init = org.pubvm.j3270.config.Settings.getMonoColorOverride(mk, monoDefs[i]);
Color init = haus.nightmare.j3270.config.Settings.getMonoColorOverride(mk, monoDefs[i]);
tempMonoColors.put(mk, init);
JPanel p = new JPanel(new BorderLayout());
@@ -439,7 +453,8 @@ public class SettingsDialog extends JDialog {
JPanel main = new JPanel(new BorderLayout());
String[] actions = {"ENTER", "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
"HOME", "END", "PAGE_UP", "PAGE_DOWN", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE", "CLEAR"};
"HOME", "END", "PAGE_UP", "PAGE_DOWN", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE", "CLEAR",
"ERASE_INPUT", "NEWLINE", "DUP", "FIELD_MARK", "ATTN", "SYSREQ", "CURSEL"};
keymapModel = new DefaultTableModel(new Object[]{"Action", "Key Binding"}, 0) {
@Override
@@ -451,8 +466,15 @@ public class SettingsDialog extends JDialog {
// Populate table from Settings or Defaults
for(String act : actions) {
String def = act;
if(def.equals("PAGE_UP")) def = "PAGE_UP"; // fallback example
String current = org.pubvm.j3270.config.Settings.getKeyBinding(act, def);
if (def.equals("ERASE_INPUT")) def = "alt E";
else if (def.equals("NEWLINE")) def = "shift ENTER";
else if (def.equals("DUP")) def = "alt D";
else if (def.equals("FIELD_MARK")) def = "alt M";
else if (def.equals("ATTN")) def = "alt A";
else if (def.equals("SYSREQ")) def = "alt S";
else if (def.equals("CURSEL")) def = "alt Q";
else if (def.equals("CLEAR")) def = "alt C";
String current = haus.nightmare.j3270.config.Settings.getKeyBinding(act, def);
tempKeyBindings.put(act, current);
keymapModel.addRow(new Object[]{act, current});
}
@@ -464,13 +486,13 @@ public class SettingsDialog extends JDialog {
defaultBinding = "shift F" + (i - 12);
}
String pf = "PF"+i;
String cur = org.pubvm.j3270.config.Settings.getKeyBinding(pf, defaultBinding);
String cur = haus.nightmare.j3270.config.Settings.getKeyBinding(pf, defaultBinding);
tempKeyBindings.put(pf, cur);
keymapModel.addRow(new Object[]{pf, cur});
}
for(int i=1;i<=3;i++){
String pa = "PA"+i;
String curPa = org.pubvm.j3270.config.Settings.getKeyBinding(pa, "alt "+i);
String curPa = haus.nightmare.j3270.config.Settings.getKeyBinding(pa, "alt "+i);
tempKeyBindings.put(pa, curPa);
keymapModel.addRow(new Object[]{pa, curPa});
}
@@ -611,6 +633,9 @@ public class SettingsDialog extends JDialog {
Settings.setFontFamily(fontFam);
}
Settings.setFontSize((Integer) fontSizeSpinner.getValue());
if (graphicsModeBox.getSelectedItem() != null) {
Settings.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsModeBox.getSelectedItem());
}
// Apply Behavior
Settings.StartupBehavior behavior = (Settings.StartupBehavior) startupBehaviorBox.getSelectedItem();
@@ -631,15 +656,15 @@ public class SettingsDialog extends JDialog {
// Propagate visual changes to the app
// Save Colors
for (int i=0; i<16; i++) {
org.pubvm.j3270.config.Settings.setColorOverride(i, tempHostColors[i]);
haus.nightmare.j3270.config.Settings.setColorOverride(i, tempHostColors[i]);
}
for (Map.Entry<String, Color> entry : tempMonoColors.entrySet()) {
org.pubvm.j3270.config.Settings.setMonoColorOverride(entry.getKey(), entry.getValue());
haus.nightmare.j3270.config.Settings.setMonoColorOverride(entry.getKey(), entry.getValue());
}
// Save Keybindings
for (Map.Entry<String, String> entry : tempKeyBindings.entrySet()) {
org.pubvm.j3270.config.Settings.setKeyBinding(entry.getKey(), entry.getValue());
haus.nightmare.j3270.config.Settings.setKeyBinding(entry.getKey(), entry.getValue());
}
parentApp.getTerminalPanel().reloadSettings();
@@ -0,0 +1,232 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ecl.ECLConstants;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import javax.swing.*;
import java.awt.*;
/**
* Status bar displaying connection state, cursor position, timing, and lock status.
* Equivalent to the OIA (Operator Information Area) on a real 3270 terminal.
*/
public class StatusBar extends JPanel {
private final JLabel connectionStatus;
private final JLabel tlsStatus;
private final JLabel luName;
private final JLabel lockStatus;
private final JLabel fieldTypeStatus;
private final JLabel codePageInfo;
private final JLabel modelInfo;
private final JLabel cursorPosition;
private Telnet3270Client client;
private TerminalPanel terminalPanel;
// OIA colors
private static final Color OIA_BG = new Color(20, 20, 20);
private static final Color OIA_FG = new Color(50, 205, 50);
private static final Color OIA_DIM = new Color(80, 80, 80);
private static final Color OIA_ALERT = new Color(255, 80, 80);
private static final Color OIA_WARN = new Color(255, 200, 80);
public StatusBar() {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setBackground(OIA_BG);
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(40, 40, 40)));
setPreferredSize(new Dimension(800, 22));
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12);
connectionStatus = createLabel("Not Connected", oiaFont, OIA_DIM);
tlsStatus = createLabel("", oiaFont, OIA_FG);
luName = createLabel("", oiaFont, OIA_FG);
lockStatus = createLabel("", oiaFont, OIA_ALERT);
fieldTypeStatus = createLabel("", oiaFont, OIA_DIM);
codePageInfo = createLabel("", oiaFont, OIA_DIM);
modelInfo = createLabel("", oiaFont, OIA_DIM);
cursorPosition = createLabel("001/001 [0000]", oiaFont, OIA_FG);
add(Box.createHorizontalStrut(6));
add(connectionStatus);
add(Box.createHorizontalStrut(10));
add(tlsStatus);
add(Box.createHorizontalStrut(10));
add(luName);
add(Box.createHorizontalStrut(12));
add(lockStatus);
add(Box.createHorizontalStrut(10));
add(fieldTypeStatus);
add(Box.createHorizontalGlue());
add(codePageInfo);
add(Box.createHorizontalStrut(12));
add(modelInfo);
add(Box.createHorizontalStrut(12));
add(cursorPosition);
add(Box.createHorizontalStrut(6));
}
private JLabel createLabel(String text, Font font, Color fg) {
JLabel label = new JLabel(text);
label.setFont(font);
label.setForeground(fg);
return label;
}
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
this.client = client;
this.terminalPanel = terminalPanel;
updateStatus();
}
public void updateStatus() {
if (client == null) {
connectionStatus.setText("Not Connected");
connectionStatus.setForeground(OIA_DIM);
tlsStatus.setText("");
luName.setText("");
lockStatus.setText("");
fieldTypeStatus.setText("");
codePageInfo.setText("");
modelInfo.setText("");
cursorPosition.setText("001/001");
return;
}
// Connection state
ConnectionState state = client.getConnectionState();
switch (state) {
case NOT_CONNECTED:
connectionStatus.setText("Not Connected");
connectionStatus.setForeground(OIA_DIM);
break;
case TCP_PENDING:
case TELNET_PENDING:
connectionStatus.setText("Connecting...");
connectionStatus.setForeground(OIA_ALERT);
break;
case CONNECTED_3270:
connectionStatus.setText("TN3270");
connectionStatus.setForeground(OIA_FG);
break;
case CONNECTED_TN3270E:
connectionStatus.setText("TN3270E");
connectionStatus.setForeground(OIA_FG);
break;
case CONNECTED_SSCP:
connectionStatus.setText("SSCP-LU");
connectionStatus.setForeground(OIA_FG);
break;
case CONNECTED_NVT:
case CONNECTED_NVT_CHAR:
case CONNECTED_E_NVT:
connectionStatus.setText("NVT");
connectionStatus.setForeground(OIA_FG);
break;
case CONNECTED_UNBOUND:
connectionStatus.setText("Unbound");
connectionStatus.setForeground(OIA_WARN);
break;
default:
connectionStatus.setText(state.name());
connectionStatus.setForeground(OIA_DIM);
break;
}
// TLS Status
if (state.isConnected() && client.getConfig().isUseTls()) {
boolean verified = client.getConfig().isTlsVerifyCert();
javax.net.ssl.SSLSession session = client.getSslSession();
String cipher = session != null ? session.getCipherSuite() : "TLS";
String protocol = session != null ? session.getProtocol() : "TLS";
if (verified) {
tlsStatus.setText("🔒 TLS");
tlsStatus.setForeground(OIA_FG);
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)");
} else {
tlsStatus.setText("🔓 TLS (Unverified)");
tlsStatus.setForeground(new Color(255, 180, 80));
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)");
}
} else {
tlsStatus.setText("");
tlsStatus.setToolTipText(null);
}
// LU Name
String lu = "";
if (client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null && !client.getTelnetFSM().getConnectedLu().isEmpty()) {
lu = "LU:" + client.getTelnetFSM().getConnectedLu();
} else if (client.getConfig().getLuName() != null && !client.getConfig().getLuName().isEmpty()) {
lu = "LU:" + client.getConfig().getLuName();
}
luName.setText(lu);
// Lock / Inhibit status
int inhibit = client.getOIA().getInputInhibited();
if (inhibit != ECLConstants.INHIBIT_NOT_INHIBITED) {
switch (inhibit) {
case ECLConstants.INHIBIT_SYSTEM_LOCK:
lockStatus.setText("X SYSTEM");
break;
case ECLConstants.INHIBIT_COMM_CHECK:
lockStatus.setText("X COMM");
break;
case ECLConstants.INHIBIT_NUMERIC_ONLY:
lockStatus.setText("X NUM");
break;
case ECLConstants.INHIBIT_PROTECTED_FIELD:
lockStatus.setText("X PROT");
break;
case ECLConstants.INHIBIT_OVERFLOW:
lockStatus.setText("X >");
break;
case ECLConstants.INHIBIT_OPERATOR_DUE:
lockStatus.setText("X OP");
break;
default:
lockStatus.setText("X LOCKED");
break;
}
lockStatus.setForeground(OIA_ALERT);
} else if (client.getInputProcessor().isInsertMode()) {
lockStatus.setText("INSERT");
lockStatus.setForeground(OIA_FG);
} else {
lockStatus.setText("");
}
// Field status: Numeric vs Alphanumeric
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
if (client.getOIA().isNumeric()) {
fieldTypeStatus.setText("NUM");
fieldTypeStatus.setForeground(OIA_WARN);
} else {
fieldTypeStatus.setText("ALPHA");
fieldTypeStatus.setForeground(OIA_DIM);
}
} else {
fieldTypeStatus.setText("");
}
// Active Code Page
String cp = client.getCodePage();
codePageInfo.setText(cp != null ? "CP" + cp : "");
codePageInfo.setToolTipText("Active EBCDIC Code Page: CP" + cp);
// Model & Dimensions info
ScreenBuffer sb = client.getScreenBuffer();
int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols();
modelInfo.setText(client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]");
// Cursor position and buffer address
int curAddr = sb.getCursorAddress();
int row = sb.getCursorRow() + 1;
int col = sb.getCursorCol() + 1;
cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr));
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,153 @@
package haus.nightmare.j3270.ui;
import javax.swing.*;
import java.awt.*;
import java.security.MessageDigest;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Modal dialog for inspecting and confirming untrusted TLS/SSL server certificates.
*/
public class UntrustedCertificateDialog extends JDialog {
private boolean accepted = false;
public UntrustedCertificateDialog(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) {
super(parent, "Untrusted SSL/TLS Certificate", true);
buildUI(host, port, chain, exception);
pack();
setLocationRelativeTo(parent);
setResizable(false);
}
private void buildUI(String host, int port, X509Certificate[] chain, CertificateException exception) {
JPanel mainPanel = new JPanel(new BorderLayout(12, 12));
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
mainPanel.setBackground(new Color(30, 30, 30));
// Header
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
headerPanel.setBackground(new Color(30, 30, 30));
JLabel iconLabel = new JLabel("⚠️");
iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28));
headerPanel.add(iconLabel);
JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2));
titleBox.setBackground(new Color(30, 30, 30));
JLabel titleLabel = new JLabel("Untrusted SSL Certificate");
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
titleLabel.setForeground(new Color(255, 180, 80));
titleBox.add(titleLabel);
JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified.");
subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
subtitleLabel.setForeground(new Color(180, 180, 180));
titleBox.add(subtitleLabel);
headerPanel.add(titleBox);
mainPanel.add(headerPanel, BorderLayout.NORTH);
// Certificate Details Area
X509Certificate cert = (chain != null && chain.length > 0) ? chain[0] : null;
StringBuilder sb = new StringBuilder();
if (exception != null) {
sb.append("Validation Error:\n ").append(exception.getMessage() != null ? exception.getMessage() : exception.toString()).append("\n\n");
}
if (cert != null) {
sb.append("Subject:\n ").append(cert.getSubjectX500Principal().getName()).append("\n\n");
sb.append("Issuer:\n ").append(cert.getIssuerX500Principal().getName()).append("\n\n");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
sb.append("Validity:\n From: ").append(sdf.format(cert.getNotBefore()))
.append("\n To: ").append(sdf.format(cert.getNotAfter())).append("\n\n");
sb.append("Serial Number:\n ").append(cert.getSerialNumber().toString(16).toUpperCase()).append("\n\n");
sb.append("SHA-256 Fingerprint:\n ").append(computeFingerprint(cert, "SHA-256")).append("\n\n");
sb.append("SHA-1 Fingerprint:\n ").append(computeFingerprint(cert, "SHA-1"));
} else {
sb.append("No peer certificate information available.");
}
JTextArea detailsArea = new JTextArea(sb.toString());
detailsArea.setEditable(false);
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
detailsArea.setBackground(new Color(20, 20, 20));
detailsArea.setForeground(new Color(210, 210, 210));
detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
JScrollPane scrollPane = new JScrollPane(detailsArea);
scrollPane.setPreferredSize(new Dimension(520, 260));
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60)));
mainPanel.add(scrollPane, BorderLayout.CENTER);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
buttonPanel.setBackground(new Color(30, 30, 30));
JButton cancelBtn = new JButton("Cancel Connection");
cancelBtn.setBackground(new Color(60, 60, 60));
cancelBtn.setForeground(new Color(220, 220, 220));
cancelBtn.addActionListener(e -> {
accepted = false;
dispose();
});
JButton trustBtn = new JButton("Connect Anyway");
trustBtn.setBackground(new Color(180, 100, 40));
trustBtn.setForeground(Color.WHITE);
trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
trustBtn.addActionListener(e -> {
accepted = true;
dispose();
});
buttonPanel.add(cancelBtn);
buttonPanel.add(trustBtn);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
getRootPane().setDefaultButton(trustBtn);
}
public boolean isAccepted() {
return accepted;
}
private static String computeFingerprint(X509Certificate cert, String algorithm) {
try {
MessageDigest md = MessageDigest.getInstance(algorithm);
byte[] digest = md.digest(cert.getEncoded());
StringBuilder sb = new StringBuilder();
for (int i = 0; i < digest.length; i++) {
if (i > 0) sb.append(':');
sb.append(String.format("%02X", digest[i]));
}
return sb.toString();
} catch (Exception e) {
return "Unable to compute fingerprint: " + e.getMessage();
}
}
/**
* Show the prompt modal dialog on the Swing EDT and return user choice.
*/
public static boolean showPrompt(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) {
if (SwingUtilities.isEventDispatchThread()) {
UntrustedCertificateDialog dialog = new UntrustedCertificateDialog(parent, host, port, chain, exception);
dialog.setVisible(true);
return dialog.isAccepted();
} else {
AtomicBoolean result = new AtomicBoolean(false);
try {
SwingUtilities.invokeAndWait(() -> {
UntrustedCertificateDialog dialog = new UntrustedCertificateDialog(parent, host, port, chain, exception);
dialog.setVisible(true);
result.set(dialog.isAccepted());
});
} catch (Exception e) {
return false;
}
return result.get();
}
}
}
@@ -1,521 +0,0 @@
package org.pubvm.j3270;
import org.lib3270j.*;
import org.lib3270j.listener.ConnectionListener;
import org.lib3270j.listener.ScreenUpdateListener;
import org.pubvm.j3270.ui.ConnectDialog;
import org.pubvm.j3270.ui.StatusBar;
import org.pubvm.j3270.ui.TerminalPanel;
import org.pubvm.j3270.ft.FileTransfer;
import org.pubvm.j3270.ft.FileTransferDialog;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.IOException;
import java.util.logging.*;
/**
* j3270 Java TN3270 Terminal Emulator.
* Main application class. Creates the main window with terminal display,
* status bar, menu bar, and manages the Telnet3270Client lifecycle.
*/
public class J3270App extends JFrame implements ConnectionListener, ScreenUpdateListener {
private static final Logger log = Logger.getLogger(J3270App.class.getName());
private Telnet3270Client client;
private TerminalPanel terminalPanel;
private StatusBar statusBar;
private Timer refreshTimer;
// Last connection details for reconnect
private String lastHost = "";
private int lastPort = 23;
private FileTransfer fileTransfer;
public J3270App() {
super("j3270 — Java TN3270 Terminal Emulator");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBackground(new Color(10, 10, 10));
buildUI();
buildMenuBar();
pack();
setLocationRelativeTo(null);
setMinimumSize(new Dimension(640, 400));
// Status refresh timer also ensures focus stays on terminal
refreshTimer = new Timer(100, e -> {
if (client != null) {
statusBar.updateStatus();
// Keep focus on terminal panel when window is active
if (isActive() && !terminalPanel.hasFocus()) {
terminalPanel.requestFocusInWindow();
}
}
});
refreshTimer.start();
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
disconnect();
if (refreshTimer != null)
refreshTimer.stop();
terminalPanel.dispose();
}
@Override
public void windowActivated(WindowEvent e) {
// When window gains focus, push to terminal panel
terminalPanel.requestFocusInWindow();
}
});
}
private void buildUI() {
terminalPanel = new TerminalPanel();
// Status bar
statusBar = new StatusBar();
// Layout
getContentPane().setLayout(new BorderLayout());
getContentPane().setBackground(new Color(10, 10, 10));
getContentPane().add(terminalPanel, BorderLayout.CENTER);
getContentPane().add(statusBar, BorderLayout.SOUTH);
}
private void buildMenuBar() {
JMenuBar menuBar = new JMenuBar();
menuBar.setBackground(new Color(30, 30, 30));
menuBar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(50, 50, 50)));
// File menu
JMenu fileMenu = createMenu("File");
fileMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
fileMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect));
fileMenu.addSeparator();
fileMenu.add(createMenuItem("Settings...", -1, this::showSettingsDialog));
fileMenu.addSeparator();
fileMenu.add(createMenuItem("Quit", KeyEvent.VK_Q, () -> {
disconnect();
System.exit(0);
}));
menuBar.add(fileMenu);
// View menu
JMenu viewMenu = createMenu("View");
viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2)));
viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2)));
viewMenu.add(createMenuItem("Reset Font", KeyEvent.VK_0, () -> {
terminalPanel.setFontSize(16);
terminalPanel.guardedPack();
}));
menuBar.add(viewMenu);
// Actions menu
JMenu actionsMenu = createMenu("Actions");
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_L, () -> {
if (client != null)
client.sendClear();
terminalPanel.repaint();
}));
actionsMenu.add(createMenuItem("Reset", KeyEvent.VK_R, () -> {
if (client != null)
client.reset();
terminalPanel.repaint();
}));
actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog));
menuBar.add(actionsMenu);
// Help menu
JMenu helpMenu = createMenu("Help");
helpMenu.add(createMenuItem("Key Mappings", -1, this::showKeyMappings));
helpMenu.add(createMenuItem("About", -1, this::showAbout));
menuBar.add(helpMenu);
setJMenuBar(menuBar);
}
private JMenu createMenu(String name) {
JMenu menu = new JMenu(name);
menu.setForeground(new Color(200, 200, 200));
return menu;
}
private JMenuItem createMenuItem(String text, int acceleratorKey, Runnable action) {
JMenuItem item = new JMenuItem(text);
item.setBackground(new Color(40, 40, 40));
item.setForeground(new Color(200, 200, 200));
if (acceleratorKey > 0) {
item.setAccelerator(KeyStroke.getKeyStroke(acceleratorKey,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()));
}
item.addActionListener(e -> action.run());
return item;
}
private void showFileTransferDialog() {
if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before using File Transfer.",
"Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
if (fileTransfer == null) {
fileTransfer = new FileTransfer(client, new FileTransfer.FileTransferCallback() {
private ProgressMonitor monitor;
@Override
public void onTransferStarted() {
monitor = new ProgressMonitor(J3270App.this, "Starting IND$FILE transfer...",
"Waiting for host...", 0, 100);
monitor.setMillisToDecideToPopup(0);
monitor.setMillisToPopup(0);
// Start a timer to check for manual cancellation
Timer cancelCheckTimer = new Timer(500, e -> {
if (monitor != null && monitor.isCanceled() && fileTransfer != null) {
fileTransfer.cancel();
((Timer)e.getSource()).stop();
}
});
cancelCheckTimer.start();
}
@Override
public void onTransferRunning() {
if (monitor != null) {
monitor.setNote("Transferring data...");
}
}
@Override
public void onBytesTransferred(long bytes) {
if (monitor != null) {
monitor.setNote(bytes + " bytes transferred");
// We don't always know max size in IND$FILE, so we just pulse/update note
}
}
@Override
public void onTransferComplete(String message) {
if (monitor != null) monitor.close();
monitor = null;
JOptionPane.showMessageDialog(J3270App.this, message, "Transfer Complete", JOptionPane.INFORMATION_MESSAGE);
}
@Override
public void onTransferAborted(String error) {
if (monitor != null) monitor.close();
monitor = null;
JOptionPane.showMessageDialog(J3270App.this, "Transfer Failed: " + error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
}
});
}
FileTransferDialog dialog = new FileTransferDialog(this, fileTransfer);
dialog.setVisible(true);
}
// ========== Connection management ==========
private void showConnectDialog() {
ConnectDialog dialog = new ConnectDialog(this);
dialog.setInitialHost(lastHost);
dialog.setInitialPort(lastPort);
dialog.setVisible(true);
if (dialog.isConfirmed()) {
ConnectionConfig config = dialog.getConnectionConfig();
lastHost = config.getHost();
lastPort = config.getPort();
connect(config);
}
}
private void showSettingsDialog() {
org.pubvm.j3270.ui.SettingsDialog dialog = new org.pubvm.j3270.ui.SettingsDialog(this);
dialog.setVisible(true);
}
public TerminalPanel getTerminalPanel() {
return terminalPanel;
}
void connect(ConnectionConfig config) {
lastHost = config.getHost();
lastPort = config.getPort();
// Save for auto-connect
org.pubvm.j3270.config.Settings.setAutoConnectHost(config.getHost());
org.pubvm.j3270.config.Settings.setAutoConnectPort(config.getPort());
// Disconnect existing connection
disconnect();
client = new Telnet3270Client(config);
client.addConnectionListener(this);
client.addScreenUpdateListener(this);
terminalPanel.setClient(client);
statusBar.setClient(client);
setTitle("j3270 — " + config.getHost() + ":" + config.getPort());
// Resize window to match model
terminalPanel.guardedPack();
new Thread(() -> {
try {
client.connect();
} catch (IOException e) {
log.log(Level.WARNING, "Connection failed", e);
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(this,
"Connection failed: " + e.getMessage(),
"Connection Error", JOptionPane.ERROR_MESSAGE);
});
}
}, "Connect-Thread").start();
// Focus the terminal panel
terminalPanel.requestFocusInWindow();
}
private void disconnect() {
if (client != null) {
if (fileTransfer != null) {
fileTransfer.cancel();
fileTransfer = null;
}
client.disconnect();
client = null;
terminalPanel.setClient(null);
statusBar.setClient(null);
terminalPanel.repaint();
setTitle("j3270 — Java TN3270 Terminal Emulator");
}
}
private void adjustFontSize(int delta) {
int current = terminalPanel.getFontSize();
int newSize = Math.max(8, Math.min(72, current + delta));
terminalPanel.setFontSize(newSize);
// Pack after font change setFontSize sets the resize guard
// to prevent the componentResized from re-triggering autoFitFont
terminalPanel.guardedPack();
}
// ========== ConnectionListener ==========
@Override
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
log.info("Connection state: " + oldState + " -> " + newState);
SwingUtilities.invokeLater(() -> {
statusBar.updateStatus();
terminalPanel.repaint();
// Auto-resize and focus on first full session
if (newState.isFullSession() && !oldState.isFullSession()) {
terminalPanel.guardedPack();
terminalPanel.requestFocusInWindow();
}
});
}
@Override
public void onConnectionError(String message) {
log.warning("Connection error: " + message);
SwingUtilities.invokeLater(statusBar::updateStatus);
}
@Override
public void onTN3270ENegotiated(String deviceType, String deviceName) {
log.info("TN3270E negotiated: type=" + deviceType + " name=" + deviceName);
SwingUtilities.invokeLater(() -> {
if (deviceName != null) {
setTitle("j3270 — " + lastHost + ":" + lastPort + " [" + deviceName + "]");
}
});
}
// ========== ScreenUpdateListener ==========
@Override
public void onScreenUpdated() {
SwingUtilities.invokeLater(() -> {
// During an active file transfer, let the CUT/DFT handler drive
// keyboard state. In x3270, ft_cut_data() runs before WCC
// keyboard-restore is applied the keyboard stays locked for the
// entire CUT transfer.
if (fileTransfer != null) {
fileTransfer.onScreenUpdated();
}
if (client != null && (fileTransfer == null || !fileTransfer.isTransferActive())) {
client.getInputProcessor().setKeyboardLocked(false);
}
terminalPanel.repaint();
statusBar.updateStatus();
});
}
@Override
public void onSoundAlarm() {
Toolkit.getDefaultToolkit().beep();
}
@Override
public void onScreenSizeChanged(int rows, int cols) {
SwingUtilities.invokeLater(() -> {
terminalPanel.guardedPack();
});
}
// ========== Help dialogs ==========
private void showKeyMappings() {
String text = "Key Mappings:\n\n" +
"Enter — Send Enter (AID)\n" +
"F1-F12 — PF1-PF12\n" +
"Shift+F1-F12 — PF13-PF24\n" +
"Alt+1/2/3 — PA1/PA2/PA3\n" +
"Tab — Tab to next field\n" +
"Shift+Tab — Back-tab\n" +
"Arrow keys — Cursor movement\n" +
"Home — Cursor to home\n" +
"End — Erase to end of field\n" +
"Delete — Delete character\n" +
"Backspace — Backspace\n" +
"Insert — Toggle insert mode\n" +
"Escape — Reset\n" +
"PageUp/Down — PF7/PF8\n" +
"Alt+C — Clear\n" +
"Alt+1/2/3 — PA1/PA2/PA3\n" +
"Cmd+D — Disconnect\n" +
"Cmd+Q — Quit\n" +
"Cmd+=/-/0 — Font size +/-/reset";
JTextArea area = new JTextArea(text);
area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
area.setBackground(new Color(30, 30, 30));
area.setForeground(new Color(200, 200, 200));
JScrollPane sp = new JScrollPane(area);
sp.setPreferredSize(new Dimension(360, 400));
JOptionPane.showMessageDialog(this, sp, "Key Mappings", JOptionPane.INFORMATION_MESSAGE);
}
private void showAbout() {
JOptionPane.showMessageDialog(this,
"j3270 — Java TN3270 Terminal Emulator\n\n" +
"A Java reimplementation of x3270.\n" +
"lib3270j v0.1.0\n\n" +
"Supports: TN3270, TN3270E (RFC 2355)\n" +
"Models: IBM 3278/3279 Models 2-5\n" +
"Colors, Extended Attributes, Query Replies",
"About j3270", JOptionPane.INFORMATION_MESSAGE);
}
// ========== Main ==========
public static void main(String[] args) {
boolean debug = false;
String configFile = null;
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
for (int i = 0; i < args.length; i++) {
if ("--debug".equals(args[i]) || "-d".equals(args[i])) {
debug = true;
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) {
configFile = args[++i];
} else {
remainingArgs.add(args[i]);
}
}
// Configure logging over the absolute root logger
Logger globalRoot = Logger.getLogger("");
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
globalRoot.removeHandler(h);
}
globalRoot.setLevel(debug ? Level.ALL : Level.WARNING);
ConsoleHandler handler = new ConsoleHandler();
handler.setLevel(Level.ALL);
handler.setFormatter(new SimpleFormatter());
globalRoot.addHandler(handler);
// Load INI config file if specified
if (configFile != null) {
try {
org.pubvm.j3270.config.Settings.loadFromIniFile(configFile);
} catch (Exception e) {
System.err.println("Failed to load config file: " + configFile);
System.err.println(e.getMessage());
System.exit(1);
}
}
// Dark look and feel
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
log.fine("Could not set system look and feel");
}
// Use native macOS menu bar if applicable
System.setProperty("apple.laf.useScreenMenuBar", "true");
System.setProperty("apple.awt.application.name", "j3270");
SwingUtilities.invokeLater(() -> {
J3270App app = new J3270App();
app.setVisible(true);
// If host:port given on command line, connect directly
if (!remainingArgs.isEmpty()) {
String host = remainingArgs.get(0);
int port = 23;
if (remainingArgs.size() >= 2) {
try {
port = Integer.parseInt(remainingArgs.get(1));
} catch (NumberFormatException e) {
}
}
TerminalModel model = TerminalModel.IBM_3279_4;
if (remainingArgs.size() >= 3) {
try {
int modelNum = Integer.parseInt(remainingArgs.get(2));
model = TerminalModel.forModel(modelNum, true);
} catch (Exception e) {
}
}
ConnectionConfig config = new ConnectionConfig(host, port, model);
app.connect(config);
} else {
org.pubvm.j3270.config.Settings.StartupBehavior behavior = org.pubvm.j3270.config.Settings
.getStartupBehavior();
switch (behavior) {
case SHOW_CONNECT:
SwingUtilities.invokeLater(app::showConnectDialog);
break;
case AUTO_CONNECT:
String host = org.pubvm.j3270.config.Settings.getAutoConnectHost();
int port = org.pubvm.j3270.config.Settings.getAutoConnectPort();
if (host != null && !host.isEmpty()) {
app.connect(new ConnectionConfig(host, port, TerminalModel.IBM_3279_4));
} else {
SwingUtilities.invokeLater(app::showConnectDialog);
}
break;
case DO_NOTHING:
default:
break;
}
}
});
}
}
@@ -1,181 +0,0 @@
package org.pubvm.j3270.ui;
import org.lib3270j.ConnectionConfig;
import org.lib3270j.TerminalModel;
import javax.swing.*;
import java.awt.*;
/**
* Connection dialog for entering host, port, model, and LU name.
*/
public class ConnectDialog extends JDialog {
private JTextField hostField;
private JTextField portField;
private JComboBox<TerminalModel> modelCombo;
private JTextField luField;
private boolean confirmed;
private ConnectionConfig result;
public ConnectDialog(Frame parent) {
super(parent, "Connect to Host", true);
buildUI();
pack();
setLocationRelativeTo(parent);
setResizable(false);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new GridBagLayout());
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
mainPanel.setBackground(new Color(30, 30, 30));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 4, 4, 4);
gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 14);
Color fg = new Color(200, 200, 200);
// Host
gbc.gridx = 0;
gbc.gridy = 0;
JLabel hostLabel = new JLabel("Host:");
hostLabel.setForeground(fg);
hostLabel.setFont(labelFont);
mainPanel.add(hostLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
hostField = createDarkField(20);
mainPanel.add(hostField, gbc);
// Port
gbc.gridx = 0;
gbc.gridy = 1;
gbc.weightx = 0;
JLabel portLabel = new JLabel("Port:");
portLabel.setForeground(fg);
portLabel.setFont(labelFont);
mainPanel.add(portLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
portField = createDarkField(6);
portField.setText("23");
mainPanel.add(portField, gbc);
// Model
gbc.gridx = 0;
gbc.gridy = 2;
gbc.weightx = 0;
JLabel modelLabel = new JLabel("Model:");
modelLabel.setForeground(fg);
modelLabel.setFont(labelFont);
mainPanel.add(modelLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
modelCombo = new JComboBox<>(TerminalModel.values());
modelCombo.setSelectedItem(TerminalModel.IBM_3279_4);
modelCombo.setBackground(new Color(45, 45, 45));
modelCombo.setForeground(fg);
modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
mainPanel.add(modelCombo, gbc);
// LU Name
gbc.gridx = 0;
gbc.gridy = 3;
gbc.weightx = 0;
JLabel luLabel = new JLabel("LU Name:");
luLabel.setForeground(fg);
luLabel.setFont(labelFont);
mainPanel.add(luLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
luField = createDarkField(12);
mainPanel.add(luField, gbc);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
buttonPanel.setBackground(new Color(30, 30, 30));
JButton connectBtn = new JButton("Connect");
connectBtn.setBackground(new Color(50, 120, 50));
connectBtn.setForeground(Color.WHITE);
connectBtn.setFont(labelFont);
connectBtn.addActionListener(e -> onConnect());
JButton cancelBtn = new JButton("Cancel");
cancelBtn.setBackground(new Color(60, 60, 60));
cancelBtn.setForeground(fg);
cancelBtn.setFont(labelFont);
cancelBtn.addActionListener(e -> {
confirmed = false;
dispose();
});
buttonPanel.add(cancelBtn);
buttonPanel.add(connectBtn);
gbc.gridx = 0;
gbc.gridy = 4;
gbc.gridwidth = 2;
mainPanel.add(buttonPanel, gbc);
setContentPane(mainPanel);
// Enter key triggers connect
getRootPane().setDefaultButton(connectBtn);
}
private JTextField createDarkField(int cols) {
JTextField field = new JTextField(cols);
field.setBackground(new Color(45, 45, 45));
field.setForeground(new Color(200, 200, 200));
field.setCaretColor(new Color(200, 200, 200));
field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
field.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(60, 60, 60)),
BorderFactory.createEmptyBorder(4, 6, 4, 6)));
return field;
}
private void onConnect() {
String host = hostField.getText().trim();
if (host.isEmpty()) {
hostField.requestFocus();
return;
}
int port;
try {
port = Integer.parseInt(portField.getText().trim());
} catch (NumberFormatException e) {
portField.requestFocus();
return;
}
result = new ConnectionConfig(host, port, (TerminalModel) modelCombo.getSelectedItem());
String lu = luField.getText().trim();
if (!lu.isEmpty()) {
result.setLuName(lu);
}
confirmed = true;
dispose();
}
public boolean isConfirmed() {
return confirmed;
}
public ConnectionConfig getConnectionConfig() {
return result;
}
/** Set initial values. */
public void setInitialHost(String host) {
hostField.setText(host);
}
public void setInitialPort(int port) {
portField.setText(String.valueOf(port));
}
}
@@ -1,140 +0,0 @@
package org.pubvm.j3270.ui;
import org.lib3270j.ConnectionState;
import org.lib3270j.Telnet3270Client;
import org.lib3270j.screen.ScreenBuffer;
import javax.swing.*;
import java.awt.*;
/**
* Status bar displaying connection state, cursor position, timing, and lock
* status.
* Equivalent to the OIA (Operator Information Area) on a real 3270 terminal.
*/
public class StatusBar extends JPanel {
private final JLabel connectionStatus;
private final JLabel cursorPosition;
private final JLabel luName;
private final JLabel lockStatus;
private final JLabel modelInfo;
private Telnet3270Client client;
// OIA colors
private static final Color OIA_BG = new Color(20, 20, 20);
private static final Color OIA_FG = new Color(50, 205, 50);
private static final Color OIA_DIM = new Color(80, 80, 80);
private static final Color OIA_ALERT = new Color(255, 80, 80);
public StatusBar() {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setBackground(OIA_BG);
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(40, 40, 40)));
setPreferredSize(new Dimension(800, 22));
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12);
connectionStatus = createLabel("Not Connected", oiaFont, OIA_DIM);
luName = createLabel("", oiaFont, OIA_FG);
lockStatus = createLabel("", oiaFont, OIA_ALERT);
modelInfo = createLabel("", oiaFont, OIA_DIM);
cursorPosition = createLabel("001/001", oiaFont, OIA_FG);
add(Box.createHorizontalStrut(6));
add(connectionStatus);
add(Box.createHorizontalStrut(12));
add(luName);
add(Box.createHorizontalStrut(12));
add(lockStatus);
add(Box.createHorizontalGlue());
add(modelInfo);
add(Box.createHorizontalStrut(12));
add(cursorPosition);
add(Box.createHorizontalStrut(6));
}
private JLabel createLabel(String text, Font font, Color fg) {
JLabel label = new JLabel(text);
label.setFont(font);
label.setForeground(fg);
return label;
}
public void setClient(Telnet3270Client client) {
this.client = client;
}
public void updateStatus() {
if (client == null)
return;
// Connection state
ConnectionState state = client.getConnectionState();
switch (state) {
case NOT_CONNECTED:
connectionStatus.setText("Not Connected");
connectionStatus.setForeground(OIA_DIM);
break;
case TCP_PENDING:
case TELNET_PENDING:
connectionStatus.setText("Connecting...");
connectionStatus.setForeground(OIA_ALERT);
break;
case CONNECTED_3270:
connectionStatus.setText("TN3270");
connectionStatus.setForeground(OIA_FG);
break;
case CONNECTED_TN3270E:
connectionStatus.setText("TN3270E");
connectionStatus.setForeground(OIA_FG);
break;
case CONNECTED_SSCP:
connectionStatus.setText("SSCP-LU");
connectionStatus.setForeground(OIA_FG);
break;
case CONNECTED_NVT:
case CONNECTED_NVT_CHAR:
case CONNECTED_E_NVT:
connectionStatus.setText("NVT");
connectionStatus.setForeground(OIA_FG);
break;
case CONNECTED_UNBOUND:
connectionStatus.setText("Unbound");
connectionStatus.setForeground(new Color(255, 255, 80));
break;
default:
connectionStatus.setText(state.name());
connectionStatus.setForeground(OIA_DIM);
break;
}
// LU name
String lu = "";
if (client.getConnectionState().isTn3270e()) {
// lu would come from FSM
}
luName.setText(lu);
// Lock status
if (client.getInputProcessor().isKeyboardLocked()) {
lockStatus.setText("X SYSTEM");
lockStatus.setForeground(OIA_ALERT);
} else if (client.getInputProcessor().isInsertMode()) {
lockStatus.setText("INSERT");
lockStatus.setForeground(OIA_FG);
} else {
lockStatus.setText("");
}
// Model info
modelInfo.setText(client.getConfig().getModel().getTerminalType());
// Cursor position
ScreenBuffer sb = client.getScreenBuffer();
int row = sb.getCursorRow() + 1;
int col = sb.getCursorCol() + 1;
cursorPosition.setText(String.format("%03d/%03d", row, col));
}
}
@@ -1,991 +0,0 @@
package org.pubvm.j3270.ui;
import org.lib3270j.Telnet3270Client;
import org.lib3270j.screen.ExtendedAttribute;
import org.lib3270j.screen.ScreenBuffer;
import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*;
import static org.lib3270j.protocol.DS3270Constants.*;
/**
* Custom JPanel that renders the 3270 screen buffer.
* Supports colors, bold, underline, reverse, blink, and all 3278/3279
* attributes.
*/
public class TerminalPanel extends JPanel {
private Telnet3270Client client;
// Font and cell dimensions
private Font terminalFont;
private int cellWidth;
private int cellHeight;
private int fontAscent;
private int fontDescent;
private int padding = 4;
private int currentFontSize = 16;
// Cursor state
private boolean cursorVisible = true;
private Timer blinkTimer;
// Host color mapping
private final Color[] hostColors = new Color[16];
// 3278 monochrome colors (based on field attributes)
private Color monoNormal;
private Color monoIntensify;
private Color monoProtected;
private Color monoProtectedHigh;
// Background
private Color bgColor;
// ========== Selection / Copy-Paste state ==========
private boolean blockSelectMode = false;
private int selectionStartRow = -1, selectionStartCol = -1;
private int selectionEndRow = -1, selectionEndCol = -1;
private boolean isDragging = false;
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 100);
// ========== Resize guard ==========
private boolean resizeGuard = false;
/**
* Compute the horizontal render offset to center the grid within the panel.
* Any leftover pixels (from integer font sizing) are split evenly.
*/
private int getRenderOffsetX() {
int termCols = 80;
if (client != null) termCols = client.getScreenBuffer().getCols();
int gridW = termCols * cellWidth;
int extra = getWidth() - gridW;
return Math.max(padding, extra / 2);
}
/**
* Compute the vertical render offset to center the grid within the panel.
*/
private int getRenderOffsetY() {
int termRows = 24;
if (client != null) termRows = client.getScreenBuffer().getRows();
int gridH = termRows * cellHeight;
int extra = getHeight() - gridH;
return Math.max(padding, extra / 2);
}
// Default Host color mapping
public static final Color[] DEFAULT_HOST_COLORS = {
new Color(0, 0, 0), // 0: Neutral Black
new Color(80, 120, 255), // 1: Blue
new Color(255, 50, 50), // 2: Red
new Color(255, 130, 180), // 3: Pink
new Color(50, 205, 50), // 4: Green
new Color(64, 224, 208), // 5: Turquoise
new Color(255, 255, 80), // 6: Yellow
new Color(255, 255, 255), // 7: Neutral White
new Color(0, 0, 0), // 8: Black
new Color(30, 60, 180), // 9: Deep Blue
new Color(255, 165, 0), // 10: Orange
new Color(180, 130, 255), // 11: Purple
new Color(144, 238, 144), // 12: Pale Green
new Color(175, 238, 238), // 13: Pale Turquoise
new Color(170, 170, 170), // 14: Grey
new Color(255, 255, 255), // 15: White
};
// Default 3278 monochrome colors
public static final Color DEFAULT_MONO_NORMAL = new Color(50, 205, 50);
public static final Color DEFAULT_MONO_INTENSIFY = new Color(255, 255, 255);
public static final Color DEFAULT_MONO_PROTECTED = new Color(80, 120, 255);
public static final Color DEFAULT_MONO_PROTECTED_HIGH = new Color(255, 255, 255);
// Default Background
public static final Color DEFAULT_BG_COLOR = new Color(10, 10, 10);
public TerminalPanel() {
setupColors();
setBackground(bgColor);
setFocusable(true);
setDoubleBuffered(true);
setFocusTraversalKeysEnabled(false);
// Use key bindings instead of KeyListener for reliable key handling
// This avoids focus/event issues with JScrollPane
setOpaque(true);
setupKeyBindings();
setupFont();
setupCursorBlink();
blockSelectMode = org.pubvm.j3270.config.Settings.getBlockSelectMode();
// Handle mouse clicks to position cursor, selection, and grab focus
MouseAdapter mouseHandler = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
requestFocusInWindow();
// Right-click context menu
if (SwingUtilities.isRightMouseButton(e)) {
showContextMenu(e);
return;
}
if (client != null && client.getConnectionState().isFullSession()) {
ScreenBuffer sb = client.getScreenBuffer();
int ox = getRenderOffsetX();
int oy = getRenderOffsetY();
int col = (e.getX() - ox) / cellWidth;
int row = (e.getY() - oy) / cellHeight;
col = Math.max(0, Math.min(col, sb.getCols() - 1));
row = Math.max(0, Math.min(row, sb.getRows() - 1));
// Start selection
selectionStartRow = row;
selectionStartCol = col;
selectionEndRow = row;
selectionEndCol = col;
isDragging = true;
repaint();
}
}
@Override
public void mouseDragged(MouseEvent e) {
if (isDragging && client != null && client.getConnectionState().isFullSession()) {
ScreenBuffer sb = client.getScreenBuffer();
int ox = getRenderOffsetX();
int oy = getRenderOffsetY();
int col = (e.getX() - ox) / cellWidth;
int row = (e.getY() - oy) / cellHeight;
col = Math.max(0, Math.min(col, sb.getCols() - 1));
row = Math.max(0, Math.min(row, sb.getRows() - 1));
selectionEndRow = row;
selectionEndCol = col;
repaint();
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (isDragging) {
isDragging = false;
// If start == end, treat as a click (position cursor, clear selection)
if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) {
if (client != null && client.getConnectionState().isFullSession()) {
ScreenBuffer sb = client.getScreenBuffer();
sb.setCursorAddress(selectionStartRow * sb.getCols() + selectionStartCol);
clearSelection();
}
}
repaint();
}
}
};
addMouseListener(mouseHandler);
addMouseMotionListener(mouseHandler);
// Handle resize: auto-fit font to window size
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
if (resizeGuard) return;
autoFitFont();
}
});
}
// ========== Selection / Copy-Paste ==========
private void clearSelection() {
selectionStartRow = -1;
selectionStartCol = -1;
selectionEndRow = -1;
selectionEndCol = -1;
}
private boolean hasSelection() {
return selectionStartRow >= 0 && selectionEndRow >= 0 &&
!(selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol);
}
/**
* Get the selected text from the screen buffer.
* In block mode: rectangular selection with newlines between rows.
* In line mode: stream selection, flowing left-to-right, top-to-bottom.
*/
public String getSelectedText() {
if (!hasSelection() || client == null) return "";
ScreenBuffer sb = client.getScreenBuffer();
int cols = sb.getCols();
int r1 = Math.min(selectionStartRow, selectionEndRow);
int r2 = Math.max(selectionStartRow, selectionEndRow);
int c1, c2;
if (blockSelectMode) {
// Block mode: rectangular selection
c1 = Math.min(selectionStartCol, selectionEndCol);
c2 = Math.max(selectionStartCol, selectionEndCol);
StringBuilder result = new StringBuilder();
for (int r = r1; r <= r2; r++) {
if (r > r1) result.append('\n');
for (int c = c1; c <= c2; c++) {
int baddr = r * cols + c;
ExtendedAttribute ea = sb.getCell(baddr);
char ch = ea.ucs4;
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
result.append(' ');
} else {
result.append(ch);
}
}
}
return result.toString();
} else {
// Line/stream mode: flow from start to end
int startAddr, endAddr;
if (selectionStartRow < selectionEndRow ||
(selectionStartRow == selectionEndRow && selectionStartCol <= selectionEndCol)) {
startAddr = selectionStartRow * cols + selectionStartCol;
endAddr = selectionEndRow * cols + selectionEndCol;
} else {
startAddr = selectionEndRow * cols + selectionEndCol;
endAddr = selectionStartRow * cols + selectionStartCol;
}
StringBuilder result = new StringBuilder();
int prevRow = startAddr / cols;
for (int addr = startAddr; addr <= endAddr; addr++) {
int curRow = addr / cols;
if (curRow != prevRow) {
result.append('\n');
prevRow = curRow;
}
ExtendedAttribute ea = sb.getCell(addr);
char ch = ea.ucs4;
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
result.append(' ');
} else {
result.append(ch);
}
}
return result.toString();
}
}
private void copySelection() {
String text = getSelectedText();
if (!text.isEmpty()) {
StringSelection ss = new StringSelection(text);
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
}
}
private void pasteClipboard() {
if (client == null || !client.getConnectionState().isFullSession()) return;
try {
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard()
.getData(DataFlavor.stringFlavor);
if (text != null) {
for (char ch : text.toCharArray()) {
if (ch == '\n' || ch == '\r') {
// Skip newlines in paste user may paste multi-line text
// but 3270 fields don't wrap the same way
continue;
}
if (ch >= 0x20 && ch != 0x7F) {
client.typeCharacter(ch);
}
}
refreshScreen();
}
} catch (Exception ex) {
// Clipboard not available or wrong type silently ignore
}
}
private void showContextMenu(MouseEvent e) {
JPopupMenu popup = new JPopupMenu();
JMenuItem copyItem = new JMenuItem("Copy");
copyItem.setEnabled(hasSelection());
copyItem.addActionListener(ev -> copySelection());
popup.add(copyItem);
JMenuItem pasteItem = new JMenuItem("Paste");
pasteItem.setEnabled(client != null && client.getConnectionState().isFullSession());
pasteItem.addActionListener(ev -> pasteClipboard());
popup.add(pasteItem);
popup.addSeparator();
JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode);
blockModeItem.addActionListener(ev -> {
blockSelectMode = blockModeItem.isSelected();
org.pubvm.j3270.config.Settings.setBlockSelectMode(blockSelectMode);
});
popup.add(blockModeItem);
popup.show(this, e.getX(), e.getY());
}
private boolean isCellSelected(int row, int col) {
if (!hasSelection()) return false;
int r1 = Math.min(selectionStartRow, selectionEndRow);
int r2 = Math.max(selectionStartRow, selectionEndRow);
if (blockSelectMode) {
int c1 = Math.min(selectionStartCol, selectionEndCol);
int c2 = Math.max(selectionStartCol, selectionEndCol);
return row >= r1 && row <= r2 && col >= c1 && col <= c2;
} else {
// Stream mode
int cols = 80;
if (client != null) cols = client.getScreenBuffer().getCols();
int addr = row * cols + col;
int startAddr, endAddr;
if (selectionStartRow < selectionEndRow ||
(selectionStartRow == selectionEndRow && selectionStartCol <= selectionEndCol)) {
startAddr = selectionStartRow * cols + selectionStartCol;
endAddr = selectionEndRow * cols + selectionEndCol;
} else {
startAddr = selectionEndRow * cols + selectionEndCol;
endAddr = selectionStartRow * cols + selectionStartCol;
}
return addr >= startAddr && addr <= endAddr;
}
}
// ========== Font auto-resize ==========
/**
* Auto-fit the font size to fill the current panel dimensions
* while respecting the terminal model's character grid.
* Any leftover pixels are handled by centering the grid
* (see getRenderOffsetX/Y).
*/
private void autoFitFont() {
int panelW = getWidth();
int panelH = getHeight();
if (panelW <= 0 || panelH <= 0) return;
int termCols = 80;
int termRows = 24;
if (client != null) {
ScreenBuffer sb = client.getScreenBuffer();
termCols = sb.getCols();
termRows = sb.getRows();
}
// Use minimal padding for the fit calculation
int availW = panelW - 2 * padding;
int availH = panelH - 2 * padding;
if (availW <= 0 || availH <= 0) return;
// Find the largest font size where the grid fits
int bestSize = 8;
for (int testSize = 8; testSize <= 72; testSize++) {
Font testFont = new Font(terminalFont.getFamily(), Font.PLAIN, testSize);
FontMetrics fm = getFontMetrics(testFont);
int testCellW = fm.charWidth('M');
int testCellH = fm.getHeight();
if (testCellW * termCols <= availW && testCellH * termRows <= availH) {
bestSize = testSize;
} else {
break;
}
}
if (bestSize != currentFontSize) {
currentFontSize = bestSize;
org.pubvm.j3270.config.Settings.setFontSize(bestSize);
terminalFont = new Font(terminalFont.getFamily(), Font.PLAIN, bestSize);
updateCellSize();
}
// No window snap any leftover pixels are centered via getRenderOffsetX/Y
repaint();
}
/**
* Called by J3270App when it needs to pack the frame (screen size changed,
* connection established, etc.). Sets the resize guard to prevent
* autoFitFont from firing during the pack.
*/
public void guardedPack() {
resizeGuard = true;
revalidate();
Container top = getTopLevelAncestor();
if (top instanceof java.awt.Window) {
((java.awt.Window) top).pack();
}
SwingUtilities.invokeLater(() -> resizeGuard = false);
}
/**
* Use InputMap/ActionMap (key bindings) instead of KeyListener.
* This works reliably even inside a JScrollPane the WHEN_FOCUSED
* condition ensures our panel receives all key events when focused.
*/
private void bindKeyToMap(InputMap im, String action, String bindingStr) {
if ("UNBOUND".equals(bindingStr) || bindingStr == null || bindingStr.isEmpty()) {
return;
}
// Support multiple bindings separated by commas
String[] bindings = bindingStr.split(",");
for (String binding : bindings) {
binding = binding.trim();
if (!binding.isEmpty() && !"UNBOUND".equals(binding)) {
KeyStroke ks = KeyStroke.getKeyStroke(binding);
if (ks != null) {
im.put(ks, "j3270-" + action);
}
}
}
}
private void setupKeyBindings() {
InputMap im = getInputMap(JComponent.WHEN_FOCUSED);
ActionMap am = getActionMap();
im.clear();
am.clear();
// Block the scroll pane from handling Tab, arrows, Page keys
String[] navKeys = { "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
"PAGE_UP", "PAGE_DOWN", "HOME", "END", "ENTER",
"ESCAPE", "INSERT", "DELETE", "BACK_SPACE" };
for (String key : navKeys) {
String binding = org.pubvm.j3270.config.Settings.getKeyBinding(key, key);
bindKeyToMap(im, key, binding);
}
// PF keys 1-24
// PF1-12 default to F1-F12, PF13-24 default to shift F1-shift F12
for (int i = 1; i <= 24; i++) {
String defaultBinding;
if (i <= 12) {
defaultBinding = "F" + i;
} else {
defaultBinding = "shift F" + (i - 12);
}
String binding = org.pubvm.j3270.config.Settings.getKeyBinding("PF" + i, defaultBinding);
bindKeyToMap(im, "PF" + i, binding);
}
// PA keys: Alt+1, Alt+2, Alt+3
String pa1Def = "alt 1";
String pa2Def = "alt 2";
String pa3Def = "alt 3";
bindKeyToMap(im, "PA1", org.pubvm.j3270.config.Settings.getKeyBinding("PA1", pa1Def));
bindKeyToMap(im, "PA2", org.pubvm.j3270.config.Settings.getKeyBinding("PA2", pa2Def));
bindKeyToMap(im, "PA3", org.pubvm.j3270.config.Settings.getKeyBinding("PA3", pa3Def));
// Clear
bindKeyToMap(im, "CLEAR", org.pubvm.j3270.config.Settings.getKeyBinding("CLEAR", "alt C"));
// Extended 3270 functions
bindKeyToMap(im, "ERASE_INPUT", org.pubvm.j3270.config.Settings.getKeyBinding("ERASE_INPUT", "alt E"));
bindKeyToMap(im, "NEWLINE", org.pubvm.j3270.config.Settings.getKeyBinding("NEWLINE", "shift ENTER"));
bindKeyToMap(im, "DUP", org.pubvm.j3270.config.Settings.getKeyBinding("DUP", "alt D"));
bindKeyToMap(im, "FIELD_MARK", org.pubvm.j3270.config.Settings.getKeyBinding("FIELD_MARK", "alt M"));
bindKeyToMap(im, "ATTN", org.pubvm.j3270.config.Settings.getKeyBinding("ATTN", "alt A"));
bindKeyToMap(im, "SYSREQ", org.pubvm.j3270.config.Settings.getKeyBinding("SYSREQ", "alt S"));
// Copy/Paste bindings Cmd+C / Cmd+V (macOS) or Ctrl+C / Ctrl+V (others)
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, shortcutMask), "j3270-PASTE");
// Create actions for all bound keys
am.put("j3270-ENTER", createAction(this::handleEnter));
am.put("j3270-ESCAPE", createAction(this::handleReset));
am.put("j3270-TAB", createAction(() -> handleTab(false)));
am.put("j3270-shift TAB", createAction(() -> handleTab(true)));
am.put("j3270-UP", createAction(() -> handleCursor("up")));
am.put("j3270-DOWN", createAction(() -> handleCursor("down")));
am.put("j3270-LEFT", createAction(() -> handleCursor("left")));
am.put("j3270-RIGHT", createAction(() -> handleCursor("right")));
am.put("j3270-HOME", createAction(() -> handleCursor("home")));
am.put("j3270-PAGE_UP", createAction(() -> handlePF(7)));
am.put("j3270-PAGE_DOWN", createAction(() -> handlePF(8)));
am.put("j3270-END", createAction(this::handleEraseEOF));
am.put("j3270-DELETE", createAction(this::handleDelete));
am.put("j3270-BACK_SPACE", createAction(this::handleBackspace));
am.put("j3270-INSERT", createAction(this::handleInsert));
am.put("j3270-CLEAR", createAction(this::handleClear));
am.put("j3270-ERASE_INPUT", createAction(this::handleEraseInput));
am.put("j3270-NEWLINE", createAction(this::handleNewline));
am.put("j3270-DUP", createAction(this::handleDup));
am.put("j3270-FIELD_MARK", createAction(this::handleFieldMark));
am.put("j3270-ATTN", createAction(this::handleAttn));
am.put("j3270-SYSREQ", createAction(this::handleSysReq));
// Copy/Paste actions
am.put("j3270-COPY", createAction(this::copySelection));
am.put("j3270-PASTE", createAction(this::pasteClipboard));
for (int i = 1; i <= 24; i++) {
final int pf = i;
am.put("j3270-PF" + i, createAction(() -> handlePF(pf)));
}
am.put("j3270-PA1", createAction(() -> handlePA(1)));
am.put("j3270-PA2", createAction(() -> handlePA(2)));
am.put("j3270-PA3", createAction(() -> handlePA(3)));
// For printable character input, we override processKeyEvent
enableEvents(AWTEvent.KEY_EVENT_MASK);
}
@Override
protected void processKeyEvent(KeyEvent e) {
// Handle character typing via processKeyEvent to capture ALL typed chars
if (e.getID() == KeyEvent.KEY_TYPED) {
char ch = e.getKeyChar();
if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED
&& !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
if (client != null && client.getConnectionState().isFullSession()) {
client.typeCharacter(ch);
refreshScreen();
e.consume();
return;
}
}
}
super.processKeyEvent(e);
}
private AbstractAction createAction(Runnable r) {
return new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
r.run();
}
};
}
// ========== Key action handlers ==========
private void handleEnter() {
if (client != null && client.getConnectionState().isFullSession()) {
client.sendEnter();
refreshScreen();
}
}
private void handleReset() {
if (client != null) {
client.reset();
refreshScreen();
}
}
private void handleTab(boolean shift) {
if (client != null && client.getConnectionState().isFullSession()) {
if (shift)
client.backTab();
else
client.tab();
refreshScreen();
}
}
private void handleCursor(String dir) {
if (client != null && client.getConnectionState().isFullSession()) {
switch (dir) {
case "up":
client.cursorUp();
break;
case "down":
client.cursorDown();
break;
case "left":
client.cursorLeft();
break;
case "right":
client.cursorRight();
break;
case "home":
client.cursorHome();
break;
}
refreshScreen();
}
}
private void handlePF(int n) {
if (client != null && client.getConnectionState().isFullSession()) {
client.sendPF(n);
refreshScreen();
}
}
private void handlePA(int n) {
if (client != null && client.getConnectionState().isFullSession()) {
client.sendPA(n);
refreshScreen();
}
}
private void handleEraseEOF() {
if (client != null && client.getConnectionState().isFullSession()) {
client.getInputProcessor().eraseEof();
refreshScreen();
}
}
private void handleDelete() {
if (client != null && client.getConnectionState().isFullSession()) {
client.getInputProcessor().deleteChar();
refreshScreen();
}
}
private void handleBackspace() {
if (client != null && client.getConnectionState().isFullSession()) {
client.getInputProcessor().backspace();
refreshScreen();
}
}
private void handleInsert() {
if (client != null) {
var ip = client.getInputProcessor();
ip.setInsertMode(!ip.isInsertMode());
refreshScreen();
}
}
private void handleClear() {
if (client != null && client.getConnectionState().isFullSession()) {
client.sendClear();
refreshScreen();
}
}
private void handleEraseInput() {
if (client != null && client.getConnectionState().isFullSession()) {
client.eraseInput();
refreshScreen();
}
}
private void handleNewline() {
if (client != null && client.getConnectionState().isFullSession()) {
client.newline();
refreshScreen();
}
}
private void handleDup() {
if (client != null && client.getConnectionState().isFullSession()) {
client.dup();
refreshScreen();
}
}
private void handleFieldMark() {
if (client != null && client.getConnectionState().isFullSession()) {
client.fieldMark();
refreshScreen();
}
}
private void handleAttn() {
if (client != null && client.isConnected()) {
client.attn();
}
}
private void handleSysReq() {
if (client != null && client.isConnected()) {
client.sysReq();
refreshScreen();
}
}
private void refreshScreen() {
repaint();
// Notify parent to update status bar too
Container parent = getParent();
while (parent != null) {
if (parent instanceof JFrame) {
parent.repaint();
break;
}
parent = parent.getParent();
}
}
private void setupFont() {
currentFontSize = org.pubvm.j3270.config.Settings.getFontSize();
String fontFamily = org.pubvm.j3270.config.Settings.getFontFamily();
terminalFont = new Font(fontFamily, Font.PLAIN, currentFontSize);
if (terminalFont.getFamily().equals("Dialog") && !fontFamily.equals("Dialog")) {
// Fallback
terminalFont = new Font(Font.MONOSPACED, Font.PLAIN, currentFontSize);
}
updateCellSize();
}
private void setupColors() {
for (int i = 0; i < 16; i++) {
hostColors[i] = org.pubvm.j3270.config.Settings.getColorOverride(i, DEFAULT_HOST_COLORS[i]);
}
monoNormal = org.pubvm.j3270.config.Settings.getMonoColorOverride("NORMAL", DEFAULT_MONO_NORMAL);
monoIntensify = org.pubvm.j3270.config.Settings.getMonoColorOverride("INTENSIFY", DEFAULT_MONO_INTENSIFY);
monoProtected = org.pubvm.j3270.config.Settings.getMonoColorOverride("PROTECTED", DEFAULT_MONO_PROTECTED);
monoProtectedHigh = org.pubvm.j3270.config.Settings.getMonoColorOverride("PROTECTED_HIGH", DEFAULT_MONO_PROTECTED_HIGH);
bgColor = org.pubvm.j3270.config.Settings.getMonoColorOverride("BACKGROUND", DEFAULT_BG_COLOR);
setBackground(bgColor);
}
public void reloadSettings() {
setupColors();
setupKeyBindings();
setupFont();
blockSelectMode = org.pubvm.j3270.config.Settings.getBlockSelectMode();
revalidate();
repaint();
}
public void setFontSize(int size) {
currentFontSize = size;
org.pubvm.j3270.config.Settings.setFontSize(size);
terminalFont = terminalFont.deriveFont((float) size);
updateCellSize();
resizeGuard = true;
revalidate();
repaint();
}
public int getFontSize() {
return currentFontSize;
}
private void updateCellSize() {
FontMetrics fm = getFontMetrics(terminalFont);
cellWidth = fm.charWidth('M');
cellHeight = fm.getHeight();
fontAscent = fm.getAscent();
fontDescent = fm.getDescent();
}
private void setupCursorBlink() {
blinkTimer = new Timer(530, e -> {
cursorVisible = !cursorVisible;
repaint();
});
blinkTimer.start();
}
public void setClient(Telnet3270Client client) {
this.client = client;
}
@Override
public Dimension getPreferredSize() {
if (client != null) {
ScreenBuffer sb = client.getScreenBuffer();
// Use the CURRENT screen dimensions (not max) to eliminate extra space.
// When the host switches to alternate screen, onScreenSizeChanged fires
// and the frame re-packs.
int displayCols = sb.getCols();
int displayRows = sb.getRows();
return new Dimension(displayCols * cellWidth + padding * 2,
displayRows * cellHeight + padding * 2);
}
return new Dimension(80 * cellWidth + padding * 2, 24 * cellHeight + padding * 2);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (client == null)
return;
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
// Clear entire panel with background color
g2.setColor(bgColor);
g2.fillRect(0, 0, getWidth(), getHeight());
// Compute centered offsets for the grid
int ox = getRenderOffsetX();
int oy = getRenderOffsetY();
ScreenBuffer sb = client.getScreenBuffer();
int rows = sb.getRows();
int cols = sb.getCols();
boolean isColorModel = client.getConfig().getModel().isColor();
// Track current field attribute for monochrome color decisions
byte currentFA = 0;
ExtendedAttribute currentFieldEa = null;
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
int baddr = row * cols + col;
ExtendedAttribute ea = sb.getCell(baddr);
int x = ox + col * cellWidth;
int y = oy + row * cellHeight;
// Determine colors and attributes
Color fgColor;
Color bgColor;
boolean bold = false;
boolean underline = false;
boolean reverse = false;
if (ea.isFieldAttribute()) {
currentFA = ea.fa;
currentFieldEa = ea;
// Field attributes display as blanks
g2.setColor(this.bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
// Selection highlight on field attribute cells too
if (isCellSelected(row, col)) {
g2.setColor(SELECTION_COLOR);
g2.fillRect(x, y, cellWidth, cellHeight);
}
continue;
}
// Determine foreground color
if (isColorModel) {
fgColor = getColorForAttribute(ea, currentFieldEa, currentFA);
bgColor = getBackgroundForAttribute(ea, currentFieldEa);
} else {
fgColor = getMonoColor(currentFA);
bgColor = this.bgColor;
}
// Graphics rendition
byte gr = ea.gr != 0 ? ea.gr : (currentFieldEa != null ? currentFieldEa.gr : 0);
if (gr != 0) {
if ((gr & GR_INTENSIFY) != 0)
bold = true;
if ((gr & GR_UNDERLINE) != 0)
underline = true;
if ((gr & GR_REVERSE) != 0)
reverse = true;
}
// Field attribute implicit intensify
if (faIsHigh(currentFA & 0xFF))
bold = true;
// Handle invisible fields (zero intensity)
if (faIsZero(currentFA & 0xFF)) {
g2.setColor(this.bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
if (isCellSelected(row, col)) {
g2.setColor(SELECTION_COLOR);
g2.fillRect(x, y, cellWidth, cellHeight);
}
continue;
}
// Apply reverse video
if (reverse) {
Color tmp = fgColor;
fgColor = bgColor;
bgColor = tmp;
}
// Draw background
g2.setColor(bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
// Draw character
char ch = ea.ucs4;
if (ch > 0x20 && ch != 0xFF) {
Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont;
g2.setFont(f);
g2.setColor(fgColor);
g2.drawString(String.valueOf(ch), x, y + fontAscent);
}
// Draw underline
if (underline) {
g2.setColor(fgColor);
g2.drawLine(x, y + cellHeight - fontDescent,
x + cellWidth - 1, y + cellHeight - fontDescent);
}
// Draw selection highlight
if (isCellSelected(row, col)) {
g2.setColor(SELECTION_COLOR);
g2.fillRect(x, y, cellWidth, cellHeight);
}
}
}
// Draw cursor
if (cursorVisible && client.getConnectionState().isFullSession()) {
int curAddr = sb.getCursorAddress();
int curRow = curAddr / cols;
int curCol = curAddr % cols;
int cx = ox + curCol * cellWidth;
int cy = oy + curRow * cellHeight;
g2.setColor(new Color(255, 255, 255, 180));
g2.setXORMode(bgColor);
g2.fillRect(cx, cy, cellWidth, cellHeight);
g2.setPaintMode();
}
}
private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
int fg = ea.fg != 0 ? (ea.fg & 0xFF)
: (currentFieldEa != null && currentFieldEa.fg != 0 ? (currentFieldEa.fg & 0xFF) : 0);
if (fg >= 0xf0 && fg <= 0xff) {
return hostColors[fg - 0xf0];
}
if (faIsProtected(currentFA & 0xFF)) {
return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_WHITE] : hostColors[HOST_COLOR_BLUE];
}
return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_RED] : hostColors[HOST_COLOR_GREEN];
}
private Color getBackgroundForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa) {
int bg = ea.bg != 0 ? (ea.bg & 0xFF)
: (currentFieldEa != null && currentFieldEa.bg != 0 ? (currentFieldEa.bg & 0xFF) : 0);
if (bg >= 0xf0 && bg <= 0xff) {
int idx = bg - 0xf0;
// Neutral black (0xf0) and black (0xf8) should use window bgColor
// to avoid visible seams between field bg and window bg
if (idx == HOST_COLOR_NEUTRAL_BLACK || idx == HOST_COLOR_BLACK) {
return bgColor;
}
return hostColors[idx];
}
return bgColor;
}
private Color getMonoColor(byte fa) {
if (faIsProtected(fa & 0xFF)) {
return faIsHigh(fa & 0xFF) ? monoProtectedHigh : monoProtected;
}
return faIsHigh(fa & 0xFF) ? monoIntensify : monoNormal;
}
public void dispose() {
if (blinkTimer != null)
blinkTimer.stop();
}
}
@@ -0,0 +1,59 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import static org.junit.jupiter.api.Assertions.*;
public class ScreenExporterTest {
private Telnet3270Client client;
@BeforeEach
public void setUp() {
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_2, false);
client = new Telnet3270Client(config);
client.getInputProcessor().setKeyboardLocked(false);
}
@Test
public void testPlainTextExport() throws IOException {
client.getPS().setText("TSO/E LOGON SCREEN - WELCOME", 0);
File tempTxt = File.createTempFile("export_test_", ".txt");
tempTxt.deleteOnExit();
ScreenExporter.exportToText(client, tempTxt);
assertTrue(tempTxt.exists());
assertTrue(tempTxt.length() > 0);
String content = Files.readString(tempTxt.toPath());
assertTrue(content.contains("TSO/E LOGON SCREEN - WELCOME"));
}
@Test
public void testHtmlExport() throws IOException {
client.getPS().setText("TSO/E LOGON SCREEN - WELCOME", 0);
File tempHtml = File.createTempFile("export_test_", ".html");
tempHtml.deleteOnExit();
ScreenExporter.exportToHtml(client, tempHtml);
assertTrue(tempHtml.exists());
assertTrue(tempHtml.length() > 0);
String html = Files.readString(tempHtml.toPath());
assertTrue(html.contains("<!DOCTYPE html>"));
assertTrue(html.contains("TSO/E LOGON SCREEN - WELCOME"));
assertTrue(html.contains("font-family: 'Courier New', Courier, monospace"));
}
}
@@ -0,0 +1,54 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import static org.junit.jupiter.api.Assertions.*;
public class StatusBarTest {
@Test
public void testStatusBarDoesNotContainLightPenButton() {
StatusBar statusBar = new StatusBar();
// Ensure no JButton exists in StatusBar components (lightpen button removed)
for (Component comp : statusBar.getComponents()) {
assertFalse(comp instanceof JButton, "StatusBar should not contain any JButton (lightpen removed from bottom bar)");
}
}
@Test
public void testStatusBarUpdatesWithClient() {
ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_4, false);
config.setLuName("TSU001");
config.setCodePage("1047");
Telnet3270Client client = new Telnet3270Client(config);
StatusBar statusBar = new StatusBar();
statusBar.setClient(client, null);
statusBar.updateStatus();
// Check labels
boolean foundCodePage = false;
boolean foundModel = false;
for (Component comp : statusBar.getComponents()) {
if (comp instanceof JLabel) {
JLabel label = (JLabel) comp;
if ("CP1047".equals(label.getText())) {
foundCodePage = true;
}
if (label.getText() != null && label.getText().contains("3279-4")) {
foundModel = true;
}
}
}
assertTrue(foundCodePage, "Should display CP1047");
assertTrue(foundModel, "Should display IBM-3279-4 model info");
}
}
+6
View File
@@ -3,6 +3,12 @@ plugins {
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
test {
useJUnitPlatform()
}
jar {
@@ -0,0 +1,234 @@
package haus.nightmare.lib3270j;
/**
* Configuration for a 3270 terminal connection.
*/
public class ConnectionConfig {
private String host;
private int port = 23;
private TerminalModel model = TerminalModel.IBM_3279_4;
private String luName = null;
private boolean extendedDataStream = true;
private boolean useTls = false;
private boolean tlsVerifyCert = true;
private haus.nightmare.lib3270j.tls.TlsCertificateVerifier certificateVerifier = null;
private String sslProtocol = "TLS";
private int connectTimeoutMs = 15000;
private int nopIntervalSeconds = 0;
private String terminalName = null; // override terminal type string
private boolean tn3270eEnabled = true;
private boolean tcpNoDelay = true;
private boolean soKeepAlive = true;
private int soTimeoutMs = 0;
private java.util.List<String> luNames = new java.util.ArrayList<>();
private boolean dynamicModel = false;
private int dynamicRows = 24;
private int dynamicCols = 80;
private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
private String codePage = "037";
private String associatedPrinterLu = null;
public ConnectionConfig() {}
public ConnectionConfig(String host, int port) {
this.host = host;
this.port = port;
}
public ConnectionConfig(String host, int port, boolean useTls) {
this.host = host;
this.port = port;
this.useTls = useTls;
}
public ConnectionConfig(String host, int port, TerminalModel model) {
this.host = host;
this.port = port;
this.model = model;
}
public ConnectionConfig(String host, int port, TerminalModel model, boolean useTls) {
this.host = host;
this.port = port;
this.model = model;
this.useTls = useTls;
}
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 TerminalModel getModel() { return model; }
public void setModel(TerminalModel model) { this.model = model; }
public String getLuName() { return luName; }
public void setLuName(String luName) { this.luName = luName; }
public boolean isExtendedDataStream() { return extendedDataStream; }
public void setExtendedDataStream(boolean ext) { this.extendedDataStream = ext; }
public boolean isUseTls() { return useTls; }
public void setUseTls(boolean useTls) { this.useTls = useTls; }
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
public void setTlsVerifyCert(boolean verify) { this.tlsVerifyCert = verify; }
public boolean isTn3270eEnabled() { return tn3270eEnabled; }
public void setTn3270eEnabled(boolean enabled) { this.tn3270eEnabled = enabled; }
public haus.nightmare.lib3270j.tls.TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; }
public void setCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) { this.certificateVerifier = verifier; }
public String getSslProtocol() { return sslProtocol; }
public void setSslProtocol(String protocol) { this.sslProtocol = protocol; }
public int getConnectTimeoutMs() { return connectTimeoutMs; }
public void setConnectTimeoutMs(int ms) { this.connectTimeoutMs = ms; }
public int getNopIntervalSeconds() { return nopIntervalSeconds; }
public void setNopIntervalSeconds(int s) { this.nopIntervalSeconds = s; }
public haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() { return graphicsMode; }
public void setGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode mode) {
this.graphicsMode = (mode != null) ? mode : haus.nightmare.lib3270j.graphics.GraphicsMode.NONE;
}
public String getCodePage() { return codePage; }
public void setCodePage(String codePage) {
this.codePage = (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037";
}
public String getTerminalName() { return terminalName; }
public void setTerminalName(String name) { this.terminalName = name; }
public boolean isTcpNoDelay() { return tcpNoDelay; }
public void setTcpNoDelay(boolean tcpNoDelay) { this.tcpNoDelay = tcpNoDelay; }
public boolean isSoKeepAlive() { return soKeepAlive; }
public void setSoKeepAlive(boolean soKeepAlive) { this.soKeepAlive = soKeepAlive; }
public int getSoTimeoutMs() { return soTimeoutMs; }
public void setSoTimeoutMs(int soTimeoutMs) { this.soTimeoutMs = soTimeoutMs; }
public java.util.List<String> getLuNames() { return luNames; }
public void setLuNames(java.util.List<String> luNames) {
this.luNames = (luNames != null) ? new java.util.ArrayList<>(luNames) : new java.util.ArrayList<>();
}
public void addLuName(String luName) {
if (luName != null && !luName.trim().isEmpty()) {
this.luNames.add(luName.trim());
}
}
public boolean isDynamicModel() { return dynamicModel; }
public void setDynamicModel(boolean dynamicModel) { this.dynamicModel = dynamicModel; }
public int getDynamicRows() { return dynamicRows; }
public int getDynamicCols() { return dynamicCols; }
public void setDynamicDimensions(int rows, int cols) {
this.dynamicModel = true;
this.dynamicRows = rows;
this.dynamicCols = cols;
}
/**
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), or standard "host:port" formats.
*/
public static ConnectionConfig parseHostString(String hostStr, int defaultPort, TerminalModel defaultModel) {
if (hostStr == null || hostStr.trim().isEmpty()) {
return new ConnectionConfig("localhost", defaultPort, defaultModel);
}
String s = hostStr.trim();
boolean tls = false;
boolean tn3270e = true;
// Parse chained x3270-style prefixes (e.g. "L:P:host:port" or "P:host:port")
boolean prefixFound = true;
while (prefixFound) {
prefixFound = false;
if (s.startsWith("L:") || s.startsWith("l:") || s.startsWith("Y:") || s.startsWith("y:")) {
tls = true;
s = s.substring(2);
prefixFound = true;
} else if (s.toLowerCase().startsWith("ssl:") || s.toLowerCase().startsWith("tls:")) {
tls = true;
s = s.substring(4);
prefixFound = true;
} else if (s.startsWith("N:") || s.startsWith("n:") || s.toLowerCase().startsWith("notls:") || s.toLowerCase().startsWith("nossl:")) {
tls = false;
int colon = s.indexOf(':');
s = s.substring(colon + 1);
prefixFound = true;
} else if (s.startsWith("P:") || s.startsWith("p:")) {
tn3270e = false;
s = s.substring(2);
prefixFound = true;
} else if (s.toLowerCase().startsWith("plain:") || s.toLowerCase().startsWith("non-e:")) {
tn3270e = false;
int colon = s.indexOf(':');
s = s.substring(colon + 1);
prefixFound = true;
}
}
String host = s;
int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23);
// Check for host:port (handle IPv6 [::1]:port)
if (s.startsWith("[") && s.contains("]")) {
int closeBracket = s.indexOf(']');
host = s.substring(1, closeBracket);
if (s.length() > closeBracket + 1 && s.charAt(closeBracket + 1) == ':') {
try {
port = Integer.parseInt(s.substring(closeBracket + 2));
} catch (NumberFormatException ignored) {}
}
} else {
int colon = s.lastIndexOf(':');
if (colon > 0 && colon < s.length() - 1) {
try {
port = Integer.parseInt(s.substring(colon + 1));
host = s.substring(0, colon);
} catch (NumberFormatException ignored) {}
}
}
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
config.setUseTls(tls);
config.setTn3270eEnabled(tn3270e);
return config;
}
/**
* Get the effective terminal type string to send during negotiation.
*/
public String getEffectiveTerminalType() {
if (terminalName != null) {
return terminalName;
}
if (dynamicModel) {
return extendedDataStream ? "IBM-DYNAMIC-E" : "IBM-DYNAMIC";
}
return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType();
}
public String getAssociatedPrinterLu() { return associatedPrinterLu; }
public void setAssociatedPrinterLu(String printerLu) { this.associatedPrinterLu = printerLu; }
public haus.nightmare.lib3270j.printer.PrinterConfig toPrinterConfig() {
haus.nightmare.lib3270j.printer.PrinterConfig pcfg = new haus.nightmare.lib3270j.printer.PrinterConfig(host, port);
pcfg.setUseTls(useTls);
pcfg.setTlsVerifyCert(tlsVerifyCert);
pcfg.setCertificateVerifier(certificateVerifier);
pcfg.setSslProtocol(sslProtocol);
pcfg.setConnectTimeoutMs(connectTimeoutMs);
pcfg.setPrinterLuName(associatedPrinterLu);
pcfg.setAssociatedDisplayLuName(luName);
pcfg.setCodePage(codePage);
return pcfg;
}
}
@@ -1,4 +1,4 @@
package org.lib3270j;
package haus.nightmare.lib3270j;
/**
* Connection state machine states.
@@ -1,8 +1,8 @@
package org.lib3270j;
package haus.nightmare.lib3270j;
import org.lib3270j.screen.ScreenBuffer;
import org.lib3270j.screen.ExtendedAttribute;
import org.lib3270j.listener.*;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.listener.*;
import java.util.logging.*;
@@ -15,7 +15,7 @@ public class DiagnosticClient {
static Telnet3270Client client;
public static void main(String[] args) throws Exception {
Logger rootLogger = Logger.getLogger("org.lib3270j");
Logger rootLogger = Logger.getLogger("haus.nightmare.lib3270j");
rootLogger.setLevel(Level.ALL);
Handler handler = new ConsoleHandler();
handler.setLevel(Level.ALL);
@@ -26,7 +26,7 @@ public class DiagnosticClient {
String host = args.length >= 1 ? args[0] : "192.168.0.30";
int port = args.length >= 2 ? Integer.parseInt(args[1]) : 3270;
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_2);
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4);
client = new Telnet3270Client(config);
client.addConnectionListener(new ConnectionListener() {
@@ -1,13 +1,13 @@
package org.lib3270j;
package haus.nightmare.lib3270j;
import org.lib3270j.charset.EbcdicTranslator;
import org.lib3270j.datastream.DataStreamProcessor;
import org.lib3270j.input.InputProcessor;
import org.lib3270j.listener.ConnectionListener;
import org.lib3270j.listener.ScreenUpdateListener;
import org.lib3270j.screen.ScreenBuffer;
import org.lib3270j.telnet.TelnetConnection;
import org.lib3270j.telnet.TelnetFSM;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.listener.ConnectionListener;
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.telnet.TelnetConnection;
import haus.nightmare.lib3270j.telnet.TelnetFSM;
import java.io.IOException;
import java.util.logging.Logger;
@@ -39,19 +39,35 @@ public class Telnet3270Client {
private final DataStreamProcessor dsProcessor;
private final TelnetFSM fsm;
private final InputProcessor inputProcessor;
private final haus.nightmare.lib3270j.ecl.ECLPS ps;
private final haus.nightmare.lib3270j.ecl.ECLOIA oia;
private final haus.nightmare.lib3270j.ecl.ECLXfer xfer;
private TelnetConnection connection;
public Telnet3270Client(ConnectionConfig config) {
this.config = config;
this.translator = new EbcdicTranslator();
this.translator = new EbcdicTranslator(config.getCodePage());
this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
this.ps = new haus.nightmare.lib3270j.ecl.ECLPS(screenBuffer, inputProcessor, translator);
this.oia = new haus.nightmare.lib3270j.ecl.ECLOIA(screenBuffer, inputProcessor, fsm);
this.xfer = new haus.nightmare.lib3270j.ecl.ECLXfer(screenBuffer, inputProcessor, dsProcessor, translator);
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
dsProcessor.setOutputSender(fsm::send3270Data);
dsProcessor.setInputProcessor(inputProcessor);
inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane());
inputProcessor.setGocaDecoder(dsProcessor.getGocaDecoder());
// Wire screen update to ECLXfer for CUT mode screen tracking
addScreenUpdateListener(new haus.nightmare.lib3270j.listener.ScreenUpdateListener() {
@Override public void onScreenUpdated() { xfer.onScreenUpdated(); }
@Override public void onScreenSizeChanged(int rows, int cols) {}
@Override public void onSoundAlarm() {}
});
}
/**
@@ -110,6 +126,12 @@ public class Telnet3270Client {
/** Get the EBCDIC translator. */
public EbcdicTranslator getTranslator() { return translator; }
/** Get the active Code Page identifier. */
public String getCodePage() { return translator.getCodePageId(); }
/** Set the active Code Page identifier. */
public void setCodePage(String codePageId) { translator.setCodePage(codePageId); }
/** Get the current connection state. */
public ConnectionState getConnectionState() { return fsm.getConnectionState(); }
@@ -122,6 +144,51 @@ public class Telnet3270Client {
/** Get the connection config. */
public ConnectionConfig getConfig() { return config; }
/** Get the ECL Presentation Space API. */
public haus.nightmare.lib3270j.ecl.ECLPS getPS() { return ps; }
/** Get the ECL Operator Information Area API. */
public haus.nightmare.lib3270j.ecl.ECLOIA getOIA() { return oia; }
/** Get the ECL File Transfer API. */
public haus.nightmare.lib3270j.ecl.ECLXfer getXfer() { return xfer; }
/** Get the list of all fields currently on screen. */
public haus.nightmare.lib3270j.ecl.ECLFieldList getFieldList() { return ps.getFieldList(); }
/** Send IBM ECL bracketed mnemonic keystrokes (e.g. "USER[tab]PASS[enter]"). */
public void sendKeys(String keys) { inputProcessor.sendKeys(keys); }
/** Set a custom or interactive TLS certificate verifier callback. */
public void setTlsCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) {
config.setCertificateVerifier(verifier);
}
/** Get the active SSLSession if connected over TLS, or null. */
public javax.net.ssl.SSLSession getSslSession() {
return connection != null ? connection.getSslSession() : null;
}
/** Get the NVT processor for ASCII / ANSI terminal processing. */
public haus.nightmare.lib3270j.nvt.NvtProcessor getNvtProcessor() {
return fsm.getNvtProcessor();
}
/** Get the Telnet state machine. */
public TelnetFSM getTelnetFSM() {
return fsm;
}
/** Send an NVT ASCII character in NVT mode. */
public void sendNVTChar(char c) throws IOException {
fsm.sendNVTChar(c);
}
/** Send an NVT ASCII string in NVT mode. */
public void sendNVTString(String s) throws IOException {
fsm.sendNVTString(s);
}
// ========== Convenience input methods ==========
/** Type a character at the cursor position. */
@@ -141,7 +208,7 @@ public class Telnet3270Client {
/** Send Enter key. */
public void sendEnter() {
inputProcessor.sendAid(org.lib3270j.protocol.DS3270Constants.AID_ENTER);
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
}
/** Send a PF key (1-24). */
@@ -179,7 +246,7 @@ public class Telnet3270Client {
/** Send Clear key. */
public void sendClear() {
inputProcessor.sendAid(org.lib3270j.protocol.DS3270Constants.AID_CLEAR);
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
}
/** Move cursor up. */
@@ -210,4 +277,20 @@ public class Telnet3270Client {
public void sysReq() { inputProcessor.sysReq(); }
/** Reset (unlock keyboard). */
public void reset() { inputProcessor.reset(); }
/** Trigger 3270 CURSR SEL (Cursor Select) key at current cursor position. */
public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
/** Trigger Light Pen selection at the specified screen address. */
public boolean lightPenSelect(int address) { return inputProcessor.lightPenSelect(address); }
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
return dsProcessor.getProgramSymbolManager();
}
public haus.nightmare.lib3270j.graphics.GraphicsPlane getGraphicsPlane() {
return dsProcessor.getGraphicsPlane();
}
public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() {
return dsProcessor.getGocaDecoder();
}
}
@@ -1,6 +1,6 @@
package org.lib3270j;
package haus.nightmare.lib3270j;
import static org.lib3270j.protocol.DS3270Constants.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/**
* Terminal model definitions for IBM 3278 and 3279 terminals.
@@ -0,0 +1,158 @@
package haus.nightmare.lib3270j.charset;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* Base implementation for Single-Byte Character Set (SBCS) EBCDIC Code Pages.
*/
public abstract class AbstractCodePage implements CodePage {
protected final String id;
protected final String description;
protected final int cpgid;
protected final int cgcsgid;
protected final int[] toUnicode = new int[256];
protected final int[] toEbcdic = new int[256];
protected final Map<Character, Integer> extendedToEbcdic = new HashMap<>();
public AbstractCodePage(String id, String description, int cpgid, int cgcsgid, int[] unicodeMapping) {
this(id, description, cpgid, cgcsgid, null, unicodeMapping);
}
public AbstractCodePage(String id, String description, int cpgid, int cgcsgid, String javaCharsetName, int[] unicodeMapping) {
this.id = id;
this.description = description;
this.cpgid = cpgid;
this.cgcsgid = cgcsgid;
int[] map = loadMapping(javaCharsetName, unicodeMapping);
System.arraycopy(map, 0, this.toUnicode, 0, 256);
initReverseMapping();
}
public static int[] loadMapping(String charsetName, int[] fallback) {
if (charsetName != null) {
String[] candidates = {
charsetName,
"IBM" + charsetName,
"Cp" + charsetName,
"IBM-" + charsetName,
"x-IBM" + charsetName
};
for (String name : candidates) {
try {
if (Charset.isSupported(name)) {
Charset cs = Charset.forName(name);
int[] map = new int[256];
for (int i = 0; i < 256; i++) {
byte[] b = new byte[]{(byte) i};
String s = new String(b, cs);
map[i] = (!s.isEmpty() && s.charAt(0) != '\uFFFD') ? s.charAt(0) : (fallback != null ? fallback[i] : 0);
}
return map;
}
} catch (Exception ignored) {}
}
}
return fallback != null ? fallback : new int[256];
}
protected void initReverseMapping() {
Arrays.fill(toEbcdic, -1);
for (int i = 0; i < 256; i++) {
int uc = toUnicode[i];
if (uc >= 0 && uc < 256) {
if (toEbcdic[uc] == -1) {
toEbcdic[uc] = i;
}
} else if (uc >= 256) {
extendedToEbcdic.putIfAbsent((char) uc, i);
}
}
}
@Override
public String getCodePageId() {
return id;
}
@Override
public String getDescription() {
return description;
}
@Override
public int getCpgid() {
return cpgid;
}
@Override
public int getCgcsgid() {
return cgcsgid;
}
@Override
public boolean isDBCS() {
return false;
}
@Override
public char ebcdicToUnicode(int ebc) {
return (char) toUnicode[ebc & 0xFF];
}
@Override
public int unicodeToEbcdic(char unicode) {
int u = unicode;
if (u >= 0 && u < 256) {
return toEbcdic[u];
}
Integer val = extendedToEbcdic.get(unicode);
return val != null ? val : -1;
}
@Override
public byte unicodeToEbcdicSafe(char unicode) {
int ebc = unicodeToEbcdic(unicode);
return (byte) (ebc >= 0 ? ebc : 0x40);
}
@Override
public char dbcsToUnicode(int b1, int b2) {
return '\uFFFD';
}
@Override
public int unicodeToDbcs(char unicode) {
return -1;
}
@Override
public String ebcdicToString(byte[] ebcdic, int offset, int length) {
if (ebcdic == null || length <= 0) return "";
char[] chars = new char[length];
for (int i = 0; i < length; i++) {
chars[i] = ebcdicToUnicode(ebcdic[offset + i]);
}
return new String(chars);
}
@Override
public byte[] stringToEbcdic(String s) {
if (s == null || s.isEmpty()) return new byte[0];
byte[] bytes = new byte[s.length()];
for (int i = 0; i < s.length(); i++) {
bytes[i] = unicodeToEbcdicSafe(s.charAt(i));
}
return bytes;
}
@Override
public String toString() {
return id + " (" + description + ")";
}
}
@@ -0,0 +1,184 @@
package haus.nightmare.lib3270j.charset;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.util.HashMap;
import java.util.Map;
/**
* Base implementation for Mixed Double-Byte Character Set (DBCS) EBCDIC Code Pages
* (e.g. Japanese Katakana 930, Japanese Latin 939, Chinese 935/937/1388, Korean 933).
*/
public abstract class AbstractDBCSCodePage extends AbstractCodePage {
public static final int SO = 0x0E; // Shift Out (enter DBCS mode)
public static final int SI = 0x0F; // Shift In (enter SBCS mode)
protected final Map<Integer, Character> dbcsToUnicodeMap = new HashMap<>();
protected final Map<Character, Integer> unicodeToDbcsMap = new HashMap<>();
protected Charset nioCharset;
public AbstractDBCSCodePage(String id, String description, int cpgid, int cgcsgid,
int[] sbcsMapping, String nioCharsetName) {
super(id, description, cpgid, cgcsgid, sbcsMapping);
if (nioCharsetName != null) {
try {
if (Charset.isSupported(nioCharsetName)) {
this.nioCharset = Charset.forName(nioCharsetName);
}
} catch (Exception ignored) {}
}
// Initialize ideographic space (0x4040 <-> \u3000)
registerDbcsPair(0x4040, '\u3000');
}
public void registerDbcsPair(int ebcdicDBCS, char unicodeChar) {
dbcsToUnicodeMap.put(ebcdicDBCS, unicodeChar);
unicodeToDbcsMap.put(unicodeChar, ebcdicDBCS);
}
@Override
public boolean isDBCS() {
return true;
}
@Override
public char dbcsToUnicode(int b1, int b2) {
int key = ((b1 & 0xFF) << 8) | (b2 & 0xFF);
Character c = dbcsToUnicodeMap.get(key);
if (c != null) {
return c;
}
if (nioCharset != null) {
try {
byte[] raw = new byte[] { (byte) SO, (byte) b1, (byte) b2, (byte) SI };
CharBuffer cb = nioCharset.decode(ByteBuffer.wrap(raw));
if (cb.hasRemaining()) {
char decoded = cb.get();
if (decoded != '\uFFFD' && decoded != 0) {
registerDbcsPair(key, decoded);
return decoded;
}
}
} catch (Exception ignored) {}
}
return '?';
}
@Override
public int unicodeToDbcs(char unicode) {
Integer val = unicodeToDbcsMap.get(unicode);
if (val != null) {
return val;
}
if (nioCharset != null) {
try {
CharsetEncoder encoder = nioCharset.newEncoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE);
ByteBuffer bb = encoder.encode(CharBuffer.wrap(new char[]{unicode}));
byte[] bytes = new byte[bb.remaining()];
bb.get(bytes);
// Check if encoded as SO + b1 + b2 + SI or b1 + b2
if (bytes.length >= 4 && (bytes[0] & 0xFF) == SO) {
int key = ((bytes[1] & 0xFF) << 8) | (bytes[2] & 0xFF);
registerDbcsPair(key, unicode);
return key;
} else if (bytes.length == 2) {
int key = ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF);
registerDbcsPair(key, unicode);
return key;
}
} catch (Exception ignored) {}
}
return -1;
}
@Override
public String ebcdicToString(byte[] ebcdic, int offset, int length) {
if (ebcdic == null || length <= 0) return "";
StringBuilder sb = new StringBuilder(length);
boolean inDBCS = false;
int end = Math.min(ebcdic.length, offset + length);
int i = offset;
while (i < end) {
int b = ebcdic[i] & 0xFF;
if (b == SO) {
inDBCS = true;
i++;
} else if (b == SI) {
inDBCS = false;
i++;
} else if (inDBCS) {
if (i + 1 < end) {
int b2 = ebcdic[i + 1] & 0xFF;
if (b2 == SI) {
// Orphaned single byte before SI
inDBCS = false;
i += 2;
} else {
sb.append(dbcsToUnicode(b, b2));
i += 2;
}
} else {
// Trailing byte
i++;
}
} else {
sb.append(ebcdicToUnicode(b));
i++;
}
}
return sb.toString();
}
@Override
public byte[] stringToEbcdic(String s) {
if (s == null || s.isEmpty()) return new byte[0];
// Manual state machine encoding
ByteArrayOutputStream out = new ByteArrayOutputStream(s.length() * 2);
boolean inDBCS = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
int dbcsCode = unicodeToDbcs(c);
if (dbcsCode >= 0) {
// Character is DBCS
if (!inDBCS) {
out.write(SO);
inDBCS = true;
}
out.write((dbcsCode >> 8) & 0xFF);
out.write(dbcsCode & 0xFF);
} else {
// Character is SBCS
if (inDBCS) {
out.write(SI);
inDBCS = false;
}
out.write(unicodeToEbcdicSafe(c));
}
}
if (inDBCS) {
out.write(SI);
}
return out.toByteArray();
}
}
@@ -0,0 +1,72 @@
package haus.nightmare.lib3270j.charset;
/**
* Interface defining character translation and metadata for EBCDIC code pages
* conforming to IBM Host On-Demand (HoD v14) converters specification.
*/
public interface CodePage {
/**
* Get the primary identifier for this code page (e.g. "037", "1047", "500", "273").
*/
String getCodePageId();
/**
* Get a human-readable description (e.g. "US / Canada - EBCDIC").
*/
String getDescription();
/**
* Get the Code Page Global ID (CPGID / CCSID) for 3270 query replies.
*/
int getCpgid();
/**
* Get the Graphic Character Set Global ID (GCSGID) for 3270 query replies.
*/
int getCgcsgid();
/**
* Translate an EBCDIC byte (0x00-0xFF) to a Unicode character.
*/
char ebcdicToUnicode(int ebc);
/**
* Translate a Unicode character to an EBCDIC byte value.
* Returns -1 if unmappable.
*/
int unicodeToEbcdic(char unicode);
/**
* Translate a Unicode character to an EBCDIC byte value, returning a safe
* fallback (default EBCDIC space 0x40) if unmappable.
*/
byte unicodeToEbcdicSafe(char unicode);
/**
* Translate an EBCDIC byte array slice to a Unicode String.
*/
String ebcdicToString(byte[] ebcdic, int offset, int length);
/**
* Translate a Unicode String to an EBCDIC byte array.
*/
byte[] stringToEbcdic(String s);
/**
* Check if this code page is a double-byte (DBCS) / mixed SBCS+DBCS code page.
*/
boolean isDBCS();
/**
* Translate a double-byte EBCDIC pair (b1, b2) to a Unicode character.
* Returns '\uFFFD' or '?' if unmappable.
*/
char dbcsToUnicode(int b1, int b2);
/**
* Translate a Unicode character to a double-byte EBCDIC code ((b1 << 8) | b2).
* Returns -1 if unmappable.
*/
int unicodeToDbcs(char unicode);
}
@@ -0,0 +1,270 @@
package haus.nightmare.lib3270j.charset;
import java.nio.charset.Charset;
import java.util.*;
import java.util.logging.Logger;
/**
* Registry and factory for 3270 EBCDIC Code Pages.
* Manages built-in SBCS/DBCS codepages, alias normalization, and dynamic JVM Charset resolution.
*/
public class CodePageRegistry {
private static final Logger log = Logger.getLogger(CodePageRegistry.class.getName());
private static final Map<String, CodePage> CODE_PAGES = new LinkedHashMap<>();
private static final Map<String, String> ALIASES = new HashMap<>();
static {
// Register core SBCS codepages
register(new Cp037());
register(new Cp1047());
register(new Cp500());
register(new Cp273());
register(new Cp277());
register(new Cp278());
register(new Cp280());
register(new Cp284());
register(new Cp285());
register(new Cp297());
register(new Cp870());
register(new Cp871());
register(new Cp875());
register(new Cp1026());
// Register Euro variants (1140-1149)
register(new CpEuroVariants.Cp1140());
register(new CpEuroVariants.Cp1141());
register(new CpEuroVariants.Cp1142());
register(new CpEuroVariants.Cp1143());
register(new CpEuroVariants.Cp1144());
register(new CpEuroVariants.Cp1145());
register(new CpEuroVariants.Cp1146());
register(new CpEuroVariants.Cp1147());
register(new CpEuroVariants.Cp1148());
register(new CpEuroVariants.Cp1149());
// Register DBCS mixed codepages
register(new Cp930());
register(new Cp939());
register(new Cp935());
register(new Cp935.Cp1388());
register(new Cp937());
register(new Cp937.Cp1371());
register(new Cp933());
// Setup common aliases
addAlias("us", "037");
addAlias("usa", "037");
addAlias("ebcdic-cp-us", "037");
addAlias("ebcdic-cp-ca", "037");
addAlias("ebcdic-cp-nl", "037");
addAlias("posix", "1047");
addAlias("unix", "1047");
addAlias("open-systems", "1047");
addAlias("zos-unix", "1047");
addAlias("intl", "500");
addAlias("international", "500");
addAlias("ebcdic-cp-ch", "500");
addAlias("de", "273");
addAlias("germany", "273");
addAlias("austria", "273");
addAlias("ebcdic-cp-de", "273");
addAlias("dk", "277");
addAlias("no", "277");
addAlias("denmark", "277");
addAlias("norway", "277");
addAlias("ebcdic-cp-dk", "277");
addAlias("ebcdic-cp-no", "277");
addAlias("se", "278");
addAlias("fi", "278");
addAlias("sweden", "278");
addAlias("finland", "278");
addAlias("ebcdic-cp-se", "278");
addAlias("ebcdic-cp-fi", "278");
addAlias("it", "280");
addAlias("italy", "280");
addAlias("ebcdic-cp-it", "280");
addAlias("es", "284");
addAlias("spain", "284");
addAlias("latin-america", "284");
addAlias("ebcdic-cp-es", "284");
addAlias("uk", "285");
addAlias("gb", "285");
addAlias("great-britain", "285");
addAlias("ebcdic-cp-gb", "285");
addAlias("fr", "297");
addAlias("france", "297");
addAlias("ebcdic-cp-fr", "297");
addAlias("latin2", "870");
addAlias("pl", "870");
addAlias("cz", "870");
addAlias("hu", "870");
addAlias("ebcdic-cp-roece", "870");
addAlias("is", "871");
addAlias("iceland", "871");
addAlias("ebcdic-cp-is", "871");
addAlias("gr", "875");
addAlias("greece", "875");
addAlias("greek", "875");
addAlias("ebcdic-cp-gr", "875");
addAlias("tr", "1026");
addAlias("turkey", "1026");
addAlias("turkish", "1026");
addAlias("ebcdic-cp-tr", "1026");
addAlias("ja", "930");
addAlias("japanese", "930");
addAlias("katakana", "930");
addAlias("ja-latin", "939");
addAlias("zh", "935");
addAlias("chinese-simplified", "935");
addAlias("zh-tw", "937");
addAlias("chinese-traditional", "937");
addAlias("ko", "933");
addAlias("korean", "933");
}
public static void register(CodePage cp) {
if (cp != null) {
CODE_PAGES.put(cp.getCodePageId(), cp);
}
}
public static void addAlias(String alias, String targetId) {
if (alias != null && targetId != null) {
ALIASES.put(normalizeKey(alias), targetId);
}
}
public static String normalizeKey(String name) {
if (name == null) return "";
String s = name.trim().toLowerCase();
s = s.replace("_", "").replace("-", "");
if (s.startsWith("ebcdiccp")) {
s = s.substring(8);
} else if (s.startsWith("ebcdic")) {
s = s.substring(6);
} else if (s.startsWith("ibm")) {
s = s.substring(3);
} else if (s.startsWith("cp")) {
s = s.substring(2);
} else if (s.startsWith("ccsid")) {
s = s.substring(5);
}
return s;
}
/**
* Look up a code page by ID or alias.
* If not found in built-ins, attempts to load via java.nio.charset.Charset.
* Falls back to CP037 if completely unresolvable.
*/
public static CodePage getCodePage(String name) {
if (name == null || name.trim().isEmpty()) {
return CODE_PAGES.get("037");
}
String raw = name.trim();
// Direct match
CodePage cp = CODE_PAGES.get(raw);
if (cp != null) return cp;
// Normalized key lookup
String norm = normalizeKey(raw);
cp = CODE_PAGES.get(norm);
if (cp != null) return cp;
// Alias lookup
String targetId = ALIASES.get(norm);
if (targetId != null) {
cp = CODE_PAGES.get(targetId);
if (cp != null) return cp;
}
// Try standard NIO Charset dynamic adapter
try {
if (Charset.isSupported(raw)) {
return new NioCodePageAdapter(raw);
}
String ibmName = "IBM" + norm;
if (Charset.isSupported(ibmName)) {
return new NioCodePageAdapter(ibmName);
}
String cpName = "Cp" + norm;
if (Charset.isSupported(cpName)) {
return new NioCodePageAdapter(cpName);
}
} catch (Exception e) {
log.fine("Dynamic charset loading failed for " + name + ": " + e.getMessage());
}
log.warning("CodePage not recognized: '" + name + "'; falling back to CP037");
return CODE_PAGES.get("037");
}
/**
* Get an unmodifiable list of all registered built-in CodePages.
*/
public static List<CodePage> getAvailableCodePages() {
return Collections.unmodifiableList(new ArrayList<>(CODE_PAGES.values()));
}
/**
* Get an array of all registered built-in CodePage IDs.
*/
public static String[] getAvailableCodePageIds() {
return CODE_PAGES.keySet().toArray(new String[0]);
}
/**
* Dynamic fallback adapter wrapping any JVM java.nio.charset.Charset.
*/
public static class NioCodePageAdapter extends AbstractCodePage {
private final Charset charset;
public NioCodePageAdapter(String charsetName) {
this(Charset.forName(charsetName));
}
public NioCodePageAdapter(Charset charset) {
super(charset.name(), "JVM Charset: " + charset.displayName(), parseCpgid(charset.name()), 697, buildMappingFromCharset(charset));
this.charset = charset;
}
private static int parseCpgid(String name) {
try {
String num = name.replaceAll("\\D+", "");
if (!num.isEmpty()) {
return Integer.parseInt(num);
}
} catch (Exception ignored) {}
return 37;
}
private static int[] buildMappingFromCharset(Charset cs) {
int[] map = new int[256];
for (int i = 0; i < 256; i++) {
byte[] b = new byte[]{(byte) i};
String s = new String(b, cs);
map[i] = !s.isEmpty() ? s.charAt(0) : (char) i;
}
return map;
}
}
}
@@ -1,16 +1,17 @@
package org.lib3270j.charset;
package haus.nightmare.lib3270j.charset;
/**
* EBCDIC Unicode translator.
* Default: Code Page 037 (US/Canada EBCDIC).
* IBM Code Page 037 (US / Canada / Netherlands / Portugal / Brazil).
* CCSID / CPGID: 37, GCSGID: 697.
*/
public class EbcdicTranslator {
public class Cp037 extends AbstractCodePage {
/**
* EBCDIC Code Page 037 to Unicode mapping.
* Index is the EBCDIC byte value (0x00-0xFF), value is the Unicode codepoint.
*/
private static final int[] CP037_TO_UNICODE = {
public static final String ID = "037";
public static final String DESCRIPTION = "US / Canada / Brazil - EBCDIC";
public static final int CPGID = 37;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
@@ -61,68 +62,7 @@ public class EbcdicTranslator {
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
/**
* Unicode to EBCDIC Code Page 037 mapping (for basic Latin + Latin-1).
* Index is the Unicode codepoint (0x00-0xFF), value is the EBCDIC byte (-1 if unmappable).
*/
private static final int[] UNICODE_TO_CP037 = new int[256];
static {
// Build reverse mapping
java.util.Arrays.fill(UNICODE_TO_CP037, -1);
for (int i = 0; i < 256; i++) {
int unicode = CP037_TO_UNICODE[i];
if (unicode < 256) {
UNICODE_TO_CP037[unicode] = i;
}
}
}
/**
* Translate EBCDIC byte to Unicode character.
*/
public char ebcdicToUnicode(int ebc) {
return (char) CP037_TO_UNICODE[ebc & 0xFF];
}
/**
* Translate Unicode character to EBCDIC byte.
* Returns -1 if the character cannot be mapped.
*/
public int unicodeToEbcdic(char unicode) {
if (unicode < 256) {
return UNICODE_TO_CP037[unicode];
}
return -1;
}
/**
* Translate Unicode character to EBCDIC, returning EBCDIC space (0x40) if unmappable.
*/
public byte unicodeToEbcdicSafe(char unicode) {
int ebc = unicodeToEbcdic(unicode);
return (byte) (ebc >= 0 ? ebc : 0x40);
}
/**
* Translate a byte array from EBCDIC to a Unicode string.
*/
public String ebcdicToString(byte[] ebcdic, int offset, int length) {
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
sb.append(ebcdicToUnicode(ebcdic[offset + i] & 0xFF));
}
return sb.toString();
}
/**
* Translate a Unicode string to EBCDIC byte array.
*/
public byte[] stringToEbcdic(String s) {
byte[] result = new byte[s.length()];
for (int i = 0; i < s.length(); i++) {
result[i] = unicodeToEbcdicSafe(s.charAt(i));
}
return result;
public Cp037() {
super(ID, DESCRIPTION, CPGID, GCSGID, "037", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1026 (Turkey - Turkish Latin-5 EBCDIC).
* CCSID / CPGID: 1026, GCSGID: 1152.
*/
public class Cp1026 extends AbstractCodePage {
public static final String ID = "1026";
public static final String DESCRIPTION = "Turkey - Turkish Latin-5 EBCDIC";
public static final int CPGID = 1026;
public static final int GCSGID = 1152;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x015E, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00D6, 0x00C7, 0x00DC, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x0130, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x00F6, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x011E, 0x00F4, 0x0131, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x00E7, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x011F, 0x00FB, 0x015F, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x00FC, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00A6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp1026() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1026", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1047 (IBM Open Systems / z/OS Unix System Services Latin-1).
* CCSID / CPGID: 1047, GCSGID: 103.
*/
public class Cp1047 extends AbstractCodePage {
public static final String ID = "1047";
public static final String DESCRIPTION = "IBM Open Systems / z/OS Unix Latin-1";
public static final int CPGID = 1047;
public static final int GCSGID = 103;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x000A, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x007C,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x0021, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x005B, 0x00DE, 0x00AE,
// B0-BF
0x00AC, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00DD, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp1047() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1047", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 273 (Germany / Austria).
* CCSID / CPGID: 273, GCSGID: 697.
*/
public class Cp273 extends AbstractCodePage {
public static final String ID = "273";
public static final String DESCRIPTION = "Germany / Austria - EBCDIC";
public static final int CPGID = 273;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x007B, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x005B, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00E4, 0x00F6, 0x00FC, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x00E4, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x00FC, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00DC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp273() {
super(ID, DESCRIPTION, CPGID, GCSGID, "273", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 277 (Denmark / Norway).
* CCSID / CPGID: 277, GCSGID: 697.
*/
public class Cp277 extends AbstractCodePage {
public static final String ID = "277";
public static final String DESCRIPTION = "Denmark / Norway - EBCDIC";
public static final int CPGID = 277;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x00A4, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00E6, 0x00F8, 0x00E5, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x00C6, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x00D8, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x00C5, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp277() {
super(ID, DESCRIPTION, CPGID, GCSGID, "277", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 278 (Sweden / Finland).
* CCSID / CPGID: 278, GCSGID: 697.
*/
public class Cp278 extends AbstractCodePage {
public static final String ID = "278";
public static final String DESCRIPTION = "Sweden / Finland - EBCDIC";
public static final int CPGID = 278;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x007B, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x00A4, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x005B, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00E4, 0x00F6, 0x00E5, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x00C4, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x00D6, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x00C5, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp278() {
super(ID, DESCRIPTION, CPGID, GCSGID, "278", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 280 (Italy).
* CCSID / CPGID: 280, GCSGID: 697.
*/
public class Cp280 extends AbstractCodePage {
public static final String ID = "280";
public static final String DESCRIPTION = "Italy - EBCDIC";
public static final int CPGID = 280;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x007B, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00B0, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x005B, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00A3, 0x00A7, 0x00E9, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x00F2, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x00F9, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x00E0, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp280() {
super(ID, DESCRIPTION, CPGID, GCSGID, "280", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 284 (Spain / Latin America).
* CCSID / CPGID: 284, GCSGID: 697.
*/
public class Cp284 extends AbstractCodePage {
public static final String ID = "284";
public static final String DESCRIPTION = "Spain / Latin America - EBCDIC";
public static final int CPGID = 284;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x007C,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x0021, 0x0024, 0x002A, 0x0029, 0x003B, 0x00AC,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00F1, 0x00D1, 0x00E7, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp284() {
super(ID, DESCRIPTION, CPGID, GCSGID, "284", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 285 (United Kingdom).
* CCSID / CPGID: 285, GCSGID: 697.
*/
public class Cp285 extends AbstractCodePage {
public static final String ID = "285";
public static final String DESCRIPTION = "United Kingdom - EBCDIC";
public static final int CPGID = 285;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x007C,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x0021, 0x0024, 0x002A, 0x0029, 0x003B, 0x00AC,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00A3, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp285() {
super(ID, DESCRIPTION, CPGID, GCSGID, "285", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 297 (France).
* CCSID / CPGID: 297, GCSGID: 697.
*/
public class Cp297 extends AbstractCodePage {
public static final String ID = "297";
public static final String DESCRIPTION = "France - EBCDIC";
public static final int CPGID = 297;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x007B, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00B0, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x005B, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00E0, 0x00E9, 0x00E8, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x00E9, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x00E8, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp297() {
super(ID, DESCRIPTION, CPGID, GCSGID, "297", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 500 (International / Western Europe Latin-1).
* CCSID / CPGID: 500, GCSGID: 697.
*/
public class Cp500 extends AbstractCodePage {
public static final String ID = "500";
public static final String DESCRIPTION = "International / Western Europe Latin-1";
public static final int CPGID = 500;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp500() {
super(ID, DESCRIPTION, CPGID, GCSGID, "500", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 870 (Latin-2 / Eastern Europe: Polish, Czech, Slovak, Hungarian, etc.).
* CCSID / CPGID: 870, GCSGID: 959.
*/
public class Cp870 extends AbstractCodePage {
public static final String ID = "870";
public static final String DESCRIPTION = "Eastern Europe / Latin-2 - EBCDIC";
public static final int CPGID = 870;
public static final int GCSGID = 959;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x0103, 0x00E4, 0x016F, 0x0165, 0x0105, 0x0155,
0x0107, 0x00E9, 0x015B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x0119, 0x00EB, 0x011B, 0x00ED, 0x00EE, 0x010F,
0x010D, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x016E, 0x0164, 0x0104, 0x0154,
0x0106, 0x00C9, 0x015A, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x0118, 0x00CB, 0x011A, 0x00CD, 0x00CE, 0x010E,
0x010C, 0x0060, 0x003A, 0x0161, 0x017C, 0x00FD, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x0160, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x017B, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp870() {
super(ID, DESCRIPTION, CPGID, GCSGID, "870", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 871 (Iceland).
* CCSID / CPGID: 871, GCSGID: 697.
*/
public class Cp871 extends AbstractCodePage {
public static final String ID = "871";
public static final String DESCRIPTION = "Iceland - EBCDIC";
public static final int CPGID = 871;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x00A4, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00FE, 0x00F0, 0x00FD, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x00DE, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x00D0, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x00DD, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp871() {
super(ID, DESCRIPTION, CPGID, GCSGID, "871", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 875 (Greece - Greek EBCDIC).
* CCSID / CPGID: 875, GCSGID: 925.
*/
public class Cp875 extends AbstractCodePage {
public static final String ID = "875";
public static final String DESCRIPTION = "Greece - Greek EBCDIC";
public static final int CPGID = 875;
public static final int GCSGID = 925;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x03AC, 0x03AD, 0x03AE, 0x03AF, 0x03CC, 0x03CD,
0x03CE, 0x00DF, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x0390, 0x03B0, 0x03CA, 0x03CB, 0x0390, 0x0385, 0x0386,
0x0388, 0x0389, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x038A, 0x038C, 0x038E, 0x038F, 0x03AA, 0x03AB,
0x00A9, 0x00AE, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7,
0x03B8, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x03B9, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF,
// 90-9F
0x03C0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x03C1, 0x03C3, 0x03C2, 0x03C4, 0x03C5, 0x03C6,
// A0-AF
0x03C7, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x03C8, 0x03C9, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C,
// E0-EF
0x005C, 0x039D, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x039E, 0x039F, 0x03A0, 0x03A1, 0x03A3, 0x03A4,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x009F,
};
public Cp875() {
super(ID, DESCRIPTION, CPGID, GCSGID, "875", MAPPING);
}
}
@@ -0,0 +1,17 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 930 (Japanese Katakana Mixed DBCS: host SBCS CP290 / DBCS CP300).
* CCSID / CPGID: 930, GCSGID: 1172.
*/
public class Cp930 extends AbstractDBCSCodePage {
public static final String ID = "930";
public static final String DESCRIPTION = "Japanese Katakana Mixed DBCS";
public static final int CPGID = 930;
public static final int GCSGID = 1172;
public Cp930() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM930");
}
}
@@ -0,0 +1,17 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 933 (Korean Mixed DBCS).
* CCSID / CPGID: 933, GCSGID: 1173.
*/
public class Cp933 extends AbstractDBCSCodePage {
public static final String ID = "933";
public static final String DESCRIPTION = "Korean Mixed DBCS";
public static final int CPGID = 933;
public static final int GCSGID = 1173;
public Cp933() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM933");
}
}
@@ -0,0 +1,28 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 935 / 1388 (Simplified Chinese Mixed DBCS).
* CCSID / CPGID: 935, GCSGID: 1175.
*/
public class Cp935 extends AbstractDBCSCodePage {
public static final String ID = "935";
public static final String DESCRIPTION = "Simplified Chinese Mixed DBCS";
public static final int CPGID = 935;
public static final int GCSGID = 1175;
public Cp935() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM935");
}
public static class Cp1388 extends AbstractDBCSCodePage {
public static final String ID = "1388";
public static final String DESCRIPTION = "Simplified Chinese Extended Mixed DBCS";
public static final int CPGID = 1388;
public static final int GCSGID = 1175;
public Cp1388() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM1388");
}
}
}
@@ -0,0 +1,28 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 937 / 1371 (Traditional Chinese Mixed DBCS).
* CCSID / CPGID: 937, GCSGID: 1174.
*/
public class Cp937 extends AbstractDBCSCodePage {
public static final String ID = "937";
public static final String DESCRIPTION = "Traditional Chinese Mixed DBCS";
public static final int CPGID = 937;
public static final int GCSGID = 1174;
public Cp937() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM937");
}
public static class Cp1371 extends AbstractDBCSCodePage {
public static final String ID = "1371";
public static final String DESCRIPTION = "Traditional Chinese Extended Mixed DBCS";
public static final int CPGID = 1371;
public static final int GCSGID = 1174;
public Cp1371() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM1371");
}
}
}
@@ -0,0 +1,17 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 939 (Japanese Latin Mixed DBCS: host SBCS CP1027 / DBCS CP300).
* CCSID / CPGID: 939, GCSGID: 1172.
*/
public class Cp939 extends AbstractDBCSCodePage {
public static final String ID = "939";
public static final String DESCRIPTION = "Japanese Latin Mixed DBCS";
public static final int CPGID = 939;
public static final int GCSGID = 1172;
public Cp939() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM939");
}
}
@@ -0,0 +1,125 @@
package haus.nightmare.lib3270j.charset;
import java.util.Arrays;
/**
* Euro currency symbol () enabled EBCDIC code pages (CP1140 through CP1149).
*/
public class CpEuroVariants {
public static class Cp1140 extends AbstractCodePage {
public static final String ID = "1140";
public static final String DESCRIPTION = "US / Canada Euro - EBCDIC";
public static final int CPGID = 1140;
public static final int GCSGID = 695;
public Cp1140() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1140", modifyEuro(Cp037.MAPPING, 0x9F));
}
}
public static class Cp1141 extends AbstractCodePage {
public static final String ID = "1141";
public static final String DESCRIPTION = "Germany / Austria Euro - EBCDIC";
public static final int CPGID = 1141;
public static final int GCSGID = 695;
public Cp1141() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1141", modifyEuro(Cp273.MAPPING, 0x9F));
}
}
public static class Cp1142 extends AbstractCodePage {
public static final String ID = "1142";
public static final String DESCRIPTION = "Denmark / Norway Euro - EBCDIC";
public static final int CPGID = 1142;
public static final int GCSGID = 695;
public Cp1142() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1142", modifyEuro(Cp277.MAPPING, 0x5A));
}
}
public static class Cp1143 extends AbstractCodePage {
public static final String ID = "1143";
public static final String DESCRIPTION = "Sweden / Finland Euro - EBCDIC";
public static final int CPGID = 1143;
public static final int GCSGID = 695;
public Cp1143() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1143", modifyEuro(Cp278.MAPPING, 0x5A));
}
}
public static class Cp1144 extends AbstractCodePage {
public static final String ID = "1144";
public static final String DESCRIPTION = "Italy Euro - EBCDIC";
public static final int CPGID = 1144;
public static final int GCSGID = 695;
public Cp1144() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1144", modifyEuro(Cp280.MAPPING, 0x9F));
}
}
public static class Cp1145 extends AbstractCodePage {
public static final String ID = "1145";
public static final String DESCRIPTION = "Spain / Latin America Euro - EBCDIC";
public static final int CPGID = 1145;
public static final int GCSGID = 695;
public Cp1145() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1145", modifyEuro(Cp284.MAPPING, 0x9F));
}
}
public static class Cp1146 extends AbstractCodePage {
public static final String ID = "1146";
public static final String DESCRIPTION = "United Kingdom Euro - EBCDIC";
public static final int CPGID = 1146;
public static final int GCSGID = 695;
public Cp1146() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1146", modifyEuro(Cp285.MAPPING, 0x9F));
}
}
public static class Cp1147 extends AbstractCodePage {
public static final String ID = "1147";
public static final String DESCRIPTION = "France Euro - EBCDIC";
public static final int CPGID = 1147;
public static final int GCSGID = 695;
public Cp1147() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1147", modifyEuro(Cp297.MAPPING, 0x9F));
}
}
public static class Cp1148 extends AbstractCodePage {
public static final String ID = "1148";
public static final String DESCRIPTION = "International Latin-1 Euro - EBCDIC";
public static final int CPGID = 1148;
public static final int GCSGID = 695;
public Cp1148() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1148", modifyEuro(Cp500.MAPPING, 0x9F));
}
}
public static class Cp1149 extends AbstractCodePage {
public static final String ID = "1149";
public static final String DESCRIPTION = "Iceland Euro - EBCDIC";
public static final int CPGID = 1149;
public static final int GCSGID = 695;
public Cp1149() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1149", modifyEuro(Cp871.MAPPING, 0x9F));
}
}
private static int[] modifyEuro(int[] baseMapping, int euroPosition) {
int[] copy = Arrays.copyOf(baseMapping, 256);
copy[euroPosition & 0xFF] = '\u20AC'; // '€'
return copy;
}
}
@@ -0,0 +1,204 @@
package haus.nightmare.lib3270j.charset;
import java.util.List;
/**
* EBCDIC Unicode character translator conforming to IBM Host On-Demand (HoD v14).
* Modular architecture delegating to pluggable CodePage implementations (SBCS and DBCS).
* Default: Code Page 037 (US/Canada EBCDIC).
*/
public class EbcdicTranslator {
/**
* EBCDIC Code Page 037 to Unicode mapping table preserved for legacy static access.
*/
public static final int[] CP037_TO_UNICODE = Cp037.MAPPING;
private CodePage activeCodePage;
public EbcdicTranslator() {
this("037");
}
public EbcdicTranslator(String codePageId) {
setCodePage(codePageId);
}
public EbcdicTranslator(CodePage codePage) {
setCodePage(codePage);
}
/**
* Set active code page by identifier or alias (e.g. "037", "1047", "500", "273", "1140", "930").
*/
public synchronized void setCodePage(String codePageId) {
this.activeCodePage = CodePageRegistry.getCodePage(codePageId);
}
/**
* Set active code page directly.
*/
public synchronized void setCodePage(CodePage codePage) {
if (codePage != null) {
this.activeCodePage = codePage;
} else {
this.activeCodePage = CodePageRegistry.getCodePage("037");
}
}
/**
* Get the active CodePage descriptor and translator.
*/
public synchronized CodePage getCodePage() {
return activeCodePage;
}
/**
* Get the active code page identifier string.
*/
public synchronized String getCodePageId() {
return activeCodePage.getCodePageId();
}
/**
* Get active Code Page Global ID (CPGID / CCSID) for Query Replies.
*/
public synchronized int getCpgid() {
return activeCodePage.getCpgid();
}
/**
* Get active Graphic Character Set Global ID (GCSGID) for Query Replies.
*/
public synchronized int getCgcsgid() {
return activeCodePage.getCgcsgid();
}
/**
* Check if active code page is Double-Byte / mixed DBCS.
*/
public synchronized boolean isDBCSCodePage() {
return activeCodePage.isDBCS();
}
public synchronized boolean isDBCS() {
return isDBCSCodePage();
}
/**
* Translate double-byte EBCDIC pair (b1, b2) to Unicode.
*/
public synchronized char dbcsToUnicode(int b1, int b2) {
return activeCodePage.dbcsToUnicode(b1, b2);
}
/**
* Translate Unicode character to double-byte EBCDIC ((b1 << 8) | b2).
*/
public synchronized int unicodeToDbcs(char unicode) {
return activeCodePage.unicodeToDbcs(unicode);
}
/**
* Translate EBCDIC byte to Unicode character using active code page.
*/
public char ebcdicToUnicode(int ebc) {
return activeCodePage.ebcdicToUnicode(ebc);
}
/**
* Static helper to translate EBCDIC byte to Unicode character (using standard CP037).
*/
public static char toUnicode(int ebc) {
return (char) CP037_TO_UNICODE[ebc & 0xFF];
}
/**
* Static helper to translate EBCDIC byte to ASCII character (using standard CP037).
*/
public static char ebcdicToAscii(int ebc) {
return toUnicode(ebc);
}
/**
* Translate Unicode character to EBCDIC byte using active code page.
* Returns -1 if the character cannot be mapped.
*/
public int unicodeToEbcdic(char unicode) {
return activeCodePage.unicodeToEbcdic(unicode);
}
/**
* Translate Unicode character to EBCDIC, returning EBCDIC space (0x40) if unmappable.
*/
public byte unicodeToEbcdicSafe(char unicode) {
return activeCodePage.unicodeToEbcdicSafe(unicode);
}
/**
* Translate a byte array from EBCDIC to a Unicode string using active code page.
*/
public String ebcdicToString(byte[] ebcdic, int offset, int length) {
return activeCodePage.ebcdicToString(ebcdic, offset, length);
}
/**
* Translate a Unicode string to EBCDIC byte array using active code page.
*/
public byte[] stringToEbcdic(String s) {
return activeCodePage.stringToEbcdic(s);
}
/**
* Get an array of all registered code page ID strings.
*/
public static String[] getAvailableCodePages() {
return CodePageRegistry.getAvailableCodePageIds();
}
/**
* Get a list of all registered CodePage instances.
*/
public static List<CodePage> getAllCodePages() {
return CodePageRegistry.getAvailableCodePages();
}
/**
* Translate an IBM 3270 Graphic Escape (GE) / APL character code to Unicode.
*/
public char getAplGraphic(int ec) {
switch (ec & 0xFF) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL)
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
case 0xD5: return '\u2510'; // Top Right 'N' -> '┐'
case 0xC4: return '\u2514'; // Bottom Left 'D' -> '└'
case 0xD4: return '\u2518'; // Bottom Right 'M' -> '┘'
case 0xC6: return '\u251C'; // T-Junction Left 'F' -> '├'
case 0xD6: return '\u2524'; // T-Junction Right 'O' -> '┤'
case 0xC7: return '\u252C'; // T-Junction Top 'G' -> '┬'
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
case 0xCB: return '\u253C'; // Cross -> '┼'
// Special math and APL symbols (matching x3270 cg.c / apl.c)
case 0x8C: return '\u2264'; // Less-than or equal '≤'
case 0xAE: return '\u2265'; // Greater-than or equal '≥'
case 0xBE: return '\u2260'; // Not equal '≠'
case 0xAD: return '['; // Left bracket
case 0xBD: return ']'; // Right bracket
case 0x8D: return '{'; // Left brace
case 0x9D: return '}'; // Right brace
case 0xB0: return '\u00B0'; // Degree '°'
case 0xB1: return '\u00B1'; // Plus-minus '±'
case 0xB2: return '\u00B2'; // Superscript 2 '²'
case 0xB3: return '\u00B3'; // Superscript 3 '³'
case 0xAF: return '\u00AF'; // Overbar '¯'
case 0xBA: return '\u03A9'; // Omega 'Ω'
case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬'
default: return ebcdicToUnicode(ec & 0xFF);
}
}
}
@@ -1,13 +1,16 @@
package org.lib3270j.datastream;
package haus.nightmare.lib3270j.datastream;
import org.lib3270j.screen.ScreenBuffer;
import org.lib3270j.screen.ExtendedAttribute;
import org.lib3270j.charset.EbcdicTranslator;
import org.lib3270j.listener.ScreenUpdateListener;
import static org.lib3270j.protocol.DS3270Constants.*;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
@@ -32,10 +35,17 @@ public class DataStreamProcessor {
private OutputSender outputSender;
// File Transfer DFT handler
private org.lib3270j.ft.FTDft ftDft;
private haus.nightmare.lib3270j.ft.FTDft ftDft;
// Input processor reference to manage keyboard locking state
private org.lib3270j.input.InputProcessor inputProcessor;
private haus.nightmare.lib3270j.input.InputProcessor inputProcessor;
// Graphics & Programmed Symbols
private final haus.nightmare.lib3270j.graphics.ProgramSymbolManager programSymbolManager = new haus.nightmare.lib3270j.graphics.ProgramSymbolManager();
private final haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(720, 384);
private final haus.nightmare.lib3270j.graphics.GocaDecoder gocaDecoder = new haus.nightmare.lib3270j.graphics.GocaDecoder(graphicsPlane);
private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream();
private int currentGocaSubtype = 0;
/** Functional interface for sending output back through the telnet stack. */
@FunctionalInterface
@@ -49,20 +59,52 @@ public class DataStreamProcessor {
this.qrBuilder = new QueryReplyBuilder(screen);
this.outputBuffer = new byte[32768];
this.outputPos = 0;
this.gocaDecoder.setProgramSymbolManager(programSymbolManager);
this.graphicsPlane.setProgramSymbolManager(programSymbolManager);
}
public QueryReplyBuilder getQueryReplyBuilder() {
return qrBuilder;
}
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
return programSymbolManager;
}
public haus.nightmare.lib3270j.graphics.GraphicsPlane getGraphicsPlane() {
return graphicsPlane;
}
public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() {
return gocaDecoder;
}
public void setOutputSender(OutputSender sender) {
this.outputSender = sender;
}
public void setFTDft(org.lib3270j.ft.FTDft ftDft) {
public void setFTDft(haus.nightmare.lib3270j.ft.FTDft ftDft) {
this.ftDft = ftDft;
}
public void setInputProcessor(org.lib3270j.input.InputProcessor inputProcessor) {
private haus.nightmare.lib3270j.printer.PrintSCS3270 embeddedScsProcessor;
public void setEmbeddedScsProcessor(haus.nightmare.lib3270j.printer.PrintSCS3270 scsProcessor) {
this.embeddedScsProcessor = scsProcessor;
}
public haus.nightmare.lib3270j.printer.PrintSCS3270 getEmbeddedScsProcessor() {
return embeddedScsProcessor;
}
public void setInputProcessor(haus.nightmare.lib3270j.input.InputProcessor inputProcessor) {
this.inputProcessor = inputProcessor;
}
public haus.nightmare.lib3270j.input.InputProcessor getInputProcessor() {
return inputProcessor;
}
public void addScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.add(l);
}
@@ -80,72 +122,97 @@ public class DataStreamProcessor {
return;
int cmd = data[offset] & 0xFF;
log.info(">>> CMD: " + commandName(cmd) + " (0x" + String.format("%02x", cmd) + ") " + length + " bytes");
synchronized (screen.getRenderLock()) {
if (log.isLoggable(Level.FINE)) {
log.fine(">>> CMD: " + commandName(cmd) + " (0x" + String.format("%02x", cmd) + ") " + length + " bytes");
}
switch (cmd) {
case CMD_W:
case SNA_CMD_W:
processWrite(data, offset, length, false);
break;
case CMD_EW:
case SNA_CMD_EW:
log.info(">>> ERASE/WRITE: clearing screen (default size)"); {
int oldRows = screen.getRows();
int oldCols = screen.getCols();
screen.erase(false);
processWrite(data, offset, length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged();
}
switch (cmd) {
case CMD_W:
case SNA_CMD_W:
programSymbolManager.commitStagedSymbols();
processWrite(data, offset, length, false);
break;
case CMD_EW:
case SNA_CMD_EW:
programSymbolManager.commitStagedSymbols();
if (log.isLoggable(Level.FINE)) {
log.fine(">>> ERASE/WRITE: clearing screen (default size)");
}
{
int oldRows = screen.getRows();
int oldCols = screen.getCols();
screen.erase(false);
graphicsPlane.clear();
processWrite(data, offset, length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged();
}
}
break;
case CMD_EWA:
case SNA_CMD_EWA:
programSymbolManager.commitStagedSymbols();
if (log.isLoggable(Level.FINE)) {
log.fine(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)");
}
{
int oldRows = screen.getRows();
int oldCols = screen.getCols();
screen.erase(true);
graphicsPlane.clear();
processWrite(data, offset, length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged();
}
}
break;
case CMD_RB:
case SNA_CMD_RB:
programSymbolManager.commitStagedSymbols();
processReadBuffer();
break;
case CMD_RM:
case SNA_CMD_RM:
programSymbolManager.commitStagedSymbols();
processReadModified(false);
break;
case CMD_RMA:
case SNA_CMD_RMA:
programSymbolManager.commitStagedSymbols();
processReadModified(true);
break;
case CMD_EAU:
case SNA_CMD_EAU:
programSymbolManager.commitStagedSymbols();
log.info(">>> EAU: erasing all unprotected fields");
screen.eraseAllUnprotected();
break;
case CMD_WSF:
case SNA_CMD_WSF:
processWriteStructuredField(data, offset, length);
keyboardRestore = true;
break;
case CMD_NOP:
log.info(">>> NOP command");
break;
default:
log.warning(">>> UNKNOWN 3270 command: " + String.format("0x%02x", cmd));
break;
}
break;
case CMD_EWA:
case SNA_CMD_EWA:
log.info(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)"); {
int oldRows = screen.getRows();
int oldCols = screen.getCols();
screen.erase(true);
processWrite(data, offset, length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged();
}
}
break;
case CMD_RB:
case SNA_CMD_RB:
processReadBuffer();
break;
case CMD_RM:
case SNA_CMD_RM:
processReadModified(false);
break;
case CMD_RMA:
case SNA_CMD_RMA:
processReadModified(true);
break;
case CMD_EAU:
case SNA_CMD_EAU:
log.info(">>> EAU: erasing all unprotected fields");
screen.eraseAllUnprotected();
break;
case CMD_WSF:
case SNA_CMD_WSF:
processWriteStructuredField(data, offset, length);
break;
case CMD_NOP:
log.info(">>> NOP command");
break;
default:
log.warning(">>> UNKNOWN 3270 command: " + String.format("0x%02x", cmd));
break;
// Translate EBCDIC to Unicode for display
screen.translateToUnicode();
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
// Translate EBCDIC to Unicode for display
screen.translateToUnicode();
screen.markAllChanged();
if (keyboardRestore && inputProcessor != null) {
inputProcessor.setKeyboardLocked(false);
}
// Debug: dump non-empty screen lines
if (cmd == SNA_CMD_EWA || cmd == CMD_EWA || cmd == SNA_CMD_EW || cmd == CMD_EW) {
if (log.isLoggable(Level.FINE) && (cmd == SNA_CMD_EWA || cmd == CMD_EWA || cmd == SNA_CMD_EW || cmd == CMD_EW)) {
int r = screen.getRows();
int c = screen.getCols();
StringBuilder dump = new StringBuilder();
@@ -158,7 +225,7 @@ public class DataStreamProcessor {
ExtendedAttribute ea = screen.getCell(addr);
byte fa = screen.getFieldAttributeAt(addr);
if ((fa & org.lib3270j.protocol.DS3270Constants.FA_MASK) == org.lib3270j.protocol.DS3270Constants.FA_INT_ZERO_NSEL) {
if ((fa & haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MASK) == haus.nightmare.lib3270j.protocol.DS3270Constants.FA_INT_ZERO_NSEL) {
if (ea.ucs4 > 0x20 && ea.ucs4 != 0xFF) {
line.append('*');
hasContent = true;
@@ -178,7 +245,7 @@ public class DataStreamProcessor {
}
}
if (dump.length() > 0) {
log.info("Screen content after " + commandName(cmd) + ":\n" + dump.toString());
log.fine("Screen content after " + commandName(cmd) + ":\n" + dump.toString());
}
}
}
@@ -451,8 +518,8 @@ public class DataStreamProcessor {
pos += 4;
}
// Fill from current position to target
while (baddr != toAddr) {
// Fill from current position to target (do-while wraps entire buffer if baddr == toAddr)
do {
ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0; // Destroy previous field attribute if any
ea.ec = (byte) fillChar;
@@ -461,7 +528,7 @@ public class DataStreamProcessor {
ea.gr = (byte) currentGr;
ea.cs = fillCs;
baddr = (baddr + 1) % size;
}
} while (baddr != toAddr);
screen.setBufferAddress(baddr);
lastWasOrder = true;
break;
@@ -476,7 +543,7 @@ public class DataStreamProcessor {
if (toAddr >= size)
toAddr = toAddr % size;
while (baddr != toAddr) {
do {
ExtendedAttribute ea = screen.getCell(baddr);
if (!ea.isFieldAttribute()) {
int faAddr = screen.findFieldAttribute(baddr);
@@ -491,7 +558,7 @@ public class DataStreamProcessor {
}
}
baddr = (baddr + 1) % size;
}
} while (baddr != toAddr);
screen.setBufferAddress(baddr);
pos += 3;
lastWasOrder = true;
@@ -582,10 +649,21 @@ public class DataStreamProcessor {
case XA_CHARSET:
ea.cs = (byte) value;
break;
case XA_VALIDATION:
case XA_OUTLINING:
case 0x84: // HoD / 3270 Outlining alternative ID
ea.ol = (byte) value;
break;
case XA_VALIDATION:
ea.vl = (byte) value;
break;
case XA_TRANSPARENCY:
ea.tr = (byte) value;
break;
case XA_INPUT_CONTROL:
// Acknowledged but not visually rendered yet
ea.ic = (byte) value;
break;
case 0x91: // DBCS Asian attributes
ea.db = (byte) value;
break;
}
}
@@ -607,21 +685,93 @@ public class DataStreamProcessor {
int size = screen.getRows() * screen.getCols();
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
? inputProcessor.getLastAid()
: AID_NO;
if (inputProcessor != null) {
inputProcessor.setLastAid(AID_NO);
}
// AID byte
outputWrite(AID_NO);
outputWrite(aid);
// Cursor address
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
outputWrite(caddr[0] & 0xFF);
outputWrite(caddr[1] & 0xFF);
// Buffer contents
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute()) {
outputWrite(ORDER_SF);
outputWrite(ea.fa & 0xFF);
} else {
outputWrite(ea.ec & 0xFF);
byte mode = screen.getReplyMode();
// Buffer contents depending on Reply Mode
if (mode == SF_SRM_XFIELD || mode == SF_SRM_CHAR) {
// Extended Field or Character Mode
byte curFg = 0, curBg = 0, curGr = 0, curCs = 0;
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute()) {
// Count number of attribute pairs
int count = 1; // 3270 FA is always present
if (ea.fg != 0) count++;
if (ea.bg != 0) count++;
if (ea.gr != 0) count++;
if (ea.cs != 0) count++;
if (ea.ol != 0) count++;
outputWrite(ORDER_SFE);
outputWrite(count);
outputWrite(XA_3270);
outputWrite(ea.fa & 0xFF);
if (ea.fg != 0) { outputWrite(XA_FOREGROUND); outputWrite(ea.fg & 0xFF); }
if (ea.bg != 0) { outputWrite(XA_BACKGROUND); outputWrite(ea.bg & 0xFF); }
if (ea.gr != 0) {
outputWrite(XA_HIGHLIGHTING);
int xah = XAH_NORMAL;
if ((ea.gr & GR_BLINK) != 0) xah = XAH_BLINK;
else if ((ea.gr & GR_REVERSE) != 0) xah = XAH_REVERSE;
else if ((ea.gr & GR_UNDERLINE) != 0) xah = XAH_UNDERSCORE;
else if ((ea.gr & GR_INTENSIFY) != 0) xah = XAH_INTENSIFY;
outputWrite(xah);
}
if (ea.cs != 0) { outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF); }
if (ea.ol != 0) { outputWrite(XA_OUTLINING); outputWrite(ea.ol & 0xFF); }
} else {
if (mode == SF_SRM_CHAR) {
// In character mode, output SA if character attributes differ
if (ea.fg != curFg) {
outputWrite(ORDER_SA); outputWrite(XA_FOREGROUND); outputWrite(ea.fg & 0xFF);
curFg = ea.fg;
}
if (ea.bg != curBg) {
outputWrite(ORDER_SA); outputWrite(XA_BACKGROUND); outputWrite(ea.bg & 0xFF);
curBg = ea.bg;
}
if (ea.gr != curGr) {
outputWrite(ORDER_SA); outputWrite(XA_HIGHLIGHTING);
int xah = XAH_NORMAL;
if ((ea.gr & GR_BLINK) != 0) xah = XAH_BLINK;
else if ((ea.gr & GR_REVERSE) != 0) xah = XAH_REVERSE;
else if ((ea.gr & GR_UNDERLINE) != 0) xah = XAH_UNDERSCORE;
else if ((ea.gr & GR_INTENSIFY) != 0) xah = XAH_INTENSIFY;
outputWrite(xah);
curGr = ea.gr;
}
if (ea.cs != curCs) {
outputWrite(ORDER_SA); outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF);
curCs = ea.cs;
}
}
outputWrite(ea.ec & 0xFF);
}
}
} else {
// Standard Field Mode (SF_SRM_FIELD)
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute()) {
outputWrite(ORDER_SF);
outputWrite(ea.fa & 0xFF);
} else {
outputWrite(ea.ec & 0xFF);
}
}
}
@@ -631,13 +781,19 @@ public class DataStreamProcessor {
// ========== Read Modified ==========
private void processReadModified(boolean all) {
if (ftDft != null) {
ftDft.readModified();
if (ftDft != null && ftDft.readModified()) {
return;
}
outputPos = 0;
int aid = AID_NO; // Last AID
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
? inputProcessor.getLastAid()
: AID_NO;
if (inputProcessor != null) {
inputProcessor.setLastAid(AID_NO);
}
int size = screen.getRows() * screen.getCols();
// AID byte
@@ -649,28 +805,33 @@ public class DataStreamProcessor {
outputWrite(caddr[1] & 0xFF);
if (!screen.isFormatted()) {
// Unformatted: send everything
if (all) {
for (int i = 0; i < size; i++) {
outputWrite(screen.getCell(i).ec & 0xFF);
// Unformatted screen: send all non-null characters (null suppression)
for (int i = 0; i < size; i++) {
int b = screen.getCell(i).ec & 0xFF;
if (b != 0x00) {
outputWrite(b);
}
}
} else {
// Formatted: send modified fields
// Formatted screen: send fields with null suppression per 3270 spec
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute() && (all || faIsModified(ea.fa & 0xFF))) {
int fieldStart = (i + 1) % size;
// Send SBA for field start
// Always send SBA and address of first character position in field
outputWrite(ORDER_SBA);
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
outputWrite(addr[0] & 0xFF);
outputWrite(addr[1] & 0xFF);
// Send field contents until next FA
// Send all non-null characters in field (suppressing 0x00)
int pos = fieldStart;
while (pos < size && !screen.getCell(pos).isFieldAttribute()) {
outputWrite(screen.getCell(pos).ec & 0xFF);
while (!screen.getCell(pos).isFieldAttribute()) {
int b = screen.getCell(pos).ec & 0xFF;
if (b != 0x00) {
outputWrite(b);
}
pos = (pos + 1) % size;
if (pos == fieldStart)
break;
@@ -706,25 +867,22 @@ public class DataStreamProcessor {
processSFReadPartition(data, pos, fieldLen);
break;
case SF_ERASE_RESET:
if (fieldLen >= 4) {
boolean alt = (data[pos + 3] & 0xFF) == SF_ER_ALT;
screen.erase(alt);
notifyScreenSizeChanged();
}
processEraseReset(data, pos, fieldLen);
break;
case SF_SET_REPLY_MODE:
if (fieldLen >= 5) {
screen.setReplyMode((byte) (data[pos + 4] & 0xFF));
}
processSetReplyMode(data, pos, fieldLen);
break;
case SF_CREATE_PART:
// Acknowledged we use implicit partition
processCreatePartition(data, pos, fieldLen);
break;
case SF_DESTROY_PART:
processDestroyPartition(data, pos, fieldLen);
break;
case SF_ACTIVATE_PART:
processActivatePartition(data, pos, fieldLen);
break;
case SF_OUTBOUND_DS:
if (fieldLen > 5) {
// Outbound DS contains another 3270 command
processRecord(data, pos + 4, fieldLen - 4, false);
}
processOutbound3270DS(data, pos, fieldLen);
break;
case SF_TRANSFER_DATA:
if (ftDft != null) {
@@ -733,6 +891,108 @@ public class DataStreamProcessor {
log.fine("SF_TRANSFER_DATA received but ftDft is null");
}
break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS_DIRECT: // 0x06: Load Programmed Symbols direct
if (fieldLen > 3) {
byte[] psData = new byte[fieldLen - 3];
System.arraycopy(data, pos + 3, psData, 0, psData.length);
programSymbolManager.loadps(psData);
}
break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS: { // 0x0F: 2-byte Structured Field prefix
if (fieldLen >= 4) {
int sfSubId = data[pos + 3] & 0xFF;
switch (sfSubId) {
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS_SUB: // 0x06: Load Programmed Symbols
if (fieldLen > 4) {
byte[] psData = new byte[fieldLen - 4];
System.arraycopy(data, pos + 4, psData, 0, psData.length);
programSymbolManager.loadps(psData);
}
break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
// Per IBM HOD processDataunit(), 0x0F activates graphic cursor and initializes data unit
log.info(String.format("SF 0x0F sub=0x0F (Data Unit Object Control len=%d): activating graphics cursor", fieldLen));
gocaDecoder.setGraphicsCursorActive(true);
notifyScreenUpdated();
break;
}
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: { // 0x10: Object Picture (Picture segments)
int flags = (fieldLen >= 6) ? (data[pos + 5] & 0xC0) : 0xC0;
int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4);
int orderLen = Math.max(0, fieldLen - (orderOffset - pos));
StringBuilder hexDump = new StringBuilder();
for (int i = 0; i < Math.min(32, fieldLen); i++) {
hexDump.append(String.format("%02x ", data[pos + i] & 0xFF));
}
log.info(String.format("SF 0x0F sub=0x%02x len=%d flags=0x%02x orderOffset=+%d orderLen=%d bytes=[%s]",
sfSubId, fieldLen, flags, (orderOffset - pos), orderLen, hexDump.toString().trim()));
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
int targetW = screen.getCols() * 9;
int targetH = screen.getRows() * 16;
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
graphicsPlane.resize(targetW, targetH);
}
if (flags == 0x80) { // SPAN_FIRST
gocaAccumulator.reset();
currentGocaSubtype = sfSubId;
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
} else if (flags == 0x00) { // SPAN_MIDDLE
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
} else if (flags == 0x40) { // SPAN_LAST
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
byte[] fullStream = gocaAccumulator.toByteArray();
gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
} else {
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
}
notifyScreenUpdated();
} else { // SPAN_ONLY (0xC0)
gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen));
if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
} else {
gocaDecoder.decodeStream(data, orderOffset, orderLen);
}
notifyScreenUpdated();
}
break;
}
default:
log.fine("Unknown SF 0x0F subtype: " + String.format("0x%02x", sfSubId));
break;
}
}
break;
}
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL: // 0x24: Object Control (Procedure orders)
if (fieldLen > 3) {
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
gocaDecoder.processProcedureOrders(data, pos + 3, fieldLen - 3);
notifyScreenUpdated();
}
break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_3270_G: // 0x20: 3270 Graphics
if (fieldLen > 3) {
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
notifyScreenUpdated();
}
break;
default:
log.fine("Unknown SF id: " + String.format("0x%02x", sfId));
break;
@@ -760,14 +1020,18 @@ public class DataStreamProcessor {
switch (type) {
case SF_RP_QUERY:
log.info("ReadPartition Query — sending all query replies");
log.info("ReadPartition Query — sending base query replies");
graphicsPlane.clear();
gocaDecoder.resetDefaults();
sendAllQueryReplies();
break;
case SF_RP_QLIST:
if (fieldLen >= 6) {
int listType = data[offset + 5] & 0xFF;
log.info("ReadPartition QueryList type=" + String.format("0x%02x", listType));
if (listType == SF_RPQ_ALL || listType == SF_RPQ_EQUIV) {
if (listType == SF_RPQ_ALL) {
sendCompleteQueryReplies();
} else if (listType == SF_RPQ_EQUIV) {
sendAllQueryReplies();
} else if (listType == SF_RPQ_LIST) {
// Send only requested query replies
@@ -809,7 +1073,22 @@ public class DataStreamProcessor {
if ((i + 1) % 32 == 0)
sb.append("\n ");
}
log.info("Query reply (" + qr.length + " bytes):\n " + sb.toString().trim());
log.warning(">>> SENDING Base Query Reply (" + qr.length + " bytes):\n " + sb.toString().trim());
if (outputSender != null) {
outputSender.send3270Data(qr);
}
}
private void sendCompleteQueryReplies() {
byte[] qr = qrBuilder.buildCompleteQueryReplies(screen.getMaxCols(), screen.getMaxRows(),
screen.getMaxCols() * screen.getMaxRows());
StringBuilder sb = new StringBuilder();
for (int i = 0; i < qr.length; i++) {
sb.append(String.format("%02x ", qr[i] & 0xFF));
if ((i + 1) % 32 == 0)
sb.append("\n ");
}
log.warning(">>> SENDING Complete Query Reply (" + qr.length + " bytes):\n " + sb.toString().trim());
if (outputSender != null) {
outputSender.send3270Data(qr);
}
@@ -824,7 +1103,7 @@ public class DataStreamProcessor {
if ((i + 1) % 32 == 0)
sb.append("\n ");
}
log.info("Requested query reply (" + qr.length + " bytes):\n " + sb.toString().trim());
log.warning(">>> SENDING Requested Query Reply (" + qr.length + " bytes):\n " + sb.toString().trim());
if (outputSender != null) {
outputSender.send3270Data(qr);
}
@@ -855,4 +1134,175 @@ public class DataStreamProcessor {
l.onScreenSizeChanged(screen.getRows(), screen.getCols());
}
}
private void notifyScreenUpdated() {
for (ScreenUpdateListener l : screenListeners) {
l.onScreenUpdated();
}
}
// ========== Structured Field Handlers & Utilities (Phase 2) ==========
public void processSetReplyMode(byte[] data, int offset, int length) {
if (length >= 5) {
byte mode = (byte) (data[offset + 4] & 0xFF);
screen.setReplyMode(mode);
log.fine("Set reply mode: " + mode);
}
}
public void processCreatePartition(byte[] data, int offset, int length) {
if (length >= 4) {
int pid = data[offset + 3] & 0xFF;
int pRows = screen.getRows();
int pCols = screen.getCols();
if (length >= 8) {
pCols = ((data[offset + 4] & 0xFF) << 8) | (data[offset + 5] & 0xFF);
pRows = ((data[offset + 6] & 0xFF) << 8) | (data[offset + 7] & 0xFF);
if (pRows <= 0) pRows = screen.getRows();
if (pCols <= 0) pCols = screen.getCols();
}
screen.createPartition(pid, pRows, pCols);
log.fine("Created partition pid=" + pid + " (" + pRows + "x" + pCols + ")");
}
graphicsPlane.clear();
gocaDecoder.resetDefaults();
}
public void processDestroyPartition(byte[] data, int offset, int length) {
if (length >= 4) {
int pid = data[offset + 3] & 0xFF;
screen.destroyPartition(pid);
log.fine("Destroyed partition pid=" + pid);
}
}
public void processActivatePartition(byte[] data, int offset, int length) {
if (length >= 4) {
int pid = data[offset + 3] & 0xFF;
screen.activatePartition(pid);
log.fine("Activated partition pid=" + pid);
}
}
public void processEraseReset(byte[] data, int offset, int length) {
boolean alt = (length >= 4) && ((data[offset + 3] & 0xFF) == SF_ER_ALT);
screen.eraseReset(alt);
graphicsPlane.clear();
gocaDecoder.resetDefaults();
notifyScreenSizeChanged();
}
public void processSCSData(byte[] data, int offset, int length) {
log.info("Received embedded SCS printer data (" + length + " bytes)");
if (embeddedScsProcessor != null && data != null && length > 0) {
embeddedScsProcessor.processHostData(data, offset, length);
}
}
public void setScreenToBindSize(int primaryRows, int primaryCols, int altRows, int altCols, int bindFlags) {
screen.setScreenToBindSize(primaryRows, primaryCols, altRows, altCols, bindFlags);
notifyScreenSizeChanged();
}
public void setScrSizetoDefault(boolean isDefault) {
screen.setScrSizetoDefault(isDefault);
notifyScreenSizeChanged();
}
public static byte[] doubleFF(byte[] data, int length) {
int count = countFF(data, length);
if (count == 0 && data.length == length) {
return data;
}
byte[] result = new byte[length + count];
int j = 0;
for (int i = 0; i < length; i++) {
byte b = data[i];
result[j++] = b;
if ((b & 0xFF) == 0xFF) {
result[j++] = (byte) 0xFF;
}
}
return result;
}
public static int countFF(byte[] data, int length) {
int count = 0;
int len = Math.min(length, data.length);
for (int i = 0; i < len; i++) {
if ((data[i] & 0xFF) == 0xFF) {
count++;
}
}
return count;
}
public static byte[] sizeuparray(byte[] data, int count) {
byte[] newArr = new byte[data.length + count];
System.arraycopy(data, 0, newArr, 0, Math.min(data.length, newArr.length));
return newArr;
}
public static int cmd2ebc(int commandCode) {
return DS3270Constants.cmd2ebc(commandCode);
}
public static int cmd2ebc(short commandCode) {
return DS3270Constants.cmd2ebc(commandCode & 0xFFFF);
}
public static int x2bin(int ebcByte) {
return DS3270Constants.x2bin(ebcByte);
}
public static int x2bin(short ebcByte) {
return DS3270Constants.x2bin(ebcByte & 0xFFFF);
}
public boolean validateStructuredFieldHeader(byte[] data, int offset, int length) {
if (length < 3) return false;
int fieldLen = ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF);
return fieldLen >= 3 && fieldLen <= length;
}
public int decodeAddressingMode(int flags) {
return (flags & 0x01) != 0 ? 14 : 12;
}
public boolean validatePartitionId(int pid) {
return pid >= 0 && pid <= 255;
}
public void processOutbound3270DS(byte[] data, int offset, int fieldLen) {
if (fieldLen > 5) {
int pid = data[offset + 3] & 0xFF;
screen.setActivePartition(pid);
processRecord(data, offset + 4, fieldLen - 4, false);
}
}
public void processQueryListOrder(byte[] data, int offset, int length) {
processSFReadPartition(data, offset, length);
}
public void processImplicitPartition(byte[] data, int offset, int length) {
screen.setActivePartition(0);
}
public void processModifyPartition(byte[] data, int offset, int length) {
if (length >= 4) {
int pid = data[offset + 3] & 0xFF;
log.fine("Modify partition pid=" + pid);
}
}
public void processResetPartition(byte[] data, int offset, int length) {
screen.setActivePartition(0);
screen.erase(false);
}
public void processNullStructuredField() {
log.fine("Processed null structured field");
}
}
@@ -0,0 +1,636 @@
package haus.nightmare.lib3270j.datastream;
import java.io.ByteArrayOutputStream;
import java.util.logging.Logger;
import haus.nightmare.lib3270j.graphics.GraphicsMode;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/**
* Builds Query Reply structured fields in response to host Read Partition queries.
* Matches IBM 3270 Architecture (GA23-0059) and x3270 sf.c exact binary layout.
*/
public class QueryReplyBuilder {
private static final Logger log = Logger.getLogger(QueryReplyBuilder.class.getName());
private static final int SW_3279_2 = 0x09;
private static final int SH_3279_2 = 0x0c;
// Usable Area physical dimensions matching IBM Host On-Demand DS3270.java (Inches, 96 dpi: 0x00010060)
private static final int Xr_HOD = 0x00010060;
private static final int Yr_HOD = 0x00010060;
private final ScreenBuffer screen;
private GraphicsMode graphicsMode = GraphicsMode.BOTH;
// Base query reply codes (text mode, matches HOD DS3270.java queryEquiv)
private static final int[] SUPPORTED_QR_BASE = {
QR_SUMMARY, // 0x80 summary must list itself
QR_USABLE_AREA, // 0x81
QR_ALPHA_PART, // 0x84
QR_CHARSETS, // 0x85
QR_COLOR, // 0x86
QR_HIGHLIGHTING, // 0x87
QR_REPLY_MODES, // 0x88
QR_DDM, // 0x95 - Distributed Data Management (file transfer)
QR_AUXDA, // 0x99 - Auxiliary Devices
QR_IMP_PART, // 0xa6 - Implicit Partition Sizes
};
// Vector graphics query reply codes matching HOD QueryReply3270Constants.java QR_3270_WITHOUT_DCBS_SUMMARY_STRING
private static final int[] SUPPORTED_QR_VECTOR = {
QR_SUMMARY, // 0x80
QR_USABLE_AREA, // 0x81
QR_ALPHA_PART, // 0x84
QR_CHARSETS, // 0x85
QR_COLOR, // 0x86
QR_HIGHLIGHTING, // 0x87
QR_REPLY_MODES, // 0x88
QR_OUTLINING, // 0x8c
QR_DDM, // 0x95
QR_AUXDA, // 0x99
QR_IMP_PART, // 0xa6
QR_TRANSPARENCY, // 0xa8
QR_SEGMENT, // 0xb0
QR_PROCEDURE, // 0xb1
QR_LINETYPE, // 0xb2
QR_PORT, // 0xb3
QR_GRCOLOR, // 0xb4
QR_GRSYMBOLSET, // 0xb6
};
public QueryReplyBuilder(ScreenBuffer screen) {
this.screen = screen;
}
public QueryReplyBuilder(ScreenBuffer screen, GraphicsMode graphicsMode) {
this.screen = screen;
this.graphicsMode = (graphicsMode != null) ? graphicsMode : GraphicsMode.NONE;
}
public GraphicsMode getGraphicsMode() {
return graphicsMode;
}
public void setGraphicsMode(GraphicsMode graphicsMode) {
this.graphicsMode = (graphicsMode != null) ? graphicsMode : GraphicsMode.NONE;
}
/**
* Build base query replies in response to a generic Read Partition Query (0x02).
* Returns base text/presentation summary structured fields (matches HOD DS3270.java line 1723).
*/
public byte[] buildAllQueryReplies(int maxCols, int maxRows, int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
// AID byte for structured field
out.write(AID_SF);
// Summary (0x80) - lists all supported capabilities
appendQueryReply(out, QR_SUMMARY, buildSummary());
// Usable Area (0x81)
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
// Alpha Partitions (0x84)
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
// Character Sets (0x85)
appendQueryReply(out, QR_CHARSETS, buildCharsets());
// Color (0x86)
appendQueryReply(out, QR_COLOR, buildColor());
// Highlighting (0x87)
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
// Reply Modes (0x88)
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
if (isDbcs) {
// Outlining (0x8C)
appendQueryReply(out, QR_OUTLINING, buildOutlining());
// DBCS Asia (0x91)
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia());
}
// Distributed Data Management (0x95)
appendQueryReply(out, QR_DDM, buildDdm(4096));
// Auxiliary Devices (0x99)
appendQueryReply(out, QR_AUXDA, buildAuxDa());
// Implicit Partition (0xA6)
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
log.info("Built " + out.size() + " bytes of base query replies (graphicsMode=" + graphicsMode + ")");
return out.toByteArray();
}
/**
* Build complete query replies including vector graphics (when SF_RPQ_ALL 0x80 is requested).
*/
public byte[] buildCompleteQueryReplies(int maxCols, int maxRows, int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
out.write(AID_SF);
appendQueryReply(out, QR_SUMMARY, buildSummary());
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
appendQueryReply(out, QR_CHARSETS, buildCharsets());
appendQueryReply(out, QR_COLOR, buildColor());
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
appendQueryReply(out, QR_OUTLINING, buildOutlining());
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
if (isDbcs) {
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia());
}
appendQueryReply(out, QR_DDM, buildDdm(4096));
appendQueryReply(out, QR_AUXDA, buildAuxDa());
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); // 0xA8
appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows)); // 0xB0
appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows)); // 0xB1
appendQueryReply(out, QR_LINETYPE, buildLineType()); // 0xB2
appendPort(out); // 0xB3
appendQueryReply(out, QR_GRCOLOR, buildGrColor()); // 0xB4
appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet()); // 0xB6
}
log.info("Built " + out.size() + " bytes of complete query replies (graphicsMode=" + graphicsMode + ")");
return out.toByteArray();
}
/**
* Build specific query replies in response to a Read Partition Query List (SF_RPQ_LIST).
* For any unsupported requested query code, emits a QR_NULL (0xFF) structured field
* matching HOD DS3270.java line 1835.
*/
public byte[] buildQueryReplies(byte[] requestedCodes, int maxCols, int maxRows, int bufferSize) {
if (requestedCodes == null || requestedCodes.length == 0) {
return buildAllQueryReplies(maxCols, maxRows, bufferSize);
}
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
out.write(AID_SF);
for (byte codeByte : requestedCodes) {
int code = codeByte & 0xFF;
switch (code) {
case QR_SUMMARY:
appendQueryReply(out, QR_SUMMARY, buildSummary());
break;
case QR_USABLE_AREA:
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
break;
case QR_ALPHA_PART:
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
break;
case QR_CHARSETS:
appendQueryReply(out, QR_CHARSETS, buildCharsets());
break;
case QR_COLOR:
appendQueryReply(out, QR_COLOR, buildColor());
break;
case QR_HIGHLIGHTING:
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
break;
case QR_REPLY_MODES:
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
break;
case QR_OUTLINING: // 0x8C
appendQueryReply(out, QR_OUTLINING, buildOutlining());
break;
case QR_DBCS_ASIA: // 0x91
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia());
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
case QR_DDM: // 0x95
appendQueryReply(out, QR_DDM, buildDdm(4096));
break;
case QR_AUXDA: // 0x99
appendQueryReply(out, QR_AUXDA, buildAuxDa());
break;
case QR_IMP_PART: // 0xA6
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
break;
case QR_TRANSPARENCY: // 0xA8
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
case QR_SEGMENT: // 0xB0
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows));
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
case QR_PROCEDURE: // 0xB1
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows));
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
case QR_LINETYPE: // 0xB2
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_LINETYPE, buildLineType());
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
case QR_PORT: // 0xB3
if (graphicsMode.isVectorGraphicsEnabled()) {
appendPort(out);
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
case QR_GRCOLOR: // 0xB4
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_GRCOLOR, buildGrColor());
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
case QR_GRSYMBOLSET: // 0xB6
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet());
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
default:
// Unsupported query reply code emit QR_NULL
appendQueryReply(out, QR_NULL, new byte[0]);
break;
}
}
log.info("Built " + out.size() + " bytes of requested query replies for " + requestedCodes.length + " codes");
return out.toByteArray();
}
private void appendQueryReply(ByteArrayOutputStream out, int code, byte[] data) {
// Length includes the 2-byte length field + SFID_QREPLY + code + data
int len = 4 + data.length;
out.write((len >> 8) & 0xFF);
out.write(len & 0xFF);
out.write(SFID_QREPLY);
out.write(code);
out.write(data, 0, data.length);
}
private byte[] buildSummary() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
for (int code : codes) {
out.write(code);
if (isDbcs && code == QR_OUTLINING) {
out.write(QR_DBCS_ASIA); // 0x91
}
}
return out.toByteArray();
}
private byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(19);
out.write(graphicsMode.isVectorGraphicsEnabled() ? 0x03 : 0x01); // 12/14-bit addressing + graphics flag (matching HOD DS3270.java)
out.write(0x00); // no special character features
out.write((maxCols >> 8) & 0xFF); // usable width high
out.write(maxCols & 0xFF); // usable width low
out.write((maxRows >> 8) & 0xFF); // usable height high
out.write(maxRows & 0xFF); // usable height low
out.write(0x00); // units (0x00 = inches, matching IBM Host On-Demand QR_USEAREA_STRING)
// Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING
out.write((Xr_HOD >> 24) & 0xFF);
out.write((Xr_HOD >> 16) & 0xFF);
out.write((Xr_HOD >> 8) & 0xFF);
out.write(Xr_HOD & 0xFF);
// Yr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING
out.write((Yr_HOD >> 24) & 0xFF);
out.write((Yr_HOD >> 16) & 0xFF);
out.write((Yr_HOD >> 8) & 0xFF);
out.write(Yr_HOD & 0xFF);
int charW = getCharWidth();
int charH = getCharHeight();
out.write(charW); // AW
out.write(charH); // AH
int buf = maxCols * maxRows;
out.write((buf >> 8) & 0xFF); // buffer size high
out.write(buf & 0xFF); // buffer size low
return out.toByteArray();
}
public int getCharWidth() {
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
return 12;
}
return SW_3279_2; // 9
}
public int getCharHeight() {
// ARCHITECTURAL NOTE ON 3179G GOCA VERTICAL ALIGNMENT & QUERY REPLIES:
// Why hardcoding SH = 12 (0x0C) in Character Sets & Usable Area failed in past iterations:
// When SDH/AH is declared as 12 (0x0C) in Query Reply, the mainframe host GDDM engine computes
// total presentation space as rows * 12 (e.g. 43 * 12 = 516 units, yMax = 257).
// GDDM then places the top menu bar at Row 1 (gy = 187..200).
// Meanwhile, the client emulator rendered into a 16-pitch grid (43 * 16 = 688 units, yMax = 343).
// On a 688-unit canvas, gy = 200 mapped to Row 9.2 (middle of the screen), leaving a massive void above.
// When the user clicked on the visual menu drawn at Row 9, the client emitted gy = 189 with cursor at Row 9,
// which GDDM rejected as outside its menu hit box (causing terminal alarm beeps).
//
// Solution: Declare SDH = 16 (0x10) when Vector Graphics is enabled (3179G standard), ensuring host GDDM
// and client GraphicsPlane share the exact same 16-pitch presentation space (720x688, yMax = 343).
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
}
private byte[] buildAlphaPartitions(int maxRows) {
int bufSize = screen.getMaxCols() * screen.getMaxRows();
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition)
out.write((bufSize >> 8) & 0xFF); // total partition storage high
out.write(bufSize & 0xFF); // total partition storage low
out.write(0x00); // flags
return out.toByteArray();
}
private byte[] buildCharsets() {
int charW = getCharWidth();
int charH = getCharHeight();
int cgcsgid = 0x02B9;
int cpgid = 0x0025;
if (screen != null && screen.getTranslator() != null) {
cgcsgid = screen.getTranslator().getCgcsgid();
cpgid = screen.getTranslator().getCpgid();
}
if (graphicsMode == GraphicsMode.PROGRAMMED_SYMBOLS) {
// Programmed Symbols only mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
out.write(0x00); // more flags
out.write(charW); // SDW (9)
out.write(charH); // SDH (16 for 3179G, 12 for 3279-2)
out.write(0x0a); // Load PS format types supported: Format 1 and Format 3
out.write(0x00); // Load PS device type (high)
out.write(0x00); // Load PS device type (low)
out.write(0x00); // reserved
out.write(0x07); // DL = 7 bytes per descriptor
// Descriptor 1 (SET 0): default character set (non-loadable, single plane, active codepage)
out.write(0x00); out.write(0x10); out.write(0x00);
out.write((cgcsgid >> 8) & 0xFF); out.write(cgcsgid & 0xFF);
out.write((cpgid >> 8) & 0xFF); out.write(cpgid & 0xFF);
// Descriptor 2 (SET 1): APL/GE character set
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
// Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3) - Flags = 0x80 (Loadable, single plane)
out.write(0x02); out.write(0x80); out.write(0x40); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x03); out.write(0x80); out.write(0x41); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
// Loadable Triple-Plane PS Sets (PSC, PSD, PSE, PSF: Slots 4, 5, 6, 7) - Flags = 0xC0 (0x80 Loadable | 0x40 Triple-plane)
out.write(0x04); out.write(0xc0); out.write(0x42); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x05); out.write(0xc0); out.write(0x43); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x06); out.write(0xc0); out.write(0x44); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x07); out.write(0xc0); out.write(0x45); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
return out.toByteArray();
}
// Standard 3179G / Base character sets (matches sf.c / HOD)
ByteArrayOutputStream out = new ByteArrayOutputStream(23);
out.write(0x82); // flags: GE, CGCSGID present
out.write(0x00); // more flags
out.write(charW); // SDW - default char width (9)
out.write(charH); // SDH - default char height (16 for 3179G, 12 for 3279-2)
out.write(0x00); // LoadPS format (0x00)
out.write(0x00);
out.write(0x00);
out.write(0x00);
out.write(0x07); // DL = 7
// Set 0 (Base EBCDIC - Non-loadable, single plane, active codepage)
out.write(0x00); out.write(0x10); out.write(0x00);
out.write((cgcsgid >> 8) & 0xFF); out.write(cgcsgid & 0xFF);
out.write((cpgid >> 8) & 0xFF); out.write(cpgid & 0xFF);
// Set 1 (APL/Text)
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
return out.toByteArray();
}
private byte[] buildColor() {
// Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total)
return new byte[] {
0x00, 0x08, 0x00, (byte) 0xF4,
(byte) 0xF1, (byte) 0xF1, // Blue
(byte) 0xF2, (byte) 0xF2, // Red
(byte) 0xF3, (byte) 0xF3, // Pink
(byte) 0xF4, (byte) 0xF4, // Green
(byte) 0xF5, (byte) 0xF5, // Turquoise
(byte) 0xF6, (byte) 0xF6, // Yellow
(byte) 0xF7, (byte) 0xF7 // Neutral/White
};
}
private byte[] buildHighlighting() {
// Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total)
return new byte[] {
0x04, 0x00, (byte) 0xF0,
(byte) 0xF1, (byte) 0xF1,
(byte) 0xF2, (byte) 0xF2,
(byte) 0xF4, (byte) 0xF4
};
}
private byte[] buildReplyModes() {
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR };
}
private byte[] buildDdm(int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(8);
out.write(0x00); // reserved
out.write(0x00); // reserved
out.write((bufferSize >> 8) & 0xFF); // inbound length limit (INLIM)
out.write(bufferSize & 0xFF);
out.write((bufferSize >> 8) & 0xFF); // outbound length limit (OUTLIM)
out.write(bufferSize & 0xFF);
out.write(0x01); // NSS = 01
out.write(0x01); // DDMSS = 01
return out.toByteArray();
}
private byte[] buildImplicitPartition(int maxCols, int maxRows) {
ByteArrayOutputStream out = new ByteArrayOutputStream(22);
// Implicit partition sizes, 2 self-defining parameters
// SDP 1: Default screen size (Model 2: 80x24)
out.write(0x00); // flags
out.write(0x00); // reserved
out.write(0x0b); // SDP length (11 bytes)
out.write(0x01); // SDP type: implicit partition sizes
out.write(0x00); // reserved
// Default size
out.write((MODEL_2_COLS >> 8) & 0xFF);
out.write(MODEL_2_COLS & 0xFF);
out.write((MODEL_2_ROWS >> 8) & 0xFF);
out.write(MODEL_2_ROWS & 0xFF);
// Alternate size (Model 4: 80x43, Model 5: 132x27, etc.)
out.write((maxCols >> 8) & 0xFF);
out.write(maxCols & 0xFF);
out.write((maxRows >> 8) & 0xFF);
out.write(maxRows & 0xFF);
return out.toByteArray();
}
public byte[] buildOutlining() {
// HOD QueryReply3270Constants.java QR_OUTLINING_STRING ("\u0000\n\u0081\u008c\u0000\u0000\u0000\u0000\u0000\u0000")
return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
}
public byte[] buildDbcsAsia() {
// HOD QueryReply3270Constants.java QR_DBCS_ASIA_STRING ("\u0000\u000b\u0081\u0091\u0000\u0003\u0001\u0080\u0003\u0002\u0001")
return new byte[]{ 0x00, 0x03, 0x01, (byte) 0x80, 0x03, 0x02, 0x01 };
}
public byte[] buildAuxDa() {
// HOD QueryReply3270Constants.java QR_AUXDA_STRING ("\u0000\u0006\u0081\u0099\u0000\u0000")
return new byte[]{ 0x00, 0x00 };
}
public byte[] buildTransparency() {
// HOD QueryReply3270Constants.java QR_TRANSPARENCY_STRING ("\u0000\t\u0081\u00a8\u0002\u0000\u00f0\u00ff\u00ff")
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
}
public byte[] buildSegment(int maxCols, int maxRows) {
// HOD QueryReply3270Constants.java QR_SEGMENT_STRING ("\u0000\u000b\u0081\u00b0\u0080\u0002\u0000\u0000\u0000\u00fc\u0000")
return new byte[]{
(byte) 0x80, 0x02,
0x00, 0x00,
0x00, (byte) 0xFC,
0x00
};
}
public byte[] buildGraphics(int maxCols, int maxRows) {
return buildSegment(maxCols, maxRows);
}
public byte[] buildProcedure(int maxCols, int maxRows) {
// HOD QueryReply3270Constants.java QR_PROCEDURE_STRING ("\u0000\u0015\u0081\u00b1\u0000\u0001\u0000\u0000\u0000\u00fc\u0000\u0006@\u0006@\u0006\u0001\u00ff\u00ff\u00ff\u00f0")
return new byte[]{
0x00, 0x01,
0x00, 0x00,
0x00, (byte) 0xFC,
0x00,
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
};
}
public byte[] buildGImage(int maxCols, int maxRows) {
return buildProcedure(maxCols, maxRows);
}
public byte[] buildLineType() {
// HOD QueryReply3270Constants.java QR_LINETYPE_STRING ("\u0000\u0018\u0081\u00b2\u0000\t\u0000\u0007\u0001\u0001\u0002\u0002\u0003\u0003\u0004\u0004\u0005\u0005\u0006\u0006\u0007\u0007\b\b")
return new byte[]{
0x00, 0x09, 0x00, 0x07,
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
};
}
public byte[] buildAuxDev() {
return buildLineType();
}
public byte[] buildAuxDevice() {
return buildLineType();
}
public void appendPort(ByteArrayOutputStream out) {
// HOD QueryReply3270Constants.java QR_PORT_STRING (4 OEM format sub-fields, 64 bytes total)
appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
});
appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02
});
appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
});
appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C
});
}
public void appendOemFmt(ByteArrayOutputStream out) {
appendPort(out);
}
public byte[] buildGrColor() {
// HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total)
ByteArrayOutputStream out = new ByteArrayOutputStream(110);
out.write(0x00); out.write(0x04); out.write(0x00); out.write(0xFF); out.write(0xFF);
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x10);
for (int i = 0; i < 16; i++) {
out.write(0x00);
out.write(i);
int argb = haus.nightmare.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
int r = (argb >> 16) & 0xFF;
int g = (argb >> 8) & 0xFF;
int b = argb & 0xFF;
out.write(r);
out.write(g);
out.write(b);
out.write(0x00); // 6th byte in HOD color table
}
return out.toByteArray();
}
public byte[] buildGColor() {
return buildGrColor();
}
public byte[] buildGrSymbolSet() {
int charW = getCharWidth();
int charH = getCharHeight();
int cgcsgid = 0x02B9;
int cpgid = 0x0025;
if (screen != null && screen.getTranslator() != null) {
cgcsgid = screen.getTranslator().getCgcsgid();
cpgid = screen.getTranslator().getCpgid();
}
return new byte[]{
0x00, 0x00, (byte) charW, (byte) charH, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
0x01, 0x00, 0x00, (byte) 0xF0, (byte) ((cgcsgid >> 8) & 0xFF), (byte) (cgcsgid & 0xFF), 0x00, 0x00,
(byte) charW, (byte) charH, 0x00, 0x00, (byte) 0xF0, (byte) ((cgcsgid >> 8) & 0xFF), (byte) (cgcsgid & 0xFF), 0x00, 0x00
};
}
public byte[] buildGSymbols() {
return buildGrSymbolSet();
}
public byte[] buildSaveRestore() {
return buildOutlining();
}
public byte[] buildRpqNames() {
return buildTransparency();
}
}

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