45 Commits

Author SHA1 Message Date
rudi 59925c2787 Add headless server and setup script
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 34s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m13s
Release j3270 / Build & Publish Release (push) Successful in 1m8s
2026-09-08 11:38:38 -04:00
rudi f8fc0e4b22 Fix the build
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 25s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 59s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 23s
2026-09-07 20:31:18 -04:00
rudi dbcb0aa1b6 Clear cruft
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 26s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 36s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Failing after 1m1s
2026-09-07 20:16:00 -04:00
rudi 50a4279446 Update README
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 30s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 33s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 24s
2026-09-06 11:31:35 -04:00
rudi 6331e0108b Update tests and Status/UI
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 26s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m5s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m10s
2026-09-06 09:53:43 -04:00
rudi 79d493c24d Update builder
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m12s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m12s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 4m22s
2026-09-05 14:11:08 -04:00
rudi 74e30d1ead IBM-DYNAMIC tweaking
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m25s
2026-09-04 20:58:19 -04:00
rudi cbb310b889 GOCA fixing
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m22s
2026-09-04 14:34:01 -04:00
rudi 6296cf4341 IBM-DYNAMIC
Release j3270 / Build & Publish Release (push) Successful in 1m23s
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m28s
2026-09-03 16:43:22 -04:00
rudi db6043cb39 Handle HeadlessException in UI tests for automated testing
Release j3270 / Build & Publish Release (push) Successful in 1m28s
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m31s
2026-09-01 10:17:29 -04:00
rudi 161364a95f Overnight churnings
Build and Test j3270 / Build JAR & Run Tests (push) Failing after 1m23s
Release j3270 / Build & Publish Release (push) Failing after 1m15s
2026-09-01 09:45:40 -04:00
rudi a56aa5f059 More NVT stuff
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m24s
2026-08-31 15:54:26 -04:00
rudi 7b5bc7ec11 Add consts and update tests
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m20s
2026-08-31 10:22:12 -04:00
rudi a9956298e2 More functional changes and UI adaptations from overnight 2026-08-31 09:16:32 -04:00
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
461 changed files with 70871 additions and 9455 deletions
+18 -10
View File
@@ -9,8 +9,12 @@ on:
jobs: jobs:
build: build:
name: Build JAR & Run Tests name: Build JAR & Run Tests (Java ${{ matrix.java }})
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
java: [ '11', '17', '21' ]
steps: steps:
- name: Configure Dynamic DNS Resolvers - name: Configure Dynamic DNS Resolvers
@@ -23,21 +27,21 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Java JDK (Temurin 17) - name: Set up Java JDK (Temurin ${{ matrix.java }})
uses: actions/setup-java@v3 uses: actions/setup-java@v3
continue-on-error: true continue-on-error: true
with: with:
java-version: '17' java-version: ${{ matrix.java }}
distribution: 'temurin' distribution: 'temurin'
- name: Ensure Java Available - name: Ensure Java Available
run: | run: |
if ! command -v javac >/dev/null 2>&1; then if ! command -v javac >/dev/null 2>&1; then
echo "Installing OpenJDK 17 via package manager..." echo "Installing OpenJDK ${{ matrix.java }} via package manager..."
if command -v apt-get >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then
apt-get update && apt-get install -y openjdk-17-jdk-headless || true apt-get update && apt-get install -y openjdk-${{ matrix.java }}-jdk-headless || true
elif command -v apk >/dev/null 2>&1; then elif command -v apk >/dev/null 2>&1; then
apk add --no-cache openjdk17 || true apk add --no-cache openjdk${{ matrix.java }} || true
fi fi
fi fi
echo "Java compiler:" echo "Java compiler:"
@@ -53,23 +57,27 @@ jobs:
run: | run: |
sh ./test_all.sh sh ./test_all.sh
- name: Prepare Versioned JAR
run: |
cp build/j3270.jar "build/j3270-java${{ matrix.java }}.jar"
- name: Upload j3270 Executable JAR Artifact - name: Upload j3270 Executable JAR Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
continue-on-error: true continue-on-error: true
with: with:
name: j3270-executable-jar name: j3270-java${{ matrix.java }}-jar
path: build/j3270.jar path: build/j3270-java${{ matrix.java }}.jar
- name: Upload Test Reports - name: Upload Test Reports
if: always() if: always()
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
continue-on-error: true continue-on-error: true
with: with:
name: test-reports name: test-reports-java${{ matrix.java }}
path: build/reports/tests/ path: build/reports/tests/
- name: Report Build Status - name: Report Build Status
if: failure() if: failure()
run: | run: |
echo "### ❌ Build or Test Failed" >> $GITHUB_STEP_SUMMARY echo "### ❌ Build or Test Failed for Java ${{ matrix.java }}" >> $GITHUB_STEP_SUMMARY
echo "Please check the runner logs above for details." >> $GITHUB_STEP_SUMMARY echo "Please check the runner logs above for details." >> $GITHUB_STEP_SUMMARY
+18 -10
View File
@@ -9,8 +9,12 @@ on:
jobs: jobs:
build: build:
name: Build JAR & Run Tests name: Build JAR & Run Tests (Java ${{ matrix.java }})
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
java: [ '11', '17', '21' ]
steps: steps:
- name: Configure Dynamic DNS Resolvers - name: Configure Dynamic DNS Resolvers
@@ -23,21 +27,21 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Java JDK (Temurin 17) - name: Set up Java JDK (Temurin ${{ matrix.java }})
uses: actions/setup-java@v3 uses: actions/setup-java@v3
continue-on-error: true continue-on-error: true
with: with:
java-version: '17' java-version: ${{ matrix.java }}
distribution: 'temurin' distribution: 'temurin'
- name: Ensure Java Available - name: Ensure Java Available
run: | run: |
if ! command -v javac >/dev/null 2>&1; then if ! command -v javac >/dev/null 2>&1; then
echo "Installing OpenJDK 17 via package manager..." echo "Installing OpenJDK ${{ matrix.java }} via package manager..."
if command -v apt-get >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then
apt-get update && apt-get install -y openjdk-17-jdk-headless || true apt-get update && apt-get install -y openjdk-${{ matrix.java }}-jdk-headless || true
elif command -v apk >/dev/null 2>&1; then elif command -v apk >/dev/null 2>&1; then
apk add --no-cache openjdk17 || true apk add --no-cache openjdk${{ matrix.java }} || true
fi fi
fi fi
echo "Java compiler:" echo "Java compiler:"
@@ -53,23 +57,27 @@ jobs:
run: | run: |
sh ./test_all.sh sh ./test_all.sh
- name: Prepare Versioned JAR
run: |
cp build/j3270.jar "build/j3270-java${{ matrix.java }}.jar"
- name: Upload j3270 Executable JAR Artifact - name: Upload j3270 Executable JAR Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
continue-on-error: true continue-on-error: true
with: with:
name: j3270-executable-jar name: j3270-java${{ matrix.java }}-jar
path: build/j3270.jar path: build/j3270-java${{ matrix.java }}.jar
- name: Upload Test Reports - name: Upload Test Reports
if: always() if: always()
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
continue-on-error: true continue-on-error: true
with: with:
name: test-reports name: test-reports-java${{ matrix.java }}
path: build/reports/tests/ path: build/reports/tests/
- name: Report Build Status - name: Report Build Status
if: failure() if: failure()
run: | run: |
echo "### ❌ Build or Test Failed" >> $GITHUB_STEP_SUMMARY echo "### ❌ Build or Test Failed for Java ${{ matrix.java }}" >> $GITHUB_STEP_SUMMARY
echo "Please check the runner logs above for details." >> $GITHUB_STEP_SUMMARY echo "Please check the runner logs above for details." >> $GITHUB_STEP_SUMMARY
+6
View File
@@ -1,6 +1,12 @@
j3270.log.* j3270.log.*
*.bin
*.txt
*.py
*.pcap
build/*
*.log *.log
.DS_Store .DS_Store
*.jar *.jar
build build
.gradle .gradle
*.txt
+129 -37
View File
@@ -2,40 +2,66 @@
[![Build & Test](https://git.hugfreevikings.wtf/rudi/j3270/actions/workflows/build.yaml/badge.svg)](https://git.hugfreevikings.wtf/rudi/j3270/actions) [![Build & Test](https://git.hugfreevikings.wtf/rudi/j3270/actions/workflows/build.yaml/badge.svg)](https://git.hugfreevikings.wtf/rudi/j3270/actions)
[![Releases](https://img.shields.io/badge/Releases-Gitea-blue.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) [![Java](https://img.shields.io/badge/Java-11%20%7C%2017%20%7C%2021%20(LTS)-blue.svg)](https://adoptium.net)
[![License](https://img.shields.io/badge/License-MIT%20%2F%20BSD-green.svg)](LICENSE) [![Tests](https://img.shields.io/badge/Tests-539%20passed-brightgreen.svg)]()
[![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. An **x3270-aligned and IBM Host On-Demand (HoD) compatible IBM 3270 mainframe terminal emulator & protocol library** written in pure Java with zero third-party runtime dependencies. Designed for high fidelity and compatibility with IBM z/OS, z/VM, and 3270-based systems over TN3270 and TN3270E.
--- ---
## 🚀 Quick Download & Run ## 🚀 Quick Download & Run
### Latest Standalone JAR ### Latest Standalone JARs
You can download the ready-to-run executable JAR directly from the releases or CI builds: Ready-to-run executable JARs are built and tested across all three Java LTS baselines (**Java 11, Java 17, and Java 21**) by Gitea Actions:
- **[Download Latest Tagged Release (`j3270.jar`)](https://git.hugfreevikings.wtf/rudi/j3270/releases)** - **[Download Latest Tagged Release (`j3270.jar`)](https://git.hugfreevikings.wtf/rudi/j3270/releases)**
- **[Latest Nightly CI Artifacts](https://git.hugfreevikings.wtf/rudi/j3270/actions)** (Download under the latest successful workflow run) - **[Latest Nightly CI Artifacts](https://git.hugfreevikings.wtf/rudi/j3270/actions)**: Download standalone JARs tailored for each LTS runtime under the latest workflow run:
- `j3270-java11-jar` (Java 11 LTS)
- `j3270-java17-jar` (Java 17 LTS — primary Gitea runner baseline)
- `j3270-java21-jar` (Java 21 LTS)
### Java Runtime Requirements
Requires **Java 11 or higher** (LTS versions: **Java 11, 17, 21**; also compatible with Java 24).
The project is tested and verified against:
- **Eclipse Temurin** (Adoptium OpenJDK HotSpot) — used in Gitea Actions CI workflows.
- **IBM Semeru Runtimes** (OpenJ9) — e.g. `11.0.32-sem`, `21.0.6-sem`, and `24.0.2-sem` via SDKMAN.
### Running `j3270` ### Running `j3270`
Requires **Java 11 or higher** (Java 11, 17, 21+):
```bash ```bash
# Launch interactive GUI # Launch interactive Swing GUI
java -jar j3270.jar java -jar j3270.jar
# Connect to TLS/SSL mainframe with custom port # Connect to mainframe with host, port, and terminal model (Models 2-5 or 'dynamic')
java -jar j3270.jar mainframe.example.com 23 4
java -jar j3270.jar mainframe.example.com 23 dynamic
# Connect to TLS/SSL mainframe on custom or standard (992) port
java -jar j3270.jar -s mainframe.example.com 992 java -jar j3270.jar -s mainframe.example.com 992
# Or using standard x3270 L: prefix # Or using standard x3270 L: prefix
java -jar j3270.jar L:mainframe.example.com:992 java -jar j3270.jar L:mainframe.example.com:992
# Connect with unverified/self-signed certificate verification bypass # Connect via Plain TN3270 (Non-E)
java -jar j3270.jar --tls --insecure mainframe.example.com 992 java -jar j3270.jar -P mainframe.example.com 23
# Or using standard x3270 P: prefix
java -jar j3270.jar P:mainframe.example.com:23
# Launch with custom configuration file # Connect with unverified/self-signed certificate verification bypass
java -jar j3270.jar -s --insecure mainframe.example.com 992
# Enable GDDM/GOCA host graphics explicitly (or disable via --no-graphics)
java -jar j3270.jar -g GOCA mainframe.example.com
java -jar j3270.jar --no-graphics mainframe.example.com
# Launch with custom INI configuration profile
java -jar j3270.jar -c config.ini java -jar j3270.jar -c config.ini
# Launch directly from source runner # Enable protocol and diagnostic debug logging to j3270.log
java -jar j3270.jar -d mainframe.example.com
# Launch directly from source runner script
./run.sh ./run.sh
``` ```
@@ -43,39 +69,108 @@ java -jar j3270.jar -c config.ini
## ✨ Features ## ✨ Features
- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, and SSL/TLS encryption. ### 🌐 Protocol & Telnet Engine
- **SSL/TLS Security**: - **TN3270 & TN3270E (RFC 2355)**: Full compliant finite-state machine, Device-Type negotiation, Functions negotiation (`BIND-IMAGE`, `RESPONSES`, `SYSREQ`), plain TN3270 fallback, and NVT (Network Virtual Terminal) mode.
- Encrypted TN3270 over TLS connections on standard port `992` or custom ports. - **SSCP-LU & Unformatted Line Mode**: Native z/VM line-mode support (`CP TERM CONMODE 3270`).
- Interactive certificate verification prompt for self-signed or untrusted certificates with fingerprint, subject, issuer, and validity inspection. - **Terminal Models & Dynamic Geometry**:
- 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.
- **Terminal Models**:
- Model 2 (24x80) - Model 2 (24x80)
- Model 3 (32x80) - Model 3 (32x80)
- Model 4 (43x80) - Model 4 (43x80)
- Model 5 (27x132) - Model 5 (27x132)
- **Zero-Dependency Core**: Modular architecture separated into: - `IBM-DYNAMIC` (Model 0 dynamic geometry auto-negotiation via Usable Area Query Reply).
- `lib3270j`: Reusable Java 3270 protocol, data stream parser, charset translation, and telnet state machine. - **Enterprise Proxy Support**: SOCKS5 and HTTP CONNECT proxy tunneling with optional authentication.
- `j3270`: Desktop Swing user interface and session management.
### 🔒 Security & Encryption
- **Modern TLS 1.2 / 1.3**: Encrypted TN3270 over TLS on standard port `992` or custom ports via JSSE.
- **Interactive Certificate Verification**: Security dialog for untrusted or self-signed certificates displaying SHA-256 fingerprints, subject, issuer, and validity dates with user approval prompt.
- **Headless Insecure Mode**: `--insecure` / `-k` option for scripted environments and test mainframes.
### 🎨 GDDM / GOCA Host Vector Graphics (IBM 3179G)
- **Vector Drawing Orders**: Polylines, relative lines, circular & elliptical arcs, fillets, and multi-polygon area fills with boundary detection and alternate/winding rules.
- **1:1 Color Calibration**: 16-color GOCA palette and base 4-color mapping calibrated 1:1 against IBM Host On-Demand (HoD).
- **Shading, Patterns & Overlays**: Patterned area fills, geometric markers, and uncompressed / run-length bit-image and wallpaper overlays.
- **Vector Symbol Fonts (VSS)**: Stroked vector typography with baseline rotation angle (`GSCA`), shear (`GSCR`), and micro-scale anti-aliased sub-pixel rendering.
- **Programmed Symbols (PSS / PSA)**: Support for single-plane and triple-plane multi-color composite symbols.
- **Interactive Graphics Input**: Mouse click pick correlation mapping display coordinates to host graphics space, 56-byte Graphic Input structured field generation, and immediate/deferred light pen selection.
### 🖨️ IBM 3287/3286 Host Printing Emulation
- **Autonomous Printer Sessions**: TN3270E host print sessions supporting SCS (SNA Character String / LU-Type 1) and DSC/DSE (Data Stream Compatibility / LU-Type 3).
- **Double-Byte DBCS Printing**: Pitch adjustments, character spacing, and grid line formatting (`PrintSCS3270DB`, `PrintPS3270DB`).
- **Flexible Print Output**: Printer Definition Table (PDT) integration, print spooling to local files, or dispatch directly to system print queues.
### 📁 IND$FILE File Transfer & Host Catalog
- **Full Protocol Coverage**: Support for both **CUT** and high-speed **DFT (DDM)** structured field transfers for TSO, VM/CMS, and CICS.
- **Translation Modes**: Automatic ASCII/EBCDIC text translation, CRLF conversions, and transparent binary transfers.
- **Dataset Formatting**: Full control over Record Format (`RECFM` F/V/U), Logical Record Length (`LRECL`), Block Size (`BLKSIZE`), and Space allocation.
- **Host Catalog Browser**: Interactive directory dialog to query, parse, and browse CMS and TSO host dataset catalogs.
### 🌍 Internationalization & Character Sets
- **22+ Single-Byte EBCDIC Codepages**: US/Canada (037), Open Systems / z/OS Unix (1047), International (500, 1148 Euro), UK (285), Germany/Austria (273, 1141 Euro), France (297), Italy (280), Spain (284), Scandinavia (277, 278), Greece (875), Turkey (1026, 1155, 905), Eastern Europe (870), Iceland (871), Cyrillic (1025, 1123, 1154, 880), Hebrew (424, 803), Arabic (420), Thai (838, 1160).
- **Mixed DBCS Support**: Japanese Katakana (930), Japanese Latin (939), Simplified Chinese (935, 1388), Traditional Chinese (937, 1371), and Korean (933) with Shift-In (`0x0E`) / Shift-Out (`0x0F`) state handling.
- **APL & Special Graphics**: APL keyboard mode, box-drawing characters, and Graphic Escape (`GE`) symbol translations.
### 🏛️ IBM Host On-Demand ECL API Alignment
- Reusable public API compliant with IBM Host On-Demand (HoD) v14 specifications under `com.ibm.eNetwork.ECL.*` and `haus.nightmare.lib3270j.ecl.*`.
- Drop-in implementations for `ECLSession`, `ECLPS` (Presentation Space), `ECLOIA` (Operator Information Area), `ECLConnection`, `ECLField`, `ECLFieldList`, `ECLScreenDesc`, `ECLScreenReco`, and `ECLXfer`.
### 🖥️ Desktop User Interface (j3270)
- **Themes**: Modern Dark and Light UI themes with instant switching.
- **Dynamic Font Scaling**: Real-time font size adjustments (`Cmd/Ctrl +`, `Cmd/Ctrl -`, `Cmd/Ctrl 0`) with smooth scaling and guarded window repacking.
- **Screen Search**: Full-screen text search dialog (`Cmd/Ctrl + F`) with forward/backward navigation and case-sensitivity controls.
- **Screen Exporter**: Save screen images and content as plain text, HTML, or PNG.
- **Field Inspector**: Interactive inspection tool displaying 3270 field boundaries, buffer addresses, protection, highlighting, and color attributes.
- **Scripting & Keystroke Macros**: Interactive mnemonic keystroke playback dialog supporting standard x3270 action strings.
- **Word Processing Modes**: Document Mode (DOC), Word Wrap, audible margin bell, and tab stop navigation.
- **Visual Aids**: Crosshair ruler and configurable cursor styles (block cursor, underline cursor with alpha blending).
- **Persistent Profiles**: Configuration profiles and session preferences saved in INI files (`j3270.ini`).
### 📦 Modular Zero-Dependency Core
- **`lib3270j`**: Standalone Java protocol and terminal engine with **zero third-party dependencies** (`java.base`, `java.desktop` only).
- **`j3270`**: Standalone desktop Swing terminal application.
--- ---
## 🛠️ Building from Source ## 🛠️ Building from Source
### Standalone Build (No Gradle Required) ### Quick Setup & Build for End Users
The project includes self-contained, portable build scripts:
Self-contained setup scripts automatically locate an installed Java 11+ JDK (searching `JAVA_HOME`, system `PATH`, macOS `java_home`, Homebrew, Linux JVM paths, Windows Registry, standard installation directories, and package managers), compile `lib3270j` and `j3270`, and package the standalone executable JAR (`build/j3270.jar`):
**Linux / macOS (POSIX Shell):**
```bash ```bash
# Build executable JAR in 2 seconds: # Setup and build standalone executable JAR:
sh ./build_all.sh ./setup.sh
# Run all 48 automated unit tests in ~500ms: # Build and immediately launch:
sh ./test_all.sh ./setup.sh --run
# Clean build and connect to host:
./setup.sh --clean --run -- mainframe.example.com 23 4
``` ```
The resulting standalone JAR is created at `build/j3270.jar`. **Windows (PowerShell):**
```powershell
# Setup and build standalone executable JAR:
.\setup.ps1
# Build and immediately launch:
.\setup.ps1 -Run
# Clean build and connect to host:
.\setup.ps1 -Clean -Run -- mainframe.example.com 23 4
```
*(Also available as `.\setup.ps`)*
### Standalone Build (Advanced / CI)
The project also provides low-level scripts and optional Gradle support:
```bash
# Direct compile and package without diagnostics:
sh ./build_all.sh
# Run all 539+ automated unit tests (~7s):
sh ./test_all.sh
```
### Gradle Build (Optional) ### Gradle Build (Optional)
```bash ```bash
@@ -83,11 +178,8 @@ The resulting standalone JAR is created at `build/j3270.jar`.
./gradlew test ./gradlew test
``` ```
--- ### Continuous Integration
All pushes and pull requests are verified across **Java 11, 17, and 21 LTS** runners via Gitea Actions (`.gitea/workflows/build.yaml`).
## 🤝 Contributing
Contributions and issue reports are welcome! Please feel free to open a bug report or feature request via the Gitea issue tracker.
--- ---
-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.
+7 -3
View File
@@ -1,5 +1,5 @@
allprojects { allprojects {
group = 'org.j3270' group = 'haus.nightmare.j3270'
version = '0.1.0' version = '0.1.0'
repositories { repositories {
@@ -10,11 +10,14 @@ allprojects {
subprojects { subprojects {
apply plugin: 'java' apply plugin: 'java'
sourceCompatibility = JavaVersion.VERSION_11 java {
targetCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
test { test {
useJUnitPlatform() useJUnitPlatform()
jvmArgs '-Djava.awt.headless=true'
testLogging { testLogging {
events "passed", "skipped", "failed" events "passed", "skipped", "failed"
} }
@@ -22,5 +25,6 @@ subprojects {
dependencies { dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
} }
} }
-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.
+79 -8
View File
@@ -14,16 +14,32 @@ if [ -n "$JAVA_HOME" ] && [ -x "$JAVA_HOME/bin/javac" ] && [ -x "$JAVA_HOME/bin/
JAVAC_BIN="$JAVA_HOME/bin/javac" JAVAC_BIN="$JAVA_HOME/bin/javac"
JAR_BIN="$JAVA_HOME/bin/jar" JAR_BIN="$JAVA_HOME/bin/jar"
elif command -v javac >/dev/null 2>&1 && command -v jar >/dev/null 2>&1; then elif command -v javac >/dev/null 2>&1 && command -v jar >/dev/null 2>&1; then
JAVAC_BIN="$(command -v javac)" if javac -version >/dev/null 2>&1; then
JAR_BIN="$(command -v jar)" JAVAC_BIN="$(command -v javac)"
else JAR_BIN="$(command -v jar)"
# Check common SDKMAN / macOS / Linux JDK locations fi
for h in "$HOME/.sdkman/candidates/java/current" \ fi
if [ -z "$JAVAC_BIN" ] || [ -z "$JAR_BIN" ]; then
if [ -x /usr/libexec/java_home ]; then
MAC_JAVA_HOME="$(/usr/libexec/java_home 2>/dev/null || true)"
if [ -n "$MAC_JAVA_HOME" ] && [ -x "$MAC_JAVA_HOME/bin/javac" ] && [ -x "$MAC_JAVA_HOME/bin/jar" ]; then
export JAVA_HOME="$MAC_JAVA_HOME"
JAVAC_BIN="$JAVA_HOME/bin/javac"
JAR_BIN="$JAVA_HOME/bin/jar"
fi
fi
fi
if [ -z "$JAVAC_BIN" ] || [ -z "$JAR_BIN" ]; then
# Check standard system JDK installation locations
for h in /usr/lib/jvm/default-java \
/usr/lib/jvm/java-21-openjdk* \ /usr/lib/jvm/java-21-openjdk* \
/usr/lib/jvm/java-17-openjdk* \ /usr/lib/jvm/java-17-openjdk* \
/usr/lib/jvm/java-11-openjdk* \ /usr/lib/jvm/java-11-openjdk* \
/usr/lib/jvm/default-java \ /Library/Java/JavaVirtualMachines/*/Contents/Home \
/Library/Java/JavaVirtualMachines/*/Contents/Home; do /opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home \
/opt/homebrew/opt/openjdk; do
if [ -d "$h" ] && [ -x "$h/bin/javac" ] && [ -x "$h/bin/jar" ]; then if [ -d "$h" ] && [ -x "$h/bin/javac" ] && [ -x "$h/bin/jar" ]; then
export JAVA_HOME="$h" export JAVA_HOME="$h"
JAVAC_BIN="$JAVA_HOME/bin/javac" JAVAC_BIN="$JAVA_HOME/bin/javac"
@@ -77,7 +93,7 @@ fi
# 4. Package executable JAR # 4. Package executable JAR
echo "Packaging j3270.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" \ "$JAR_BIN" cvfm "$BUILD_DIR/j3270.jar" "$BUILD_DIR/MANIFEST.MF" \
-C "$BUILD_DIR/lib3270j" . \ -C "$BUILD_DIR/lib3270j" . \
@@ -87,3 +103,58 @@ echo "=== Build Complete ==="
echo "Executable JAR created: $BUILD_DIR/j3270.jar" echo "Executable JAR created: $BUILD_DIR/j3270.jar"
ls -lh "$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")
)
.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));
System.exit(summary.getTotalFailureCount() > 0 ? 1 : 0);
}
}
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
+5 -5
View File
@@ -1,10 +1,10 @@
# Gitea Actions Runner Configuration for j3270 # Gitea Actions Runner Configuration for j3270
This repository includes Gitea Actions workflows in `.gitea/workflows/build.yaml` to automatically: This repository includes Gitea Actions workflows in `.gitea/workflows/build.yaml` to automatically:
1. Compile and run the JUnit 5 test suite via `test_all.sh`. 1. Compile and run the JUnit 5 test suite across the 3 LTS Java versions (Java 11, 17, and 21) via `test_all.sh`.
2. Build the standalone `j3270.jar` executable via `build_all.sh`. 2. Build standalone executables for each LTS version via `build_all.sh`.
3. Upload `j3270.jar` as a downloadable artifact. 3. Upload `j3270-java11.jar`, `j3270-java17.jar`, and `j3270-java21.jar` as downloadable artifacts.
4. Report test results and diagnostics. 4. Report test results and diagnostics per runtime.
--- ---
@@ -98,7 +98,7 @@ This means Docker's bridge network inside the runner container cannot resolve th
You can verify the entire build and test suite locally at any time without external tools: You can verify the entire build and test suite locally at any time without external tools:
```bash ```bash
# Compile and run all 25 unit tests (130ms): # Compile and run all 539 unit tests (~7s):
sh ./test_all.sh sh ./test_all.sh
# Build executable standalone JAR: # Build executable standalone JAR:
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
Vendored
+248 -1
View File
@@ -1,2 +1,249 @@
#!/bin/sh #!/bin/sh
exec "$(dirname "$0")/build_all.sh"
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
+92
View File
@@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+2 -2
View File
@@ -7,12 +7,12 @@ dependencies {
} }
application { application {
mainClass = 'org.pubvm.j3270.J3270App' mainClass = 'haus.nightmare.j3270.J3270App'
} }
jar { jar {
manifest { manifest {
attributes 'Main-Class': 'org.pubvm.j3270.J3270App', attributes 'Main-Class': 'haus.nightmare.j3270.J3270App',
'Implementation-Title': 'j3270', 'Implementation-Title': 'j3270',
'Implementation-Version': archiveVersion 'Implementation-Version': archiveVersion
} }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,809 @@
package haus.nightmare.j3270.config;
import haus.nightmare.j3270.ui.UITheme;
import java.util.prefs.Preferences;
import java.awt.Color;
import java.io.*;
import java.io.PrintWriter;
import java.util.logging.Logger;
import java.util.logging.Level;
public class Settings {
private static final Preferences prefs = Preferences.userNodeForPackage(Settings.class);
private static final Logger log = Logger.getLogger(Settings.class.getName());
public enum StartupBehavior {
SHOW_CONNECT,
DO_NOTHING,
AUTO_CONNECT
}
public static UITheme getJavaUiTheme() {
String themeStr = prefs.get("javaUiTheme", UITheme.DARK.name());
return UITheme.fromString(themeStr);
}
public static void setJavaUiTheme(UITheme theme) {
prefs.put("javaUiTheme", (theme != null ? theme : UITheme.DARK).name());
}
public static String getFontFamily() {
return prefs.get("fontFamily", "Monospaced");
}
public static void setFontFamily(String family) {
prefs.put("fontFamily", family);
flushPrefs();
}
public static int getFontSize() {
return prefs.getInt("fontSize", 16);
}
public static void setFontSize(int size) {
prefs.putInt("fontSize", size);
flushPrefs();
}
private static void flushPrefs() {
try {
prefs.flush();
} catch (Exception ignored) {
}
}
public static StartupBehavior getStartupBehavior() {
String behaviorStr = prefs.get("startupBehavior", StartupBehavior.SHOW_CONNECT.name());
try {
return StartupBehavior.valueOf(behaviorStr);
} catch (IllegalArgumentException e) {
return StartupBehavior.SHOW_CONNECT;
}
}
public static void setStartupBehavior(StartupBehavior behavior) {
prefs.put("startupBehavior", behavior.name());
}
public static String getAutoConnectHost() {
return prefs.get("autoConnectHost", "");
}
public static void setAutoConnectHost(String host) {
prefs.put("autoConnectHost", host);
}
public static int getAutoConnectPort() {
return prefs.getInt("autoConnectPort", 23);
}
public static void setAutoConnectPort(int port) {
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 boolean getAutoSysUnlock() {
return prefs.getBoolean("autoSysUnlock", true);
}
public static void setAutoSysUnlock(boolean autoSysUnlock) {
prefs.putBoolean("autoSysUnlock", autoSysUnlock);
flushPrefs();
}
public static boolean getAutoConnectKeepAlive() {
return prefs.getBoolean("autoConnectKeepAlive", true);
}
public static void setAutoConnectKeepAlive(boolean keepAlive) {
prefs.putBoolean("autoConnectKeepAlive", keepAlive);
flushPrefs();
}
public static int getAutoConnectKeepAliveInterval() {
return prefs.getInt("autoConnectKeepAliveInterval", 120);
}
public static void setAutoConnectKeepAliveInterval(int interval) {
prefs.putInt("autoConnectKeepAliveInterval", interval);
flushPrefs();
}
public static boolean getAutoConnectAutoReconnect() {
return prefs.getBoolean("autoConnectAutoReconnect", false);
}
public static void setAutoConnectAutoReconnect(boolean autoReconnect) {
prefs.putBoolean("autoConnectAutoReconnect", autoReconnect);
flushPrefs();
}
public static int getAutoConnectReconnectMaxRetries() {
return prefs.getInt("autoConnectReconnectMaxRetries", 5);
}
public static void setAutoConnectReconnectMaxRetries(int retries) {
prefs.putInt("autoConnectReconnectMaxRetries", retries);
flushPrefs();
}
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 int getDynamicRows() {
return prefs.getInt("dynamicRows", 62);
}
public static void setDynamicRows(int rows) {
prefs.putInt("dynamicRows", Math.max(1, rows));
}
public static int getDynamicCols() {
return prefs.getInt("dynamicCols", 160);
}
public static void setDynamicCols(int cols) {
prefs.putInt("dynamicCols", Math.max(1, cols));
}
public static Color getColorOverride(int index, Color defaultColor) {
String hex = prefs.get("color_" + index, null);
try {
return hex != null ? Color.decode(hex) : defaultColor;
} catch (NumberFormatException e) {
return defaultColor;
}
}
public static void setColorOverride(int index, Color color) {
prefs.put("color_" + index, String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue()));
}
public static Color getMonoColorOverride(String key, Color defaultColor) {
String hex = prefs.get("mono_" + key, null);
try {
return hex != null ? Color.decode(hex) : defaultColor;
} catch (NumberFormatException e) {
return defaultColor;
}
}
public static void setMonoColorOverride(String key, Color color) {
prefs.put("mono_" + key, String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue()));
}
public static boolean isMac() {
String os = System.getProperty("os.name");
return os != null && os.toLowerCase().contains("mac");
}
public static String getDefaultBinding(String action) {
boolean mac = isMac();
switch (action) {
case "COPY":
return mac ? "meta C, ctrl C, ctrl INSERT, ctrl shift C" : "ctrl C, ctrl INSERT, ctrl shift C";
case "PASTE":
return mac ? "meta V, ctrl V, shift INSERT, ctrl shift V" : "ctrl V, shift INSERT, ctrl shift V";
case "SELECTALL":
return mac ? "meta A, ctrl A" : "ctrl A";
case "INSERT":
return mac ? "INSERT, HELP, alt I" : "INSERT, ctrl I";
case "ERASE_INPUT":
return "alt E";
case "NEWLINE":
return "shift ENTER";
case "DUP":
return "alt D";
case "FIELD_MARK":
return "alt M";
case "ATTN":
return "alt A";
case "SYSREQ":
return "alt S";
case "CURSEL":
return "alt Q";
case "CLEAR":
return "alt C";
case "STATUS_BAR":
return "alt B";
default:
return action;
}
}
public static String getKeyBinding(String action, String defaultBinding) {
String val = prefs.get("key_" + action, null);
if (val == null) {
return defaultBinding;
}
// If stored binding is the legacy single-key "INSERT" default, migrate it to the current platform default
if ("INSERT".equals(action) && "INSERT".equals(val)) {
prefs.put("key_INSERT", defaultBinding);
return defaultBinding;
}
// If stored binding is the legacy or corrupted "alt STE" for PASTE, migrate it to the current platform default
if ("PASTE".equals(action) && ("alt STE".equals(val) || "PASTE".equals(val))) {
prefs.put("key_PASTE", defaultBinding);
return defaultBinding;
}
return val;
}
public static void setKeyBinding(String action, String binding) {
prefs.put("key_" + action, binding);
}
/**
* Load settings from an INI-style configuration file.
* Sections: [appearance], [behavior], [colors], [keybindings]
*
* Example file:
* <pre>
* [appearance]
* fontFamily = IBM 3270
* fontSize = 18
*
* [behavior]
* startupBehavior = AUTO_CONNECT
* autoConnectHost = mainframe.example.com
* autoConnectPort = 23
*
* [colors]
* ; Host colors 0-15
* color_0 = #000000
* color_4 = #00ff00
* ; Monochrome / background
* mono_NORMAL = #32cd32
* mono_BACKGROUND = #0a0a0a
*
* [keybindings]
* ; Comma-separated for multiple bindings
* PF12 = F12, shift F12
* PF24 = UNBOUND
* CLEAR = alt C
* </pre>
*/
public static void loadFromIniFile(String path) throws IOException {
File file = new File(path);
if (!file.exists()) {
throw new FileNotFoundException("Config file not found: " + path);
}
log.info("Loading configuration from: " + path);
String currentSection = "";
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
int lineNum = 0;
while ((line = reader.readLine()) != null) {
lineNum++;
line = line.trim();
// Skip empty lines and comments
if (line.isEmpty() || line.startsWith(";") || line.startsWith("#")) {
continue;
}
// Section header
if (line.startsWith("[") && line.endsWith("]")) {
currentSection = line.substring(1, line.length() - 1).toLowerCase().trim();
continue;
}
// Key = Value
int eq = line.indexOf('=');
if (eq < 0) {
log.warning("Config line " + lineNum + ": no '=' found, skipping: " + line);
continue;
}
String key = line.substring(0, eq).trim();
String value = line.substring(eq + 1).trim();
try {
applyConfigEntry(currentSection, key, value);
} catch (Exception e) {
log.log(Level.WARNING, "Config line " + lineNum + ": error applying " + key + "=" + value, e);
}
}
}
log.info("Configuration loaded successfully.");
}
// ========== Block select mode ==========
public static boolean getBlockSelectMode() {
return prefs.getBoolean("blockSelectMode", false);
}
public static void setBlockSelectMode(boolean block) {
prefs.putBoolean("blockSelectMode", block);
}
// ========== Clipboard & Tabular Paste ==========
public static boolean getEnablePasteFromExcel() {
return prefs.getBoolean("enablePasteFromExcel", true);
}
public static void setEnablePasteFromExcel(boolean val) {
prefs.putBoolean("enablePasteFromExcel", val);
flushPrefs();
}
public static boolean getPasteStopAtProtectedLine() {
return prefs.getBoolean("pasteStopAtProtectedLine", false);
}
public static void setPasteStopAtProtectedLine(boolean val) {
prefs.putBoolean("pasteStopAtProtectedLine", val);
flushPrefs();
}
// ========== Crosshair Ruler ==========
public static boolean getCrosshairRuler() {
return prefs.getBoolean("crosshairRuler", false);
}
public static void setCrosshairRuler(boolean enabled) {
prefs.putBoolean("crosshairRuler", enabled);
}
// ========== Status Bar Visibility ==========
public static boolean getStatusBarVisible() {
return prefs.getBoolean("statusBarVisible", true);
}
public static void setStatusBarVisible(boolean visible) {
prefs.putBoolean("statusBarVisible", visible);
}
// ========== Cursor Style ==========
public static String getCursorStyle() {
return prefs.get("cursorStyle", "BLOCK");
}
public static void setCursorStyle(String style) {
prefs.put("cursorStyle", style != null ? style.toUpperCase() : "BLOCK");
}
// ========== Entry Assist & Modes ==========
public static boolean getEntryAssistDocMode() {
return prefs.getBoolean("entryAssistDocMode", false);
}
public static void setEntryAssistDocMode(boolean docMode) {
prefs.putBoolean("entryAssistDocMode", docMode);
}
public static boolean getEntryAssistWordWrap() {
return prefs.getBoolean("entryAssistWordWrap", false);
}
public static void setEntryAssistWordWrap(boolean wordWrap) {
prefs.putBoolean("entryAssistWordWrap", wordWrap);
}
public static int getEntryAssistStartCol() {
return prefs.getInt("entryAssistStartCol", 1);
}
public static void setEntryAssistStartCol(int startCol) {
prefs.putInt("entryAssistStartCol", startCol);
}
public static int getEntryAssistEndCol() {
return prefs.getInt("entryAssistEndCol", 80);
}
public static void setEntryAssistEndCol(int endCol) {
prefs.putInt("entryAssistEndCol", endCol);
}
public static boolean getEntryAssistBell() {
return prefs.getBoolean("entryAssistBell", false);
}
public static void setEntryAssistBell(boolean bell) {
prefs.putBoolean("entryAssistBell", bell);
}
public static int getEntryAssistBellCol() {
return prefs.getInt("entryAssistBellCol", 75);
}
public static void setEntryAssistBellCol(int col) {
prefs.putInt("entryAssistBellCol", col);
}
public static String getEntryAssistTabStops() {
return prefs.get("entryAssistTabStops", "5,10,15,20,25,30,35,40,45,50,55,60,65,70,75");
}
public static void setEntryAssistTabStops(String stops) {
prefs.put("entryAssistTabStops", stops != null ? stops : "");
}
public static int[] getEntryAssistTabStopsArray() {
String s = getEntryAssistTabStops();
if (s == null || s.trim().isEmpty()) return new int[0];
String[] parts = s.split(",");
java.util.List<Integer> list = new java.util.ArrayList<>();
for (String p : parts) {
try {
int val = Integer.parseInt(p.trim());
if (val > 0) list.add(val - 1);
} catch (NumberFormatException ignored) {}
}
int[] res = new int[list.size()];
for (int i = 0; i < list.size(); i++) res[i] = list.get(i);
return res;
}
public static boolean getInsertOffOnAid() {
return prefs.getBoolean("insertOffOnAid", true);
}
public static void setInsertOffOnAid(boolean val) {
prefs.putBoolean("insertOffOnAid", val);
}
public static boolean getFourColorOverride() {
return prefs.getBoolean("fourColorOverride", false);
}
public static void setFourColorOverride(boolean val) {
prefs.putBoolean("fourColorOverride", val);
}
public static boolean getNumericFieldLock() {
return prefs.getBoolean("numericFieldLock", true);
}
public static void setNumericFieldLock(boolean val) {
prefs.putBoolean("numericFieldLock", val);
}
public static boolean getAutoSkipEnabled() {
return prefs.getBoolean("autoSkipEnabled", true);
}
public static void setAutoSkipEnabled(boolean val) {
prefs.putBoolean("autoSkipEnabled", val);
}
private static void applyConfigEntry(String section, String key, String value) {
switch (section) {
case "appearance":
switch (key) {
case "fontFamily": setFontFamily(value); break;
case "fontSize": setFontSize(Integer.parseInt(value)); break;
case "javaUiTheme":
case "theme":
case "uiTheme":
setJavaUiTheme(UITheme.fromString(value));
break;
case "crosshairRuler":
case "ruler":
setCrosshairRuler(Boolean.parseBoolean(value));
break;
case "statusBarVisible":
case "statusbar":
case "statusBar":
case "showStatusBar":
setStatusBarVisible(Boolean.parseBoolean(value));
break;
case "cursorStyle":
setCursorStyle(value);
break;
default:
log.warning("Unknown appearance key: " + key);
}
break;
case "behavior":
case "connection":
switch (key) {
case "startupBehavior":
setStartupBehavior(StartupBehavior.valueOf(value.toUpperCase()));
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 "dynamicRows":
case "dynamic_rows":
setDynamicRows(Integer.parseInt(value));
break;
case "dynamicCols":
case "dynamic_cols":
setDynamicCols(Integer.parseInt(value));
break;
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
case "autoSysUnlock":
case "auto_sys_unlock":
setAutoSysUnlock(Boolean.parseBoolean(value));
break;
case "enablePasteFromExcel":
case "pasteFromExcel":
case "excelPaste":
setEnablePasteFromExcel(Boolean.parseBoolean(value));
break;
case "pasteStopAtProtectedLine":
case "stopAtProtected":
case "pasteStopAtProtected":
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
break;
default:
log.warning("Unknown behavior/connection key: " + key);
}
break;
case "clipboard":
switch (key) {
case "enablePasteFromExcel":
case "pasteFromExcel":
case "excelPaste":
setEnablePasteFromExcel(Boolean.parseBoolean(value));
break;
case "pasteStopAtProtectedLine":
case "stopAtProtected":
case "pasteStopAtProtected":
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
break;
default:
log.warning("Unknown clipboard key: " + key);
}
break;
case "colors":
if (key.startsWith("color_")) {
int index = Integer.parseInt(key.substring(6));
setColorOverride(index, Color.decode(value));
} else if (key.startsWith("mono_")) {
String monoKey = key.substring(5);
setMonoColorOverride(monoKey, Color.decode(value));
} else {
log.warning("Unknown colors key: " + key);
}
break;
case "keybindings":
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;
case "entryassist":
case "modes":
switch (key) {
case "docMode":
case "entryAssistDocMode":
setEntryAssistDocMode(Boolean.parseBoolean(value));
break;
case "wordWrap":
case "entryAssistWordWrap":
setEntryAssistWordWrap(Boolean.parseBoolean(value));
break;
case "startCol":
case "entryAssistStartCol":
setEntryAssistStartCol(Integer.parseInt(value));
break;
case "endCol":
case "entryAssistEndCol":
setEntryAssistEndCol(Integer.parseInt(value));
break;
case "bell":
case "entryAssistBell":
setEntryAssistBell(Boolean.parseBoolean(value));
break;
case "bellCol":
case "entryAssistBellCol":
setEntryAssistBellCol(Integer.parseInt(value));
break;
case "tabStops":
case "entryAssistTabStops":
setEntryAssistTabStops(value);
break;
case "insertOffOnAid":
setInsertOffOnAid(Boolean.parseBoolean(value));
break;
case "fourColorOverride":
setFourColorOverride(Boolean.parseBoolean(value));
break;
case "numericFieldLock":
setNumericFieldLock(Boolean.parseBoolean(value));
break;
case "autoSkip":
case "autoSkipEnabled":
setAutoSkipEnabled(Boolean.parseBoolean(value));
break;
default:
log.warning("Unknown entryassist/modes key: " + key);
}
break;
default:
// Allow bare keys outside any section — treat as raw prefs
log.fine("Setting raw preference: " + key + " = " + value);
prefs.put(key, value);
break;
}
}
// ========== Export to INI ==========
/**
* Export the current configuration to an INI-style file.
* Writes all settings including appearance, behavior, colors, and keybindings.
*/
public static void exportToIniFile(String path) throws IOException {
try (PrintWriter w = new PrintWriter(new BufferedWriter(new FileWriter(path)))) {
w.println("; j3270 Configuration File");
w.println("; Exported on " + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()));
w.println("; Load with: java -jar j3270.jar -c " + new File(path).getName());
w.println();
// [appearance]
w.println("[appearance]");
w.println("javaUiTheme = " + getJavaUiTheme().name());
w.println("fontFamily = " + getFontFamily());
w.println("fontSize = " + getFontSize());
w.println("crosshairRuler = " + getCrosshairRuler());
w.println("statusBarVisible = " + getStatusBarVisible());
w.println("cursorStyle = " + getCursorStyle());
w.println();
// [behavior]
w.println("[behavior]");
w.println("startupBehavior = " + getStartupBehavior().name());
String acHost = getAutoConnectHost();
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("dynamicRows = " + getDynamicRows());
w.println("dynamicCols = " + getDynamicCols());
w.println("blockSelectMode = " + getBlockSelectMode());
w.println("autoSysUnlock = " + getAutoSysUnlock());
w.println("enablePasteFromExcel = " + getEnablePasteFromExcel());
w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine());
w.println();
// [entryassist]
w.println("[entryassist]");
w.println("docMode = " + getEntryAssistDocMode());
w.println("wordWrap = " + getEntryAssistWordWrap());
w.println("startCol = " + getEntryAssistStartCol());
w.println("endCol = " + getEntryAssistEndCol());
w.println("bell = " + getEntryAssistBell());
w.println("bellCol = " + getEntryAssistBellCol());
w.println("tabStops = " + getEntryAssistTabStops());
w.println();
// [modes]
w.println("[modes]");
w.println("insertOffOnAid = " + getInsertOffOnAid());
w.println("fourColorOverride = " + getFourColorOverride());
w.println("numericFieldLock = " + getNumericFieldLock());
w.println("autoSkip = " + getAutoSkipEnabled());
w.println();
// [graphics]
w.println("[graphics]");
w.println("graphicsMode = " + getGraphicsMode().name());
w.println();
// [colors]
w.println("[colors]");
// Host colors 0-15
for (int i = 0; i < 16; i++) {
String hex = prefs.get("color_" + i, null);
if (hex != null) {
w.println("color_" + i + " = " + hex);
}
}
// Mono colors
String[] monoKeys = {"NORMAL", "INTENSIFY", "PROTECTED", "PROTECTED_HIGH", "BACKGROUND"};
for (String mk : monoKeys) {
String hex = prefs.get("mono_" + mk, null);
if (hex != null) {
w.println("mono_" + mk + " = " + hex);
}
}
w.println();
// [keybindings]
w.println("[keybindings]");
// Navigation & clipboard keys
String[] navActions = {"ENTER", "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
"HOME", "END", "PAGE_UP", "PAGE_DOWN", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE", "CLEAR",
"STATUS_BAR", "COPY", "PASTE", "SELECTALL"};
for (String act : navActions) {
String val = prefs.get("key_" + act, null);
if (val != null) {
w.println(act + " = " + val);
}
}
// PF keys
for (int i = 1; i <= 24; i++) {
String val = prefs.get("key_PF" + i, null);
if (val != null) {
w.println("PF" + i + " = " + val);
}
}
// PA keys
for (int i = 1; i <= 3; i++) {
String val = prefs.get("key_PA" + i, null);
if (val != null) {
w.println("PA" + i + " = " + val);
}
}
}
log.info("Configuration exported to: " + path);
}
}
@@ -1,10 +1,10 @@
package org.pubvm.j3270.ft; package haus.nightmare.j3270.ft;
import org.lib3270j.Telnet3270Client; import haus.nightmare.lib3270j.Telnet3270Client;
import org.lib3270j.ft.FTConfig; import haus.nightmare.lib3270j.ft.FTConfig;
import org.lib3270j.ft.FTConstants.FTState; import haus.nightmare.lib3270j.ft.FTConstants.FTState;
import org.lib3270j.ft.FTCut; import haus.nightmare.lib3270j.ft.FTCut;
import org.lib3270j.ft.FTDft; import haus.nightmare.lib3270j.ft.FTDft;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -53,6 +53,10 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
this.callback = callback; this.callback = callback;
} }
public Telnet3270Client getClient() {
return client;
}
/** /**
* Start a new file transfer with the given configuration. * Start a new file transfer with the given configuration.
* @return null if started successfully, error message otherwise. * @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) { if (state == FTState.RUNNING || state == FTState.AWAIT_ACK) {
log.info("User cancelled transfer"); log.info("User cancelled transfer");
setState(FTState.ABORT_WAIT); // Signal handlers to abort at next chance 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) { } else if (state != FTState.NONE) {
log.info("Forcing cancel from state " + state); log.info("Forcing cancel from state " + state);
completeTransfer("Transfer cancelled."); completeTransfer("Transfer cancelled.");
@@ -0,0 +1,488 @@
package haus.nightmare.j3270.ft;
import haus.nightmare.j3270.ui.HostDirectoryDialog;
import haus.nightmare.j3270.ui.ThemeManager;
import haus.nightmare.lib3270j.ft.FTConfig;
import haus.nightmare.lib3270j.ft.FTConstants;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.io.File;
public class FileTransferDialog extends JDialog {
public static class TransferSessionState {
public FTConfig.HostType hostType = FTConfig.HostType.TSO;
public boolean isSend = false;
public String localFile = "";
public String hostFile = "";
public boolean isAscii = true;
public int mtu = FTConstants.DFT_BUF;
public boolean crFlag = true;
public boolean remapFlag = true;
public boolean append = false;
public boolean overwrite = false;
public String recfm = "";
public String lrecl = "";
public String blksize = "";
public String space = "";
public String options = "";
}
private static TransferSessionState lastTransferState = null;
public static TransferSessionState getLastTransferState() {
return lastTransferState;
}
public static void setLastTransferState(TransferSessionState state) {
lastTransferState = state;
}
public static void resetSessionState() {
lastTransferState = null;
}
public static String formatHostFilename(String localPath, FTConfig.HostType hostType) {
if (localPath == null || localPath.trim().isEmpty()) {
return "";
}
File f = new File(localPath.trim());
String name = f.getName();
if (hostType == FTConfig.HostType.CMS) {
name = name.replace('.', ' ');
}
return name;
}
private final FileTransfer coordinator;
private final Frame owner;
private JComboBox<FTConfig.HostType> hostTypeCombo;
private JRadioButton sendRadio;
private JRadioButton receiveRadio;
private JTextField localFileField;
private JButton browseLocalButton;
private JTextField hostFileField;
private JButton browseHostButton;
private JComboBox<Integer> mtuCombo;
// Options
private JRadioButton asciiRadio;
private JRadioButton binaryRadio;
private JCheckBox crCheck;
private JCheckBox remapCheck;
private JCheckBox appendCheck;
private JCheckBox overwriteCheck;
// TSO specific
private JTextField recfmField;
private JTextField lreclField;
private JTextField blksizeField;
private JTextField spaceField;
// VM specific
private JTextField optionsField;
private JButton transferButton;
private JButton cancelButton;
public FileTransferDialog(Frame owner, FileTransfer coordinator) {
super(owner, "File Transfer (IND$FILE)", false);
this.owner = owner;
this.coordinator = coordinator;
buildUI();
pack();
setLocationRelativeTo(owner);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
mainPanel.setBackground(new Color(25, 25, 25));
// Create form panel
JPanel formPanel = new JPanel();
formPanel.setLayout(new BoxLayout(formPanel, BoxLayout.Y_AXIS));
formPanel.setOpaque(false);
// Host Type
hostTypeCombo = new JComboBox<>(FTConfig.HostType.values());
hostTypeCombo.addActionListener(e -> {
updateOptionStates();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
if (hostType == FTConfig.HostType.CMS) {
String hostText = hostFileField.getText().trim();
if (hostText.contains(".")) {
hostFileField.setText(hostText.replace('.', ' '));
}
}
});
formPanel.add(createRow("Host Environment:", hostTypeCombo));
// Direction
sendRadio = new JRadioButton("Send to Host");
receiveRadio = new JRadioButton("Receive from Host");
receiveRadio.setSelected(true);
ButtonGroup dirGroup = new ButtonGroup();
dirGroup.add(sendRadio);
dirGroup.add(receiveRadio);
JPanel dirPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
dirPanel.setOpaque(false);
dirPanel.add(receiveRadio);
dirPanel.add(Box.createRigidArea(new Dimension(15, 0)));
dirPanel.add(sendRadio);
sendRadio.addActionListener(e -> updateOptionStates());
receiveRadio.addActionListener(e -> updateOptionStates());
formPanel.add(createRow("Direction:", dirPanel));
// Local File
localFileField = new JTextField(20);
localFileField.addFocusListener(new java.awt.event.FocusAdapter() {
@Override
public void focusLost(java.awt.event.FocusEvent e) {
if (hostFileField.getText().trim().isEmpty() && !localFileField.getText().trim().isEmpty()) {
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
hostFileField.setText(formatHostFilename(localFileField.getText(), hostType));
}
}
});
browseLocalButton = new JButton("Browse...");
ThemeManager.styleButton(browseLocalButton, ThemeManager.ButtonVariant.DEFAULT);
browseLocalButton.addActionListener(e -> browseLocalFile());
JPanel localPanel = new JPanel(new BorderLayout(5, 0));
localPanel.setOpaque(false);
localPanel.add(localFileField, BorderLayout.CENTER);
localPanel.add(browseLocalButton, BorderLayout.EAST);
formPanel.add(createRow("Local File:", localPanel));
// Host File
hostFileField = new JTextField(20);
browseHostButton = new JButton("Browse Host...");
ThemeManager.styleButton(browseHostButton, ThemeManager.ButtonVariant.DEFAULT);
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");
asciiRadio.setSelected(true);
binaryRadio = new JRadioButton("Binary");
ButtonGroup modeGroup = new ButtonGroup();
modeGroup.add(asciiRadio);
modeGroup.add(binaryRadio);
JPanel modePanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
modePanel.setOpaque(false);
modePanel.add(asciiRadio);
modePanel.add(Box.createRigidArea(new Dimension(15, 0)));
modePanel.add(binaryRadio);
asciiRadio.addActionListener(e -> updateOptionStates());
binaryRadio.addActionListener(e -> updateOptionStates());
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);
remapCheck = new JCheckBox("Remap Character Set");
remapCheck.setSelected(true);
appendCheck = new JCheckBox("Append to file");
overwriteCheck = new JCheckBox("Overwrite existing");
JPanel optionsPanel1 = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
optionsPanel1.setOpaque(false);
optionsPanel1.add(crCheck);
optionsPanel1.add(Box.createRigidArea(new Dimension(15, 0)));
optionsPanel1.add(remapCheck);
JPanel optionsPanel2 = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
optionsPanel2.setOpaque(false);
optionsPanel2.add(appendCheck);
optionsPanel2.add(Box.createRigidArea(new Dimension(15, 0)));
optionsPanel2.add(overwriteCheck);
formPanel.add(createRow("Text Options:", optionsPanel1));
formPanel.add(createRow("File Options:", optionsPanel2));
// Host-specific options panel (TSO dataset allocation)
JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5));
hostOptsPanel.setOpaque(false);
hostOptsPanel.setBorder(ThemeManager.createTitledBorder("TSO Allocation Options (Send Only)"));
hostOptsPanel.add(new JLabel("RECFM:"));
recfmField = new JTextField(5);
hostOptsPanel.add(recfmField);
hostOptsPanel.add(new JLabel("LRECL:"));
lreclField = new JTextField(5);
hostOptsPanel.add(lreclField);
hostOptsPanel.add(new JLabel("BLKSIZE:"));
blksizeField = new JTextField(5);
hostOptsPanel.add(blksizeField);
hostOptsPanel.add(new JLabel("SPACE:"));
spaceField = new JTextField(15);
hostOptsPanel.add(spaceField);
formPanel.add(Box.createRigidArea(new Dimension(0, 10)));
formPanel.add(hostOptsPanel);
// CMS options
JPanel vmOptsPanel = new JPanel(new BorderLayout(5, 0));
vmOptsPanel.setOpaque(false);
vmOptsPanel.setBorder(ThemeManager.createTitledBorder("CMS Options"));
optionsField = new JTextField(20);
vmOptsPanel.add(new JLabel("Additional Options: "), BorderLayout.WEST);
vmOptsPanel.add(optionsField, BorderLayout.CENTER);
formPanel.add(Box.createRigidArea(new Dimension(0, 10)));
formPanel.add(vmOptsPanel);
mainPanel.add(formPanel, BorderLayout.CENTER);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
buttonPanel.setOpaque(false);
transferButton = new JButton("Start Transfer");
ThemeManager.styleButton(transferButton, ThemeManager.ButtonVariant.PRIMARY);
transferButton.addActionListener(e -> startTransfer());
cancelButton = new JButton("Close");
ThemeManager.styleButton(cancelButton, ThemeManager.ButtonVariant.CANCEL);
cancelButton.addActionListener(e -> dispose());
buttonPanel.add(cancelButton);
buttonPanel.add(transferButton);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
prefillFromLastTransfer();
ThemeManager.applyThemeToWindow(this);
updateOptionStates();
}
private void prefillFromLastTransfer() {
if (lastTransferState != null) {
if (lastTransferState.hostType != null) {
hostTypeCombo.setSelectedItem(lastTransferState.hostType);
}
sendRadio.setSelected(lastTransferState.isSend);
receiveRadio.setSelected(!lastTransferState.isSend);
if (lastTransferState.localFile != null) {
localFileField.setText(lastTransferState.localFile);
}
if (lastTransferState.hostFile != null) {
hostFileField.setText(lastTransferState.hostFile);
}
asciiRadio.setSelected(lastTransferState.isAscii);
binaryRadio.setSelected(!lastTransferState.isAscii);
if (lastTransferState.mtu > 0) {
mtuCombo.setSelectedItem(lastTransferState.mtu);
}
crCheck.setSelected(lastTransferState.crFlag);
remapCheck.setSelected(lastTransferState.remapFlag);
appendCheck.setSelected(lastTransferState.append);
overwriteCheck.setSelected(lastTransferState.overwrite);
if (lastTransferState.recfm != null) recfmField.setText(lastTransferState.recfm);
if (lastTransferState.lrecl != null) lreclField.setText(lastTransferState.lrecl);
if (lastTransferState.blksize != null) blksizeField.setText(lastTransferState.blksize);
if (lastTransferState.space != null) spaceField.setText(lastTransferState.space);
if (lastTransferState.options != null) optionsField.setText(lastTransferState.options);
}
}
private void updateOptionStates() {
boolean isSend = sendRadio.isSelected();
boolean isAscii = asciiRadio.isSelected();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
boolean isTso = (hostType == FTConfig.HostType.TSO);
boolean isCms = (hostType == FTConfig.HostType.CMS);
appendCheck.setEnabled(!isSend);
overwriteCheck.setEnabled(!isSend);
crCheck.setEnabled(isAscii);
remapCheck.setEnabled(isAscii);
recfmField.setEnabled(isSend && isTso);
lreclField.setEnabled(isSend && isTso);
blksizeField.setEnabled(isSend && isTso);
spaceField.setEnabled(isSend && isTso);
optionsField.setEnabled(isCms);
}
private JPanel createRow(String labelText, Component comp) {
JPanel row = new JPanel(new BorderLayout(10, 0));
row.setOpaque(false);
JLabel label = new JLabel(labelText);
label.setPreferredSize(new Dimension(130, 25));
row.add(label, BorderLayout.WEST);
row.add(comp, BorderLayout.CENTER);
row.setBorder(new EmptyBorder(0, 0, 5, 0));
return row;
}
private void browseLocalFile() {
JFileChooser chooser = new JFileChooser();
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
File selected = chooser.getSelectedFile();
localFileField.setText(selected.getAbsolutePath());
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
boolean shouldAutofill = hostFileField.getText().trim().isEmpty() ||
(lastTransferState != null && hostFileField.getText().trim().equals(lastTransferState.hostFile));
if (shouldAutofill) {
hostFileField.setText(formatHostFilename(selected.getName(), hostType));
}
}
}
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());
boolean shouldAutofill = localFileField.getText().trim().isEmpty() ||
(lastTransferState != null && localFileField.getText().trim().equals(lastTransferState.localFile));
if (shouldAutofill) {
String cleanName = dialog.getSelectedHostFile().replace("'", "").replace("\"", "");
if (hostType == FTConfig.HostType.CMS) {
String[] tokens = cleanName.trim().split("\\s+");
if (tokens.length >= 2) {
cleanName = tokens[0].toLowerCase() + "." + tokens[1].toLowerCase();
} else if (tokens.length == 1) {
cleanName = tokens[0].toLowerCase();
}
} else {
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.",
"Error", JOptionPane.ERROR_MESSAGE);
return;
}
FTConfig config = new FTConfig();
config.setHostType((FTConfig.HostType) hostTypeCombo.getSelectedItem());
config.setLocalFilename(localFileField.getText().trim());
config.setHostFilename(hostFileField.getText().trim());
config.setReceive(receiveRadio.isSelected());
config.setAscii(asciiRadio.isSelected());
config.setCrFlag(crCheck.isSelected());
config.setRemapFlag(remapCheck.isSelected());
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());
config.setSpace(spaceField.getText().trim());
config.setOptions(optionsField.getText().trim());
recordSessionTransfer();
String error = coordinator.startTransfer(config);
if (error != null) {
JOptionPane.showMessageDialog(this, error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
} else {
dispose();
}
}
public void recordSessionTransfer() {
TransferSessionState state = new TransferSessionState();
state.hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
state.isSend = sendRadio.isSelected();
state.localFile = localFileField.getText().trim();
state.hostFile = hostFileField.getText().trim();
state.isAscii = asciiRadio.isSelected();
Integer mtu = (Integer) mtuCombo.getSelectedItem();
state.mtu = (mtu != null) ? mtu : FTConstants.DFT_BUF;
state.crFlag = crCheck.isSelected();
state.remapFlag = remapCheck.isSelected();
state.append = appendCheck.isSelected();
state.overwrite = overwriteCheck.isSelected();
state.recfm = recfmField.getText().trim();
state.lrecl = lreclField.getText().trim();
state.blksize = blksizeField.getText().trim();
state.space = spaceField.getText().trim();
state.options = optionsField.getText().trim();
lastTransferState = state;
}
public JComboBox<FTConfig.HostType> getHostTypeCombo() { return hostTypeCombo; }
public JRadioButton getSendRadio() { return sendRadio; }
public JRadioButton getReceiveRadio() { return receiveRadio; }
public JTextField getLocalFileField() { return localFileField; }
public JTextField getHostFileField() { return hostFileField; }
public JRadioButton getAsciiRadio() { return asciiRadio; }
public JRadioButton getBinaryRadio() { return binaryRadio; }
public JComboBox<Integer> getMtuCombo() { return mtuCombo; }
public JCheckBox getCrCheck() { return crCheck; }
public JCheckBox getRemapCheck() { return remapCheck; }
public JCheckBox getAppendCheck() { return appendCheck; }
public JCheckBox getOverwriteCheck() { return overwriteCheck; }
public JTextField getRecfmField() { return recfmField; }
public JTextField getLreclField() { return lreclField; }
public JTextField getBlksizeField() { return blksizeField; }
public JTextField getSpaceField() { return spaceField; }
public JTextField getOptionsField() { return optionsField; }
private void applyTheme(Container container) {
Color fg = new Color(200, 200, 200);
Color bg = new Color(40, 40, 40);
for (Component c : container.getComponents()) {
c.setForeground(fg);
if (c instanceof JTextField || c instanceof JComboBox) {
c.setBackground(bg);
if (c instanceof JTextField) {
((JTextField) c).setCaretColor(fg);
((JTextField) c).setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(60, 60, 60)),
new EmptyBorder(2, 5, 2, 5)));
}
} else if (c instanceof JButton) {
c.setBackground(new Color(50, 50, 50));
c.setFocusable(false);
} else if (c instanceof JRadioButton || c instanceof JCheckBox) {
((JComponent) c).setOpaque(false);
c.setFocusable(false);
}
if (c instanceof Container) {
applyTheme((Container) c);
}
}
}
}
@@ -0,0 +1,88 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.graphics.DefaultPixelBuffer;
import haus.nightmare.lib3270j.graphics.PixelBuffer;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.awt.image.Raster;
import java.awt.image.SinglePixelPackedSampleModel;
import java.awt.image.WritableRaster;
/**
* Desktop Swing/AWT bridge for converting between lib3270j PixelBuffer and java.awt BufferedImage.
*/
public final class AwtPixelBufferBridge {
private AwtPixelBufferBridge() {}
/**
* Converts a PixelBuffer to a BufferedImage.
* Uses TYPE_INT_ARGB with backed raster for optimal Java2D hardware acceleration.
*/
public static BufferedImage toBufferedImage(PixelBuffer buffer) {
if (buffer == null) return null;
int w = buffer.getWidth();
int h = buffer.getHeight();
if (w <= 0 || h <= 0) {
return new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
}
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
copyIntoBufferedImage(buffer, img);
return img;
}
/**
* Copies pixels from a PixelBuffer directly into an existing BufferedImage.
*/
public static void copyIntoBufferedImage(PixelBuffer buffer, BufferedImage target) {
if (buffer == null || target == null) return;
int w = Math.min(buffer.getWidth(), target.getWidth());
int h = Math.min(buffer.getHeight(), target.getHeight());
if (w <= 0 || h <= 0) return;
int[] srcPixels = buffer.getPixels();
if (srcPixels == null) return;
if (target.getType() == BufferedImage.TYPE_INT_ARGB || target.getType() == BufferedImage.TYPE_INT_RGB) {
if (target.getRaster().getDataBuffer() instanceof DataBufferInt) {
int[] dstPixels = ((DataBufferInt) target.getRaster().getDataBuffer()).getData();
int srcW = buffer.getWidth();
int dstW = target.getWidth();
if (srcW == dstW && srcW == w && srcPixels.length >= w * h && dstPixels.length >= w * h) {
System.arraycopy(srcPixels, 0, dstPixels, 0, w * h);
return;
}
for (int y = 0; y < h; y++) {
System.arraycopy(srcPixels, y * srcW, dstPixels, y * dstW, w);
}
return;
}
}
// Fallback for custom or incompatible image formats
target.setRGB(0, 0, w, h, srcPixels, 0, buffer.getWidth());
}
/**
* Wraps or converts a java.awt BufferedImage into a PixelBuffer.
*/
public static PixelBuffer toPixelBuffer(BufferedImage img) {
if (img == null) return null;
int w = img.getWidth();
int h = img.getHeight();
int[] pixels;
if ((img.getType() == BufferedImage.TYPE_INT_ARGB || img.getType() == BufferedImage.TYPE_INT_RGB)
&& img.getRaster().getDataBuffer() instanceof DataBufferInt) {
pixels = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
} else {
pixels = new int[w * h];
img.getRGB(0, 0, w, h, pixels, 0, w);
}
return new DefaultPixelBuffer(w, h, pixels);
}
}
@@ -0,0 +1,409 @@
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 JLabel dynamicDimLabel;
private JPanel dynamicDimPanel;
private JSpinner dynamicRowsSpinner;
private JSpinner dynamicColsSpinner;
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 JCheckBox keepAliveCheckBox;
private JCheckBox autoReconnectCheckBox;
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));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 6, 5, 6);
gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
// Host
gbc.gridx = 0;
gbc.gridy = 0;
JLabel hostLabel = new JLabel("Host:");
hostLabel.setFont(labelFont);
mainPanel.add(hostLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
hostField = createField(20);
mainPanel.add(hostField, gbc);
// Port
gbc.gridx = 0;
gbc.gridy = 1;
gbc.weightx = 0;
JLabel portLabel = new JLabel("Port:");
portLabel.setFont(labelFont);
mainPanel.add(portLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
portField = createField(6);
portField.setText("23");
mainPanel.add(portField, gbc);
// Model
gbc.gridx = 0;
gbc.gridy = 2;
gbc.weightx = 0;
JLabel modelLabel = new JLabel("Model:");
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.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
ThemeManager.styleComboBox(modelCombo);
mainPanel.add(modelCombo, gbc);
// Dynamic Dimensions (shown when IBM-DYNAMIC is selected)
gbc.gridx = 0;
gbc.gridy = 3;
gbc.weightx = 0;
dynamicDimLabel = new JLabel("Screen Size:");
dynamicDimLabel.setFont(labelFont);
mainPanel.add(dynamicDimLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
dynamicDimPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
dynamicDimPanel.setOpaque(false);
JLabel rowsLabel = new JLabel("Rows:");
rowsLabel.setFont(labelFont);
dynamicRowsSpinner = new JSpinner(new SpinnerNumberModel(haus.nightmare.j3270.config.Settings.getDynamicRows(), 24, 255, 1));
ThemeManager.styleSpinner(dynamicRowsSpinner);
JLabel colsLabel = new JLabel("Cols:");
colsLabel.setFont(labelFont);
dynamicColsSpinner = new JSpinner(new SpinnerNumberModel(haus.nightmare.j3270.config.Settings.getDynamicCols(), 80, 255, 1));
ThemeManager.styleSpinner(dynamicColsSpinner);
dynamicDimPanel.add(rowsLabel);
dynamicDimPanel.add(dynamicRowsSpinner);
dynamicDimPanel.add(colsLabel);
dynamicDimPanel.add(dynamicColsSpinner);
mainPanel.add(dynamicDimPanel, gbc);
Runnable updateDynamicVisibility = () -> {
TerminalModel m = (TerminalModel) modelCombo.getSelectedItem();
boolean isDyn = (m != null && m.isDynamic());
dynamicDimLabel.setVisible(isDyn);
dynamicDimPanel.setVisible(isDyn);
pack();
};
modelCombo.addActionListener(e -> updateDynamicVisibility.run());
updateDynamicVisibility.run();
// LU Name
gbc.gridx = 0;
gbc.gridy = 4;
gbc.weightx = 0;
JLabel luLabel = new JLabel("LU Name:");
luLabel.setFont(labelFont);
mainPanel.add(luLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
luField = createField(12);
mainPanel.add(luField, gbc);
// Graphics Mode
gbc.gridx = 0;
gbc.gridy = 5;
gbc.weightx = 0;
JLabel graphicsLabel = new JLabel("Graphics:");
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.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
ThemeManager.styleComboBox(graphicsCombo);
mainPanel.add(graphicsCombo, gbc);
// Code Page
gbc.gridx = 0;
gbc.gridy = 6;
gbc.weightx = 0;
JLabel cpLabel = new JLabel("Code Page:");
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 Latin-5)",
"1155 - Turkey (Turkish Latin-5 Euro \u20AC)",
"905 - Turkey (Turkish Latin-3)",
"420 - Arabic Bilingual",
"424 - Hebrew (with Lowercase)",
"803 - Hebrew Old / Standard",
"838 - Thai",
"1160 - Thai (Euro \u20AC)",
"1025 - Cyrillic Multilingual",
"1123 - Cyrillic Ukraine",
"1154 - Cyrillic Multilingual (Euro \u20AC)",
"880 - Cyrillic Russian",
"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",
"1388 - Simplified Chinese Extended Mixed DBCS",
"937 - Traditional Chinese Mixed DBCS",
"1371 - Traditional Chinese Extended 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.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
ThemeManager.styleComboBox(codePageCombo);
mainPanel.add(codePageCombo, gbc);
// TLS / SSL Checkbox
gbc.gridx = 1;
gbc.gridy = 7;
gbc.weightx = 1.0;
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
ThemeManager.styleCheckBox(tlsCheckBox);
tlsCheckBox.setFont(labelFont);
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 = 8;
verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
ThemeManager.styleCheckBox(verifyCertCheckBox);
verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
verifyCertCheckBox.setSelected(true);
verifyCertCheckBox.setEnabled(false);
mainPanel.add(verifyCertCheckBox, gbc);
// TN3270E Checkbox
gbc.gridx = 1;
gbc.gridy = 9;
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
ThemeManager.styleCheckBox(tn3270eCheckBox);
tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
mainPanel.add(tn3270eCheckBox, gbc);
// Keep-Alive Checkbox
gbc.gridx = 1;
gbc.gridy = 10;
keepAliveCheckBox = new JCheckBox("Enable Keep-Alive Heartbeat (NOP)");
ThemeManager.styleCheckBox(keepAliveCheckBox);
keepAliveCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
keepAliveCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectKeepAlive());
mainPanel.add(keepAliveCheckBox, gbc);
// Auto-Reconnect Checkbox
gbc.gridx = 1;
gbc.gridy = 11;
autoReconnectCheckBox = new JCheckBox("Auto-Reconnect on Disconnect");
ThemeManager.styleCheckBox(autoReconnectCheckBox);
autoReconnectCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
autoReconnectCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
mainPanel.add(autoReconnectCheckBox, gbc);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
JButton connectBtn = new JButton("Connect");
ThemeManager.styleButton(connectBtn, ThemeManager.ButtonVariant.PRIMARY);
connectBtn.setFont(labelFont);
connectBtn.addActionListener(e -> onConnect());
JButton cancelBtn = new JButton("Cancel");
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
cancelBtn.setFont(labelFont);
cancelBtn.addActionListener(e -> {
confirmed = false;
dispose();
});
buttonPanel.add(cancelBtn);
buttonPanel.add(connectBtn);
gbc.gridx = 0;
gbc.gridy = 12;
gbc.gridwidth = 2;
mainPanel.add(buttonPanel, gbc);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
// Enter key triggers connect
getRootPane().setDefaultButton(connectBtn);
}
private JTextField createField(int cols) {
JTextField field = new JTextField(cols);
field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
ThemeManager.styleTextField(field);
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;
}
TerminalModel selectedModel = (TerminalModel) modelCombo.getSelectedItem();
result = new ConnectionConfig(host, port, selectedModel);
if (selectedModel != null && selectedModel.isDynamic()) {
int dRows = (Integer) dynamicRowsSpinner.getValue();
int dCols = (Integer) dynamicColsSpinner.getValue();
result.setDynamicDimensions(dRows, dCols);
haus.nightmare.j3270.config.Settings.setDynamicRows(dRows);
haus.nightmare.j3270.config.Settings.setDynamicCols(dCols);
}
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());
result.setAutoSysUnlock(haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
boolean ka = keepAliveCheckBox.isSelected();
result.setKeepAliveEnabled(ka);
result.setKeepAliveIntervalSeconds(haus.nightmare.j3270.config.Settings.getAutoConnectKeepAliveInterval());
haus.nightmare.j3270.config.Settings.setAutoConnectKeepAlive(ka);
boolean ar = autoReconnectCheckBox.isSelected();
result.setAutoReconnect(ar);
result.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
haus.nightmare.j3270.config.Settings.setAutoConnectAutoReconnect(ar);
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,216 @@
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;
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));
// Top info bar
JPanel topPanel = new JPanel(new BorderLayout());
topPanel.setOpaque(false);
countLabel = new JLabel("0 fields detected on screen");
countLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
topPanel.add(countLabel, BorderLayout.WEST);
JButton refreshBtn = new JButton("Refresh");
ThemeManager.styleButton(refreshBtn, ThemeManager.ButtonVariant.DEFAULT);
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);
ThemeManager.styleTable(table);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20);
JTableHeader header = table.getTableHeader();
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);
ThemeManager.styleScrollPane(scrollPane);
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");
ThemeManager.styleButton(jumpBtn, ThemeManager.ButtonVariant.PRIMARY);
jumpBtn.addActionListener(e -> {
int row = table.getSelectedRow();
if (row >= 0) {
selectFieldOnTerminal(row);
}
});
JButton closeBtn = new JButton("Close");
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
closeBtn.addActionListener(e -> dispose());
bottomPanel.add(jumpBtn);
bottomPanel.add(closeBtn);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
}
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,218 @@
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));
// 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;
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.setFont(labelFont);
formPanel.add(findLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
searchField = new JTextField(20);
searchField.setText(lastSearchText);
searchField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
ThemeManager.styleTextField(searchField);
formPanel.add(searchField, gbc);
// Options
gbc.gridx = 1;
gbc.gridy = 1;
matchCaseCheck = new JCheckBox("Match case", lastMatchCase);
ThemeManager.styleCheckBox(matchCaseCheck);
matchCaseCheck.setFont(labelFont);
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.setFont(labelFont);
forwardRadio = new JRadioButton("Down", lastForward);
backwardRadio = new JRadioButton("Up", !lastForward);
ThemeManager.styleRadioButton(forwardRadio);
ThemeManager.styleRadioButton(backwardRadio);
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(ThemeManager.getOiaFgAlert());
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");
ThemeManager.styleButton(findNextBtn, ThemeManager.ButtonVariant.PRIMARY);
findNextBtn.setFont(labelFont);
findNextBtn.addActionListener(e -> findNext());
JButton closeBtn = new JButton("Close");
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
closeBtn.setFont(labelFont);
closeBtn.addActionListener(e -> dispose());
buttonPanel.add(findNextBtn);
buttonPanel.add(closeBtn);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
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,269 @@
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));
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.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);
ThemeManager.styleComboBox(hostTypeCombo);
topPanel.add(hostTypeCombo, gbc);
gbc.gridx = 2;
JLabel queryLabel = new JLabel("Query Pattern / Text:");
queryLabel.setFont(font);
topPanel.add(queryLabel, gbc);
gbc.gridx = 3;
gbc.weightx = 1.0;
queryField = new JTextField(initialQuery != null ? initialQuery : "", 16);
queryField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
ThemeManager.styleTextField(queryField);
topPanel.add(queryField, gbc);
gbc.gridx = 4;
gbc.weightx = 0;
JButton parseBtn = new JButton("Query / Parse");
ThemeManager.styleButton(parseBtn, ThemeManager.ButtonVariant.DEFAULT);
parseBtn.setFont(font);
parseBtn.addActionListener(e -> runQuery());
topPanel.add(parseBtn, gbc);
mainPanel.add(topPanel, BorderLayout.NORTH);
// Table
tableModel = new DefaultTableModel();
table = new JTable(tableModel);
ThemeManager.styleTable(table);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20);
JTableHeader header = table.getTableHeader();
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);
ThemeManager.styleScrollPane(scrollPane);
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(ThemeManager.getFgMuted());
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");
ThemeManager.styleButton(pasteScreenBtn, ThemeManager.ButtonVariant.DEFAULT);
pasteScreenBtn.setFont(font);
pasteScreenBtn.addActionListener(e -> parseCurrentScreen());
JButton selectBtn = new JButton("Select Dataset");
ThemeManager.styleButton(selectBtn, ThemeManager.ButtonVariant.PRIMARY);
selectBtn.setFont(font);
selectBtn.addActionListener(e -> onConfirmSelection());
JButton cancelBtn = new JButton("Cancel");
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
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);
ThemeManager.applyThemeToWindow(this);
// 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,427 @@
package haus.nightmare.j3270.ui;
import java.io.*;
import java.util.*;
import java.util.logging.Logger;
/**
* Keyboard mapping profile (.kmp) file importer and keybinding utility.
* Parses keyboard mapping profile files, translates key scan codes and mnemonics
* to j3270 action handlers.
*/
public class KeyBindings {
private static final Logger log = Logger.getLogger(KeyBindings.class.getName());
// IBM scan code to standard Java KeyStroke key name mapping
private static final Map<Integer, String> SCAN_CODE_MAP = new HashMap<>();
// Mnemonic to j3270 action mapping
private static final Map<String, String> MNEMONIC_MAP = new HashMap<>();
static {
// Alphanumeric and symbol keys
SCAN_CODE_MAP.put(1, "BACK_QUOTE");
SCAN_CODE_MAP.put(2, "1");
SCAN_CODE_MAP.put(3, "2");
SCAN_CODE_MAP.put(4, "3");
SCAN_CODE_MAP.put(5, "4");
SCAN_CODE_MAP.put(6, "5");
SCAN_CODE_MAP.put(7, "6");
SCAN_CODE_MAP.put(8, "7");
SCAN_CODE_MAP.put(9, "8");
SCAN_CODE_MAP.put(10, "9");
SCAN_CODE_MAP.put(11, "0");
SCAN_CODE_MAP.put(12, "MINUS");
SCAN_CODE_MAP.put(13, "EQUALS");
SCAN_CODE_MAP.put(14, "BACK_SPACE");
SCAN_CODE_MAP.put(15, "TAB");
SCAN_CODE_MAP.put(16, "Q");
SCAN_CODE_MAP.put(17, "W");
SCAN_CODE_MAP.put(18, "E");
SCAN_CODE_MAP.put(19, "R");
SCAN_CODE_MAP.put(20, "T");
SCAN_CODE_MAP.put(21, "Y");
SCAN_CODE_MAP.put(22, "U");
SCAN_CODE_MAP.put(23, "I");
SCAN_CODE_MAP.put(24, "O");
SCAN_CODE_MAP.put(25, "P");
SCAN_CODE_MAP.put(26, "OPEN_BRACKET");
SCAN_CODE_MAP.put(27, "CLOSE_BRACKET");
SCAN_CODE_MAP.put(28, "BACK_SLASH");
SCAN_CODE_MAP.put(29, "CAPS_LOCK");
SCAN_CODE_MAP.put(30, "A");
SCAN_CODE_MAP.put(31, "S");
SCAN_CODE_MAP.put(32, "D");
SCAN_CODE_MAP.put(33, "F");
SCAN_CODE_MAP.put(34, "G");
SCAN_CODE_MAP.put(35, "H");
SCAN_CODE_MAP.put(36, "J");
SCAN_CODE_MAP.put(37, "K");
SCAN_CODE_MAP.put(38, "L");
SCAN_CODE_MAP.put(39, "SEMICOLON");
SCAN_CODE_MAP.put(40, "QUOTE");
SCAN_CODE_MAP.put(41, "BACK_QUOTE");
SCAN_CODE_MAP.put(42, "ENTER");
SCAN_CODE_MAP.put(43, "ENTER");
SCAN_CODE_MAP.put(45, "Z");
SCAN_CODE_MAP.put(46, "X");
SCAN_CODE_MAP.put(47, "C");
SCAN_CODE_MAP.put(48, "V");
SCAN_CODE_MAP.put(49, "B");
SCAN_CODE_MAP.put(50, "N");
SCAN_CODE_MAP.put(51, "M");
SCAN_CODE_MAP.put(52, "COMMA");
SCAN_CODE_MAP.put(53, "PERIOD");
SCAN_CODE_MAP.put(54, "SLASH");
SCAN_CODE_MAP.put(57, "SPACE");
// Editing & Navigation keys
SCAN_CODE_MAP.put(75, "INSERT");
SCAN_CODE_MAP.put(76, "DELETE");
SCAN_CODE_MAP.put(79, "LEFT");
SCAN_CODE_MAP.put(80, "HOME");
SCAN_CODE_MAP.put(81, "END");
SCAN_CODE_MAP.put(83, "UP");
SCAN_CODE_MAP.put(84, "DOWN");
SCAN_CODE_MAP.put(85, "PAGE_UP");
SCAN_CODE_MAP.put(86, "PAGE_DOWN");
SCAN_CODE_MAP.put(89, "RIGHT");
// Numeric Keypad
SCAN_CODE_MAP.put(90, "NUM_LOCK");
SCAN_CODE_MAP.put(91, "NUMPAD7");
SCAN_CODE_MAP.put(92, "NUMPAD4");
SCAN_CODE_MAP.put(93, "NUMPAD1");
SCAN_CODE_MAP.put(95, "DIVIDE");
SCAN_CODE_MAP.put(96, "NUMPAD8");
SCAN_CODE_MAP.put(97, "NUMPAD5");
SCAN_CODE_MAP.put(98, "NUMPAD2");
SCAN_CODE_MAP.put(99, "NUMPAD0");
SCAN_CODE_MAP.put(100, "MULTIPLY");
SCAN_CODE_MAP.put(101, "NUMPAD9");
SCAN_CODE_MAP.put(102, "NUMPAD6");
SCAN_CODE_MAP.put(103, "NUMPAD3");
SCAN_CODE_MAP.put(104, "DECIMAL");
SCAN_CODE_MAP.put(105, "SUBTRACT");
SCAN_CODE_MAP.put(106, "ADD");
SCAN_CODE_MAP.put(108, "ENTER");
// Function keys & Escape
SCAN_CODE_MAP.put(110, "ESCAPE");
SCAN_CODE_MAP.put(112, "F1");
SCAN_CODE_MAP.put(113, "F2");
SCAN_CODE_MAP.put(114, "F3");
SCAN_CODE_MAP.put(115, "F4");
SCAN_CODE_MAP.put(116, "F5");
SCAN_CODE_MAP.put(117, "F6");
SCAN_CODE_MAP.put(118, "F7");
SCAN_CODE_MAP.put(119, "F8");
SCAN_CODE_MAP.put(120, "F9");
SCAN_CODE_MAP.put(121, "F10");
SCAN_CODE_MAP.put(122, "F11");
SCAN_CODE_MAP.put(123, "F12");
// Mnemonics mapping
MNEMONIC_MAP.put("enter", "ENTER");
MNEMONIC_MAP.put("enterreset", "ENTER");
MNEMONIC_MAP.put("newline", "NEWLINE");
MNEMONIC_MAP.put("tab", "TAB");
MNEMONIC_MAP.put("backtab", "shift TAB");
MNEMONIC_MAP.put("reset", "ESCAPE");
MNEMONIC_MAP.put("clear", "CLEAR");
MNEMONIC_MAP.put("eraseeof", "END");
MNEMONIC_MAP.put("eof", "END");
MNEMONIC_MAP.put("erasefld", "ERASE_INPUT");
MNEMONIC_MAP.put("erinp", "ERASE_INPUT");
MNEMONIC_MAP.put("eraseinput", "ERASE_INPUT");
MNEMONIC_MAP.put("dup", "DUP");
MNEMONIC_MAP.put("fieldmark", "FIELD_MARK");
MNEMONIC_MAP.put("fldmark", "FIELD_MARK");
MNEMONIC_MAP.put("fldext", "NEWLINE");
MNEMONIC_MAP.put("field-exit", "NEWLINE");
MNEMONIC_MAP.put("field+", "TAB");
MNEMONIC_MAP.put("field-", "TAB");
MNEMONIC_MAP.put("attn", "ATTN");
MNEMONIC_MAP.put("sysreq", "SYSREQ");
MNEMONIC_MAP.put("cursel", "CURSEL");
MNEMONIC_MAP.put("copy", "COPY");
MNEMONIC_MAP.put("paste", "PASTE");
MNEMONIC_MAP.put("selectall", "SELECTALL");
MNEMONIC_MAP.put("insert", "INSERT");
MNEMONIC_MAP.put("delete", "DELETE");
MNEMONIC_MAP.put("backspace", "BACK_SPACE");
MNEMONIC_MAP.put("home", "HOME");
MNEMONIC_MAP.put("end", "END");
MNEMONIC_MAP.put("up", "UP");
MNEMONIC_MAP.put("down", "DOWN");
MNEMONIC_MAP.put("left", "LEFT");
MNEMONIC_MAP.put("right", "RIGHT");
MNEMONIC_MAP.put("pageup", "PAGE_UP");
MNEMONIC_MAP.put("pagedn", "PAGE_DOWN");
MNEMONIC_MAP.put("pagedown", "PAGE_DOWN");
MNEMONIC_MAP.put("docmode", "DOCMODE");
MNEMONIC_MAP.put("wordwrap", "WORDWRAP");
MNEMONIC_MAP.put("apl", "APL");
MNEMONIC_MAP.put("rule", "CROSSHAIR_RULER");
MNEMONIC_MAP.put("statusbar", "STATUS_BAR");
for (int i = 1; i <= 24; i++) {
MNEMONIC_MAP.put("pf" + i, "PF" + i);
}
for (int i = 1; i <= 3; i++) {
MNEMONIC_MAP.put("pa" + i, "PA" + i);
}
// Shifted PF keys 1-12 = PF13-PF24
for (int i = 1; i <= 12; i++) {
MNEMONIC_MAP.put("spf" + i, "PF" + (i + 12));
}
}
/**
* Parse a .kmp file from a File object.
*
* @param file the .kmp file to parse
* @return Map of j3270 action names to key stroke bindings
* @throws IOException on I/O error
*/
public static Map<String, String> parseKmp(File file) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
return parseKmp(reader);
}
}
/**
* Parse a .kmp file from an InputStream.
*
* @param in the InputStream to parse
* @return Map of j3270 action names to key stroke bindings
* @throws IOException on I/O error
*/
public static Map<String, String> parseKmp(InputStream in) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
return parseKmp(reader);
}
}
/**
* Parse a .kmp file from a Reader.
* Supports both INI-style KEY<n>=action formats and bind statements.
*
* @param reader the reader to parse from
* @return Map of j3270 action names to key stroke bindings
* @throws IOException on I/O error
*/
public static Map<String, String> parseKmp(Reader reader) throws IOException {
BufferedReader br = (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader);
Map<String, List<String>> actionToBindings = new LinkedHashMap<>();
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
// Strip comments
int commentIdx = line.indexOf(';');
if (commentIdx >= 0) {
line = line.substring(0, commentIdx).trim();
}
commentIdx = line.indexOf('#');
if (commentIdx >= 0) {
line = line.substring(0, commentIdx).trim();
}
if (line.isEmpty() || line.startsWith("[")) {
continue; // Skip section headers and blank lines
}
// Syntax 1: bind [mnemonic] KeyCombination OR bind action KeyCombination
if (line.toLowerCase().startsWith("bind ")) {
parseBindStatement(line.substring(5).trim(), actionToBindings);
continue;
}
// Syntax 2: KEY<n>=action OR Modifier-KEY<n>=action OR KeyStroke=[mnemonic]
int eq = line.indexOf('=');
if (eq > 0) {
String left = line.substring(0, eq).trim();
String right = line.substring(eq + 1).trim();
parseKeyAssignment(left, right, actionToBindings);
}
}
// Format into combined comma-separated bindings map
Map<String, String> result = new LinkedHashMap<>();
for (Map.Entry<String, List<String>> entry : actionToBindings.entrySet()) {
String action = entry.getKey();
List<String> bindings = entry.getValue();
if (!bindings.isEmpty()) {
result.put(action, String.join(", ", bindings));
}
}
return result;
}
private static void parseBindStatement(String stmt, Map<String, List<String>> actionToBindings) {
String[] parts = stmt.split("\\s+", 2);
if (parts.length < 2) return;
String target = parts[0].trim();
String keyStrokeStr = parts[1].trim();
String action = resolveMnemonic(target);
if (action == null) {
// Check if parts[1] was the mnemonic instead (e.g. "bind KeyCombination [mnemonic]")
String altAction = resolveMnemonic(keyStrokeStr);
if (altAction != null) {
action = altAction;
keyStrokeStr = target;
}
}
if (action != null) {
String normKeyStroke = normalizeKeyStroke(keyStrokeStr);
if (normKeyStroke != null && !normKeyStroke.isEmpty()) {
addBinding(actionToBindings, action, normKeyStroke);
}
}
}
private static void parseKeyAssignment(String left, String right, Map<String, List<String>> actionToBindings) {
// Case A: left is KEY<n> or Modifier-KEY<n>, right is mnemonic
String action = resolveMnemonic(right);
String keyStroke = null;
if (action != null) {
keyStroke = parseScanCodeEntry(left);
} else {
// Case B: left is mnemonic, right is KeyStroke
action = resolveMnemonic(left);
if (action != null) {
keyStroke = normalizeKeyStroke(right);
} else {
// Case C: left is KeyStroke, right is mnemonic
action = resolveMnemonic(right);
if (action != null) {
keyStroke = normalizeKeyStroke(left);
}
}
}
if (action != null && keyStroke != null && !keyStroke.isEmpty()) {
addBinding(actionToBindings, action, keyStroke);
}
}
/**
* Parse scan code entry like "KEY43", "S-KEY112", "C-KEY43", "CS-KEY85", etc.
*/
public static String parseScanCodeEntry(String entry) {
if (entry == null || entry.isEmpty()) return null;
String upper = entry.toUpperCase().trim();
int keyPos = upper.indexOf("KEY");
if (keyPos < 0) {
return normalizeKeyStroke(entry);
}
String prefix = upper.substring(0, keyPos);
if (prefix.endsWith("-")) {
prefix = prefix.substring(0, prefix.length() - 1);
}
String numPart = upper.substring(keyPos + 3);
int scanCode;
try {
scanCode = Integer.parseInt(numPart.trim());
} catch (NumberFormatException e) {
return null;
}
String baseKey = SCAN_CODE_MAP.get(scanCode);
if (baseKey == null) {
return null;
}
StringBuilder sb = new StringBuilder();
if (prefix.contains("C")) sb.append("ctrl ");
if (prefix.contains("A") || prefix.contains("2")) sb.append("alt ");
if (prefix.contains("S")) sb.append("shift ");
if (prefix.contains("M")) sb.append("meta ");
sb.append(baseKey);
return sb.toString().trim();
}
/**
* Normalize key stroke expression (e.g. "Ctrl+Shift+C" -> "ctrl shift C").
*/
public static String normalizeKeyStroke(String stroke) {
if (stroke == null || stroke.trim().isEmpty()) return null;
String s = stroke.trim();
// Check if it is an IBM scan code
if (s.toUpperCase().contains("KEY")) {
String fromScan = parseScanCodeEntry(s);
if (fromScan != null) return fromScan;
}
// Replace '+' with space
s = s.replace("+", " ");
String[] parts = s.split("\\s+");
StringBuilder mods = new StringBuilder();
String mainKey = null;
for (String p : parts) {
String lp = p.toLowerCase();
if ("ctrl".equals(lp) || "control".equals(lp)) {
mods.append("ctrl ");
} else if ("shift".equals(lp)) {
mods.append("shift ");
} else if ("alt".equals(lp)) {
mods.append("alt ");
} else if ("meta".equals(lp) || "cmd".equals(lp) || "command".equals(lp)) {
mods.append("meta ");
} else {
mainKey = p.toUpperCase();
}
}
if (mainKey == null) return null;
return (mods.toString() + mainKey).trim();
}
/**
* Resolve a mnemonic token (e.g. "[enter]", "enter", "[pf12]", "eraseeof")
* into a j3270 action name.
*/
public static String resolveMnemonic(String token) {
if (token == null) return null;
String clean = token.trim();
if (clean.startsWith("[") && clean.endsWith("]")) {
clean = clean.substring(1, clean.length() - 1);
}
clean = clean.toLowerCase();
String direct = MNEMONIC_MAP.get(clean);
if (direct != null) {
return direct;
}
// Case-insensitive match on existing action names
String upper = clean.toUpperCase();
if (MNEMONIC_MAP.containsValue(upper)) {
return upper;
}
return null;
}
private static void addBinding(Map<String, List<String>> map, String action, String keyStroke) {
List<String> list = map.computeIfAbsent(action, k -> new ArrayList<>());
if (!list.contains(keyStroke)) {
list.add(keyStroke);
}
}
}
@@ -0,0 +1,452 @@
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;
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));
// Top: Configuration Panel
JPanel topPanel = new JPanel(new GridBagLayout());
topPanel.setOpaque(false);
topPanel.setBorder(ThemeManager.createTitledBorder("Printer Session Configuration"));
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.setFont(labelFont);
topPanel.add(hLbl, gbc);
gbc.gridx = 1; gbc.weightx = 1.0;
hostField = createField(config.getHost() != null ? config.getHost() : "localhost", 14);
topPanel.add(hostField, gbc);
gbc.gridx = 2; gbc.weightx = 0;
JLabel pLbl = new JLabel("Port:");
pLbl.setFont(labelFont);
topPanel.add(pLbl, gbc);
gbc.gridx = 3; gbc.weightx = 0.5;
portField = createField(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.setFont(labelFont);
topPanel.add(pluLbl, gbc);
gbc.gridx = 1; gbc.weightx = 1.0;
printerLuField = createField("", 10);
topPanel.add(printerLuField, gbc);
gbc.gridx = 2; gbc.weightx = 0;
JLabel assocLbl = new JLabel("Assoc LU:");
assocLbl.setFont(labelFont);
topPanel.add(assocLbl, gbc);
gbc.gridx = 3; gbc.weightx = 0.5;
displayLuField = createField("", 10);
topPanel.add(displayLuField, gbc);
// CodePage & TLS
gbc.gridx = 0; gbc.gridy = 2; gbc.weightx = 0;
JLabel cpLbl = new JLabel("CodePage:");
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"});
ThemeManager.styleComboBox(codePageCombo);
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());
ThemeManager.styleCheckBox(tlsCheck);
verifyCertCheck = new JCheckBox("Verify Cert", config.isTlsVerifyCert());
ThemeManager.styleCheckBox(verifyCertCheck);
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.setFont(labelFont);
topPanel.add(destLbl, gbc);
gbc.gridx = 1;
destinationCombo = new JComboBox<>(PrinterConfig.DestinationType.values());
destinationCombo.setSelectedItem(PrinterConfig.DestinationType.MEMORY);
ThemeManager.styleComboBox(destinationCombo);
topPanel.add(destinationCombo, gbc);
gbc.gridx = 2;
JLabel tgtLbl = new JLabel("Target Path:");
tgtLbl.setFont(labelFont);
topPanel.add(tgtLbl, gbc);
gbc.gridx = 3;
targetField = createField("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");
ThemeManager.styleButton(connectBtn, ThemeManager.ButtonVariant.PRIMARY);
connectBtn.addActionListener(e -> startPrinterSession());
disconnectBtn = new JButton("Stop Session");
ThemeManager.styleButton(disconnectBtn, ThemeManager.ButtonVariant.DANGER);
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(ThemeManager.createTitledBorder("Printer Spool & Status"));
// 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(ThemeManager.getFgMuted());
statusLabel.setFont(labelFont);
sessionTypeLabel = new JLabel("Session: -");
sessionTypeLabel.setFont(labelFont);
pagesLabel = new JLabel("Pages: 0");
pagesLabel.setFont(labelFont);
bytesLabel = new JLabel("Bytes: 0");
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.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
spoolArea.setEditable(false);
ThemeManager.styleTextArea(spoolArea);
JScrollPane spoolScroll = new JScrollPane(spoolArea);
ThemeManager.styleScrollPane(spoolScroll);
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");
ThemeManager.styleButton(clearSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
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...");
ThemeManager.styleButton(saveSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
saveSpoolBtn.addActionListener(e -> saveSpool());
JButton printSpoolBtn = new JButton("Print Spool...");
ThemeManager.styleButton(printSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
printSpoolBtn.addActionListener(e -> printSpool());
JButton closeBtn = new JButton("Close");
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
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);
ThemeManager.applyThemeToWindow(this);
}
private JTextField createField(String text, int cols) {
JTextField tf = new JTextField(text, cols);
tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
ThemeManager.styleTextField(tf);
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,255 @@
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: #7890f0; }\n");
writer.write(".c-red { color: #ff0000; }\n");
writer.write(".c-pink { color: #ff00ff; }\n");
writer.write(".c-green { color: #00ff00; }\n");
writer.write(".c-turq { color: #00ffff; }\n");
writer.write(".c-yellow { color: #ffff00; }\n");
writer.write(".c-white { color: #ffffff; }\n");
writer.write(".c-black { color: #000000; }\n");
writer.write(".c-deepblue { color: #000080; }\n");
writer.write(".c-orange { color: #ffa200; }\n");
writer.write(".c-purple { color: #800080; }\n");
writer.write(".c-palegreen { color: #008000; }\n");
writer.write(".c-paleturq { color: #008080; }\n");
writer.write(".c-mustard { color: #a0a000; }\n");
writer.write(".c-grey { color: #c0c0c0; }\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-deepblue";
case 10: return "c-orange";
case 11: return "c-purple";
case 12: return "c-palegreen";
case 13: return "c-paleturq";
case 14: return "c-mustard";
case 15: return "c-grey";
}
}
if (faIsProtected(currentFA & 0xFF)) {
return faIsHigh(currentFA & 0xFF) ? "c-white" : "c-turq";
}
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,278 @@
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));
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.setFont(labelFont);
topPanel.add(descLabel, BorderLayout.CENTER);
mainPanel.add(topPanel, BorderLayout.NORTH);
// Script area
scriptArea = new JTextArea();
scriptArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
scriptArea.setLineWrap(true);
scriptArea.setWrapStyleWord(false);
ThemeManager.styleTextArea(scriptArea);
JScrollPane scrollPane = new JScrollPane(scriptArea);
ThemeManager.styleScrollPane(scrollPane);
// Mnemonic helper buttons
JPanel tokenPanel = new JPanel(new WrapLayout(FlowLayout.LEFT, 4, 4));
tokenPanel.setOpaque(false);
tokenPanel.setBorder(ThemeManager.createTitledBorder("Insert Keystroke Token"));
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));
ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.DEFAULT);
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(ThemeManager.getFgMuted());
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...");
ThemeManager.styleButton(loadBtn, ThemeManager.ButtonVariant.DEFAULT);
loadBtn.setFont(labelFont);
loadBtn.addActionListener(e -> loadScript());
JButton saveBtn = new JButton("Save Script...");
ThemeManager.styleButton(saveBtn, ThemeManager.ButtonVariant.DEFAULT);
saveBtn.setFont(labelFont);
saveBtn.addActionListener(e -> saveScript());
JButton runBtn = new JButton("Execute");
ThemeManager.styleButton(runBtn, ThemeManager.ButtonVariant.PRIMARY);
runBtn.setFont(labelFont);
runBtn.addActionListener(e -> executeScript());
JButton closeBtn = new JButton("Close");
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
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);
ThemeManager.applyThemeToWindow(this);
}
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,12 +1,13 @@
package org.pubvm.j3270.ui; package haus.nightmare.j3270.ui;
import javax.swing.*; import javax.swing.*;
import org.pubvm.j3270.J3270App; import haus.nightmare.j3270.J3270App;
import org.pubvm.j3270.config.Settings; import haus.nightmare.j3270.config.Settings;
import java.awt.*; import java.awt.*;
import java.awt.event.*; import java.awt.event.*;
import java.io.File;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.swing.table.DefaultTableModel; import javax.swing.table.DefaultTableModel;
@@ -19,19 +20,11 @@ public class SettingsDialog extends JDialog {
private final J3270App parentApp; private final J3270App parentApp;
// Dark theme colors
private static final Color DARK_BG = new Color(43, 43, 43);
private static final Color DARK_BG_LIGHTER = new Color(55, 55, 55);
private static final Color DARK_FG = new Color(224, 224, 224);
private static final Color DARK_BORDER = new Color(70, 70, 70);
private static final Color DARK_SELECTION = new Color(75, 110, 175);
private static final Color DARK_BUTTON_BG = new Color(60, 63, 65);
private static final Color DARK_FIELD_BG = new Color(50, 50, 50);
// Appearance tab // Appearance tab
private JComboBox<UITheme> uiThemeBox;
private JComboBox<String> fontBox; private JComboBox<String> fontBox;
private JSpinner fontSizeSpinner; private JSpinner fontSizeSpinner;
private JComboBox<org.lib3270j.graphics.GraphicsMode> graphicsModeBox; private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsModeBox;
// Behavior tab // Behavior tab
private JComboBox<Settings.StartupBehavior> startupBehaviorBox; private JComboBox<Settings.StartupBehavior> startupBehaviorBox;
@@ -39,6 +32,23 @@ public class SettingsDialog extends JDialog {
private JTextField hostField; private JTextField hostField;
private JTextField portField; private JTextField portField;
private JCheckBox blockSelectCheck; private JCheckBox blockSelectCheck;
private JSpinner dynamicRowsSpinner;
private JSpinner dynamicColsSpinner;
private JCheckBox enablePasteFromExcelCheck;
private JCheckBox pasteStopAtProtectedCheck;
// Entry Assist & Modes tab
private JCheckBox docModeCheck;
private JCheckBox wordWrapCheck;
private JSpinner startColSpinner;
private JSpinner endColSpinner;
private JCheckBox bellCheck;
private JSpinner bellColSpinner;
private JTextField tabStopsField;
private JCheckBox insertOffOnAidCheck;
private JCheckBox fourColorOverrideCheck;
private JCheckBox numericFieldLockCheck;
private JCheckBox autoSkipCheck;
// Advanced tab state tracking // Advanced tab state tracking
private final Color[] tempHostColors = new Color[16]; private final Color[] tempHostColors = new Color[16];
@@ -51,22 +61,31 @@ public class SettingsDialog extends JDialog {
this.parentApp = parent; this.parentApp = parent;
initComponents(); initComponents();
setSize(550, 450); setSize(600, 520);
setLocationRelativeTo(parent); setLocationRelativeTo(parent);
} }
private void initComponents() { private void initComponents() {
JTabbedPane tabbedPane = new JTabbedPane(); JTabbedPane tabbedPane = new JTabbedPane();
ThemeManager.styleTabbedPane(tabbedPane);
tabbedPane.addTab("Appearance", createAppearancePanel()); tabbedPane.addTab("Appearance", createAppearancePanel());
tabbedPane.addTab("Behavior", createBehaviorPanel()); tabbedPane.addTab("Behavior", createBehaviorPanel());
tabbedPane.addTab("Entry Assist & Modes", createEntryAssistPanel());
tabbedPane.addTab("Advanced", createAdvancedPanel()); tabbedPane.addTab("Advanced", createAdvancedPanel());
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 8));
JButton btnExport = new JButton("Export Config..."); JButton btnExport = new JButton("Export Config...");
JButton btnOk = new JButton("OK"); ThemeManager.styleButton(btnExport, ThemeManager.ButtonVariant.DEFAULT);
JButton btnApply = new JButton("Apply"); JButton btnApply = new JButton("Apply");
ThemeManager.styleButton(btnApply, ThemeManager.ButtonVariant.DEFAULT);
JButton btnCancel = new JButton("Cancel"); JButton btnCancel = new JButton("Cancel");
ThemeManager.styleButton(btnCancel, ThemeManager.ButtonVariant.CANCEL);
JButton btnOk = new JButton("OK");
ThemeManager.styleButton(btnOk, ThemeManager.ButtonVariant.PRIMARY);
btnExport.addActionListener((ActionEvent e) -> { btnExport.addActionListener((ActionEvent e) -> {
exportConfig(); exportConfig();
@@ -96,132 +115,11 @@ public class SettingsDialog extends JDialog {
getContentPane().add(tabbedPane, BorderLayout.CENTER); getContentPane().add(tabbedPane, BorderLayout.CENTER);
getContentPane().add(buttonPanel, BorderLayout.SOUTH); getContentPane().add(buttonPanel, BorderLayout.SOUTH);
// Apply dark theme to all components for cross-platform readability ThemeManager.applyThemeToWindow(this);
applyDarkTheme(getContentPane());
applyDarkTheme(tabbedPane);
applyDarkTheme(buttonPanel);
getContentPane().setBackground(DARK_BG);
} }
// ========== Dark Theme Utility ==========
/**
* Recursively apply dark theme to a component and all its children.
* Ensures the Settings dialog is readable on Windows, Linux, and macOS.
*/
private void applyDarkTheme(Component comp) { private void applyDarkTheme(Component comp) {
if (comp instanceof JTabbedPane) { ThemeManager.applyTheme(comp);
JTabbedPane tp = (JTabbedPane) comp;
tp.setBackground(DARK_BG);
tp.setForeground(DARK_FG);
for (int i = 0; i < tp.getTabCount(); i++) {
applyDarkTheme(tp.getComponentAt(i));
}
return;
}
if (comp instanceof JTable) {
JTable table = (JTable) comp;
table.setBackground(DARK_FIELD_BG);
table.setForeground(DARK_FG);
table.setSelectionBackground(DARK_SELECTION);
table.setSelectionForeground(Color.WHITE);
table.setGridColor(DARK_BORDER);
JTableHeader header = table.getTableHeader();
if (header != null) {
header.setBackground(DARK_BG_LIGHTER);
header.setForeground(DARK_FG);
DefaultTableCellRenderer headerRenderer = new DefaultTableCellRenderer();
headerRenderer.setBackground(DARK_BG_LIGHTER);
headerRenderer.setForeground(DARK_FG);
header.setDefaultRenderer(headerRenderer);
}
return;
}
if (comp instanceof JScrollPane) {
JScrollPane sp = (JScrollPane) comp;
sp.setBackground(DARK_BG);
sp.getViewport().setBackground(DARK_FIELD_BG);
applyDarkTheme(sp.getViewport().getView());
return;
}
if (comp instanceof JButton) {
JButton btn = (JButton) comp;
btn.setBackground(DARK_BUTTON_BG);
btn.setForeground(DARK_FG);
btn.setFocusPainted(false);
btn.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(DARK_BORDER, 1),
BorderFactory.createEmptyBorder(3, 10, 3, 10)));
btn.setOpaque(true);
return;
}
if (comp instanceof JComboBox) {
JComboBox<?> cb = (JComboBox<?>) comp;
cb.setBackground(DARK_FIELD_BG);
cb.setForeground(DARK_FG);
return;
}
if (comp instanceof JSpinner) {
JSpinner sp = (JSpinner) comp;
sp.setBackground(DARK_FIELD_BG);
sp.setForeground(DARK_FG);
JComponent editor = sp.getEditor();
if (editor instanceof JSpinner.DefaultEditor) {
JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField();
tf.setBackground(DARK_FIELD_BG);
tf.setForeground(DARK_FG);
tf.setCaretColor(DARK_FG);
}
return;
}
if (comp instanceof JTextField) {
JTextField tf = (JTextField) comp;
tf.setBackground(DARK_FIELD_BG);
tf.setForeground(DARK_FG);
tf.setCaretColor(DARK_FG);
return;
}
if (comp instanceof JCheckBox) {
JCheckBox cb = (JCheckBox) comp;
cb.setBackground(DARK_BG);
cb.setForeground(DARK_FG);
return;
}
if (comp instanceof JLabel) {
comp.setForeground(DARK_FG);
return;
}
// Generic panel / container
// Skip color swatch panels their background IS the color
if (comp instanceof JPanel && "colorSwatch".equals(comp.getName())) {
return;
}
comp.setBackground(DARK_BG);
comp.setForeground(DARK_FG);
if (comp instanceof JPanel) {
JPanel panel = (JPanel) comp;
// Style titled borders
if (panel.getBorder() instanceof TitledBorder) {
TitledBorder tb = (TitledBorder) panel.getBorder();
tb.setTitleColor(DARK_FG);
}
}
if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
applyDarkTheme(child);
}
}
} }
// ========== Tab Panels ========== // ========== Tab Panels ==========
@@ -232,18 +130,51 @@ public class SettingsDialog extends JDialog {
gbc.insets = new Insets(10, 10, 10, 10); gbc.insets = new Insets(10, 10, 10, 10);
gbc.anchor = GridBagConstraints.WEST; gbc.anchor = GridBagConstraints.WEST;
// Font family // UI Theme
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 0; gbc.gridy = 0;
JLabel themeLabel = new JLabel("Java UI Theme:");
panel.add(themeLabel, gbc);
uiThemeBox = new JComboBox<>(UITheme.values());
uiThemeBox.setSelectedItem(Settings.getJavaUiTheme());
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(uiThemeBox, gbc);
// Font family
gbc.gridx = 0;
gbc.gridy = 1;
gbc.fill = GridBagConstraints.NONE;
JLabel fontLabel = new JLabel("Terminal Font:"); JLabel fontLabel = new JLabel("Terminal Font:");
panel.add(fontLabel, gbc); panel.add(fontLabel, gbc);
String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
fontBox = new JComboBox<>(fonts); java.util.List<String> fontList = new java.util.ArrayList<>();
// Find default boolean hasMonospaced = false;
String currentFont = Settings.getFontFamily(); String currentFont = Settings.getFontFamily();
for (int i = 0; i < fonts.length; i++) { for (String f : fonts) {
if (fonts[i].equalsIgnoreCase(currentFont)) { if ("Monospaced".equalsIgnoreCase(f)) {
hasMonospaced = true;
}
fontList.add(f);
}
if (!hasMonospaced) {
fontList.add(0, "Monospaced");
}
boolean hasCurrent = false;
for (String f : fontList) {
if (f.equalsIgnoreCase(currentFont)) {
hasCurrent = true;
break;
}
}
if (!hasCurrent && currentFont != null && !currentFont.trim().isEmpty()) {
fontList.add(0, currentFont);
}
fontBox = new JComboBox<>(fontList.toArray(new String[0]));
for (int i = 0; i < fontBox.getItemCount(); i++) {
if (fontBox.getItemAt(i).equalsIgnoreCase(currentFont)) {
fontBox.setSelectedIndex(i); fontBox.setSelectedIndex(i);
break; break;
} }
@@ -254,7 +185,7 @@ public class SettingsDialog extends JDialog {
// Font size // Font size
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 1; gbc.gridy = 2;
gbc.fill = GridBagConstraints.NONE; gbc.fill = GridBagConstraints.NONE;
JLabel sizeLabel = new JLabel("Font Size:"); JLabel sizeLabel = new JLabel("Font Size:");
panel.add(sizeLabel, gbc); panel.add(sizeLabel, gbc);
@@ -267,19 +198,19 @@ public class SettingsDialog extends JDialog {
// Graphics Mode // Graphics Mode
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 2; gbc.gridy = 3;
gbc.fill = GridBagConstraints.NONE; gbc.fill = GridBagConstraints.NONE;
JLabel graphicsLabel = new JLabel("Graphics Mode:"); JLabel graphicsLabel = new JLabel("Graphics Mode:");
panel.add(graphicsLabel, gbc); panel.add(graphicsLabel, gbc);
graphicsModeBox = new JComboBox<>(org.lib3270j.graphics.GraphicsMode.values()); graphicsModeBox = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
graphicsModeBox.setSelectedItem(Settings.getGraphicsMode()); graphicsModeBox.setSelectedItem(Settings.getGraphicsMode());
gbc.gridx = 1; gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(graphicsModeBox, gbc); panel.add(graphicsModeBox, gbc);
// Fill remaining space // Fill remaining space
gbc.gridy = 3; gbc.gridy = 4;
gbc.weighty = 1.0; gbc.weighty = 1.0;
panel.add(Box.createGlue(), gbc); panel.add(Box.createGlue(), gbc);
@@ -343,14 +274,143 @@ public class SettingsDialog extends JDialog {
blockSelectCheck = new JCheckBox("Block selection mode (rectangular select)", Settings.getBlockSelectMode()); blockSelectCheck = new JCheckBox("Block selection mode (rectangular select)", Settings.getBlockSelectMode());
panel.add(blockSelectCheck, gbc); panel.add(blockSelectCheck, gbc);
// Placeholder for potentially more behavior options below // Default Dynamic Screen Size
gbc.gridy = 3; gbc.gridy = 3;
gbc.gridwidth = 1;
gbc.gridx = 0;
panel.add(new JLabel("Default Dynamic Screen:"), gbc);
JPanel dynDimPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
dynDimPanel.setOpaque(false);
dynDimPanel.add(new JLabel("Rows:"));
dynamicRowsSpinner = new JSpinner(new SpinnerNumberModel(Settings.getDynamicRows(), 24, 255, 1));
ThemeManager.styleSpinner(dynamicRowsSpinner);
dynDimPanel.add(dynamicRowsSpinner);
dynDimPanel.add(new JLabel("Cols:"));
dynamicColsSpinner = new JSpinner(new SpinnerNumberModel(Settings.getDynamicCols(), 80, 255, 1));
ThemeManager.styleSpinner(dynamicColsSpinner);
dynDimPanel.add(dynamicColsSpinner);
gbc.gridx = 1;
panel.add(dynDimPanel, gbc);
// Clipboard & Tabular Paste options
gbc.gridx = 0;
gbc.gridy = 4;
gbc.gridwidth = 2;
enablePasteFromExcelCheck = new JCheckBox("Enable Excel / Tabular Paste (advance with tabs & newlines)", Settings.getEnablePasteFromExcel());
panel.add(enablePasteFromExcelCheck, gbc);
gbc.gridy = 5;
pasteStopAtProtectedCheck = new JCheckBox("Stop Paste at Protected Boundary", Settings.getPasteStopAtProtectedLine());
panel.add(pasteStopAtProtectedCheck, gbc);
gbc.gridy = 6;
gbc.weighty = 1.0; gbc.weighty = 1.0;
panel.add(Box.createGlue(), gbc); panel.add(Box.createGlue(), gbc);
return panel; return panel;
} }
private JPanel createEntryAssistPanel() {
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// Group 1: Entry Assist / Document Mode
JPanel eaGroup = new JPanel(new GridBagLayout());
eaGroup.setBorder(BorderFactory.createTitledBorder("Entry Assist (Document Mode)"));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 6, 4, 6);
gbc.anchor = GridBagConstraints.WEST;
docModeCheck = new JCheckBox("Enable Document Mode (DOC)", Settings.getEntryAssistDocMode());
gbc.gridx = 0;
gbc.gridy = 0;
gbc.gridwidth = 2;
eaGroup.add(docModeCheck, gbc);
wordWrapCheck = new JCheckBox("Enable Word Wrap (V)", Settings.getEntryAssistWordWrap());
gbc.gridy = 1;
eaGroup.add(wordWrapCheck, gbc);
// Margins
gbc.gridy = 2;
gbc.gridwidth = 1;
eaGroup.add(new JLabel("Margins:"), gbc);
JPanel marginPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
marginPanel.setOpaque(false);
marginPanel.add(new JLabel("Left:"));
startColSpinner = new JSpinner(new SpinnerNumberModel(Settings.getEntryAssistStartCol(), 1, 80, 1));
ThemeManager.styleSpinner(startColSpinner);
marginPanel.add(startColSpinner);
marginPanel.add(new JLabel("Right:"));
endColSpinner = new JSpinner(new SpinnerNumberModel(Settings.getEntryAssistEndCol(), 1, 80, 1));
ThemeManager.styleSpinner(endColSpinner);
marginPanel.add(endColSpinner);
gbc.gridx = 1;
eaGroup.add(marginPanel, gbc);
// End-of-Line Bell
gbc.gridx = 0;
gbc.gridy = 3;
bellCheck = new JCheckBox("Audible EOL Bell at Col:", Settings.getEntryAssistBell());
eaGroup.add(bellCheck, gbc);
bellColSpinner = new JSpinner(new SpinnerNumberModel(Settings.getEntryAssistBellCol(), 1, 80, 1));
ThemeManager.styleSpinner(bellColSpinner);
gbc.gridx = 1;
eaGroup.add(bellColSpinner, gbc);
// Tab Stops
gbc.gridx = 0;
gbc.gridy = 4;
eaGroup.add(new JLabel("Tab Stops:"), gbc);
tabStopsField = new JTextField(Settings.getEntryAssistTabStops(), 20);
tabStopsField.setToolTipText("Comma-separated column numbers (1-80, e.g. 1,9,17,25,33,41,49,57,65,73)");
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
eaGroup.add(tabStopsField, gbc);
panel.add(eaGroup);
panel.add(Box.createVerticalStrut(10));
// Group 2: Terminal Operational Modes
JPanel modesGroup = new JPanel(new GridBagLayout());
modesGroup.setBorder(BorderFactory.createTitledBorder("Terminal Operational Modes"));
GridBagConstraints gbcM = new GridBagConstraints();
gbcM.insets = new Insets(4, 6, 4, 6);
gbcM.anchor = GridBagConstraints.WEST;
gbcM.fill = GridBagConstraints.HORIZONTAL;
gbcM.gridx = 0;
gbcM.weightx = 1.0;
insertOffOnAidCheck = new JCheckBox("Reset Insert mode on AID key (Enter, PF, PA, Clear)", Settings.getInsertOffOnAid());
gbcM.gridy = 0;
modesGroup.add(insertOffOnAidCheck, gbcM);
fourColorOverrideCheck = new JCheckBox("Base 4-Color Override mode (3279 green/white/red/turquoise)", Settings.getFourColorOverride());
gbcM.gridy = 1;
modesGroup.add(fourColorOverrideCheck, gbcM);
numericFieldLockCheck = new JCheckBox("Lock keyboard on non-numeric input in numeric fields (-NUMERIC)", Settings.getNumericFieldLock());
gbcM.gridy = 2;
modesGroup.add(numericFieldLockCheck, gbcM);
autoSkipCheck = new JCheckBox("Auto-Skip to next unprotected field when field is filled", Settings.getAutoSkipEnabled());
gbcM.gridy = 3;
modesGroup.add(autoSkipCheck, gbcM);
panel.add(modesGroup);
panel.add(Box.createVerticalGlue());
return panel;
}
private JPanel createAdvancedPanel() { private JPanel createAdvancedPanel() {
JPanel panel = new JPanel(new BorderLayout()); JPanel panel = new JPanel(new BorderLayout());
@@ -370,7 +430,7 @@ public class SettingsDialog extends JDialog {
hostPanel.setBorder(BorderFactory.createTitledBorder("Host Colors")); hostPanel.setBorder(BorderFactory.createTitledBorder("Host Colors"));
for (int i = 0; i < 16; i++) { 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; tempHostColors[i] = init;
JPanel cb = new JPanel(); JPanel cb = new JPanel();
cb.setName("colorSwatch"); cb.setName("colorSwatch");
@@ -402,7 +462,7 @@ public class SettingsDialog extends JDialog {
for (int i = 0; i < monoKeys.length; i++) { for (int i = 0; i < monoKeys.length; i++) {
String mk = monoKeys[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); tempMonoColors.put(mk, init);
JPanel p = new JPanel(new BorderLayout()); JPanel p = new JPanel(new BorderLayout());
@@ -429,6 +489,7 @@ public class SettingsDialog extends JDialog {
JPanel resetPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); JPanel resetPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton btnResetColors = new JButton("Reset to Defaults"); JButton btnResetColors = new JButton("Reset to Defaults");
ThemeManager.styleButton(btnResetColors, ThemeManager.ButtonVariant.DEFAULT);
btnResetColors.addActionListener(e -> { btnResetColors.addActionListener(e -> {
for (int i=0; i<16; i++) { for (int i=0; i<16; i++) {
tempHostColors[i] = TerminalPanel.DEFAULT_HOST_COLORS[i]; tempHostColors[i] = TerminalPanel.DEFAULT_HOST_COLORS[i];
@@ -436,8 +497,6 @@ public class SettingsDialog extends JDialog {
for (int i=0; i<monoKeys.length; i++) { for (int i=0; i<monoKeys.length; i++) {
tempMonoColors.put(monoKeys[i], monoDefs[i]); tempMonoColors.put(monoKeys[i], monoDefs[i]);
} }
// Repaint container implicitly handled if we trigger a UI update,
// but for simplicity user can just close/reopen or have it refresh on save
JOptionPane.showMessageDialog(main, "Colors reset. Click Apply to save."); JOptionPane.showMessageDialog(main, "Colors reset. Click Apply to save.");
}); });
resetPanel.add(btnResetColors); resetPanel.add(btnResetColors);
@@ -453,7 +512,10 @@ public class SettingsDialog extends JDialog {
JPanel main = new JPanel(new BorderLayout()); JPanel main = new JPanel(new BorderLayout());
String[] actions = {"ENTER", "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT", 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",
"STATUS_BAR",
"ERASE_INPUT", "NEWLINE", "DUP", "FIELD_MARK", "ATTN", "SYSREQ", "CURSEL",
"COPY", "PASTE", "SELECTALL"};
keymapModel = new DefaultTableModel(new Object[]{"Action", "Key Binding"}, 0) { keymapModel = new DefaultTableModel(new Object[]{"Action", "Key Binding"}, 0) {
@Override @Override
@@ -464,9 +526,8 @@ public class SettingsDialog extends JDialog {
// Populate table from Settings or Defaults // Populate table from Settings or Defaults
for(String act : actions) { for(String act : actions) {
String def = act; String def = getDefaultBinding(act);
if(def.equals("PAGE_UP")) def = "PAGE_UP"; // fallback example String current = haus.nightmare.j3270.config.Settings.getKeyBinding(act, def);
String current = org.pubvm.j3270.config.Settings.getKeyBinding(act, def);
tempKeyBindings.put(act, current); tempKeyBindings.put(act, current);
keymapModel.addRow(new Object[]{act, current}); keymapModel.addRow(new Object[]{act, current});
} }
@@ -478,18 +539,19 @@ public class SettingsDialog extends JDialog {
defaultBinding = "shift F" + (i - 12); defaultBinding = "shift F" + (i - 12);
} }
String pf = "PF"+i; 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); tempKeyBindings.put(pf, cur);
keymapModel.addRow(new Object[]{pf, cur}); keymapModel.addRow(new Object[]{pf, cur});
} }
for(int i=1;i<=3;i++){ for(int i=1;i<=3;i++){
String pa = "PA"+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); tempKeyBindings.put(pa, curPa);
keymapModel.addRow(new Object[]{pa, curPa}); keymapModel.addRow(new Object[]{pa, curPa});
} }
JTable table = new JTable(keymapModel); JTable table = new JTable(keymapModel);
ThemeManager.styleTable(table);
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setFillsViewportHeight(true); table.setFillsViewportHeight(true);
@@ -502,9 +564,8 @@ public class SettingsDialog extends JDialog {
captureDialog.setSize(320, 100); captureDialog.setSize(320, 100);
captureDialog.setLocationRelativeTo(this); captureDialog.setLocationRelativeTo(this);
JLabel lbl = new JLabel("Press any key combination now...", SwingConstants.CENTER); JLabel lbl = new JLabel("Press any key combination now...", SwingConstants.CENTER);
lbl.setForeground(DARK_FG);
captureDialog.getContentPane().setBackground(DARK_BG);
captureDialog.add(lbl); captureDialog.add(lbl);
ThemeManager.applyThemeToWindow(captureDialog);
captureDialog.addKeyListener(new KeyAdapter() { captureDialog.addKeyListener(new KeyAdapter() {
@Override @Override
public void keyPressed(KeyEvent e) { public void keyPressed(KeyEvent e) {
@@ -524,6 +585,7 @@ public class SettingsDialog extends JDialog {
// Replace Binding sets the action to exactly one new key // Replace Binding sets the action to exactly one new key
JButton btnReplace = new JButton("Replace Binding"); JButton btnReplace = new JButton("Replace Binding");
ThemeManager.styleButton(btnReplace, ThemeManager.ButtonVariant.DEFAULT);
btnReplace.addActionListener(e -> { btnReplace.addActionListener(e -> {
int row = table.getSelectedRow(); int row = table.getSelectedRow();
if (row < 0) return; if (row < 0) return;
@@ -537,6 +599,7 @@ public class SettingsDialog extends JDialog {
// Add Binding appends an additional key to the existing binding(s) // Add Binding appends an additional key to the existing binding(s)
JButton btnAdd = new JButton("Add Binding"); JButton btnAdd = new JButton("Add Binding");
ThemeManager.styleButton(btnAdd, ThemeManager.ButtonVariant.DEFAULT);
btnAdd.addActionListener(e -> { btnAdd.addActionListener(e -> {
int row = table.getSelectedRow(); int row = table.getSelectedRow();
if (row < 0) return; if (row < 0) return;
@@ -557,6 +620,7 @@ public class SettingsDialog extends JDialog {
// Remove Last removes the last comma-separated binding entry // Remove Last removes the last comma-separated binding entry
JButton btnRemoveLast = new JButton("Remove Last"); JButton btnRemoveLast = new JButton("Remove Last");
ThemeManager.styleButton(btnRemoveLast, ThemeManager.ButtonVariant.DEFAULT);
btnRemoveLast.addActionListener(e -> { btnRemoveLast.addActionListener(e -> {
int row = table.getSelectedRow(); int row = table.getSelectedRow();
if (row < 0) return; if (row < 0) return;
@@ -577,6 +641,7 @@ public class SettingsDialog extends JDialog {
// Unbind All clears all bindings for the action // Unbind All clears all bindings for the action
JButton btnUnbind = new JButton("Unbind All"); JButton btnUnbind = new JButton("Unbind All");
ThemeManager.styleButton(btnUnbind, ThemeManager.ButtonVariant.DEFAULT);
btnUnbind.addActionListener(e -> { btnUnbind.addActionListener(e -> {
int row = table.getSelectedRow(); int row = table.getSelectedRow();
if (row < 0) return; if (row < 0) return;
@@ -588,6 +653,7 @@ public class SettingsDialog extends JDialog {
// Reset Keymaps restore all defaults // Reset Keymaps restore all defaults
JButton btnReset = new JButton("Reset All"); JButton btnReset = new JButton("Reset All");
ThemeManager.styleButton(btnReset, ThemeManager.ButtonVariant.DEFAULT);
btnReset.addActionListener(e -> { btnReset.addActionListener(e -> {
for (int row = 0; row < keymapModel.getRowCount(); row++) { for (int row = 0; row < keymapModel.getRowCount(); row++) {
String action = (String) keymapModel.getValueAt(row, 0); String action = (String) keymapModel.getValueAt(row, 0);
@@ -598,6 +664,45 @@ public class SettingsDialog extends JDialog {
}); });
btnPanel.add(btnReset); btnPanel.add(btnReset);
// Import Keymap import from .kmp file
JButton btnImportKmp = new JButton("Import Keymap...");
ThemeManager.styleButton(btnImportKmp, ThemeManager.ButtonVariant.DEFAULT);
btnImportKmp.addActionListener(e -> {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("Import Keymap Profile");
chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("Keyboard Map Profile (*.kmp)", "kmp"));
int ret = chooser.showOpenDialog(this);
if (ret == JFileChooser.APPROVE_OPTION) {
File f = chooser.getSelectedFile();
try {
Map<String, String> imported = KeyBindings.parseKmp(f);
int updatedCount = 0;
for (Map.Entry<String, String> entry : imported.entrySet()) {
String action = entry.getKey();
String binding = entry.getValue();
tempKeyBindings.put(action, binding);
for (int row = 0; row < keymapModel.getRowCount(); row++) {
if (action.equalsIgnoreCase((String) keymapModel.getValueAt(row, 0))) {
keymapModel.setValueAt(binding, row, 1);
updatedCount++;
break;
}
}
}
JOptionPane.showMessageDialog(this,
"Successfully imported " + updatedCount + " key bindings from " + f.getName(),
"Keymap Imported",
JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
JOptionPane.showMessageDialog(this,
"Failed to import keymap: " + ex.getMessage(),
"Import Error",
JOptionPane.ERROR_MESSAGE);
}
}
});
btnPanel.add(btnImportKmp);
main.add(new JScrollPane(table), BorderLayout.CENTER); main.add(new JScrollPane(table), BorderLayout.CENTER);
main.add(btnPanel, BorderLayout.SOUTH); main.add(btnPanel, BorderLayout.SOUTH);
@@ -606,13 +711,12 @@ public class SettingsDialog extends JDialog {
/** Returns the factory-default binding for a given action name. */ /** Returns the factory-default binding for a given action name. */
private String getDefaultBinding(String action) { private String getDefaultBinding(String action) {
if (action.startsWith("PF")) { if (action.matches("PF[0-9]+")) {
int n = Integer.parseInt(action.substring(2)); int n = Integer.parseInt(action.substring(2));
return n <= 12 ? "F" + n : "shift F" + (n - 12); return n <= 12 ? "F" + n : "shift F" + (n - 12);
} }
if (action.startsWith("PA")) return "alt " + action.substring(2); if (action.matches("PA[1-3]")) return "alt " + action.substring(2);
if (action.equals("CLEAR")) return "alt C"; return haus.nightmare.j3270.config.Settings.getDefaultBinding(action);
return action; // nav keys default to their own name
} }
// ========== Apply Settings ========== // ========== Apply Settings ==========
@@ -620,13 +724,19 @@ public class SettingsDialog extends JDialog {
private boolean applySettings() { private boolean applySettings() {
try { try {
// Apply Appearance // Apply Appearance
UITheme selectedTheme = (UITheme) uiThemeBox.getSelectedItem();
if (selectedTheme != null) {
Settings.setJavaUiTheme(selectedTheme);
ThemeManager.setTheme(selectedTheme);
}
String fontFam = (String) fontBox.getSelectedItem(); String fontFam = (String) fontBox.getSelectedItem();
if (fontFam != null) { if (fontFam != null) {
Settings.setFontFamily(fontFam); Settings.setFontFamily(fontFam);
} }
Settings.setFontSize((Integer) fontSizeSpinner.getValue()); Settings.setFontSize((Integer) fontSizeSpinner.getValue());
if (graphicsModeBox.getSelectedItem() != null) { if (graphicsModeBox.getSelectedItem() != null) {
Settings.setGraphicsMode((org.lib3270j.graphics.GraphicsMode) graphicsModeBox.getSelectedItem()); Settings.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsModeBox.getSelectedItem());
} }
// Apply Behavior // Apply Behavior
@@ -645,21 +755,55 @@ public class SettingsDialog extends JDialog {
// Block select mode // Block select mode
Settings.setBlockSelectMode(blockSelectCheck.isSelected()); Settings.setBlockSelectMode(blockSelectCheck.isSelected());
// Clipboard & Tabular Paste options
if (enablePasteFromExcelCheck != null) {
Settings.setEnablePasteFromExcel(enablePasteFromExcelCheck.isSelected());
}
if (pasteStopAtProtectedCheck != null) {
Settings.setPasteStopAtProtectedLine(pasteStopAtProtectedCheck.isSelected());
}
// Default Dynamic screen dimensions
if (dynamicRowsSpinner != null && dynamicColsSpinner != null) {
Settings.setDynamicRows((Integer) dynamicRowsSpinner.getValue());
Settings.setDynamicCols((Integer) dynamicColsSpinner.getValue());
}
// Entry Assist & Modes
if (docModeCheck != null) {
Settings.setEntryAssistDocMode(docModeCheck.isSelected());
Settings.setEntryAssistWordWrap(wordWrapCheck.isSelected());
Settings.setEntryAssistStartCol((Integer) startColSpinner.getValue());
Settings.setEntryAssistEndCol((Integer) endColSpinner.getValue());
Settings.setEntryAssistBell(bellCheck.isSelected());
Settings.setEntryAssistBellCol((Integer) bellColSpinner.getValue());
Settings.setEntryAssistTabStops(tabStopsField.getText().trim());
Settings.setInsertOffOnAid(insertOffOnAidCheck.isSelected());
Settings.setFourColorOverride(fourColorOverrideCheck.isSelected());
Settings.setNumericFieldLock(numericFieldLockCheck.isSelected());
Settings.setAutoSkipEnabled(autoSkipCheck.isSelected());
}
// Propagate visual changes to the app // Propagate visual changes to the app
// Save Colors // Save Colors
for (int i=0; i<16; i++) { 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()) { 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 // Save Keybindings
for (Map.Entry<String, String> entry : tempKeyBindings.entrySet()) { 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(); parentApp.getTerminalPanel().reloadSettings();
if (parentApp != null && (parentApp.getExtendedState() & Frame.MAXIMIZED_BOTH) == 0) {
parentApp.getTerminalPanel().guardedPack();
}
ThemeManager.applyThemeToWindow(this);
return true; return true;
} catch (Exception e) { } catch (Exception e) {
@@ -0,0 +1,364 @@
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 insertStatus;
private final JLabel aplStatus;
private final JLabel fieldTypeStatus;
private final JLabel docModeStatus;
private final JLabel wordWrapStatus;
private final JLabel codePageInfo;
private final JLabel modelInfo;
private final JLabel cursorPosition;
private Telnet3270Client client;
private TerminalPanel terminalPanel;
public StatusBar() {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setBackground(ThemeManager.getStatusBarBg());
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder()));
setPreferredSize(new Dimension(800, 22));
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12);
connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim());
tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
luName = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
lockStatus = createLabel("", oiaFont, ThemeManager.getOiaInputInhibited());
insertStatus = createClickableLabel("", oiaFont, ThemeManager.getOiaStatusSysAvail(), "Insert Mode (^ / Insert key) - Click to toggle", () -> {
if (terminalPanel != null) {
terminalPanel.toggleInsertMode();
} else if (client != null) {
client.toggleInsert();
updateStatus();
}
});
aplStatus = createClickableLabel("", oiaFont, ThemeManager.getOiaFgAlert(), "APL Keyboard Mode (Alt+F3) - Click to toggle", () -> {
if (terminalPanel != null) {
terminalPanel.toggleAplMode();
} else if (client != null) {
client.toggleAplMode();
updateStatus();
}
});
fieldTypeStatus = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
docModeStatus = createClickableLabel("", oiaFont, ThemeManager.getOiaStatusSysAvail(), "Entry Assist Document Mode (Alt+F1) - Click to toggle", () -> {
if (terminalPanel != null) {
terminalPanel.toggleDocMode();
} else if (client != null) {
client.toggleDocMode();
updateStatus();
}
});
wordWrapStatus = createClickableLabel("", oiaFont, ThemeManager.getOiaStatusSysAvail(), "Entry Assist Word Wrap (Alt+F2) - Click to toggle", () -> {
if (terminalPanel != null) {
terminalPanel.toggleWordWrap();
} else if (client != null) {
client.toggleWordWrap();
updateStatus();
}
});
codePageInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
modelInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
cursorPosition = createLabel("001/001 [0000]", oiaFont, ThemeManager.getOiaFgNormal());
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(8));
add(insertStatus);
add(Box.createHorizontalStrut(8));
add(aplStatus);
add(Box.createHorizontalStrut(10));
add(fieldTypeStatus);
add(Box.createHorizontalGlue());
add(docModeStatus);
add(Box.createHorizontalStrut(8));
add(wordWrapStatus);
add(Box.createHorizontalStrut(12));
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;
}
private JLabel createClickableLabel(String text, Font font, Color fg, String tooltip, Runnable onClick) {
JLabel label = new JLabel(text);
label.setFont(font);
label.setForeground(fg);
label.setToolTipText(tooltip);
label.setCursor(java.awt.Cursor.getPredefinedCursor(java.awt.Cursor.HAND_CURSOR));
label.addMouseListener(new java.awt.event.MouseAdapter() {
@Override
public void mouseClicked(java.awt.event.MouseEvent e) {
if (onClick != null) {
onClick.run();
}
}
});
return label;
}
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
this.client = client;
this.terminalPanel = terminalPanel;
updateStatus();
}
public void applyTheme(UITheme theme) {
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(() -> applyTheme(theme));
return;
}
setBackground(ThemeManager.getStatusBarBg(theme));
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme)));
updateStatus();
}
public void updateStatus() {
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(this::updateStatus);
return;
}
UITheme theme = ThemeManager.getTheme();
if (client == null) {
connectionStatus.setText("Not Connected");
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
tlsStatus.setText("");
luName.setText("");
lockStatus.setText("");
lockStatus.setForeground(ThemeManager.getOiaInputInhibited(theme));
insertStatus.setText("");
aplStatus.setText("");
fieldTypeStatus.setText("");
docModeStatus.setText("");
wordWrapStatus.setText("");
codePageInfo.setText("");
modelInfo.setText("");
cursorPosition.setText("001/001");
cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme));
return;
}
// Connection state
ConnectionState state = client.getConnectionState();
switch (state) {
case NOT_CONNECTED:
connectionStatus.setText("Not Connected");
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
break;
case RECONNECTING:
connectionStatus.setText("Reconnecting...");
connectionStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
break;
case TCP_PENDING:
case TELNET_PENDING:
connectionStatus.setText("Connecting...");
connectionStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
break;
case CONNECTED_3270:
connectionStatus.setText("TN3270");
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
break;
case CONNECTED_TN3270E:
connectionStatus.setText("TN3270E");
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
break;
case CONNECTED_SSCP:
connectionStatus.setText("SSCP-LU");
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
break;
case CONNECTED_NVT:
case CONNECTED_NVT_CHAR:
case CONNECTED_E_NVT:
connectionStatus.setText("NVT");
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
break;
case CONNECTED_UNBOUND:
connectionStatus.setText("Unbound");
connectionStatus.setForeground(ThemeManager.getOiaAttention(theme));
break;
default:
connectionStatus.setText(state.name());
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
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(ThemeManager.getOiaStatusSysAvail(theme));
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)");
} else {
tlsStatus.setText("🔓 TLS (Unverified)");
tlsStatus.setForeground(ThemeManager.getOiaAttention(theme));
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);
luName.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
// Lock / Inhibit status
int inhibit = client.getOIA().getInputInhibited();
if (inhibit != ECLConstants.INHIBIT_NOT_INHIBITED) {
Color lockFg = ThemeManager.getOiaInputInhibited(theme);
switch (inhibit) {
case ECLConstants.INHIBIT_SYSTEM_LOCK:
lockStatus.setText("X SYSTEM");
lockFg = ThemeManager.getOiaInputInhibited(theme);
break;
case ECLConstants.INHIBIT_COMM_CHECK:
lockStatus.setText("X COMM");
lockFg = ThemeManager.getOiaCommCheck(theme);
break;
case ECLConstants.INHIBIT_NUMERIC_ONLY:
lockStatus.setText("X NUM");
lockFg = ThemeManager.getOiaAttention(theme);
break;
case ECLConstants.INHIBIT_PROTECTED_FIELD:
lockStatus.setText("X PROT");
lockFg = ThemeManager.getOiaInputInhibited(theme);
break;
case ECLConstants.INHIBIT_OVERFLOW:
lockStatus.setText("X >");
lockFg = ThemeManager.getOiaAttention(theme);
break;
case ECLConstants.INHIBIT_OPERATOR_DUE:
lockStatus.setText("X OP");
lockFg = ThemeManager.getOiaAttention(theme);
break;
default:
lockStatus.setText("X LOCKED");
lockFg = ThemeManager.getOiaInputInhibited(theme);
break;
}
lockStatus.setForeground(lockFg);
} else {
lockStatus.setText("");
}
// Insert Mode Indicator
boolean isInsert = false;
if (terminalPanel != null) {
isInsert = terminalPanel.isInsertMode();
} else if (client.getInputProcessor() != null) {
isInsert = client.getInputProcessor().isInsertMode();
}
if (isInsert) {
insertStatus.setText("^ INS");
insertStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
} else {
insertStatus.setText("");
}
// APL Keyboard Mode Indicator
if (client.getInputProcessor() != null && client.getInputProcessor().isAplKeyboardMode()) {
aplStatus.setText("APL");
aplStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
} else {
aplStatus.setText("");
}
// Entry Assist DOC Mode Indicator
if (client.getScreenBuffer() != null && client.getScreenBuffer().isEntryAssistDOCmode()) {
docModeStatus.setText("DOC");
docModeStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
} else {
docModeStatus.setText("");
}
// Entry Assist Word Wrap Indicator
if (client.getScreenBuffer() != null && client.getScreenBuffer().isEntryAssistWordWrap()) {
wordWrapStatus.setText("V");
wordWrapStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
} else {
wordWrapStatus.setText("");
}
// Field status: Numeric vs Alphanumeric
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
if (client.getOIA().isNumeric()) {
fieldTypeStatus.setText("NUM");
fieldTypeStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
} else {
fieldTypeStatus.setText("ALPHA");
fieldTypeStatus.setForeground(ThemeManager.getOiaFgDim(theme));
}
} else {
fieldTypeStatus.setText("");
}
// Active Code Page
String cp = client.getCodePage();
codePageInfo.setText(cp != null ? "CP" + cp : "");
codePageInfo.setForeground(ThemeManager.getOiaFgDim(theme));
codePageInfo.setToolTipText("Active EBCDIC Code Page: CP" + cp);
// Model & Dimensions info
ScreenBuffer sb = client.getScreenBuffer();
int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols();
String modelName;
if (client.getConfig().isDynamicModel() || (client.getConfig().getModel() != null && client.getConfig().getModel().isDynamic())) {
modelName = "IBM-DYNAMIC [" + rows + "x" + cols + "]";
} else {
modelName = client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]";
}
modelInfo.setText(modelName);
modelInfo.setForeground(ThemeManager.getOiaFgDim(theme));
// 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));
cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme));
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,997 @@
package haus.nightmare.j3270.ui;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;
import javax.swing.plaf.basic.BasicButtonUI;
import javax.swing.plaf.basic.BasicMenuBarUI;
import javax.swing.plaf.basic.BasicMenuItemUI;
import javax.swing.plaf.basic.BasicMenuUI;
import javax.swing.plaf.basic.BasicPopupMenuUI;
import javax.swing.plaf.basic.BasicTabbedPaneUI;
import javax.swing.table.DefaultTableCellRenderer;
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;
import java.util.function.Consumer;
/**
* Centralized theme manager for j3270 Java desktop UI.
* Provides high-contrast, platform-independent Light and Dark theme palettes,
* custom Swing UI delegates, and recursive styling helpers for all dialogs and menus.
*/
public final class ThemeManager {
private static UITheme currentTheme = UITheme.DARK;
private static final List<Consumer<UITheme>> themeChangeListeners = new ArrayList<>();
private ThemeManager() {}
public static UITheme getTheme() {
return currentTheme;
}
public static boolean isDark() {
return currentTheme == UITheme.DARK;
}
public static boolean isLight() {
return currentTheme == UITheme.LIGHT;
}
public static void setTheme(UITheme theme) {
if (theme == null) theme = UITheme.DARK;
if (currentTheme != theme) {
currentTheme = theme;
applyUIManagerDefaults(theme);
for (Consumer<UITheme> listener : new ArrayList<>(themeChangeListeners)) {
try {
listener.accept(theme);
} catch (Exception ignored) {}
}
}
}
public static void addThemeChangeListener(Consumer<UITheme> listener) {
if (listener != null && !themeChangeListeners.contains(listener)) {
themeChangeListeners.add(listener);
}
}
public static void removeThemeChangeListener(Consumer<UITheme> listener) {
themeChangeListeners.remove(listener);
}
// =========================================================================
// Color Palette Tokens
// =========================================================================
public static Color getBgMain() { return getBgMain(currentTheme); }
public static Color getBgMain(UITheme t) {
return t == UITheme.DARK ? new Color(28, 28, 28) : new Color(242, 242, 244);
}
public static Color getBgPanel() { return getBgPanel(currentTheme); }
public static Color getBgPanel(UITheme t) {
return t == UITheme.DARK ? new Color(36, 36, 36) : new Color(250, 250, 252);
}
public static Color getBgComponent() { return getBgComponent(currentTheme); }
public static Color getBgComponent(UITheme t) {
return t == UITheme.DARK ? new Color(46, 46, 46) : Color.WHITE;
}
public static Color getBgComponentLighter() { return getBgComponentLighter(currentTheme); }
public static Color getBgComponentLighter(UITheme t) {
return t == UITheme.DARK ? new Color(56, 56, 56) : new Color(236, 236, 238);
}
public static Color getFgMain() { return getFgMain(currentTheme); }
public static Color getFgMain(UITheme t) {
return t == UITheme.DARK ? new Color(238, 238, 238) : new Color(24, 24, 24);
}
public static Color getFgMuted() { return getFgMuted(currentTheme); }
public static Color getFgMuted(UITheme t) {
return t == UITheme.DARK ? new Color(175, 175, 175) : new Color(90, 90, 90);
}
public static Color getFgDisabled() { return getFgDisabled(currentTheme); }
public static Color getFgDisabled(UITheme t) {
return t == UITheme.DARK ? new Color(110, 110, 110) : new Color(160, 160, 160);
}
public static Color getBorder() { return getBorder(currentTheme); }
public static Color getBorder(UITheme t) {
return t == UITheme.DARK ? new Color(65, 65, 65) : new Color(205, 205, 205);
}
public static Color getBorderColor() { return getBorder(currentTheme); }
public static Color getBorderColor(UITheme t) { return getBorder(t); }
public static Color getBorderFocused() { return getBorderFocused(currentTheme); }
public static Color getBorderFocused(UITheme t) {
return t == UITheme.DARK ? new Color(80, 140, 230) : new Color(50, 115, 220);
}
public static Color getSelectionBg() { return getSelectionBg(currentTheme); }
public static Color getSelectionBg(UITheme t) {
return t == UITheme.DARK ? new Color(55, 105, 180) : new Color(60, 125, 215);
}
public static Color getSelectionFg() { return getSelectionFg(currentTheme); }
public static Color getSelectionFg(UITheme t) {
return Color.WHITE;
}
public static Color getMenuBarBg() { return getMenuBarBg(currentTheme); }
public static Color getMenuBarBg(UITheme t) {
return t == UITheme.DARK ? new Color(26, 26, 26) : new Color(240, 240, 242);
}
public static Color getMenuBarFg() { return getMenuBarFg(currentTheme); }
public static Color getMenuBarFg(UITheme t) {
return t == UITheme.DARK ? new Color(235, 235, 235) : new Color(25, 25, 25);
}
public static Color getMenuPopupBg() { return getMenuPopupBg(currentTheme); }
public static Color getMenuPopupBg(UITheme t) {
return t == UITheme.DARK ? new Color(38, 38, 38) : Color.WHITE;
}
public static Color getMenuItemFg() { return getMenuItemFg(currentTheme); }
public static Color getMenuItemFg(UITheme t) {
return t == UITheme.DARK ? new Color(230, 230, 230) : new Color(30, 30, 30);
}
public static Color getMenuItemHoverBg() { return getMenuItemHoverBg(currentTheme); }
public static Color getMenuItemHoverBg(UITheme t) {
return t == UITheme.DARK ? new Color(55, 95, 160) : new Color(220, 232, 250);
}
public static Color getMenuItemHoverFg() { return getMenuItemHoverFg(currentTheme); }
public static Color getMenuItemHoverFg(UITheme t) {
return t == UITheme.DARK ? Color.WHITE : new Color(15, 15, 15);
}
public static Color getTabBg() { return getTabBg(currentTheme); }
public static Color getTabBg(UITheme t) {
return t == UITheme.DARK ? new Color(34, 34, 34) : new Color(230, 230, 232);
}
public static Color getTabFg() { return getTabFg(currentTheme); }
public static Color getTabFg(UITheme t) {
return t == UITheme.DARK ? new Color(185, 185, 185) : new Color(75, 75, 75);
}
public static Color getTabSelectedBg() { return getTabSelectedBg(currentTheme); }
public static Color getTabSelectedBg(UITheme t) {
return t == UITheme.DARK ? new Color(50, 50, 50) : Color.WHITE;
}
public static Color getTabSelectedFg() { return getTabSelectedFg(currentTheme); }
public static Color getTabSelectedFg(UITheme t) {
return t == UITheme.DARK ? Color.WHITE : new Color(20, 20, 20);
}
public static Color getTableHeaderBg() { return getTableHeaderBg(currentTheme); }
public static Color getTableHeaderBg(UITheme t) {
return t == UITheme.DARK ? new Color(46, 46, 46) : new Color(232, 232, 236);
}
public static Color getTableHeaderFg() { return getTableHeaderFg(currentTheme); }
public static Color getTableHeaderFg(UITheme t) {
return t == UITheme.DARK ? new Color(235, 235, 235) : new Color(30, 30, 30);
}
public static Color getTableRowEven() { return getTableRowEven(currentTheme); }
public static Color getTableRowEven(UITheme t) {
return t == UITheme.DARK ? new Color(38, 38, 38) : Color.WHITE;
}
public static Color getTableRowOdd() { return getTableRowOdd(currentTheme); }
public static Color getTableRowOdd(UITheme t) {
return t == UITheme.DARK ? new Color(33, 33, 33) : new Color(246, 247, 249);
}
public static Color getTableGrid() { return getTableGrid(currentTheme); }
public static Color getTableGrid(UITheme t) {
return t == UITheme.DARK ? new Color(55, 55, 55) : new Color(225, 225, 225);
}
public static Color getCodeAreaBg() { return getCodeAreaBg(currentTheme); }
public static Color getCodeAreaBg(UITheme t) {
return t == UITheme.DARK ? new Color(22, 22, 22) : new Color(252, 252, 252);
}
public static Color getCodeAreaFg() { return getCodeAreaFg(currentTheme); }
public static Color getCodeAreaFg(UITheme t) {
return t == UITheme.DARK ? new Color(80, 230, 80) : new Color(0, 120, 0);
}
public static Color getStatusBarBg() { return getStatusBarBg(currentTheme); }
public static Color getStatusBarBg(UITheme t) {
return t == UITheme.DARK ? new Color(20, 20, 20) : new Color(235, 235, 238);
}
public static Color getStatusBarBorder() { return getStatusBarBorder(currentTheme); }
public static Color getStatusBarBorder(UITheme t) {
return t == UITheme.DARK ? new Color(42, 42, 42) : new Color(210, 210, 215);
}
public static Color getOiaFgNormal() { return getOiaFgNormal(currentTheme); }
public static Color getOiaFgNormal(UITheme t) {
return t == UITheme.DARK ? new Color(50, 205, 50) : new Color(0, 130, 0);
}
public static Color getOiaFgDim() { return getOiaFgDim(currentTheme); }
public static Color getOiaFgDim(UITheme t) {
return t == UITheme.DARK ? new Color(110, 110, 110) : new Color(90, 90, 90);
}
public static Color getOiaFgAlert() { return getOiaFgAlert(currentTheme); }
public static Color getOiaFgAlert(UITheme t) {
return t == UITheme.DARK ? new Color(255, 80, 80) : new Color(190, 20, 20);
}
public static Color getOiaFgWarn() { return getOiaFgWarn(currentTheme); }
public static Color getOiaFgWarn(UITheme t) {
return t == UITheme.DARK ? new Color(255, 190, 70) : new Color(180, 100, 0);
}
// Host On-Demand OIA Category Colors (oSI, oII, oAI, oEI, oOB)
public static final Color HOD_OIA_STATUS_SYS_AVAIL = new Color(120, 144, 240); // oSI: CUSTOMBLUE
public static final Color HOD_OIA_INPUT_INHIBITED = new Color(255, 255, 255); // oII: White
public static final Color HOD_OIA_ATTENTION_WARN = new Color(255, 255, 0); // oAI: Yellow
public static final Color HOD_OIA_COMM_CHECK_ERROR = new Color(255, 0, 0); // oEI: Red
public static final Color HOD_OIA_BG_BLACK = new Color(0, 0, 0); // oOB: Black
public static Color getOiaStatusSysAvail() { return getOiaStatusSysAvail(currentTheme); }
public static Color getOiaStatusSysAvail(UITheme t) {
return t == UITheme.DARK ? HOD_OIA_STATUS_SYS_AVAIL : new Color(0, 90, 200);
}
public static Color getOiaInputInhibited() { return getOiaInputInhibited(currentTheme); }
public static Color getOiaInputInhibited(UITheme t) {
return t == UITheme.DARK ? HOD_OIA_INPUT_INHIBITED : new Color(20, 20, 20);
}
public static Color getOiaAttention() { return getOiaAttention(currentTheme); }
public static Color getOiaAttention(UITheme t) {
return t == UITheme.DARK ? HOD_OIA_ATTENTION_WARN : new Color(180, 100, 0);
}
public static Color getOiaCommCheck() { return getOiaCommCheck(currentTheme); }
public static Color getOiaCommCheck(UITheme t) {
return t == UITheme.DARK ? HOD_OIA_COMM_CHECK_ERROR : new Color(190, 20, 20);
}
public static Color getOiaBackground() { return getOiaBackground(currentTheme); }
public static Color getOiaBackground(UITheme t) {
return t == UITheme.DARK ? HOD_OIA_BG_BLACK : getStatusBarBg(t);
}
// =========================================================================
// Button Variants & Color helpers
// =========================================================================
public enum ButtonVariant {
DEFAULT,
PRIMARY,
CANCEL,
DANGER,
ACCENT
}
public static Color getButtonBg(ButtonVariant variant) {
return getButtonBg(variant, currentTheme, false, false);
}
public static Color getButtonBg(ButtonVariant variant, UITheme t) {
return getButtonBg(variant, t, false, false);
}
public static Color getButtonBg(ButtonVariant variant, UITheme t, boolean hover, boolean pressed) {
switch (variant) {
case PRIMARY:
if (pressed) return new Color(25, 105, 35);
if (hover) return new Color(38, 145, 48);
return new Color(32, 128, 42);
case DANGER:
if (pressed) return new Color(125, 30, 30);
if (hover) return new Color(165, 45, 45);
return new Color(145, 38, 38);
case ACCENT:
if (pressed) return new Color(35, 80, 145);
if (hover) return new Color(55, 115, 195);
return new Color(45, 100, 175);
case CANCEL:
case DEFAULT:
default:
if (t == UITheme.DARK) {
if (pressed) return new Color(42, 42, 42);
if (hover) return new Color(68, 68, 68);
return new Color(56, 56, 56);
} else {
if (pressed) return new Color(210, 210, 212);
if (hover) return new Color(228, 228, 232);
return new Color(238, 238, 240);
}
}
}
public static Color getButtonFg(ButtonVariant variant, UITheme t) {
switch (variant) {
case PRIMARY:
case DANGER:
case ACCENT:
return Color.WHITE;
case CANCEL:
case DEFAULT:
default:
return t == UITheme.DARK ? new Color(235, 235, 235) : new Color(30, 30, 30);
}
}
// =========================================================================
// UIManager Defaults Configuration
// =========================================================================
public static void applyUIManagerDefaults(UITheme theme) {
Color bgMain = getBgMain(theme);
Color bgPanel = getBgPanel(theme);
Color bgComp = getBgComponent(theme);
Color fgMain = getFgMain(theme);
Color fgMuted = getFgMuted(theme);
Color border = getBorder(theme);
Color selBg = getSelectionBg(theme);
Color selFg = getSelectionFg(theme);
UIManager.put("Panel.background", bgPanel);
UIManager.put("Panel.foreground", fgMain);
UIManager.put("Label.foreground", fgMain);
UIManager.put("TextField.background", bgComp);
UIManager.put("TextField.foreground", fgMain);
UIManager.put("TextField.caretForeground", fgMain);
UIManager.put("TextField.selectionBackground", selBg);
UIManager.put("TextField.selectionForeground", selFg);
UIManager.put("TextArea.background", bgComp);
UIManager.put("TextArea.foreground", fgMain);
UIManager.put("TextArea.caretForeground", fgMain);
UIManager.put("TextArea.selectionBackground", selBg);
UIManager.put("TextArea.selectionForeground", selFg);
UIManager.put("Button.background", getBgComponentLighter(theme));
UIManager.put("Button.foreground", fgMain);
UIManager.put("Button.select", selBg);
UIManager.put("ComboBox.background", bgComp);
UIManager.put("ComboBox.foreground", fgMain);
UIManager.put("ComboBox.selectionBackground", selBg);
UIManager.put("ComboBox.selectionForeground", selFg);
UIManager.put("Table.background", bgComp);
UIManager.put("Table.foreground", fgMain);
UIManager.put("Table.selectionBackground", selBg);
UIManager.put("Table.selectionForeground", selFg);
UIManager.put("Table.gridColor", getTableGrid(theme));
UIManager.put("TableHeader.background", getTableHeaderBg(theme));
UIManager.put("TableHeader.foreground", getTableHeaderFg(theme));
UIManager.put("ScrollPane.background", bgPanel);
UIManager.put("Viewport.background", bgComp);
UIManager.put("MenuBar.background", getMenuBarBg(theme));
UIManager.put("MenuBar.foreground", getMenuBarFg(theme));
UIManager.put("Menu.background", getMenuBarBg(theme));
UIManager.put("Menu.foreground", getMenuBarFg(theme));
UIManager.put("Menu.selectionBackground", selBg);
UIManager.put("Menu.selectionForeground", selFg);
UIManager.put("PopupMenu.background", getMenuPopupBg(theme));
UIManager.put("PopupMenu.foreground", getMenuItemFg(theme));
UIManager.put("MenuItem.background", getMenuPopupBg(theme));
UIManager.put("MenuItem.foreground", getMenuItemFg(theme));
UIManager.put("MenuItem.selectionBackground", selBg);
UIManager.put("MenuItem.selectionForeground", selFg);
UIManager.put("CheckBoxMenuItem.background", getMenuPopupBg(theme));
UIManager.put("CheckBoxMenuItem.foreground", getMenuItemFg(theme));
UIManager.put("CheckBoxMenuItem.selectionBackground", selBg);
UIManager.put("CheckBoxMenuItem.selectionForeground", selFg);
UIManager.put("RadioButtonMenuItem.background", getMenuPopupBg(theme));
UIManager.put("RadioButtonMenuItem.foreground", getMenuItemFg(theme));
UIManager.put("RadioButtonMenuItem.selectionBackground", selBg);
UIManager.put("RadioButtonMenuItem.selectionForeground", selFg);
UIManager.put("Separator.background", border);
UIManager.put("Separator.foreground", border);
UIManager.put("TabbedPane.background", getTabBg(theme));
UIManager.put("TabbedPane.foreground", getTabFg(theme));
UIManager.put("TabbedPane.selected", getTabSelectedBg(theme));
UIManager.put("TabbedPane.selectHighlight", getTabSelectedBg(theme));
UIManager.put("CheckBox.background", bgPanel);
UIManager.put("CheckBox.foreground", fgMain);
UIManager.put("RadioButton.background", bgPanel);
UIManager.put("RadioButton.foreground", fgMain);
UIManager.put("TitledBorder.titleColor", fgMain);
UIManager.put("OptionPane.background", bgPanel);
UIManager.put("OptionPane.messageForeground", fgMain);
}
// =========================================================================
// Component Styling & Custom UI Delegates
// =========================================================================
/**
* Styles a button with custom rendering, ensuring consistent high contrast
* and proper background color across Windows, macOS, and Linux.
*/
public static JButton styleButton(JButton button, ButtonVariant variant) {
if (button == null) return null;
button.setUI(new StyledButtonUI(variant));
button.setFocusPainted(false);
button.setOpaque(false);
button.setContentAreaFilled(false);
button.setBorder(BorderFactory.createEmptyBorder(6, 14, 6, 14));
button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
return button;
}
public static JButton createStyledButton(String text, ButtonVariant variant, Runnable action) {
JButton btn = new JButton(text);
styleButton(btn, variant);
if (action != null) {
btn.addActionListener(e -> action.run());
}
return btn;
}
public static JTextField styleTextField(JTextField field) {
if (field == null) return null;
field.setBackground(getBgComponent());
field.setForeground(getFgMain());
field.setCaretColor(getFgMain());
field.setSelectionColor(getSelectionBg());
field.setSelectedTextColor(getSelectionFg());
field.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(getBorder(), 1, true),
new EmptyBorder(4, 8, 4, 8)));
return field;
}
public static JTextArea styleTextArea(JTextArea area) {
if (area == null) return null;
area.setBackground(getCodeAreaBg());
area.setForeground(getCodeAreaFg());
area.setCaretColor(getFgMain());
area.setSelectionColor(getSelectionBg());
area.setSelectedTextColor(getSelectionFg());
return area;
}
public static JComboBox<?> styleComboBox(JComboBox<?> box) {
if (box == null) return null;
box.setBackground(getBgComponent());
box.setForeground(getFgMain());
box.setRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
setBorder(new EmptyBorder(4, 8, 4, 8));
if (isSelected) {
setBackground(getSelectionBg());
setForeground(getSelectionFg());
} else {
setBackground(getBgComponent());
setForeground(getFgMain());
}
return this;
}
});
return box;
}
public static JTable styleTable(JTable table) {
if (table == null) return null;
table.setBackground(getBgComponent());
table.setForeground(getFgMain());
table.setSelectionBackground(getSelectionBg());
table.setSelectionForeground(getSelectionFg());
table.setGridColor(getTableGrid());
table.setRowHeight(24);
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) {
super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col);
setBorder(new EmptyBorder(2, 6, 2, 6));
if (isSel) {
setBackground(getSelectionBg());
setForeground(getSelectionFg());
} else {
setBackground(row % 2 == 0 ? getTableRowEven() : getTableRowOdd());
setForeground(getFgMain());
}
return this;
}
});
JTableHeader header = table.getTableHeader();
if (header != null) {
header.setBackground(getTableHeaderBg());
header.setForeground(getTableHeaderFg());
header.setFont(header.getFont().deriveFont(Font.BOLD));
header.setDefaultRenderer(new DefaultTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) {
super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col);
setBackground(getTableHeaderBg());
setForeground(getTableHeaderFg());
setFont(getFont().deriveFont(Font.BOLD));
setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 1, 1, getBorderColor()),
new EmptyBorder(4, 6, 4, 6)));
return this;
}
});
}
return table;
}
public static JTabbedPane styleTabbedPane(JTabbedPane tp) {
if (tp == null) return null;
tp.setUI(new StyledTabbedPaneUI());
tp.setBackground(getBgMain());
tp.setForeground(getFgMain());
return tp;
}
public static JMenuBar styleMenuBar(JMenuBar bar) {
if (bar == null) return null;
bar.setUI(new StyledMenuBarUI());
bar.setBackground(getMenuBarBg());
bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, getBorder()));
return bar;
}
public static JMenu styleMenu(JMenu menu) {
if (menu == null) return null;
menu.setUI(new StyledMenuUI());
menu.setForeground(getMenuBarFg());
menu.setBackground(getMenuBarBg());
menu.setOpaque(false);
return menu;
}
public static JMenuItem styleMenuItem(JMenuItem item) {
if (item == null) return null;
item.setUI(new StyledMenuItemUI());
item.setBackground(getMenuPopupBg());
item.setForeground(getMenuItemFg());
return item;
}
public static JPopupMenu stylePopupMenu(JPopupMenu popup) {
if (popup == null) return null;
popup.setUI(new StyledPopupMenuUI());
popup.setBackground(getMenuPopupBg());
popup.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(getBorder(), 1),
new EmptyBorder(4, 0, 4, 0)));
return popup;
}
public static JScrollPane styleScrollPane(JScrollPane sp) {
if (sp == null) return null;
sp.setBackground(getBgPanel());
if (sp.getViewport() != null) {
sp.getViewport().setBackground(getBgComponent());
}
sp.setBorder(new LineBorder(getBorder(), 1));
return sp;
}
public static JSpinner styleSpinner(JSpinner sp) {
if (sp == null) return null;
sp.setBackground(getBgComponent());
sp.setForeground(getFgMain());
sp.setBorder(new LineBorder(getBorder(), 1));
JComponent editor = sp.getEditor();
if (editor instanceof JSpinner.DefaultEditor) {
JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField();
tf.setBackground(getBgComponent());
tf.setForeground(getFgMain());
tf.setCaretColor(getFgMain());
tf.setBorder(new EmptyBorder(2, 4, 2, 4));
}
return sp;
}
public static JCheckBox styleCheckBox(JCheckBox cb) {
if (cb == null) return null;
cb.setOpaque(false);
cb.setForeground(getFgMain());
cb.setFocusPainted(false);
return cb;
}
public static JRadioButton styleRadioButton(JRadioButton rb) {
if (rb == null) return null;
rb.setOpaque(false);
rb.setForeground(getFgMain());
rb.setFocusPainted(false);
return rb;
}
public static Border createTitledBorder(String title) {
return BorderFactory.createTitledBorder(
new LineBorder(getBorder(), 1),
title,
TitledBorder.DEFAULT_JUSTIFICATION,
TitledBorder.DEFAULT_POSITION,
new Font(Font.SANS_SERIF, Font.BOLD, 12),
getFgMain());
}
// =========================================================================
// Recursive Component Styling
// =========================================================================
public static void applyTheme(Component comp) {
applyTheme(comp, currentTheme);
}
public static void applyTheme(Component comp, UITheme theme) {
if (comp == null) return;
// Skip swatch preview panels whose background is the user-configured color
if ("colorSwatch".equals(comp.getName())) {
return;
}
if (comp instanceof StatusBar) {
((StatusBar) comp).applyTheme(theme);
return;
}
if (comp instanceof TerminalPanel) {
comp.repaint();
return;
}
if (comp instanceof JMenuBar) {
styleMenuBar((JMenuBar) comp);
for (int i = 0; i < ((JMenuBar) comp).getMenuCount(); i++) {
applyTheme(((JMenuBar) comp).getMenu(i), theme);
}
return;
}
if (comp instanceof JMenu) {
styleMenu((JMenu) comp);
JMenu menu = (JMenu) comp;
for (int i = 0; i < menu.getItemCount(); i++) {
JMenuItem item = menu.getItem(i);
if (item != null) applyTheme(item, theme);
}
return;
}
if (comp instanceof JMenuItem) {
styleMenuItem((JMenuItem) comp);
return;
}
if (comp instanceof JPopupMenu) {
stylePopupMenu((JPopupMenu) comp);
for (Component child : ((JPopupMenu) comp).getComponents()) {
applyTheme(child, theme);
}
return;
}
if (comp instanceof JTabbedPane) {
styleTabbedPane((JTabbedPane) comp);
JTabbedPane tp = (JTabbedPane) comp;
for (int i = 0; i < tp.getTabCount(); i++) {
applyTheme(tp.getComponentAt(i), theme);
}
return;
}
if (comp instanceof JTable) {
styleTable((JTable) comp);
return;
}
if (comp instanceof JScrollPane) {
styleScrollPane((JScrollPane) comp);
JScrollPane sp = (JScrollPane) comp;
if (sp.getViewport() != null) {
applyTheme(sp.getViewport().getView(), theme);
}
return;
}
if (comp instanceof JButton) {
JButton btn = (JButton) comp;
if (!(btn.getUI() instanceof StyledButtonUI)) {
styleButton(btn, ButtonVariant.DEFAULT);
}
return;
}
if (comp instanceof JComboBox) {
styleComboBox((JComboBox<?>) comp);
return;
}
if (comp instanceof JSpinner) {
styleSpinner((JSpinner) comp);
return;
}
if (comp instanceof JTextArea) {
styleTextArea((JTextArea) comp);
return;
}
if (comp instanceof JTextField) {
styleTextField((JTextField) comp);
return;
}
if (comp instanceof JCheckBox) {
styleCheckBox((JCheckBox) comp);
return;
}
if (comp instanceof JRadioButton) {
styleRadioButton((JRadioButton) comp);
return;
}
if (comp instanceof JLabel) {
comp.setForeground(getFgMain(theme));
return;
}
if (comp instanceof JPanel) {
JPanel panel = (JPanel) comp;
if (panel.isOpaque()) {
panel.setBackground(getBgPanel(theme));
}
panel.setForeground(getFgMain(theme));
Border b = panel.getBorder();
if (b instanceof TitledBorder) {
TitledBorder tb = (TitledBorder) b;
tb.setTitleColor(getFgMain(theme));
tb.setBorder(new LineBorder(getBorder(theme), 1));
}
} else if (comp instanceof Container) {
comp.setBackground(getBgMain(theme));
comp.setForeground(getFgMain(theme));
}
if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
applyTheme(child, theme);
}
}
}
public static void applyThemeToWindow(Window window) {
if (window == null) return;
window.setBackground(getBgMain());
if (window instanceof RootPaneContainer) {
RootPaneContainer rpc = (RootPaneContainer) window;
if (rpc.getContentPane() != null) {
rpc.getContentPane().setBackground(getBgMain());
applyTheme(rpc.getContentPane());
}
if (rpc.getRootPane() != null && rpc.getRootPane().getJMenuBar() != null) {
applyTheme(rpc.getRootPane().getJMenuBar());
}
}
window.repaint();
}
// =========================================================================
// Custom UI Implementations
// =========================================================================
public static class StyledButtonUI extends BasicButtonUI {
private final ButtonVariant variant;
private boolean hover = false;
public StyledButtonUI(ButtonVariant variant) {
this.variant = variant != null ? variant : ButtonVariant.DEFAULT;
}
@Override
public void installUI(JComponent c) {
super.installUI(c);
AbstractButton b = (AbstractButton) c;
b.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
hover = true;
b.repaint();
}
@Override
public void mouseExited(MouseEvent e) {
hover = false;
b.repaint();
}
});
}
@Override
public void paint(Graphics g, JComponent c) {
AbstractButton b = (AbstractButton) c;
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int w = c.getWidth();
int h = c.getHeight();
ButtonModel model = b.getModel();
boolean pressed = model.isArmed() && model.isPressed();
boolean enabled = b.isEnabled();
UITheme t = ThemeManager.getTheme();
Color bg = enabled ? ThemeManager.getButtonBg(variant, t, hover, pressed) : (t == UITheme.DARK ? new Color(40, 40, 40) : new Color(220, 220, 220));
Color fg = enabled ? ThemeManager.getButtonFg(variant, t) : ThemeManager.getFgDisabled(t);
// Background
g2.setColor(bg);
g2.fillRoundRect(0, 0, w, h, 6, 6);
// Border
if (variant == ButtonVariant.DEFAULT || variant == ButtonVariant.CANCEL) {
g2.setColor(ThemeManager.getBorder(t));
g2.drawRoundRect(0, 0, w - 1, h - 1, 6, 6);
}
// Text / Icon
FontMetrics fm = g2.getFontMetrics(b.getFont());
String text = b.getText();
if (text != null && !text.isEmpty()) {
g2.setColor(fg);
g2.setFont(b.getFont());
int tx = (w - fm.stringWidth(text)) / 2;
int ty = (h - fm.getHeight()) / 2 + fm.getAscent();
g2.drawString(text, tx, ty);
}
g2.dispose();
}
}
public static class StyledMenuBarUI extends BasicMenuBarUI {
@Override
public void paint(Graphics g, JComponent c) {
g.setColor(ThemeManager.getMenuBarBg());
g.fillRect(0, 0, c.getWidth(), c.getHeight());
}
}
public static class StyledMenuUI extends BasicMenuUI {
@Override
protected void paintBackground(Graphics g, JMenuItem menuItem, Color bgColor) {
ButtonModel model = menuItem.getModel();
if (model.isArmed() || model.isSelected()) {
g.setColor(ThemeManager.getMenuItemHoverBg());
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
} else {
g.setColor(ThemeManager.getMenuBarBg());
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
}
}
@Override
protected void paintText(Graphics g, JMenuItem menuItem, Rectangle textRect, String text) {
ButtonModel model = menuItem.getModel();
if (model.isArmed() || model.isSelected()) {
g.setColor(ThemeManager.getMenuItemHoverFg());
} else {
g.setColor(ThemeManager.getMenuBarFg());
}
super.paintText(g, menuItem, textRect, text);
}
}
public static class StyledMenuItemUI extends BasicMenuItemUI {
@Override
protected void paintBackground(Graphics g, JMenuItem menuItem, Color bgColor) {
ButtonModel model = menuItem.getModel();
if (model.isArmed() || model.isSelected()) {
g.setColor(ThemeManager.getSelectionBg());
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
} else {
g.setColor(ThemeManager.getMenuPopupBg());
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
}
}
@Override
protected void paintText(Graphics g, JMenuItem menuItem, Rectangle textRect, String text) {
ButtonModel model = menuItem.getModel();
if (model.isArmed() || model.isSelected()) {
g.setColor(ThemeManager.getSelectionFg());
} else {
g.setColor(ThemeManager.getMenuItemFg());
}
super.paintText(g, menuItem, textRect, text);
}
}
public static class StyledPopupMenuUI extends BasicPopupMenuUI {
@Override
public void paint(Graphics g, JComponent c) {
g.setColor(ThemeManager.getMenuPopupBg());
g.fillRect(0, 0, c.getWidth(), c.getHeight());
}
}
public static class StyledTabbedPaneUI extends BasicTabbedPaneUI {
@Override
protected void installDefaults() {
super.installDefaults();
tabInsets = new Insets(6, 16, 6, 16);
selectedTabPadInsets = new Insets(2, 2, 2, 2);
tabAreaInsets = new Insets(4, 4, 0, 4);
}
@Override
protected void paintTabBackground(Graphics g, int tabPlacement, int tabIndex, int x, int y, int w, int h, boolean isSelected) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(isSelected ? ThemeManager.getTabSelectedBg() : ThemeManager.getTabBg());
g2.fillRoundRect(x, y, w, h + 4, 6, 6);
g2.setColor(ThemeManager.getBorder());
g2.drawRoundRect(x, y, w - 1, h + 4, 6, 6);
g2.dispose();
}
@Override
protected void paintText(Graphics g, int tabPlacement, Font font, FontMetrics metrics, int tabIndex, String title, Rectangle textRect, boolean isSelected) {
g.setFont(font);
g.setColor(isSelected ? ThemeManager.getTabSelectedFg() : ThemeManager.getTabFg());
g.drawString(title, textRect.x, textRect.y + metrics.getAscent());
}
@Override
protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {
int width = tabPane.getWidth();
int height = tabPane.getHeight();
Insets insets = tabPane.getInsets();
int x = insets.left;
int y = insets.top + calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
int w = width - insets.right - insets.left;
int h = height - insets.top - insets.bottom - calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
g.setColor(ThemeManager.getTabSelectedBg());
g.fillRect(x, y, w, h);
g.setColor(ThemeManager.getBorder());
g.drawRect(x, y, w - 1, h - 1);
}
}
}
@@ -0,0 +1,33 @@
package haus.nightmare.j3270.ui;
/**
* Supported UI themes for the j3270 Java desktop interface.
*/
public enum UITheme {
DARK("Dark Mode"),
LIGHT("Light Mode");
private final String displayName;
UITheme(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
@Override
public String toString() {
return displayName;
}
public static UITheme fromString(String name) {
if (name == null) return DARK;
String s = name.trim().toUpperCase();
if ("LIGHT".equals(s) || "LIGHT MODE".equals(s) || "LIGHT_MODE".equals(s)) {
return LIGHT;
}
return DARK;
}
}
@@ -1,4 +1,4 @@
package org.pubvm.j3270.ui; package haus.nightmare.j3270.ui;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
@@ -26,25 +26,24 @@ public class UntrustedCertificateDialog extends JDialog {
private void buildUI(String host, int port, X509Certificate[] chain, CertificateException exception) { private void buildUI(String host, int port, X509Certificate[] chain, CertificateException exception) {
JPanel mainPanel = new JPanel(new BorderLayout(12, 12)); JPanel mainPanel = new JPanel(new BorderLayout(12, 12));
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16)); mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
mainPanel.setBackground(new Color(30, 30, 30));
// Header // Header
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
headerPanel.setBackground(new Color(30, 30, 30)); headerPanel.setOpaque(false);
JLabel iconLabel = new JLabel("⚠️"); JLabel iconLabel = new JLabel("⚠️");
iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28)); iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28));
headerPanel.add(iconLabel); headerPanel.add(iconLabel);
JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2)); JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2));
titleBox.setBackground(new Color(30, 30, 30)); titleBox.setOpaque(false);
JLabel titleLabel = new JLabel("Untrusted SSL Certificate"); JLabel titleLabel = new JLabel("Untrusted SSL Certificate");
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
titleLabel.setForeground(new Color(255, 180, 80)); titleLabel.setForeground(ThemeManager.getOiaFgWarn());
titleBox.add(titleLabel); titleBox.add(titleLabel);
JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified."); 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.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
subtitleLabel.setForeground(new Color(180, 180, 180)); subtitleLabel.setForeground(ThemeManager.getFgMuted());
titleBox.add(subtitleLabel); titleBox.add(subtitleLabel);
headerPanel.add(titleBox); headerPanel.add(titleBox);
mainPanel.add(headerPanel, BorderLayout.NORTH); mainPanel.add(headerPanel, BorderLayout.NORTH);
@@ -71,30 +70,27 @@ public class UntrustedCertificateDialog extends JDialog {
JTextArea detailsArea = new JTextArea(sb.toString()); JTextArea detailsArea = new JTextArea(sb.toString());
detailsArea.setEditable(false); detailsArea.setEditable(false);
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
detailsArea.setBackground(new Color(20, 20, 20)); ThemeManager.styleTextArea(detailsArea);
detailsArea.setForeground(new Color(210, 210, 210));
detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
JScrollPane scrollPane = new JScrollPane(detailsArea); JScrollPane scrollPane = new JScrollPane(detailsArea);
scrollPane.setPreferredSize(new Dimension(520, 260)); scrollPane.setPreferredSize(new Dimension(520, 260));
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60))); ThemeManager.styleScrollPane(scrollPane);
mainPanel.add(scrollPane, BorderLayout.CENTER); mainPanel.add(scrollPane, BorderLayout.CENTER);
// Buttons // Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
buttonPanel.setBackground(new Color(30, 30, 30)); buttonPanel.setOpaque(false);
JButton cancelBtn = new JButton("Cancel Connection"); JButton cancelBtn = new JButton("Cancel Connection");
cancelBtn.setBackground(new Color(60, 60, 60)); ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
cancelBtn.setForeground(new Color(220, 220, 220));
cancelBtn.addActionListener(e -> { cancelBtn.addActionListener(e -> {
accepted = false; accepted = false;
dispose(); dispose();
}); });
JButton trustBtn = new JButton("Connect Anyway"); JButton trustBtn = new JButton("Connect Anyway");
trustBtn.setBackground(new Color(180, 100, 40)); ThemeManager.styleButton(trustBtn, ThemeManager.ButtonVariant.DANGER);
trustBtn.setForeground(Color.WHITE);
trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13)); trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
trustBtn.addActionListener(e -> { trustBtn.addActionListener(e -> {
accepted = true; accepted = true;
@@ -106,6 +102,7 @@ public class UntrustedCertificateDialog extends JDialog {
mainPanel.add(buttonPanel, BorderLayout.SOUTH); mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel); setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
getRootPane().setDefaultButton(trustBtn); getRootPane().setDefaultButton(trustBtn);
} }
@@ -1,601 +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);
String initialHost = lastHost != null && !lastHost.isEmpty() ? lastHost : org.pubvm.j3270.config.Settings.getAutoConnectHost();
int initialPort = lastPort > 0 ? lastPort : org.pubvm.j3270.config.Settings.getAutoConnectPort();
dialog.setInitialHost(initialHost);
dialog.setInitialPort(initialPort);
dialog.setInitialTls(org.pubvm.j3270.config.Settings.getAutoConnectTls());
dialog.setInitialVerifyCert(org.pubvm.j3270.config.Settings.getAutoConnectVerifyCert());
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());
org.pubvm.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
org.pubvm.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
// Disconnect existing connection
disconnect();
// Wire interactive certificate verifier if none configured
if (config.getCertificateVerifier() == null) {
config.setCertificateVerifier((chain, authType, exception) ->
org.pubvm.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);
String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
// 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;
boolean cliTls = false;
boolean cliNoVerifyCert = false;
org.lib3270j.graphics.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 (args[i].startsWith("--graphics=")) {
cliGraphicsMode = org.lib3270j.graphics.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 = org.lib3270j.graphics.GraphicsMode.fromString(args[++i]);
} else if ("--no-graphics".equals(args[i])) {
cliGraphicsMode = org.lib3270j.graphics.GraphicsMode.NONE;
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) {
configFile = args[++i];
} else {
remainingArgs.add(args[i]);
}
}
// Configure logging for application packages
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("org.lib3270j") || name.startsWith("org.pubvm.j3270"));
};
ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(logLevel);
consoleHandler.setFormatter(new SimpleFormatter());
consoleHandler.setFilter(appFilter);
globalRoot.addHandler(consoleHandler);
Logger.getLogger("org.pubvm").setLevel(logLevel);
Logger.getLogger("org.pubvm.j3270").setLevel(logLevel);
Logger.getLogger("org.lib3270j").setLevel(logLevel);
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());
}
// 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");
final boolean finalTls = cliTls;
final boolean finalNoVerify = cliNoVerifyCert;
final org.lib3270j.graphics.GraphicsMode finalGraphicsMode = cliGraphicsMode;
SwingUtilities.invokeLater(() -> {
J3270App app = new J3270App();
app.setVisible(true);
// If host:port given on command line, connect directly
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 (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode);
} else {
config.setGraphicsMode(org.pubvm.j3270.config.Settings.getGraphicsMode());
}
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();
boolean tls = org.pubvm.j3270.config.Settings.getAutoConnectTls();
boolean verify = org.pubvm.j3270.config.Settings.getAutoConnectVerifyCert();
if (host != null && !host.isEmpty()) {
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
config.setTlsVerifyCert(verify);
if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode);
} else {
config.setGraphicsMode(org.pubvm.j3270.config.Settings.getGraphicsMode());
}
app.connect(config);
} else {
SwingUtilities.invokeLater(app::showConnectDialog);
}
break;
case DO_NOTHING:
default:
break;
}
}
});
}
}
@@ -1,368 +0,0 @@
package org.pubvm.j3270.config;
import java.util.prefs.Preferences;
import java.awt.Color;
import java.io.*;
import java.io.PrintWriter;
import java.util.logging.Logger;
import java.util.logging.Level;
public class Settings {
private static final Preferences prefs = Preferences.userNodeForPackage(Settings.class);
private static final Logger log = Logger.getLogger(Settings.class.getName());
public enum StartupBehavior {
SHOW_CONNECT,
DO_NOTHING,
AUTO_CONNECT
}
public static String getFontFamily() {
return prefs.get("fontFamily", "Monospaced");
}
public static void setFontFamily(String family) {
prefs.put("fontFamily", family);
}
public static int getFontSize() {
return prefs.getInt("fontSize", 16);
}
public static void setFontSize(int size) {
prefs.putInt("fontSize", size);
}
public static StartupBehavior getStartupBehavior() {
String behaviorStr = prefs.get("startupBehavior", StartupBehavior.SHOW_CONNECT.name());
try {
return StartupBehavior.valueOf(behaviorStr);
} catch (IllegalArgumentException e) {
return StartupBehavior.SHOW_CONNECT;
}
}
public static void setStartupBehavior(StartupBehavior behavior) {
prefs.put("startupBehavior", behavior.name());
}
public static String getAutoConnectHost() {
return prefs.get("autoConnectHost", "");
}
public static void setAutoConnectHost(String host) {
prefs.put("autoConnectHost", host);
}
public static int getAutoConnectPort() {
return prefs.getInt("autoConnectPort", 23);
}
public static void setAutoConnectPort(int port) {
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 org.lib3270j.graphics.GraphicsMode getGraphicsMode() {
String modeStr = prefs.get("graphicsMode", org.lib3270j.graphics.GraphicsMode.NONE.name());
return org.lib3270j.graphics.GraphicsMode.fromString(modeStr);
}
public static void setGraphicsMode(org.lib3270j.graphics.GraphicsMode mode) {
prefs.put("graphicsMode", (mode != null ? mode : org.lib3270j.graphics.GraphicsMode.NONE).name());
}
public static Color getColorOverride(int index, Color defaultColor) {
String hex = prefs.get("color_" + index, null);
try {
return hex != null ? Color.decode(hex) : defaultColor;
} catch (NumberFormatException e) {
return defaultColor;
}
}
public static void setColorOverride(int index, Color color) {
prefs.put("color_" + index, String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue()));
}
public static Color getMonoColorOverride(String key, Color defaultColor) {
String hex = prefs.get("mono_" + key, null);
try {
return hex != null ? Color.decode(hex) : defaultColor;
} catch (NumberFormatException e) {
return defaultColor;
}
}
public static void setMonoColorOverride(String key, Color color) {
prefs.put("mono_" + key, String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue()));
}
public static String getKeyBinding(String action, String defaultBinding) {
return prefs.get("key_" + action, defaultBinding);
}
public static void setKeyBinding(String action, String binding) {
prefs.put("key_" + action, binding);
}
/**
* Load settings from an INI-style configuration file.
* Sections: [appearance], [behavior], [colors], [keybindings]
*
* Example file:
* <pre>
* [appearance]
* fontFamily = IBM 3270
* fontSize = 18
*
* [behavior]
* startupBehavior = AUTO_CONNECT
* autoConnectHost = mainframe.example.com
* autoConnectPort = 23
*
* [colors]
* ; Host colors 0-15
* color_0 = #000000
* color_4 = #00ff00
* ; Monochrome / background
* mono_NORMAL = #32cd32
* mono_BACKGROUND = #0a0a0a
*
* [keybindings]
* ; Comma-separated for multiple bindings
* PF12 = F12, shift F12
* PF24 = UNBOUND
* CLEAR = alt C
* </pre>
*/
public static void loadFromIniFile(String path) throws IOException {
File file = new File(path);
if (!file.exists()) {
throw new FileNotFoundException("Config file not found: " + path);
}
log.info("Loading configuration from: " + path);
String currentSection = "";
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
int lineNum = 0;
while ((line = reader.readLine()) != null) {
lineNum++;
line = line.trim();
// Skip empty lines and comments
if (line.isEmpty() || line.startsWith(";") || line.startsWith("#")) {
continue;
}
// Section header
if (line.startsWith("[") && line.endsWith("]")) {
currentSection = line.substring(1, line.length() - 1).toLowerCase().trim();
continue;
}
// Key = Value
int eq = line.indexOf('=');
if (eq < 0) {
log.warning("Config line " + lineNum + ": no '=' found, skipping: " + line);
continue;
}
String key = line.substring(0, eq).trim();
String value = line.substring(eq + 1).trim();
try {
applyConfigEntry(currentSection, key, value);
} catch (Exception e) {
log.log(Level.WARNING, "Config line " + lineNum + ": error applying " + key + "=" + value, e);
}
}
}
log.info("Configuration loaded successfully.");
}
// ========== Block select mode ==========
public static boolean getBlockSelectMode() {
return prefs.getBoolean("blockSelectMode", false);
}
public static void setBlockSelectMode(boolean block) {
prefs.putBoolean("blockSelectMode", block);
}
private static void applyConfigEntry(String section, String key, String value) {
switch (section) {
case "appearance":
switch (key) {
case "fontFamily": setFontFamily(value); break;
case "fontSize": setFontSize(Integer.parseInt(value)); break;
default:
log.warning("Unknown appearance key: " + key);
}
break;
case "behavior":
case "connection":
switch (key) {
case "startupBehavior":
setStartupBehavior(StartupBehavior.valueOf(value.toUpperCase()));
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 "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
default:
log.warning("Unknown behavior/connection key: " + key);
}
break;
case "colors":
if (key.startsWith("color_")) {
int index = Integer.parseInt(key.substring(6));
setColorOverride(index, Color.decode(value));
} else if (key.startsWith("mono_")) {
String monoKey = key.substring(5);
setMonoColorOverride(monoKey, Color.decode(value));
} else {
log.warning("Unknown colors key: " + key);
}
break;
case "keybindings":
setKeyBinding(key, value);
break;
case "graphics":
if ("graphicsMode".equalsIgnoreCase(key) || "mode".equalsIgnoreCase(key)) {
setGraphicsMode(org.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);
prefs.put(key, value);
break;
}
}
// ========== Export to INI ==========
/**
* Export the current configuration to an INI-style file.
* Writes all settings including appearance, behavior, colors, and keybindings.
*/
public static void exportToIniFile(String path) throws IOException {
try (PrintWriter w = new PrintWriter(new BufferedWriter(new FileWriter(path)))) {
w.println("; j3270 Configuration File");
w.println("; Exported on " + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()));
w.println("; Load with: java -jar j3270.jar -c " + new File(path).getName());
w.println();
// [appearance]
w.println("[appearance]");
w.println("fontFamily = " + getFontFamily());
w.println("fontSize = " + getFontSize());
w.println();
// [behavior]
w.println("[behavior]");
w.println("startupBehavior = " + getStartupBehavior().name());
String acHost = getAutoConnectHost();
if (acHost != null && !acHost.isEmpty()) {
w.println("autoConnectHost = " + acHost);
w.println("autoConnectPort = " + getAutoConnectPort());
w.println("autoConnectTls = " + getAutoConnectTls());
w.println("autoConnectVerifyCert = " + getAutoConnectVerifyCert());
}
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
for (int i = 0; i < 16; i++) {
String hex = prefs.get("color_" + i, null);
if (hex != null) {
w.println("color_" + i + " = " + hex);
}
}
// Mono colors
String[] monoKeys = {"NORMAL", "INTENSIFY", "PROTECTED", "PROTECTED_HIGH", "BACKGROUND"};
for (String mk : monoKeys) {
String hex = prefs.get("mono_" + mk, null);
if (hex != null) {
w.println("mono_" + mk + " = " + hex);
}
}
w.println();
// [keybindings]
w.println("[keybindings]");
// Navigation keys
String[] navActions = {"ENTER", "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
"HOME", "END", "PAGE_UP", "PAGE_DOWN", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE", "CLEAR"};
for (String act : navActions) {
String val = prefs.get("key_" + act, null);
if (val != null) {
w.println(act + " = " + val);
}
}
// PF keys
for (int i = 1; i <= 24; i++) {
String val = prefs.get("key_PF" + i, null);
if (val != null) {
w.println("PF" + i + " = " + val);
}
}
// PA keys
for (int i = 1; i <= 3; i++) {
String val = prefs.get("key_PA" + i, null);
if (val != null) {
w.println("PA" + i + " = " + val);
}
}
}
log.info("Configuration exported to: " + path);
}
}
@@ -1,305 +0,0 @@
package org.pubvm.j3270.ft;
import org.lib3270j.ft.FTConfig;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.io.File;
public class FileTransferDialog extends JDialog {
private final FileTransfer coordinator;
private final Frame owner;
private JComboBox<FTConfig.HostType> hostTypeCombo;
private JRadioButton sendRadio;
private JRadioButton receiveRadio;
private JTextField localFileField;
private JButton browseLocalButton;
private JTextField hostFileField;
// Options
private JRadioButton asciiRadio;
private JRadioButton binaryRadio;
private JCheckBox crCheck;
private JCheckBox remapCheck;
private JCheckBox appendCheck;
private JCheckBox overwriteCheck;
// TSO specific
private JTextField recfmField;
private JTextField lreclField;
private JTextField blksizeField;
private JTextField spaceField;
// VM specific
private JTextField optionsField;
private JButton transferButton;
private JButton cancelButton;
public FileTransferDialog(Frame owner, FileTransfer coordinator) {
super(owner, "File Transfer (IND$FILE)", false); // non-modal to see progress
this.owner = owner;
this.coordinator = coordinator;
buildUI();
pack();
setLocationRelativeTo(owner);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
mainPanel.setBackground(new Color(25, 25, 25));
// Create form panel
JPanel formPanel = new JPanel();
formPanel.setLayout(new BoxLayout(formPanel, BoxLayout.Y_AXIS));
formPanel.setOpaque(false);
// Host Type
hostTypeCombo = new JComboBox<>(FTConfig.HostType.values());
formPanel.add(createRow("Host Environment:", hostTypeCombo));
// Direction
sendRadio = new JRadioButton("Send to Host");
receiveRadio = new JRadioButton("Receive from Host");
receiveRadio.setSelected(true);
ButtonGroup dirGroup = new ButtonGroup();
dirGroup.add(sendRadio);
dirGroup.add(receiveRadio);
JPanel dirPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
dirPanel.setOpaque(false);
dirPanel.add(receiveRadio);
dirPanel.add(Box.createRigidArea(new Dimension(15, 0)));
dirPanel.add(sendRadio);
// Disable append/overwrite when sending
sendRadio.addActionListener(e -> updateOptionStates());
receiveRadio.addActionListener(e -> updateOptionStates());
formPanel.add(createRow("Direction:", dirPanel));
// Local File
localFileField = new JTextField(20);
browseLocalButton = new JButton("Browse...");
browseLocalButton.addActionListener(e -> browseLocalFile());
JPanel localPanel = new JPanel(new BorderLayout(5, 0));
localPanel.setOpaque(false);
localPanel.add(localFileField, BorderLayout.CENTER);
localPanel.add(browseLocalButton, BorderLayout.EAST);
formPanel.add(createRow("Local File:", localPanel));
// Host File
hostFileField = new JTextField(20);
formPanel.add(createRow("Host File:", hostFileField));
// Mode
asciiRadio = new JRadioButton("ASCII");
asciiRadio.setSelected(true);
binaryRadio = new JRadioButton("Binary");
ButtonGroup modeGroup = new ButtonGroup();
modeGroup.add(asciiRadio);
modeGroup.add(binaryRadio);
JPanel modePanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
modePanel.setOpaque(false);
modePanel.add(asciiRadio);
modePanel.add(Box.createRigidArea(new Dimension(15, 0)));
modePanel.add(binaryRadio);
asciiRadio.addActionListener(e -> updateOptionStates());
binaryRadio.addActionListener(e -> updateOptionStates());
formPanel.add(createRow("Transfer Mode:", modePanel));
// Options (checkboxes)
crCheck = new JCheckBox("Add/Remove CR");
crCheck.setSelected(true);
remapCheck = new JCheckBox("Remap Character Set");
remapCheck.setSelected(true);
appendCheck = new JCheckBox("Append to file");
overwriteCheck = new JCheckBox("Overwrite existing");
JPanel optionsPanel1 = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
optionsPanel1.setOpaque(false);
optionsPanel1.add(crCheck);
optionsPanel1.add(Box.createRigidArea(new Dimension(15, 0)));
optionsPanel1.add(remapCheck);
JPanel optionsPanel2 = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
optionsPanel2.setOpaque(false);
optionsPanel2.add(appendCheck);
optionsPanel2.add(Box.createRigidArea(new Dimension(15, 0)));
optionsPanel2.add(overwriteCheck);
formPanel.add(createRow("Text Options:", optionsPanel1));
formPanel.add(createRow("File Options:", optionsPanel2));
// Host-specific options panel (TSO dataset allocation)
JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5));
hostOptsPanel.setOpaque(false);
hostOptsPanel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createLineBorder(new Color(60, 60, 60)),
"TSO Allocation Options (Send Only)"));
((javax.swing.border.TitledBorder)hostOptsPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
hostOptsPanel.add(new JLabel("RECFM:"));
recfmField = new JTextField(5);
hostOptsPanel.add(recfmField);
hostOptsPanel.add(new JLabel("LRECL:"));
lreclField = new JTextField(5);
hostOptsPanel.add(lreclField);
hostOptsPanel.add(new JLabel("BLKSIZE:"));
blksizeField = new JTextField(5);
hostOptsPanel.add(blksizeField);
hostOptsPanel.add(new JLabel("SPACE:"));
spaceField = new JTextField(15);
hostOptsPanel.add(spaceField);
formPanel.add(Box.createRigidArea(new Dimension(0, 10)));
formPanel.add(hostOptsPanel);
// CMS options
JPanel vmOptsPanel = new JPanel(new BorderLayout(5, 0));
vmOptsPanel.setOpaque(false);
vmOptsPanel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createLineBorder(new Color(60, 60, 60)),
"CMS Options"));
((javax.swing.border.TitledBorder)vmOptsPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
optionsField = new JTextField(20);
vmOptsPanel.add(new JLabel("Additional Options: "), BorderLayout.WEST);
vmOptsPanel.add(optionsField, BorderLayout.CENTER);
formPanel.add(Box.createRigidArea(new Dimension(0, 10)));
formPanel.add(vmOptsPanel);
mainPanel.add(formPanel, BorderLayout.CENTER);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
buttonPanel.setOpaque(false);
transferButton = new JButton("Start Transfer");
transferButton.addActionListener(e -> startTransfer());
cancelButton = new JButton("Close");
cancelButton.addActionListener(e -> dispose());
buttonPanel.add(cancelButton);
buttonPanel.add(transferButton);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
// Theme styling for components
applyTheme(mainPanel);
// Initialize state
updateOptionStates();
}
private void updateOptionStates() {
boolean isSend = sendRadio.isSelected();
boolean isAscii = asciiRadio.isSelected();
appendCheck.setEnabled(!isSend);
overwriteCheck.setEnabled(!isSend);
crCheck.setEnabled(isAscii);
remapCheck.setEnabled(isAscii);
recfmField.setEnabled(isSend);
lreclField.setEnabled(isSend);
blksizeField.setEnabled(isSend);
spaceField.setEnabled(isSend);
}
private JPanel createRow(String labelText, Component comp) {
JPanel row = new JPanel(new BorderLayout(10, 0));
row.setOpaque(false);
JLabel label = new JLabel(labelText);
label.setPreferredSize(new Dimension(130, 25));
row.add(label, BorderLayout.WEST);
row.add(comp, BorderLayout.CENTER);
row.setBorder(new EmptyBorder(0, 0, 5, 0));
return row;
}
private void browseLocalFile() {
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 startTransfer() {
if (localFileField.getText().trim().isEmpty() || hostFileField.getText().trim().isEmpty()) {
JOptionPane.showMessageDialog(this, "Local and Host filenames are required.",
"Error", JOptionPane.ERROR_MESSAGE);
return;
}
FTConfig config = new FTConfig();
config.setHostType((FTConfig.HostType) hostTypeCombo.getSelectedItem());
config.setLocalFilename(localFileField.getText().trim());
config.setHostFilename(hostFileField.getText().trim());
config.setReceive(receiveRadio.isSelected());
config.setAscii(asciiRadio.isSelected());
config.setCrFlag(crCheck.isSelected());
config.setRemapFlag(remapCheck.isSelected());
config.setAppend(appendCheck.isSelected());
config.setOverwrite(overwriteCheck.isSelected());
config.setRecfm(recfmField.getText().trim());
config.setLrecl(lreclField.getText().trim());
config.setBlksize(blksizeField.getText().trim());
config.setSpace(spaceField.getText().trim());
config.setOptions(optionsField.getText().trim());
String error = coordinator.startTransfer(config);
if (error != null) {
JOptionPane.showMessageDialog(this, error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
} else {
// Success, close dialog (progress will be shown separately)
dispose();
}
}
private void applyTheme(Container container) {
Color fg = new Color(200, 200, 200);
Color bg = new Color(40, 40, 40);
for (Component c : container.getComponents()) {
c.setForeground(fg);
if (c instanceof JTextField || c instanceof JComboBox) {
c.setBackground(bg);
if (c instanceof JTextField) {
((JTextField) c).setCaretColor(fg);
((JTextField) c).setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(60, 60, 60)),
new EmptyBorder(2, 5, 2, 5)));
}
} else if (c instanceof JButton) {
c.setBackground(new Color(50, 50, 50));
c.setFocusable(false);
} else if (c instanceof JRadioButton || c instanceof JCheckBox) {
((JComponent) c).setOpaque(false);
c.setFocusable(false);
}
if (c instanceof Container) {
applyTheme((Container) c);
}
}
}
}
@@ -1,246 +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 JComboBox<org.lib3270j.graphics.GraphicsMode> graphicsCombo;
private JTextField luField;
private JCheckBox tlsCheckBox;
private JCheckBox verifyCertCheckBox;
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<>(org.lib3270j.graphics.GraphicsMode.values());
graphicsCombo.setSelectedItem(org.pubvm.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);
// TLS / SSL Checkbox
gbc.gridx = 1;
gbc.gridy = 5;
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 = 6;
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);
// 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 = 7;
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((org.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
result.setUseTls(tlsCheckBox.isSelected());
result.setTlsVerifyCert(verifyCertCheckBox.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);
}
}
@@ -1,164 +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 tlsStatus;
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);
tlsStatus = createLabel("", oiaFont, OIA_FG);
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(10));
add(tlsStatus);
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;
}
// 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.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));
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,102 @@
package haus.nightmare.j3270.ft;
import haus.nightmare.lib3270j.ft.FTConfig;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.awt.HeadlessException;
import static org.junit.jupiter.api.Assertions.*;
public class FileTransferSessionAndCmsTest {
@BeforeEach
@AfterEach
public void cleanup() {
FileTransferDialog.resetSessionState();
}
@Test
public void testCmsFilenamePeriodReplacement() {
// Test replacing periods with spaces for CMS
assertEquals("fscms exec", FileTransferDialog.formatHostFilename("fscms.exec", FTConfig.HostType.CMS));
assertEquals("fscms exec", FileTransferDialog.formatHostFilename("/var/tmp/fscms.exec", FTConfig.HostType.CMS));
assertEquals("test script exec", FileTransferDialog.formatHostFilename("test.script.exec", FTConfig.HostType.CMS));
assertEquals("noextension", FileTransferDialog.formatHostFilename("noextension", FTConfig.HostType.CMS));
// TSO and CICS should retain periods
assertEquals("fscms.exec", FileTransferDialog.formatHostFilename("fscms.exec", FTConfig.HostType.TSO));
assertEquals("my.dataset.name", FileTransferDialog.formatHostFilename("/path/to/my.dataset.name", FTConfig.HostType.TSO));
assertEquals("cics.file", FileTransferDialog.formatHostFilename("cics.file", FTConfig.HostType.CICS));
}
@Test
public void testSessionStateRememberedInMemory() {
assertNull(FileTransferDialog.getLastTransferState());
FileTransferDialog.TransferSessionState state = new FileTransferDialog.TransferSessionState();
state.hostType = FTConfig.HostType.CMS;
state.isSend = true;
state.localFile = "/tmp/fscms.exec";
state.hostFile = "fscms exec";
state.isAscii = true;
state.mtu = 8192;
state.crFlag = false;
state.remapFlag = false;
state.append = true;
state.overwrite = true;
state.options = "CLEAR";
FileTransferDialog.setLastTransferState(state);
FileTransferDialog.TransferSessionState retrieved = FileTransferDialog.getLastTransferState();
assertNotNull(retrieved);
assertEquals(FTConfig.HostType.CMS, retrieved.hostType);
assertTrue(retrieved.isSend);
assertEquals("/tmp/fscms.exec", retrieved.localFile);
assertEquals("fscms exec", retrieved.hostFile);
assertTrue(retrieved.isAscii);
assertEquals(8192, retrieved.mtu);
assertFalse(retrieved.crFlag);
assertFalse(retrieved.remapFlag);
assertTrue(retrieved.append);
assertTrue(retrieved.overwrite);
assertEquals("CLEAR", retrieved.options);
}
@Test
public void testDialogPrefillingFromSessionState() {
FileTransferDialog.TransferSessionState state = new FileTransferDialog.TransferSessionState();
state.hostType = FTConfig.HostType.CMS;
state.isSend = true;
state.localFile = "/tmp/fscms.exec";
state.hostFile = "fscms exec";
state.isAscii = false; // Binary
state.mtu = 4096;
state.crFlag = true;
state.remapFlag = true;
state.append = false;
state.overwrite = true;
state.options = "ASCII";
FileTransferDialog.setLastTransferState(state);
try {
FileTransferDialog dialog = new FileTransferDialog(null, null);
assertEquals(FTConfig.HostType.CMS, dialog.getHostTypeCombo().getSelectedItem());
assertTrue(dialog.getSendRadio().isSelected());
assertFalse(dialog.getReceiveRadio().isSelected());
assertEquals("/tmp/fscms.exec", dialog.getLocalFileField().getText());
assertEquals("fscms exec", dialog.getHostFileField().getText());
assertFalse(dialog.getAsciiRadio().isSelected());
assertTrue(dialog.getBinaryRadio().isSelected());
assertEquals(4096, dialog.getMtuCombo().getSelectedItem());
assertTrue(dialog.getOverwriteCheck().isSelected());
assertEquals("ASCII", dialog.getOptionsField().getText());
dialog.dispose();
} catch (HeadlessException e) {
// Headless environment; verified through state retention
}
}
}
@@ -0,0 +1,80 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.config.Settings;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.awt.Dimension;
import java.awt.HeadlessException;
import java.awt.event.ComponentEvent;
import static org.junit.jupiter.api.Assertions.*;
public class FontSettingsPersistenceTest {
private int originalFontSize;
private String originalFontFamily;
@BeforeEach
public void setup() {
originalFontSize = Settings.getFontSize();
originalFontFamily = Settings.getFontFamily();
}
@AfterEach
public void tearDown() {
Settings.setFontSize(originalFontSize);
Settings.setFontFamily(originalFontFamily);
}
@Test
public void testSettingsPersistence() {
Settings.setFontSize(22);
Settings.setFontFamily("Monospaced");
assertEquals(22, Settings.getFontSize());
assertEquals("Monospaced", Settings.getFontFamily());
Settings.setFontSize(18);
assertEquals(18, Settings.getFontSize());
}
@Test
public void testTerminalPanelResizeDoesNotResetFontSize() {
try {
Settings.setFontSize(24);
Settings.setFontFamily("Monospaced");
TerminalPanel panel = new TerminalPanel();
assertEquals(24, panel.getFontSize());
assertEquals(24, Settings.getFontSize());
// Simulate window / component resize events
panel.setSize(new Dimension(800, 600));
ComponentEvent resizeEvent1 = new ComponentEvent(panel, ComponentEvent.COMPONENT_RESIZED);
for (java.awt.event.ComponentListener cl : panel.getComponentListeners()) {
cl.componentResized(resizeEvent1);
}
// Verify font size and family did NOT reset
assertEquals(24, panel.getFontSize());
assertEquals(24, Settings.getFontSize());
assertEquals("Monospaced", Settings.getFontFamily());
// Simulate another resize to smaller dimension
panel.setSize(new Dimension(400, 300));
ComponentEvent resizeEvent2 = new ComponentEvent(panel, ComponentEvent.COMPONENT_RESIZED);
for (java.awt.event.ComponentListener cl : panel.getComponentListeners()) {
cl.componentResized(resizeEvent2);
}
assertEquals(24, panel.getFontSize());
assertEquals(24, Settings.getFontSize());
assertEquals("Monospaced", Settings.getFontFamily());
panel.dispose();
} catch (HeadlessException e) {
// In headless environment without display, Settings persistence is verified above
}
}
}
@@ -0,0 +1,337 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.HeadlessException;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.*;
public class KeyBindingsTest {
@Test
public void testDefaultBindingsContainRequiredKeys() {
String insertBindings = Settings.getDefaultBinding("INSERT");
assertNotNull(insertBindings);
assertTrue(insertBindings.contains("INSERT"), "Default INSERT binding should contain INSERT key");
if (Settings.isMac()) {
assertTrue(insertBindings.contains("HELP"), "macOS INSERT binding should contain HELP key");
assertTrue(insertBindings.contains("alt I"), "macOS INSERT binding should contain alt I fallback");
} else {
assertTrue(insertBindings.contains("ctrl I"), "Non-macOS INSERT binding should contain ctrl I fallback");
}
String copyBindings = Settings.getDefaultBinding("COPY");
assertNotNull(copyBindings);
assertTrue(copyBindings.contains("ctrl INSERT"), "Default COPY binding should support 3270 Ctrl+Insert");
assertTrue(copyBindings.contains("ctrl C"), "Default COPY binding should support Ctrl+C");
assertTrue(copyBindings.contains("ctrl shift C"), "Default COPY binding should support Ctrl+Shift+C");
if (Settings.isMac()) {
assertTrue(copyBindings.contains("meta C"), "macOS COPY binding should support Cmd+C");
}
String pasteBindings = Settings.getDefaultBinding("PASTE");
assertNotNull(pasteBindings);
assertTrue(pasteBindings.contains("shift INSERT"), "Default PASTE binding should support 3270 Shift+Insert");
assertTrue(pasteBindings.contains("ctrl V"), "Default PASTE binding should support Ctrl+V");
assertTrue(pasteBindings.contains("ctrl shift V"), "Default PASTE binding should support Ctrl+Shift+V");
if (Settings.isMac()) {
assertTrue(pasteBindings.contains("meta V"), "macOS PASTE binding should support Cmd+V");
}
String selectAllBindings = Settings.getDefaultBinding("SELECTALL");
assertNotNull(selectAllBindings);
assertTrue(selectAllBindings.contains("ctrl A"), "Default SELECTALL binding should support Ctrl+A");
if (Settings.isMac()) {
assertTrue(selectAllBindings.contains("meta A"), "macOS SELECTALL binding should support Cmd+A");
}
}
@Test
public void testTerminalPanelInputMapAndAction() {
TerminalPanel panel = new TerminalPanel();
InputMap im = panel.getInputMap(JComponent.WHEN_FOCUSED);
ActionMap am = panel.getActionMap();
// Check INSERT key mappings
KeyStroke ksInsert = KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0);
assertEquals("j3270-INSERT", im.get(ksInsert), "VK_INSERT must map to j3270-INSERT");
if (Settings.isMac()) {
KeyStroke ksHelp = KeyStroke.getKeyStroke(KeyEvent.VK_HELP, 0);
assertEquals("j3270-INSERT", im.get(ksHelp), "VK_HELP must map to j3270-INSERT on Mac");
KeyStroke ksAltI = KeyStroke.getKeyStroke("alt I");
assertEquals("j3270-INSERT", im.get(ksAltI), "alt I must map to j3270-INSERT on Mac");
}
// Check COPY key mappings
KeyStroke ksCtrlIns = KeyStroke.getKeyStroke("ctrl INSERT");
assertEquals("j3270-COPY", im.get(ksCtrlIns), "ctrl INSERT must map to j3270-COPY");
KeyStroke ksCtrlC = KeyStroke.getKeyStroke("ctrl C");
assertEquals("j3270-COPY", im.get(ksCtrlC), "ctrl C must map to j3270-COPY");
if (Settings.isMac()) {
KeyStroke ksMetaC = KeyStroke.getKeyStroke("meta C");
assertEquals("j3270-COPY", im.get(ksMetaC), "meta C must map to j3270-COPY on Mac");
}
// Check PASTE key mappings
KeyStroke ksShiftIns = KeyStroke.getKeyStroke("shift INSERT");
assertEquals("j3270-PASTE", im.get(ksShiftIns), "shift INSERT must map to j3270-PASTE");
KeyStroke ksCtrlV = KeyStroke.getKeyStroke("ctrl V");
assertEquals("j3270-PASTE", im.get(ksCtrlV), "ctrl V must map to j3270-PASTE");
if (Settings.isMac()) {
KeyStroke ksMetaV = KeyStroke.getKeyStroke("meta V");
assertEquals("j3270-PASTE", im.get(ksMetaV), "meta V must map to j3270-PASTE on Mac");
}
// Check SELECTALL key mappings
KeyStroke ksCtrlA = KeyStroke.getKeyStroke("ctrl A");
assertEquals("j3270-SELECTALL", im.get(ksCtrlA), "ctrl A must map to j3270-SELECTALL");
if (Settings.isMac()) {
KeyStroke ksMetaA = KeyStroke.getKeyStroke("meta A");
assertEquals("j3270-SELECTALL", im.get(ksMetaA), "meta A must map to j3270-SELECTALL on Mac");
}
// Test insert mode toggle action
assertFalse(panel.isInsertMode(), "Initial insertMode should be false");
Action insertAction = am.get("j3270-INSERT");
assertNotNull(insertAction, "j3270-INSERT action must exist");
AtomicBoolean modeChangedFired = new AtomicBoolean(false);
panel.addModeChangeListener(() -> modeChangedFired.set(true));
insertAction.actionPerformed(new ActionEvent(panel, ActionEvent.ACTION_PERFORMED, ""));
assertTrue(panel.isInsertMode(), "insertMode should be true after invoking action");
assertTrue(modeChangedFired.get(), "ModeChangeListener should have fired");
modeChangedFired.set(false);
insertAction.actionPerformed(new ActionEvent(panel, ActionEvent.ACTION_PERFORMED, ""));
assertFalse(panel.isInsertMode(), "insertMode should be false after invoking action again");
assertTrue(modeChangedFired.get(), "ModeChangeListener should have fired again");
}
@Test
public void testInsertModeSyncWithClient() {
TerminalPanel panel = new TerminalPanel();
panel.toggleInsertMode();
assertTrue(panel.isInsertMode());
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
panel.setClient(client);
// When client is attached, client's inputProcessor should receive current insertMode
assertNotNull(client.getInputProcessor());
assertTrue(client.getInputProcessor().isInsertMode(), "Client inputProcessor should inherit insertMode");
assertTrue(panel.isInsertMode());
// Toggling on panel updates client
panel.toggleInsertMode();
assertFalse(client.getInputProcessor().isInsertMode());
assertFalse(panel.isInsertMode());
panel.toggleInsertMode();
assertTrue(client.getInputProcessor().isInsertMode());
assertTrue(panel.isInsertMode());
}
@Test
public void testJ3270AppModesMenuSync() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
JMenuBar mb = app.getJMenuBar();
assertNotNull(mb);
JMenu modesMenu = null;
for (int i = 0; i < mb.getMenuCount(); i++) {
JMenu m = mb.getMenu(i);
if (m != null && "Modes".equals(m.getText())) {
modesMenu = m;
break;
}
}
assertNotNull(modesMenu, "Modes menu should exist");
JCheckBoxMenuItem insertItem = null;
for (int i = 0; i < modesMenu.getItemCount(); i++) {
JMenuItem item = modesMenu.getItem(i);
if (item instanceof JCheckBoxMenuItem && "Insert Mode".equals(item.getText())) {
insertItem = (JCheckBoxMenuItem) item;
break;
}
}
assertNotNull(insertItem, "Insert Mode menu item should exist");
assertFalse(insertItem.isSelected(), "Initial Insert Mode item should not be selected");
// Find terminal panel and toggle
TerminalPanel panel = null;
for (java.awt.Component c : app.getContentPane().getComponents()) {
if (c instanceof TerminalPanel) {
panel = (TerminalPanel) c;
break;
}
}
assertNotNull(panel, "TerminalPanel should be found");
// Toggling via panel updates menu item
panel.toggleInsertMode();
assertTrue(panel.isInsertMode());
assertTrue(insertItem.isSelected(), "Menu item should reflect true insert mode");
panel.toggleInsertMode();
assertFalse(panel.isInsertMode());
assertFalse(insertItem.isSelected(), "Menu item should reflect false insert mode");
// Clicking menu item toggles insert mode on panel
insertItem.doClick();
assertTrue(panel.isInsertMode(), "Panel should be in insert mode after clicking menu item");
assertTrue(insertItem.isSelected(), "Menu item should be selected after click");
insertItem.doClick();
assertFalse(panel.isInsertMode(), "Panel should exit insert mode after clicking menu item");
assertFalse(insertItem.isSelected(), "Menu item should be deselected after click");
} finally {
app.dispose();
}
}
@Test
public void testCopyAndPasteActions() {
try {
java.awt.Toolkit.getDefaultToolkit().getSystemClipboard();
} catch (HeadlessException e) {
return;
}
TerminalPanel panel = new TerminalPanel();
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
panel.setClient(client);
var sb = client.getScreenBuffer();
sb.setChar(0, 0, 'J');
sb.setChar(0, 1, '3');
sb.setChar(0, 2, '2');
sb.setChar(0, 3, '7');
sb.setChar(0, 4, '0');
panel.setSelectionRange(0, 4);
assertTrue(panel.hasSelection());
assertEquals("J3270", panel.getSelectedText());
Action copyAction = panel.getActionMap().get("j3270-COPY");
assertNotNull(copyAction);
copyAction.actionPerformed(new ActionEvent(panel, ActionEvent.ACTION_PERFORMED, ""));
try {
String clip = (String) java.awt.Toolkit.getDefaultToolkit().getSystemClipboard()
.getData(java.awt.datatransfer.DataFlavor.stringFlavor);
assertEquals("J3270", clip);
} catch (Exception e) {
fail("Clipboard read failed: " + e.getMessage());
}
// Test multiline paste handling
try {
java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().setContents(
new java.awt.datatransfer.StringSelection("LINE1\nLINE2"), null);
Action pasteAction = panel.getActionMap().get("j3270-PASTE");
assertNotNull(pasteAction);
pasteAction.actionPerformed(new ActionEvent(panel, ActionEvent.ACTION_PERFORMED, ""));
// Shouldn't throw any exceptions
} catch (Exception e) {
fail("Paste action failed: " + e.getMessage());
}
}
@Test
public void testToggleStatusBar() {
assertEquals("alt B", Settings.getDefaultBinding("STATUS_BAR"));
TerminalPanel panel = new TerminalPanel();
InputMap im = panel.getInputMap(JComponent.WHEN_FOCUSED);
ActionMap am = panel.getActionMap();
KeyStroke ksAltB = KeyStroke.getKeyStroke("alt B");
assertEquals("j3270-STATUS_BAR", im.get(ksAltB), "alt B must map to j3270-STATUS_BAR");
assertNotNull(am.get("j3270-STATUS_BAR"), "j3270-STATUS_BAR action must exist");
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
JMenuBar mb = app.getJMenuBar();
assertNotNull(mb);
JMenu viewMenu = null;
for (int i = 0; i < mb.getMenuCount(); i++) {
JMenu m = mb.getMenu(i);
if (m != null && "View".equals(m.getText())) {
viewMenu = m;
break;
}
}
assertNotNull(viewMenu);
JCheckBoxMenuItem statusBarItem = null;
for (int i = 0; i < viewMenu.getItemCount(); i++) {
JMenuItem item = viewMenu.getItem(i);
if (item instanceof JCheckBoxMenuItem && "Status Bar".equals(item.getText())) {
statusBarItem = (JCheckBoxMenuItem) item;
break;
}
}
assertNotNull(statusBarItem, "Status Bar menu item must exist in View menu");
assertTrue(statusBarItem.isSelected(), "Status bar should be selected by default");
StatusBar bar = null;
for (java.awt.Component c : app.getContentPane().getComponents()) {
if (c instanceof StatusBar) {
bar = (StatusBar) c;
break;
}
}
assertNotNull(bar, "StatusBar component should exist in content pane");
assertTrue(bar.isVisible(), "StatusBar should be visible by default");
// Test toggling via toggleStatusBar()
app.toggleStatusBar();
assertFalse(bar.isVisible(), "StatusBar should be hidden after toggle");
assertFalse(statusBarItem.isSelected(), "Menu item should be unchecked");
assertFalse(Settings.getStatusBarVisible());
app.toggleStatusBar();
assertTrue(bar.isVisible(), "StatusBar should be shown after second toggle");
assertTrue(statusBarItem.isSelected(), "Menu item should be checked");
assertTrue(Settings.getStatusBarVisible());
// Test toggling via menu item click
statusBarItem.doClick();
assertFalse(bar.isVisible(), "StatusBar should be hidden after menu click");
assertFalse(statusBarItem.isSelected(), "Menu item should be unchecked");
statusBarItem.doClick();
assertTrue(bar.isVisible(), "StatusBar should be shown after menu click");
assertTrue(statusBarItem.isSelected(), "Menu item should be checked");
} finally {
app.dispose();
Settings.setStatusBarVisible(true);
}
}
}
@@ -0,0 +1,105 @@
package haus.nightmare.j3270.ui;
import org.junit.jupiter.api.Test;
import java.io.StringReader;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for Phase 13.3: Keyboard Remap (.kmp) File Importer.
* Tests parsing of scan codes, modifier combinations, bind statements,
* mnemonic actions, and comments.
*/
public class KmpKeymapImportTest {
@Test
public void testParseScanCodeAssignments() throws Exception {
String kmpContent =
"# Sample keyboard profile mapping\n" +
"; IBM scan code mappings\n" +
"KEY43=[enter]\n" +
"S-KEY112=[pf13]\n" +
"C-KEY43=[newline]\n" +
"A-KEY110=[reset]\n" +
"KEY15=[tab]\n" +
"S-KEY15=[backtab]\n";
Map<String, String> imported = KeyBindings.parseKmp(new StringReader(kmpContent));
assertNotNull(imported);
// KEY43 is ENTER -> ENTER
assertEquals("ENTER", imported.get("ENTER"));
// S-KEY112 is Shift+F1 -> PF13
assertEquals("shift F1", imported.get("PF13"));
// C-KEY43 is Ctrl+ENTER -> NEWLINE
assertEquals("ctrl ENTER", imported.get("NEWLINE"));
// A-KEY110 is Alt+ESCAPE -> ESCAPE
assertEquals("alt ESCAPE", imported.get("ESCAPE"));
// KEY15 is TAB -> TAB
assertEquals("TAB", imported.get("TAB"));
// S-KEY15 is Shift+TAB -> shift TAB
assertEquals("shift TAB", imported.get("shift TAB"));
}
@Test
public void testParseBindStatements() throws Exception {
String kmpContent =
"bind [enter] Ctrl+Enter\n" +
"bind [pf1] F1\n" +
"bind [pf24] Shift+F12\n" +
"bind [eraseeof] End\n" +
"bind [clear] Pause\n";
Map<String, String> imported = KeyBindings.parseKmp(new StringReader(kmpContent));
assertNotNull(imported);
assertEquals("ctrl ENTER", imported.get("ENTER"));
assertEquals("F1", imported.get("PF1"));
assertEquals("shift F12", imported.get("PF24"));
assertEquals("END", imported.get("END"));
assertEquals("PAUSE", imported.get("CLEAR"));
}
@Test
public void testScanCodeWithMultipleModifiers() {
// C-S-KEY85 -> Ctrl+Shift+PAGE_UP
String res = KeyBindings.parseScanCodeEntry("CS-KEY85");
assertEquals("ctrl shift PAGE_UP", res);
// 2-KEY80 -> Alt+HOME (2- prefix is Alt in scan code syntax)
String altRes = KeyBindings.parseScanCodeEntry("2-KEY80");
assertEquals("alt HOME", altRes);
}
@Test
public void testMnemonicResolution() {
assertEquals("ENTER", KeyBindings.resolveMnemonic("[enter]"));
assertEquals("ENTER", KeyBindings.resolveMnemonic("enterreset"));
assertEquals("NEWLINE", KeyBindings.resolveMnemonic("[newline]"));
assertEquals("NEWLINE", KeyBindings.resolveMnemonic("[field-exit]"));
assertEquals("PF1", KeyBindings.resolveMnemonic("[pf1]"));
assertEquals("PF12", KeyBindings.resolveMnemonic("[pf12]"));
assertEquals("PF13", KeyBindings.resolveMnemonic("[spf1]"));
assertEquals("ESCAPE", KeyBindings.resolveMnemonic("[reset]"));
assertEquals("ERASE_INPUT", KeyBindings.resolveMnemonic("[erasefld]"));
assertEquals("DOCMODE", KeyBindings.resolveMnemonic("[docmode]"));
assertEquals("WORDWRAP", KeyBindings.resolveMnemonic("[wordwrap]"));
}
@Test
public void testCommentsAndBlankLinesIgnored() throws Exception {
String kmpContent =
"[ProfileHeader]\n" +
" \n" +
"# Full line comment\n" +
"; Another comment\n" +
"KEY42=[enter] ; inline comment\n" +
" \n";
Map<String, String> imported = KeyBindings.parseKmp(new StringReader(kmpContent));
assertEquals(1, imported.size());
assertEquals("ENTER", imported.get("ENTER"));
}
}
@@ -0,0 +1,116 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.HeadlessException;
import java.awt.event.KeyEvent;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
public class MenuBarShortcutsTest {
private Map<String, JMenuItem> collectMenuItems(JMenu menu) {
Map<String, JMenuItem> map = new HashMap<>();
for (int i = 0; i < menu.getItemCount(); i++) {
JMenuItem item = menu.getItem(i);
if (item != null) {
map.put(item.getText(), item);
}
}
return map;
}
@Test
public void testActionAndViewShortcutsUseAlt() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
// In automated/headless environments, JFrame cannot be initialized
return;
}
try {
JMenuBar mb = app.getJMenuBar();
assertNotNull(mb, "JMenuBar should be present");
JMenu viewMenu = null;
JMenu actionsMenu = null;
JMenu fileMenu = null;
JMenu editMenu = null;
for (int i = 0; i < mb.getMenuCount(); i++) {
JMenu m = mb.getMenu(i);
if (m != null) {
if ("View".equals(m.getText())) viewMenu = m;
else if ("Actions".equals(m.getText())) actionsMenu = m;
else if ("File".equals(m.getText())) fileMenu = m;
else if ("Edit".equals(m.getText())) editMenu = m;
}
}
assertNotNull(viewMenu, "View menu should exist");
assertNotNull(actionsMenu, "Actions menu should exist");
assertNotNull(fileMenu, "File menu should exist");
assertNotNull(editMenu, "Edit menu should exist");
// Verify View Menu items
Map<String, JMenuItem> viewItems = collectMenuItems(viewMenu);
assertAcceleratorUsesAlt(viewItems.get("Font Size +"), KeyEvent.VK_EQUALS);
assertAcceleratorUsesAlt(viewItems.get("Font Size -"), KeyEvent.VK_MINUS);
assertAcceleratorUsesAlt(viewItems.get("Reset Font"), KeyEvent.VK_0);
assertAcceleratorUsesAlt(viewItems.get("Status Bar"), KeyEvent.VK_B);
// Crosshair Ruler must use Alt+Shift+R
JMenuItem rulerItem = viewItems.get("Crosshair Ruler");
assertNotNull(rulerItem, "Crosshair Ruler menu item must exist");
KeyStroke rulerKs = rulerItem.getAccelerator();
assertNotNull(rulerKs, "Crosshair Ruler should have an accelerator");
assertEquals(KeyEvent.VK_R, rulerKs.getKeyCode(), "Key code mismatch for Crosshair Ruler");
int rulerMods = rulerKs.getModifiers();
assertTrue((rulerMods & (KeyEvent.ALT_DOWN_MASK | KeyEvent.ALT_MASK)) != 0,
"Crosshair Ruler accelerator must have ALT modifier");
assertTrue((rulerMods & (KeyEvent.SHIFT_DOWN_MASK | KeyEvent.SHIFT_MASK)) != 0,
"Crosshair Ruler accelerator must have SHIFT modifier");
assertEquals(0, rulerMods & (KeyEvent.CTRL_DOWN_MASK | KeyEvent.CTRL_MASK | KeyEvent.META_DOWN_MASK | KeyEvent.META_MASK),
"Crosshair Ruler accelerator must NOT have CTRL or META modifiers");
// Verify Actions Menu items use ALT_DOWN_MASK
Map<String, JMenuItem> actionItems = collectMenuItems(actionsMenu);
assertAcceleratorUsesAlt(actionItems.get("Send Enter"), KeyEvent.VK_ENTER);
assertAcceleratorUsesAlt(actionItems.get("Clear"), KeyEvent.VK_K);
assertAcceleratorUsesAlt(actionItems.get("Reset"), KeyEvent.VK_R);
assertAcceleratorUsesAlt(actionItems.get("Erase Input"), KeyEvent.VK_E);
assertAcceleratorUsesAlt(actionItems.get("Attention"), KeyEvent.VK_A);
assertAcceleratorUsesAlt(actionItems.get("System Request"), KeyEvent.VK_S);
assertAcceleratorUsesAlt(actionItems.get("Cursor Select"), KeyEvent.VK_Q);
assertAcceleratorUsesAlt(actionItems.get("Toggle Light Pen (Alt+L)"), KeyEvent.VK_L);
assertAcceleratorUsesAlt(actionItems.get("File Transfer..."), KeyEvent.VK_T);
// Ensure Crosshair Ruler and Reset do not collide
assertNotEquals(rulerKs, actionItems.get("Reset").getAccelerator(),
"Crosshair Ruler and Reset accelerators must not collide");
} finally {
// Dispose frame
app.dispose();
}
}
private void assertAcceleratorUsesAlt(JMenuItem item, int expectedKeyCode) {
assertNotNull(item, "Menu item must exist");
KeyStroke ks = item.getAccelerator();
assertNotNull(ks, "Menu item " + item.getText() + " should have an accelerator");
assertEquals(expectedKeyCode, ks.getKeyCode(), "Key code mismatch for " + item.getText());
int mods = ks.getModifiers();
assertTrue((mods & (KeyEvent.ALT_DOWN_MASK | KeyEvent.ALT_MASK)) != 0,
"Menu item " + item.getText() + " accelerator must have ALT modifier");
assertEquals(0, mods & (KeyEvent.SHIFT_DOWN_MASK | KeyEvent.SHIFT_MASK),
"Menu item " + item.getText() + " accelerator must NOT have SHIFT modifier");
assertEquals(0, mods & (KeyEvent.CTRL_DOWN_MASK | KeyEvent.CTRL_MASK | KeyEvent.META_DOWN_MASK | KeyEvent.META_MASK),
"Menu item " + item.getText() + " accelerator must NOT have CTRL or META modifiers");
}
}
@@ -0,0 +1,114 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.config.Settings;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.Color;
import java.awt.HeadlessException;
import static org.junit.jupiter.api.Assertions.*;
/**
* Verification test suite for Phase 1 UI Overlays and Color Alignment.
* Covers items 1.1, 1.2, and 1.5 of the PhasedUpdates specification.
*/
public class Phase1UiOverlayTest {
@Test
@DisplayName("Item 1.1 & 1.2: TerminalPanel host and mono colors match IBM Host On-Demand specs")
public void testTerminalPanelColorAlignment() {
// Index 10: Orange (255, 162, 0)
assertEquals(new Color(255, 162, 0), TerminalPanel.DEFAULT_HOST_COLORS[10]);
// Index 14: Mustard (160, 160, 0)
assertEquals(new Color(160, 160, 0), TerminalPanel.DEFAULT_HOST_COLORS[14]);
// Index 1: Blue (120, 144, 240)
assertEquals(new Color(120, 144, 240), TerminalPanel.DEFAULT_HOST_COLORS[1]);
// Base 4-Color mono defaults
assertEquals(new Color(0, 255, 0), TerminalPanel.DEFAULT_MONO_NORMAL);
assertEquals(new Color(255, 0, 0), TerminalPanel.DEFAULT_MONO_INTENSIFY);
assertEquals(new Color(0, 255, 255), TerminalPanel.DEFAULT_MONO_PROTECTED);
assertEquals(new Color(255, 255, 255), TerminalPanel.DEFAULT_MONO_PROTECTED_HIGH);
}
@Test
@DisplayName("Item 1.5: ThemeManager HoD OIA category colors match specification")
public void testThemeManagerOiaColors() {
ThemeManager.setTheme(UITheme.DARK);
// oSI: Status / System Available -> CUSTOMBLUE (120, 144, 240)
assertEquals(new Color(120, 144, 240), ThemeManager.getOiaStatusSysAvail());
// oII: Input Inhibited / X SYSTEM -> White (255, 255, 255)
assertEquals(new Color(255, 255, 255), ThemeManager.getOiaInputInhibited());
// oAI: Attention / Reminders / Message Waiting -> Yellow (255, 255, 0)
assertEquals(new Color(255, 255, 0), ThemeManager.getOiaAttention());
// oEI: Error Checks / Comm Check -> Red (255, 0, 0)
assertEquals(new Color(255, 0, 0), ThemeManager.getOiaCommCheck());
// oOB: OIA Separator / Background -> Black (0, 0, 0)
assertEquals(new Color(0, 0, 0), ThemeManager.getOiaBackground());
// Light mode OIA colors should be legible and not white on grey
ThemeManager.setTheme(UITheme.LIGHT);
assertNotEquals(Color.WHITE, ThemeManager.getOiaInputInhibited());
assertEquals(new Color(20, 20, 20), ThemeManager.getOiaInputInhibited());
assertEquals(new Color(0, 90, 200), ThemeManager.getOiaStatusSysAvail());
assertEquals(new Color(180, 100, 0), ThemeManager.getOiaAttention());
assertEquals(new Color(190, 20, 20), ThemeManager.getOiaCommCheck());
assertEquals(ThemeManager.getStatusBarBg(UITheme.LIGHT), ThemeManager.getOiaBackground());
}
@Test
@DisplayName("Item 1.5: TerminalPanel Crosshair Ruler and Cursor Style controls")
public void testTerminalPanelRulerAndCursorStyle() {
try {
TerminalPanel panel = new TerminalPanel();
// Default cursor style is BLOCK
assertEquals(TerminalPanel.CursorStyle.BLOCK, panel.getCursorStyle());
panel.setCursorStyle(TerminalPanel.CursorStyle.UNDERLINE);
assertEquals(TerminalPanel.CursorStyle.UNDERLINE, panel.getCursorStyle());
// Crosshair ruler toggling
assertFalse(panel.isCrosshairRulerEnabled());
panel.setCrosshairRulerEnabled(true);
assertTrue(panel.isCrosshairRulerEnabled());
panel.toggleCrosshairRuler();
assertFalse(panel.isCrosshairRulerEnabled());
// Wallpaper image setter
assertNull(panel.getWallpaperImage());
} catch (HeadlessException e) {
// Handled gracefully in headless CI environments
}
}
@Test
@DisplayName("Settings persistence for Crosshair Ruler and Cursor Style")
public void testSettingsPersistence() {
boolean originalRuler = Settings.getCrosshairRuler();
String originalCursor = Settings.getCursorStyle();
try {
Settings.setCrosshairRuler(true);
assertTrue(Settings.getCrosshairRuler());
Settings.setCrosshairRuler(false);
assertFalse(Settings.getCrosshairRuler());
Settings.setCursorStyle("UNDERLINE");
assertEquals("UNDERLINE", Settings.getCursorStyle());
Settings.setCursorStyle("BLOCK");
assertEquals("BLOCK", Settings.getCursorStyle());
} finally {
Settings.setCrosshairRuler(originalRuler);
Settings.setCursorStyle(originalCursor);
}
}
}
@@ -0,0 +1,95 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.graphics.GocaConstants;
import haus.nightmare.lib3270j.graphics.GraphicsPlane;
import haus.nightmare.lib3270j.graphics.HODWallpaper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.HeadlessException;
import java.awt.Image;
import java.awt.image.BufferedImage;
import static org.junit.jupiter.api.Assertions.*;
/**
* Verification test suite for Phase 2 UI Vector Graphics, Wallpaper, and Cursor Overlays.
*/
public class Phase2UiGraphicsTest {
@Test
@DisplayName("Test TerminalPanel Wallpaper Integration (Tile, Center, Stretch)")
public void testTerminalPanelWallpaperModes() {
try {
TerminalPanel panel = new TerminalPanel();
assertNull(panel.getHodWallpaper());
assertNull(panel.getWallpaperImage());
BufferedImage img = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
panel.setWallpaperImage(img);
assertNotNull(panel.getWallpaperImage());
assertNotNull(panel.getHodWallpaper());
assertEquals(HODWallpaper.HOD_STRETCH, panel.getHodWallpaper().getDisplay());
panel.setWallpaperMode(HODWallpaper.HOD_TILE);
assertEquals(HODWallpaper.HOD_TILE, panel.getHodWallpaper().getDisplay());
panel.setWallpaperMode(HODWallpaper.HOD_CENTER);
assertEquals(HODWallpaper.HOD_CENTER, panel.getHodWallpaper().getDisplay());
HODWallpaper customWp = new HODWallpaper(img, HODWallpaper.HOD_TILE);
panel.setHodWallpaper(customWp);
assertSame(customWp, panel.getHodWallpaper());
// Clear wallpaper
panel.setWallpaperImage(null);
assertNull(panel.getHodWallpaper());
assertNull(panel.getWallpaperImage());
} catch (HeadlessException e) {
// Handled gracefully in headless environments
}
}
@Test
@DisplayName("Test Graphics Plane Cursor Synchronization and Rendering")
public void testGraphicsCursorOverlay() {
try {
TerminalPanel panel = new TerminalPanel();
panel.setSize(new Dimension(800, 600));
panel.setClient(new haus.nightmare.lib3270j.Telnet3270Client(new haus.nightmare.lib3270j.ConnectionConfig("localhost", 23)));
GraphicsPlane plane = panel.getClient() != null ? panel.getClient().getGraphicsPlane() : null;
if (plane != null) {
// Attach graphics cursor
plane.attachGraphicCursor(100, 100);
plane.setHodCursorShape(GocaConstants.GCURSOR_SHAPE_CROSSHAIR);
assertTrue(plane.isGraphicCursorAttached());
assertEquals(100, plane.getGraphicCursorX());
assertEquals(100, plane.getGraphicCursorY());
assertEquals(1, plane.getHodCursorShape());
// Paint component to verify no exceptions during rendering
BufferedImage offscreen = new BufferedImage(800, 600, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = offscreen.createGraphics();
panel.paint(g2);
g2.dispose();
// Change cursor shape to box
plane.setHodCursorShape(GocaConstants.GCURSOR_SHAPE_BOX);
assertEquals(2, plane.getHodCursorShape());
offscreen = new BufferedImage(800, 600, BufferedImage.TYPE_INT_ARGB);
g2 = offscreen.createGraphics();
panel.paint(g2);
g2.dispose();
// Detach graphics cursor
plane.detachGraphicCursor();
assertFalse(plane.isGraphicCursorAttached());
}
} catch (HeadlessException e) {
// Handled gracefully in headless environments
}
}
}
@@ -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,62 @@
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() {
try {
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)");
}
} catch (HeadlessException e) {
// Ignored in headless environments
}
}
@Test
public void testStatusBarUpdatesWithClient() {
try {
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");
} catch (HeadlessException e) {
// Ignored in headless environments
}
}
}
@@ -0,0 +1,265 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.config.Settings;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.io.File;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
public class ThemeManagerTest {
@BeforeEach
public void setup() {
ThemeManager.setTheme(UITheme.DARK);
}
@Test
public void testUIThemeEnum() {
assertEquals("Dark Mode", UITheme.DARK.getDisplayName());
assertEquals("Light Mode", UITheme.LIGHT.getDisplayName());
assertEquals(UITheme.DARK, UITheme.fromString("DARK"));
assertEquals(UITheme.DARK, UITheme.fromString("dark"));
assertEquals(UITheme.DARK, UITheme.fromString("Dark Mode"));
assertEquals(UITheme.LIGHT, UITheme.fromString("LIGHT"));
assertEquals(UITheme.LIGHT, UITheme.fromString("light"));
assertEquals(UITheme.LIGHT, UITheme.fromString("Light Mode"));
assertEquals(UITheme.DARK, UITheme.fromString("invalid_value"));
}
@Test
public void testColorTokensContrast() {
for (UITheme theme : UITheme.values()) {
Color bg = ThemeManager.getBgMain(theme);
Color fg = ThemeManager.getFgMain(theme);
assertNotNull(bg);
assertNotNull(fg);
// Compute luminance contrast
double bgLum = (0.299 * bg.getRed() + 0.587 * bg.getGreen() + 0.114 * bg.getBlue());
double fgLum = (0.299 * fg.getRed() + 0.587 * fg.getGreen() + 0.114 * fg.getBlue());
double lumDiff = Math.abs(bgLum - fgLum);
// Minimum luminance difference to guarantee readability
assertTrue(lumDiff > 120, "Theme " + theme + " text contrast luminance difference " + lumDiff + " must be > 120");
// Verify Menu Bar colors
Color menuBg = ThemeManager.getMenuBarBg(theme);
Color menuFg = ThemeManager.getMenuItemFg(theme);
double menuBgLum = (0.299 * menuBg.getRed() + 0.587 * menuBg.getGreen() + 0.114 * menuBg.getBlue());
double menuFgLum = (0.299 * menuFg.getRed() + 0.587 * menuFg.getGreen() + 0.114 * menuFg.getBlue());
assertTrue(Math.abs(menuBgLum - menuFgLum) > 100, "Menu contrast in " + theme + " must be high");
// Verify Status Bar colors
Color sbBg = ThemeManager.getStatusBarBg(theme);
Color sbNormal = ThemeManager.getOiaFgNormal(theme);
Color sbInhibited = ThemeManager.getOiaInputInhibited(theme);
Color sbSysAvail = ThemeManager.getOiaStatusSysAvail(theme);
Color sbAttention = ThemeManager.getOiaAttention(theme);
Color sbCommCheck = ThemeManager.getOiaCommCheck(theme);
assertNotNull(sbBg);
assertNotNull(sbNormal);
assertNotNull(sbInhibited);
assertNotNull(sbSysAvail);
assertNotNull(sbAttention);
assertNotNull(sbCommCheck);
// Verify status bar contrast: ensure NO white on grey in light mode
double sbBgLum = (0.299 * sbBg.getRed() + 0.587 * sbBg.getGreen() + 0.114 * sbBg.getBlue());
double sbInhibitedLum = (0.299 * sbInhibited.getRed() + 0.587 * sbInhibited.getGreen() + 0.114 * sbInhibited.getBlue());
double sbInhibitedDiff = Math.abs(sbBgLum - sbInhibitedLum);
assertTrue(sbInhibitedDiff > 120, "Status bar input inhibited (X PROT) contrast in " + theme + " must be > 120 (was " + sbInhibitedDiff + ")");
double sbSysAvailLum = (0.299 * sbSysAvail.getRed() + 0.587 * sbSysAvail.getGreen() + 0.114 * sbSysAvail.getBlue());
assertTrue(Math.abs(sbBgLum - sbSysAvailLum) > 100, "Status bar sys avail contrast in " + theme + " must be > 100");
double sbAttentionLum = (0.299 * sbAttention.getRed() + 0.587 * sbAttention.getGreen() + 0.114 * sbAttention.getBlue());
assertTrue(Math.abs(sbBgLum - sbAttentionLum) > 100, "Status bar attention contrast in " + theme + " must be > 100");
if (theme == UITheme.LIGHT) {
assertNotEquals(Color.WHITE, sbInhibited, "Status bar input inhibited text must not be pure white in light mode");
assertNotEquals(Color.WHITE, sbNormal, "Status bar normal text must not be pure white in light mode");
}
// Verify Button colors
Color btnDefBg = ThemeManager.getButtonBg(ThemeManager.ButtonVariant.DEFAULT, theme);
Color btnDefFg = ThemeManager.getButtonFg(ThemeManager.ButtonVariant.DEFAULT, theme);
assertNotNull(btnDefBg);
assertNotNull(btnDefFg);
Color btnPriBg = ThemeManager.getButtonBg(ThemeManager.ButtonVariant.PRIMARY, theme);
Color btnPriFg = ThemeManager.getButtonFg(ThemeManager.ButtonVariant.PRIMARY, theme);
assertNotNull(btnPriBg);
assertNotNull(btnPriFg);
}
}
@Test
public void testThemeChangeListeners() {
AtomicReference<UITheme> notifiedTheme = new AtomicReference<>(null);
ThemeManager.addThemeChangeListener(notifiedTheme::set);
ThemeManager.setTheme(UITheme.LIGHT);
assertEquals(UITheme.LIGHT, ThemeManager.getTheme());
assertEquals(UITheme.LIGHT, notifiedTheme.get());
assertTrue(ThemeManager.isLight());
assertFalse(ThemeManager.isDark());
ThemeManager.setTheme(UITheme.DARK);
assertEquals(UITheme.DARK, ThemeManager.getTheme());
assertEquals(UITheme.DARK, notifiedTheme.get());
assertTrue(ThemeManager.isDark());
assertFalse(ThemeManager.isLight());
}
@Test
public void testComponentStyling() {
try {
JButton btn = new JButton("Test");
ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.PRIMARY);
assertNotNull(btn.getUI());
assertTrue(btn.getUI() instanceof ThemeManager.StyledButtonUI);
JTextField tf = new JTextField("Test");
ThemeManager.styleTextField(tf);
assertNotNull(tf.getCaretColor());
JTextArea ta = new JTextArea("Test area");
ThemeManager.styleTextArea(ta);
assertNotNull(ta.getCaretColor());
JComboBox<String> cb = new JComboBox<>(new String[]{"A", "B"});
ThemeManager.styleComboBox(cb);
assertNotNull(cb.getUI());
JTable table = new JTable(new DefaultTableModel(new Object[]{"Col1"}, 1));
ThemeManager.styleTable(table);
assertNotNull(table.getSelectionBackground());
JTabbedPane tp = new JTabbedPane();
tp.addTab("Tab1", new JPanel());
ThemeManager.styleTabbedPane(tp);
assertNotNull(tp.getUI());
JMenuBar mb = new JMenuBar();
ThemeManager.styleMenuBar(mb);
JMenu menu = new JMenu("File");
ThemeManager.styleMenu(menu);
JMenuItem mi = new JMenuItem("Open");
ThemeManager.styleMenuItem(mi);
JPopupMenu popup = new JPopupMenu();
ThemeManager.stylePopupMenu(popup);
JCheckBox chk = new JCheckBox("Check");
ThemeManager.styleCheckBox(chk);
JRadioButton rb = new JRadioButton("Radio");
ThemeManager.styleRadioButton(rb);
Border tb = ThemeManager.createTitledBorder("Title");
assertNotNull(tb);
} catch (HeadlessException e) {
// Ignored in headless environments
}
}
@Test
public void testRecursiveApplyTheme() {
try {
JPanel root = new JPanel(new BorderLayout());
JButton b = new JButton("OK");
JTextField f = new JTextField("Data");
JTabbedPane tp = new JTabbedPane();
JPanel tabContent = new JPanel();
JLabel lbl = new JLabel("Label");
tabContent.add(lbl);
tp.addTab("T1", tabContent);
root.add(b, BorderLayout.NORTH);
root.add(f, BorderLayout.CENTER);
root.add(tp, BorderLayout.SOUTH);
ThemeManager.setTheme(UITheme.LIGHT);
ThemeManager.applyTheme(root);
assertEquals(ThemeManager.getBgPanel(UITheme.LIGHT), root.getBackground());
assertEquals(ThemeManager.getFgMain(UITheme.LIGHT), lbl.getForeground());
ThemeManager.setTheme(UITheme.DARK);
ThemeManager.applyTheme(root);
assertEquals(ThemeManager.getBgPanel(UITheme.DARK), root.getBackground());
assertEquals(ThemeManager.getFgMain(UITheme.DARK), lbl.getForeground());
} catch (HeadlessException e) {
// Ignored in headless environments
}
}
@Test
public void testSettingsPersistence() throws Exception {
Settings.setJavaUiTheme(UITheme.LIGHT);
assertEquals(UITheme.LIGHT, Settings.getJavaUiTheme());
File tmpIni = File.createTempFile("j3270_test_config", ".ini");
tmpIni.deleteOnExit();
try {
// Write INI file with javaUiTheme = dark
try (PrintWriter pw = new PrintWriter(tmpIni)) {
pw.println("[j3270]");
pw.println("javaUiTheme = dark");
pw.println("fontFamily = Monospaced");
pw.println("fontSize = 16");
}
Settings.loadFromIniFile(tmpIni.getAbsolutePath());
assertEquals(UITheme.DARK, Settings.getJavaUiTheme());
// Switch to light and export
Settings.setJavaUiTheme(UITheme.LIGHT);
File exportFile = File.createTempFile("j3270_export_config", ".ini");
exportFile.deleteOnExit();
Settings.exportToIniFile(exportFile.getAbsolutePath());
String exportedContent = new String(Files.readAllBytes(exportFile.toPath()));
assertTrue(exportedContent.toLowerCase().contains("javauitheme = light"));
// Reload exported file
Settings.loadFromIniFile(exportFile.getAbsolutePath());
assertEquals(UITheme.LIGHT, Settings.getJavaUiTheme());
} finally {
tmpIni.delete();
}
}
@Test
public void testStatusBarLightModeLegibility() {
try {
ThemeManager.setTheme(UITheme.LIGHT);
StatusBar sb = new StatusBar();
sb.applyTheme(UITheme.LIGHT);
assertEquals(ThemeManager.getStatusBarBg(UITheme.LIGHT), sb.getBackground());
assertNotEquals(Color.WHITE, ThemeManager.getOiaInputInhibited());
assertNotEquals(Color.WHITE, ThemeManager.getOiaInputInhibited(UITheme.LIGHT));
// Verify recursive applyTheme does not break StatusBar background
ThemeManager.applyTheme(sb, UITheme.LIGHT);
assertEquals(ThemeManager.getStatusBarBg(UITheme.LIGHT), sb.getBackground());
} catch (HeadlessException e) {
// Handled in headless CI
}
}
}
@@ -0,0 +1,502 @@
package haus.nightmare.lib3270j;
/**
* Configuration for a 3270 terminal connection.
*/
public class ConnectionConfig {
public enum ProxyType {
NONE, HTTP, SOCKS4, SOCKS5
}
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 String keyStorePath = null;
private String keyStorePassword = null;
private String keyStoreType = null;
private String keyStoreAlias = null;
private String trustStorePath = null;
private String trustStorePassword = null;
private String trustStoreType = null;
private ClassLoader customizedCAsClassLoader = null;
private java.util.List<String> enabledProtocols = new java.util.ArrayList<>();
private java.util.List<String> enabledCipherSuites = new java.util.ArrayList<>();
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 = 62;
private int dynamicCols = 160;
private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
private String codePage = "037";
private String associatedPrinterLu = null;
private boolean nvtLocalEcho = false;
// Proxy configuration
private ProxyType proxyType = ProxyType.NONE;
private String proxyHost = null;
private int proxyPort = 0;
private String proxyUsername = null;
private String proxyPassword = null;
// STARTTLS (Telnet Option 46) dynamic socket elevation
private boolean startTlsEnabled = true;
// RFC 1572 / RFC 2877 Environment variables (Express Logon)
private java.util.Map<String, String> environmentVariables = new java.util.LinkedHashMap<>();
private java.util.Map<String, String> userVariables = new java.util.LinkedHashMap<>();
// IBM HoD autoSysUnlock parity
private boolean autoSysUnlock = true;
// Phase 11: Enterprise Connection Resilience & Heartbeat
private boolean keepAliveEnabled = true;
private int keepAliveIntervalSeconds = 120;
private String keepAliveType = "NOP";
private boolean autoReconnect = false;
private int reconnectMaxRetries = 5;
private int tcpUserTimeoutMs = 0;
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;
if (model != null && model.isDynamic()) {
this.dynamicModel = true;
}
}
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 String getKeyStorePath() { return keyStorePath; }
public void setKeyStorePath(String path) { this.keyStorePath = path; }
public String getKeyStorePassword() { return keyStorePassword; }
public void setKeyStorePassword(String password) { this.keyStorePassword = password; }
public String getKeyStoreType() { return keyStoreType; }
public void setKeyStoreType(String type) { this.keyStoreType = type; }
public String getKeyStoreAlias() { return keyStoreAlias; }
public void setKeyStoreAlias(String alias) { this.keyStoreAlias = alias; }
public String getTrustStorePath() { return trustStorePath; }
public void setTrustStorePath(String path) { this.trustStorePath = path; }
public String getTrustStorePassword() { return trustStorePassword; }
public void setTrustStorePassword(String password) { this.trustStorePassword = password; }
public String getTrustStoreType() { return trustStoreType; }
public void setTrustStoreType(String type) { this.trustStoreType = type; }
public ClassLoader getCustomizedCAsClassLoader() { return customizedCAsClassLoader; }
public void setCustomizedCAsClassLoader(ClassLoader cl) { this.customizedCAsClassLoader = cl; }
public java.util.List<String> getEnabledProtocols() { return enabledProtocols; }
public void setEnabledProtocols(java.util.List<String> protocols) {
this.enabledProtocols = protocols != null ? new java.util.ArrayList<>(protocols) : new java.util.ArrayList<>();
}
public void setEnabledProtocols(String... protocols) {
this.enabledProtocols = new java.util.ArrayList<>();
if (protocols != null) {
for (String p : protocols) if (p != null) this.enabledProtocols.add(p);
}
}
public java.util.List<String> getEnabledCipherSuites() { return enabledCipherSuites; }
public void setEnabledCipherSuites(java.util.List<String> cipherSuites) {
this.enabledCipherSuites = cipherSuites != null ? new java.util.ArrayList<>(cipherSuites) : new java.util.ArrayList<>();
}
public void setEnabledCipherSuites(String... cipherSuites) {
this.enabledCipherSuites = new java.util.ArrayList<>();
if (cipherSuites != null) {
for (String c : cipherSuites) if (c != null) this.enabledCipherSuites.add(c);
}
}
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 boolean isNvtLocalEcho() { return nvtLocalEcho; }
public void setNvtLocalEcho(boolean nvtLocalEcho) { this.nvtLocalEcho = nvtLocalEcho; }
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 || (model != null && model.isDynamic());
}
public void setDynamicModel(boolean dynamicModel) {
this.dynamicModel = dynamicModel;
if (dynamicModel && (model == null || !model.isDynamic())) {
this.model = TerminalModel.IBM_DYNAMIC;
} else if (!dynamicModel && model != null && model.isDynamic()) {
this.model = TerminalModel.IBM_3279_4;
}
}
public void setDynamic(boolean dynamic) {
setDynamicModel(dynamic);
}
public int getDynamicRows() { return dynamicRows; }
public int getDynamicCols() { return dynamicCols; }
public void setDynamicDimensions(int rows, int cols) {
this.dynamicModel = true;
this.dynamicRows = Math.max(1, rows);
this.dynamicCols = Math.max(1, cols);
this.model = TerminalModel.IBM_DYNAMIC;
}
public ProxyType getProxyType() { return proxyType; }
public void setProxyType(ProxyType proxyType) { this.proxyType = proxyType != null ? proxyType : ProxyType.NONE; }
public String getProxyHost() { return proxyHost; }
public void setProxyHost(String proxyHost) { this.proxyHost = proxyHost; }
public int getProxyPort() { return proxyPort; }
public void setProxyPort(int proxyPort) { this.proxyPort = proxyPort; }
public String getProxyUsername() { return proxyUsername; }
public void setProxyUsername(String proxyUsername) { this.proxyUsername = proxyUsername; }
public String getProxyPassword() { return proxyPassword; }
public void setProxyPassword(String proxyPassword) { this.proxyPassword = proxyPassword; }
public void setProxy(ProxyType type, String host, int port, String username, String password) {
this.proxyType = type != null ? type : ProxyType.NONE;
this.proxyHost = host;
this.proxyPort = port;
this.proxyUsername = username;
this.proxyPassword = password;
}
public boolean isStartTlsEnabled() { return startTlsEnabled; }
public void setStartTlsEnabled(boolean enabled) { this.startTlsEnabled = enabled; }
public java.util.Map<String, String> getEnvironmentVariables() { return environmentVariables; }
public void setEnvironmentVariables(java.util.Map<String, String> vars) {
this.environmentVariables = (vars != null) ? new java.util.LinkedHashMap<>(vars) : new java.util.LinkedHashMap<>();
}
public void setEnvironmentVariable(String name, String value) {
if (name != null) {
if (value != null) this.environmentVariables.put(name, value);
else this.environmentVariables.remove(name);
}
}
public java.util.Map<String, String> getUserVariables() { return userVariables; }
public void setUserVariables(java.util.Map<String, String> vars) {
this.userVariables = (vars != null) ? new java.util.LinkedHashMap<>(vars) : new java.util.LinkedHashMap<>();
}
public void setUserVariable(String name, String value) {
if (name != null) {
if (value != null) this.userVariables.put(name, value);
else this.userVariables.remove(name);
}
}
public boolean isAutoSysUnlock() { return autoSysUnlock; }
public void setAutoSysUnlock(boolean autoSysUnlock) { this.autoSysUnlock = autoSysUnlock; }
public boolean isKeepAliveEnabled() { return keepAliveEnabled; }
public void setKeepAliveEnabled(boolean enabled) { this.keepAliveEnabled = enabled; }
public int getKeepAliveIntervalSeconds() { return keepAliveIntervalSeconds; }
public void setKeepAliveIntervalSeconds(int seconds) { this.keepAliveIntervalSeconds = seconds; }
public String getKeepAliveType() { return keepAliveType; }
public void setKeepAliveType(String type) { this.keepAliveType = (type != null) ? type.trim().toUpperCase() : "NOP"; }
public boolean isAutoReconnect() { return autoReconnect; }
public void setAutoReconnect(boolean autoReconnect) { this.autoReconnect = autoReconnect; }
public int getReconnectMaxRetries() { return reconnectMaxRetries; }
public void setReconnectMaxRetries(int retries) { this.reconnectMaxRetries = Math.max(0, retries); }
public int getTcpUserTimeoutMs() { return tcpUserTimeoutMs; }
public void setTcpUserTimeoutMs(int timeoutMs) { this.tcpUserTimeoutMs = Math.max(0, timeoutMs); }
/**
* 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"), proxy flags (e.g. "--proxy=http://proxy:8080 host:23"),
* 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;
boolean dynamic = false;
int dynRows = 62;
int dynCols = 160;
// Parse --proxy=<url> or -proxy=<url> flags
ProxyType pType = ProxyType.NONE;
String pHost = null;
int pPort = 0;
String pUser = null;
String pPass = null;
boolean keepAlive = true;
boolean autoReconnect = false;
String[] tokens = s.split("\\s+");
StringBuilder remaining = new StringBuilder();
for (String tok : tokens) {
if (tok.equalsIgnoreCase("--keepalive") || tok.equalsIgnoreCase("-keepalive")) {
keepAlive = true;
} else if (tok.equalsIgnoreCase("--no-keepalive") || tok.equalsIgnoreCase("-no-keepalive")) {
keepAlive = false;
} else if (tok.equalsIgnoreCase("--autoreconnect") || tok.equalsIgnoreCase("-autoreconnect")) {
autoReconnect = true;
} else if (tok.equalsIgnoreCase("--no-autoreconnect") || tok.equalsIgnoreCase("-no-autoreconnect")) {
autoReconnect = false;
} else if (tok.startsWith("--proxy=") || tok.startsWith("-proxy=")) {
String proxyUrl = tok.substring(tok.indexOf('=') + 1).trim();
try {
java.net.URI uri = new java.net.URI(proxyUrl);
String scheme = uri.getScheme() != null ? uri.getScheme().toLowerCase() : "http";
if (scheme.equals("http") || scheme.equals("https")) {
pType = ProxyType.HTTP;
pPort = uri.getPort() > 0 ? uri.getPort() : 8080;
} else if (scheme.equals("socks4") || scheme.equals("socks4a")) {
pType = ProxyType.SOCKS4;
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
} else if (scheme.equals("socks5") || scheme.equals("socks")) {
pType = ProxyType.SOCKS5;
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
}
pHost = uri.getHost();
String userInfo = uri.getUserInfo();
if (userInfo != null) {
int colon = userInfo.indexOf(':');
if (colon >= 0) {
pUser = userInfo.substring(0, colon);
pPass = userInfo.substring(colon + 1);
} else {
pUser = userInfo;
}
}
} catch (Exception ignored) {}
} else {
if (remaining.length() > 0) remaining.append(" ");
remaining.append(tok);
}
}
s = remaining.toString().trim();
// 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;
} else if (s.startsWith("D:") || s.startsWith("d:")) {
dynamic = true;
s = s.substring(2);
prefixFound = true;
} else if (s.toLowerCase().startsWith("dyn:") || s.toLowerCase().startsWith("dynamic:") ||
s.toLowerCase().startsWith("dyn[") || s.toLowerCase().startsWith("dynamic[")) {
int colon = s.indexOf(':');
if (colon > 0) {
String prefix = s.substring(0, colon);
s = s.substring(colon + 1);
prefixFound = true;
dynamic = true;
if (prefix.contains("[") && prefix.contains("]")) {
String dim = prefix.substring(prefix.indexOf('[') + 1, prefix.indexOf(']'));
String[] parts = dim.toLowerCase().split("x");
if (parts.length == 2) {
try {
dynRows = Integer.parseInt(parts[0].trim());
dynCols = Integer.parseInt(parts[1].trim());
} catch (NumberFormatException ignored) {}
}
}
}
}
}
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);
config.setKeepAliveEnabled(keepAlive);
config.setAutoReconnect(autoReconnect);
if (dynamic) {
config.setDynamicDimensions(dynRows, dynCols);
}
if (pType != ProxyType.NONE && pHost != null) {
config.setProxy(pType, pHost, pPort, pUser, pPass);
}
return config;
}
/**
* Get the effective terminal type string to send during negotiation.
*/
public String getEffectiveTerminalType() {
if (terminalName != null) {
return terminalName;
}
if (isDynamicModel()) {
return "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. * Connection state machine states.
@@ -59,4 +59,32 @@ public enum ConnectionState {
public boolean isFullSession() { public boolean isFullSession() {
return isNvt() || is3270(); return isNvt() || is3270();
} }
/**
* Map to IBM Host On-Demand ECL connection state integer codes.
* 0 = Disconnected, 1 = Connecting/Resolving, 2 = Connected (NVT/Unbound), 3 = Bound (Full 3270 session).
*/
public int toHoDStateCode() {
switch (this) {
case NOT_CONNECTED:
return 0;
case RECONNECTING:
case RESOLVING:
case TCP_PENDING:
case TLS_PENDING:
case PROXY_PENDING:
case TELNET_PENDING:
return 1;
case CONNECTED_NVT:
case CONNECTED_NVT_CHAR:
case CONNECTED_UNBOUND:
case CONNECTED_E_NVT:
case CONNECTED_SSCP:
return 2;
case CONNECTED_3270:
case CONNECTED_TN3270E:
default:
return 3;
}
}
} }
@@ -1,8 +1,8 @@
package org.lib3270j; package haus.nightmare.lib3270j;
import org.lib3270j.screen.ScreenBuffer; import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.lib3270j.screen.ExtendedAttribute; import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import org.lib3270j.listener.*; import haus.nightmare.lib3270j.listener.*;
import java.util.logging.*; import java.util.logging.*;
@@ -15,7 +15,7 @@ public class DiagnosticClient {
static Telnet3270Client client; static Telnet3270Client client;
public static void main(String[] args) throws Exception { 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); rootLogger.setLevel(Level.ALL);
Handler handler = new ConsoleHandler(); Handler handler = new ConsoleHandler();
handler.setLevel(Level.ALL); handler.setLevel(Level.ALL);
@@ -26,7 +26,7 @@ public class DiagnosticClient {
String host = args.length >= 1 ? args[0] : "192.168.0.30"; String host = args.length >= 1 ? args[0] : "192.168.0.30";
int port = args.length >= 2 ? Integer.parseInt(args[1]) : 3270; 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 = new Telnet3270Client(config);
client.addConnectionListener(new ConnectionListener() { client.addConnectionListener(new ConnectionListener() {
@@ -0,0 +1,708 @@
package haus.nightmare.lib3270j;
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;
/**
* Main API entry point for lib3270j.
*
* Provides a high-level interface for connecting to a TN3270 host,
* managing the screen buffer, and handling user input.
*
* Usage:
* <pre>
* ConnectionConfig config = new ConnectionConfig("hostname", 23, TerminalModel.IBM_3279_4);
* Telnet3270Client client = new Telnet3270Client(config);
* client.addConnectionListener(myListener);
* client.addScreenUpdateListener(myScreenListener);
* client.connect();
* // ... interact with screen ...
* client.disconnect();
* </pre>
*/
public class Telnet3270Client {
private static final Logger log = Logger.getLogger(Telnet3270Client.class.getName());
private final ConnectionConfig config;
private final EbcdicTranslator translator;
private final ScreenBuffer screenBuffer;
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;
private final java.util.concurrent.atomic.AtomicBoolean reconnecting = new java.util.concurrent.atomic.AtomicBoolean(false);
private volatile Thread reconnectThread;
public Telnet3270Client(ConnectionConfig config) {
this.config = config;
this.translator = new EbcdicTranslator(config.getCodePage());
if (config.isDynamicModel() || (config.getModel() != null && config.getModel().isDynamic())) {
this.screenBuffer = new ScreenBuffer(
haus.nightmare.lib3270j.protocol.DS3270Constants.MODEL_2_ROWS,
haus.nightmare.lib3270j.protocol.DS3270Constants.MODEL_2_COLS,
config.getDynamicRows(),
config.getDynamicCols(),
translator);
} else {
this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
}
this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
this.dsProcessor.setAutoSysUnlock(config.isAutoSysUnlock());
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.inputProcessor.setOIA(oia);
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 and ECLPS for event dispatching
addScreenUpdateListener(new haus.nightmare.lib3270j.listener.ScreenUpdateListener() {
@Override public void onScreenUpdated() {
xfer.onScreenUpdated();
ps.notifyPSUpdate(0, 0, screenBuffer.getRows() - 1, screenBuffer.getCols() - 1, true);
}
@Override public void onCursorMoved(int oldAddress, int newAddress) {
ps.notifyCursorMoved(oldAddress, newAddress);
}
@Override public void onScreenSizeChanged(int rows, int cols) {
ps.notifyScreenResized(rows, cols);
}
@Override public void onSoundAlarm() {
ps.notifyAlarm();
}
@Override public void onKeyboardUnlocked() {
ps.notifyKeyUnlocked();
}
});
// Phase 11: Auto-reconnection and OIA status coordinator
fsm.addConnectionListener(new ConnectionListener() {
@Override
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
if (newState == ConnectionState.RECONNECTING) {
if (oia != null) {
oia.writeToOIA("X RECONNECT");
}
initiateAutoReconnect();
} else if (newState == ConnectionState.NOT_CONNECTED) {
if (oia != null) {
oia.setInputInhibited(haus.nightmare.lib3270j.ecl.ECLOIA.INHIBIT_COMMCHECK);
}
} else if (newState.isFullSession()) {
if (oia != null) {
oia.setInputInhibited(-1);
}
}
}
@Override
public void onConnectionError(String message) {}
});
}
/**
* Connect to the configured host.
* This method blocks until the TCP connection is established,
* then returns while telnet/TN3270E negotiation continues asynchronously.
*/
public void connect() throws IOException {
log.info("Connecting to " + config.getHost() + ":" + config.getPort() +
" model=" + config.getModel());
connection = new TelnetConnection(config, fsm);
fsm.setConnection(connection);
// Establish TCP connection
connection.connect();
// Notify FSM that TCP is connected begins telnet negotiation
fsm.onConnected();
}
/**
* Connect to the configured host synchronously, blocking until the full data session is established
* (or timeout expires).
* @param timeoutMs maximum time to wait in milliseconds
* @return true if successfully connected, false if timed out
* @throws IOException if network connection fails
*/
public boolean connect(long timeoutMs) throws IOException {
connect();
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
ConnectionState state = getConnectionState();
if (state.isFullSession() || (state.isFullyConnected() && isConnected())) {
return true;
}
if (state == ConnectionState.NOT_CONNECTED && !isConnected()) {
return false;
}
try {
Thread.sleep(25);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return getConnectionState().isFullSession() || isConnected();
}
/**
* Connect to the configured host synchronously and block until the presentation space matches
* the specified screen descriptor.
* @param timeoutMs maximum time to wait in milliseconds
* @param desc ECLScreenDesc descriptor to match against
* @return true if successfully connected and screen matched, false otherwise
* @throws IOException if network connection fails
*/
public boolean connect(long timeoutMs, haus.nightmare.lib3270j.ecl.ECLScreenDesc desc) throws IOException {
long start = System.currentTimeMillis();
if (!connect(timeoutMs)) {
return false;
}
long elapsed = System.currentTimeMillis() - start;
long remaining = Math.max(1, timeoutMs - elapsed);
return ps.waitForScreen(desc, remaining);
}
/**
* Disconnect from the host.
*/
public void disconnect() {
cancelAutoReconnect();
if (connection != null) {
connection.disconnect();
connection = null;
}
fsm.onDisconnect(false);
}
public void cancelAutoReconnect() {
reconnecting.set(false);
if (reconnectThread != null) {
reconnectThread.interrupt();
reconnectThread = null;
}
}
public void initiateAutoReconnect() {
if (config == null || !config.isAutoReconnect()) {
return;
}
if (!reconnecting.compareAndSet(false, true)) {
return;
}
reconnectThread = new Thread(this::runAutoReconnect, "TN3270-AutoReconnect");
reconnectThread.setDaemon(true);
reconnectThread.start();
}
private void runAutoReconnect() {
int maxRetries = (config != null) ? config.getReconnectMaxRetries() : 5;
log.info("Starting automatic reconnection loop (maxRetries=" + maxRetries + ")");
try {
for (int attempt = 1; attempt <= maxRetries && reconnecting.get(); attempt++) {
// Exponential backoff: 1s, 2s, 4s, 8s, 16s, capped at 30s
long delaySeconds = Math.min(30, (long) Math.pow(2, attempt - 1));
log.info("Auto-reconnect attempt " + attempt + "/" + maxRetries + " scheduled in " + delaySeconds + "s");
for (int s = 0; s < delaySeconds * 10; s++) {
if (!reconnecting.get() || Thread.currentThread().isInterrupted()) {
log.info("Auto-reconnect cancelled during backoff delay");
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
log.info("Auto-reconnect thread interrupted");
return;
}
}
if (!reconnecting.get()) {
return;
}
try {
log.info("Executing auto-reconnect attempt " + attempt + "/" + maxRetries + " to " + config.getHost() + ":" + config.getPort());
if (connection != null) {
connection.disconnect();
connection = null;
}
fsm.resetSessionState();
screenBuffer.erase(false);
connection = new TelnetConnection(config, fsm);
fsm.setConnection(connection);
connection.connect();
fsm.onConnected();
log.info("Auto-reconnect successful on attempt " + attempt);
reconnecting.set(false);
fsm.notifyScreenUpdate();
return;
} catch (IOException e) {
log.warning("Auto-reconnect attempt " + attempt + " failed: " + e.getMessage());
if (attempt < maxRetries && reconnecting.get()) {
fsm.setConnectionState(ConnectionState.RECONNECTING);
}
}
}
// All retries failed
log.warning("All " + maxRetries + " automatic reconnection attempts failed");
reconnecting.set(false);
fsm.onDisconnect(false);
fsm.onError("Automatic reconnection failed after " + maxRetries + " attempts");
} finally {
reconnecting.set(false);
reconnectThread = null;
}
}
public boolean isAutoReconnect() {
return (config != null) && config.isAutoReconnect();
}
public void setAutoReconnect(boolean autoReconnect) {
if (config != null) {
config.setAutoReconnect(autoReconnect);
}
}
public int getReconnectMaxRetries() {
return (config != null) ? config.getReconnectMaxRetries() : 5;
}
public void setReconnectMaxRetries(int retries) {
if (config != null) {
config.setReconnectMaxRetries(retries);
}
}
public boolean isReconnecting() {
return reconnecting.get() || fsm.getConnectionState() == ConnectionState.RECONNECTING;
}
public boolean isKeepAliveEnabled() {
return (config != null) && config.isKeepAliveEnabled();
}
public void setKeepAliveEnabled(boolean enabled) {
if (config != null) {
config.setKeepAliveEnabled(enabled);
}
if (connection != null) {
if (enabled) connection.startKeepAlive();
else connection.stopKeepAlive();
}
}
public int getKeepAliveIntervalSeconds() {
return (config != null) ? config.getKeepAliveIntervalSeconds() : 120;
}
public void setKeepAliveIntervalSeconds(int seconds) {
if (config != null) {
config.setKeepAliveIntervalSeconds(seconds);
}
if (connection != null && config != null && config.isKeepAliveEnabled()) {
connection.startKeepAlive();
}
}
/**
* Disconnect from host with synchronous teardown.
* @param timeoutMs maximum time to wait for graceful disconnect in milliseconds
*/
public void disconnect(long timeoutMs) {
disconnect();
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
if (getConnectionState() == ConnectionState.NOT_CONNECTED && !isConnected()) {
break;
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
/** HoD StopCommunication compatibility alias. */
public void stopCommunication() {
disconnect();
}
public void stopCommunication(long timeoutMs) {
disconnect(timeoutMs);
}
/**
* Check if connected (any state past TCP pending).
*/
public boolean isConnected() {
return connection != null && connection.isConnected();
}
// ========== Listener management ==========
public void addConnectionListener(ConnectionListener l) {
fsm.addConnectionListener(l);
}
public void removeConnectionListener(ConnectionListener l) {
fsm.removeConnectionListener(l);
}
public void addScreenUpdateListener(ScreenUpdateListener l) {
fsm.addScreenUpdateListener(l);
dsProcessor.addScreenUpdateListener(l);
}
public void removeScreenUpdateListener(ScreenUpdateListener l) {
fsm.removeScreenUpdateListener(l);
dsProcessor.removeScreenUpdateListener(l);
}
public void addSCSInboundListener(haus.nightmare.lib3270j.listener.SCSInboundListener l) {
fsm.addSCSInboundListener(l);
}
public void removeSCSInboundListener(haus.nightmare.lib3270j.listener.SCSInboundListener l) {
fsm.removeSCSInboundListener(l);
}
// ========== Screen access ==========
/** Get the screen buffer for rendering. */
public ScreenBuffer getScreenBuffer() { return screenBuffer; }
/** 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(); }
/** Get the input processor for keyboard operations. */
public InputProcessor getInputProcessor() { return inputProcessor; }
/** Get the data stream processor. */
public DataStreamProcessor getDataStreamProcessor() { return dsProcessor; }
/** Get the underlying telnet connection. */
public TelnetConnection getConnection() { return connection; }
/** 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;
}
public boolean isAutoSysUnlock() {
return (config != null) ? config.isAutoSysUnlock() : true;
}
public void setAutoSysUnlock(boolean autoSysUnlock) {
if (config != null) {
config.setAutoSysUnlock(autoSysUnlock);
}
if (dsProcessor != null) {
dsProcessor.setAutoSysUnlock(autoSysUnlock);
}
}
public boolean isContentionResolution() {
return fsm != null && fsm.isContentionResolutionNegotiated();
}
/** 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);
}
/** Send an NVT key event mapped through NVT processor. */
public boolean sendNVTKey(int keyCode, char keyChar, boolean shift, boolean ctrl, boolean alt) throws IOException {
haus.nightmare.lib3270j.nvt.NvtProcessor nvt = getNvtProcessor();
if (nvt != null) {
String seq = nvt.mapKey(keyCode, keyChar, shift, ctrl, alt);
if (seq != null && !seq.isEmpty()) {
fsm.sendNvtData(seq.getBytes(java.nio.charset.StandardCharsets.US_ASCII));
return true;
}
}
return false;
}
public void addNvtTitleListener(haus.nightmare.lib3270j.nvt.NvtProcessor.NvtTitleListener l) {
if (getNvtProcessor() != null) {
getNvtProcessor().addTitleListener(l);
}
}
public void removeNvtTitleListener(haus.nightmare.lib3270j.nvt.NvtProcessor.NvtTitleListener l) {
if (getNvtProcessor() != null) {
getNvtProcessor().removeTitleListener(l);
}
}
public void setNvtClipboardHandler(haus.nightmare.lib3270j.nvt.NvtProcessor.ClipboardHandler h) {
if (getNvtProcessor() != null) {
getNvtProcessor().setClipboardHandler(h);
}
}
// ========== Convenience input methods & HoD compatibility ==========
/** Type a character at the cursor position. */
public void typeCharacter(char ch) { inputProcessor.typeCharacter(ch); }
/** Process a character at the current cursor position. */
public void processChar(char ch) { inputProcessor.processChar(ch); }
/** Process a character with explicit position and insert mode flag. */
public void processChar(char ch, int pos, boolean insert) { inputProcessor.processChar(ch, pos, insert); }
/** Type a string at the cursor position. */
public void typeString(String s) {
for (char ch : s.toCharArray()) {
inputProcessor.typeCharacter(ch);
}
}
/** Emulate input of a string, pressing Enter on newlines. */
public void emulateInput(String s) {
inputProcessor.emulateInput(s);
}
/** Send Enter key. */
public void sendEnter() {
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
}
public void processEnter() { inputProcessor.processEnter(); }
/** Send a PF key (1-24). */
public void sendPF(int number) {
int aid;
switch (number) {
case 1: aid = 0xf1; break; case 2: aid = 0xf2; break;
case 3: aid = 0xf3; break; case 4: aid = 0xf4; break;
case 5: aid = 0xf5; break; case 6: aid = 0xf6; break;
case 7: aid = 0xf7; break; case 8: aid = 0xf8; break;
case 9: aid = 0xf9; break; case 10: aid = 0x7a; break;
case 11: aid = 0x7b; break; case 12: aid = 0x7c; break;
case 13: aid = 0xc1; break; case 14: aid = 0xc2; break;
case 15: aid = 0xc3; break; case 16: aid = 0xc4; break;
case 17: aid = 0xc5; break; case 18: aid = 0xc6; break;
case 19: aid = 0xc7; break; case 20: aid = 0xc8; break;
case 21: aid = 0xc9; break; case 22: aid = 0x4a; break;
case 23: aid = 0x4b; break; case 24: aid = 0x4c; break;
default: return;
}
inputProcessor.sendAid(aid);
}
public void processPF(int number) { inputProcessor.processPF(number); }
/** Send a PA key (1-3). */
public void sendPA(int number) {
int aid;
switch (number) {
case 1: aid = 0x6c; break;
case 2: aid = 0x6e; break;
case 3: aid = 0x6b; break;
default: return;
}
inputProcessor.sendAid(aid);
}
public void processPA(int number) { inputProcessor.processPA(number); }
/** Send Clear key. */
public void sendClear() {
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
}
public void processClear() { inputProcessor.processClear(); }
/** Move cursor up. */
public void cursorUp() { inputProcessor.cursorUp(); }
public void processCursorUp() { inputProcessor.processCursorUp(); }
/** Move cursor down. */
public void cursorDown() { inputProcessor.cursorDown(); }
public void processCursorDown() { inputProcessor.processCursorDown(); }
/** Move cursor left. */
public void cursorLeft() { inputProcessor.cursorLeft(); }
public void processCursorLeft() { inputProcessor.processCursorLeft(); }
/** Move cursor right. */
public void cursorRight() { inputProcessor.cursorRight(); }
public void processCursorRight() { inputProcessor.processCursorRight(); }
/** Move cursor to home position. */
public void cursorHome() { inputProcessor.cursorHome(); }
public void processHome() { inputProcessor.processHome(); }
/** Tab to next unprotected field. */
public void tab() { inputProcessor.tab(); }
public void processTab() { inputProcessor.processTab(); }
/** Back-tab to previous unprotected field. */
public void backTab() { inputProcessor.backTab(); }
public void processBackTab() { inputProcessor.processBackTab(); }
/** Move cursor to next line. */
public void newline() { inputProcessor.newline(); }
public void processNewline() { inputProcessor.processNewline(); }
/** Delete character under cursor. */
public void deleteChar() { inputProcessor.deleteChar(); }
public void processDelete() { inputProcessor.processDelete(); }
/** Backspace character before cursor. */
public void backspace() { inputProcessor.backspace(); }
public void processBackspace() { inputProcessor.processBackspace(); }
/** Erase to end of field. */
public void eraseEof() { inputProcessor.eraseEof(); }
public void processEraseEOF() { inputProcessor.processEraseEOF(); }
/** Erase all unprotected fields. */
public void eraseInput() { inputProcessor.eraseInput(); }
public void processEraseInput() { inputProcessor.processEraseInput(); }
/** Insert Duplicate order. */
public void dup() { inputProcessor.dup(); }
public void processDup() { inputProcessor.processDup(); }
/** Insert Field Mark order. */
public void fieldMark() { inputProcessor.fieldMark(); }
public void processFieldMark() { inputProcessor.processFieldMark(); }
/** Toggle Insert Mode. */
public boolean isInsertMode() { return inputProcessor != null && inputProcessor.isInsertMode(); }
public void toggleInsert() { if (inputProcessor != null) inputProcessor.setInsertMode(!inputProcessor.isInsertMode()); }
public void processToggleInsert() { if (inputProcessor != null) inputProcessor.processToggleInsert(); }
/** Document Mode (Entry Assist) operations. */
public boolean isDocMode() { return screenBuffer != null && screenBuffer.isEntryAssistDOCmode(); }
public void setDocMode(boolean b) { if (screenBuffer != null) screenBuffer.setEntryAssistDOCmode(b); if (oia != null) oia.notifyOIAChanged(); }
public void toggleDocMode() { setDocMode(!isDocMode()); }
public boolean isWordWrap() { return screenBuffer != null && screenBuffer.isEntryAssistWordWrap(); }
public void setWordWrap(boolean b) { if (screenBuffer != null) screenBuffer.setEntryAssistWordWrap(b); if (oia != null) oia.notifyOIAChanged(); }
public void toggleWordWrap() { setWordWrap(!isWordWrap()); }
/** APL Keyboard Mode operations. */
public boolean isAplMode() { return inputProcessor != null && inputProcessor.isAplKeyboardMode(); }
public void setAplMode(boolean b) { if (inputProcessor != null) inputProcessor.setAplKeyboardMode(b); }
public void toggleAplMode() { if (inputProcessor != null) inputProcessor.toggleAplKeyboardMode(); }
/** Move word left. */
public void processWordLeft() { inputProcessor.processWordLeft(); }
/** Move word right. */
public void processWordRight() { inputProcessor.processWordRight(); }
/** Move to field end. */
public void processFieldEnd() { inputProcessor.processFieldEnd(); }
/** Attention key. */
public void attn() { inputProcessor.attn(); }
public void processAttn() { inputProcessor.processAttn(); }
/** SysReq key. */
public void sysReq() { inputProcessor.sysReq(); }
public void processSysReq() { inputProcessor.processSysReq(); }
/** Reset (unlock keyboard, reset OIA). */
public void reset() { inputProcessor.reset(); }
public void processReset() { inputProcessor.processReset(); }
/** Trigger 3270 CURSR SEL (Cursor Select) key at current cursor position. */
public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
public boolean processCurSel() { return inputProcessor.processCurSel(); }
public boolean processCursorSelect() { return inputProcessor.processCursorSelect(); }
/** Trigger Light Pen selection at the specified screen address or cursor position. */
public boolean lightPenSelect(int address) { return inputProcessor.lightPenSelect(address); }
public boolean processLightPen() { return inputProcessor.processLightPen(); }
public boolean processLightPen(int addr) { return inputProcessor.processLightPen(addr); }
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. * Terminal model definitions for IBM 3278 and 3279 terminals.
@@ -14,7 +14,8 @@ public enum TerminalModel {
IBM_3279_2(2, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_2_ROWS, MODEL_2_COLS), IBM_3279_2(2, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_2_ROWS, MODEL_2_COLS),
IBM_3279_3(3, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_3_ROWS, MODEL_3_COLS), IBM_3279_3(3, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_3_ROWS, MODEL_3_COLS),
IBM_3279_4(4, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_4_ROWS, MODEL_4_COLS), IBM_3279_4(4, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_4_ROWS, MODEL_4_COLS),
IBM_3279_5(5, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_5_ROWS, MODEL_5_COLS); IBM_3279_5(5, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_5_ROWS, MODEL_5_COLS),
IBM_DYNAMIC(0, true, MODEL_2_ROWS, MODEL_2_COLS, 62, 160);
private final int modelNumber; private final int modelNumber;
private final boolean color; private final boolean color;
@@ -40,12 +41,17 @@ public enum TerminalModel {
public int getDefaultCols() { return defaultCols; } public int getDefaultCols() { return defaultCols; }
public int getAlternateRows() { return alternateRows; } public int getAlternateRows() { return alternateRows; }
public int getAlternateCols() { return alternateCols; } public int getAlternateCols() { return alternateCols; }
public boolean isDynamic() { return modelNumber == 0; }
/** /**
* Returns the terminal type string for TN3270E negotiation. * Returns the terminal type string for TN3270E negotiation.
* e.g., "IBM-3279-4-E" for a color model 4 with extended data stream. * e.g., "IBM-3279-4-E" for a color model 4 with extended data stream,
* or "IBM-DYNAMIC" for dynamic model.
*/ */
public String getTerminalType() { public String getTerminalType() {
if (modelNumber == 0) {
return "IBM-DYNAMIC";
}
return String.format("IBM-327%c-%d-E", color ? '9' : '8', modelNumber); return String.format("IBM-327%c-%d-E", color ? '9' : '8', modelNumber);
} }
@@ -53,6 +59,9 @@ public enum TerminalModel {
* Returns the base terminal type without "-E" suffix (for non-extended mode). * Returns the base terminal type without "-E" suffix (for non-extended mode).
*/ */
public String getBaseTerminalType() { public String getBaseTerminalType() {
if (modelNumber == 0) {
return "IBM-DYNAMIC";
}
return String.format("IBM-327%c-%d", color ? '9' : '8', modelNumber); return String.format("IBM-327%c-%d", color ? '9' : '8', modelNumber);
} }
@@ -60,6 +69,9 @@ public enum TerminalModel {
* Look up a model by number and color mode. * Look up a model by number and color mode.
*/ */
public static TerminalModel forModel(int number, boolean isColor) { public static TerminalModel forModel(int number, boolean isColor) {
if (number == 0) {
return IBM_DYNAMIC;
}
for (TerminalModel m : values()) { for (TerminalModel m : values()) {
if (m.modelNumber == number && m.color == isColor) { if (m.modelNumber == number && m.color == isColor) {
return m; return m;
@@ -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,253 @@
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) {
return ebcdicToString(ebcdic, offset, length, false);
}
/**
* Translate an EBCDIC byte array slice to a Unicode String with optional SO/SI escape preservation.
*
* @param ebcdic source byte array
* @param offset start offset
* @param length number of bytes
* @param preserveSOSI if true, preserves Shift-Out (\u000E) and Shift-In (\u000F) control characters
* @return translated Unicode String
*/
public String ebcdicToString(byte[] ebcdic, int offset, int length, boolean preserveSOSI) {
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;
if (preserveSOSI) {
sb.append((char) SO);
}
i++;
} else if (b == SI) {
inDBCS = false;
if (preserveSOSI) {
sb.append((char) SI);
}
i++;
} else if (inDBCS) {
if (i + 1 < end) {
int b2 = ebcdic[i + 1] & 0xFF;
if (b2 == SI) {
// Orphaned single byte before SI
inDBCS = false;
if (preserveSOSI) {
sb.append(ebcdicToUnicode(b));
sb.append((char) SI);
}
i += 2;
} else if (b2 == SO) {
// Unmatched byte followed by another SO
sb.append(ebcdicToUnicode(b));
i++;
} else {
sb.append(dbcsToUnicode(b, b2));
i += 2;
}
} else {
// Trailing single byte inside unclosed DBCS shift
sb.append(ebcdicToUnicode(b));
i++;
}
} else {
sb.append(ebcdicToUnicode(b));
i++;
}
}
return sb.toString();
}
@Override
public byte[] stringToEbcdic(String s) {
return stringToEbcdic(s, true);
}
/**
* Translate a Unicode String to EBCDIC byte array with optional parsing of embedded SO/SI markers.
*
* @param s input Unicode string
* @param parseSOSIMarkers whether to interpret embedded \u000E (SO) and \u000F (SI) characters
* @return EBCDIC byte array with balanced SO/SI framing
*/
public byte[] stringToEbcdic(String s, boolean parseSOSIMarkers) {
if (s == null || s.isEmpty()) return new byte[0];
ByteArrayOutputStream out = new ByteArrayOutputStream(s.length() * 2);
boolean inDBCS = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (parseSOSIMarkers && c == (char) SO) {
if (!inDBCS) {
out.write(SO);
inDBCS = true;
}
continue;
} else if (parseSOSIMarkers && c == (char) SI) {
if (inDBCS) {
out.write(SI);
inDBCS = false;
}
continue;
}
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));
}
}
// Auto-close unclosed DBCS shift sequence
if (inDBCS) {
out.write(SI);
}
return out.toByteArray();
}
/**
* Helper to decode an EBCDIC byte array to UTF-8 String preserving SO/SI control codes.
*/
public String ebcdicToUtf8WithSOSI(byte[] ebcdic) {
if (ebcdic == null) return "";
return ebcdicToString(ebcdic, 0, ebcdic.length, true);
}
/**
* Helper to encode a UTF-8/Unicode String with embedded SO/SI markers to EBCDIC.
*/
public byte[] utf8WithSOSIToEbcdic(String s) {
return stringToEbcdic(s, true);
}
}
@@ -0,0 +1,156 @@
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);
/**
* Convert an EBCDIC byte buffer to a char array matching IBM HoD conversion.
*/
default char[] convBuffByte2Char(byte[] buf, int offset, int length) {
if (buf == null || length <= 0) return new char[0];
char[] out = new char[length];
for (int i = 0; i < length; i++) {
out[i] = ebcdicToUnicode(buf[offset + i] & 0xFF);
}
return out;
}
/**
* Convert a char array to an EBCDIC byte array matching IBM HoD conversion.
*/
default byte[] convBuffChar2Byte(char[] buf, int offset, int length) {
if (buf == null || length <= 0) return new byte[0];
byte[] out = new byte[length];
for (int i = 0; i < length; i++) {
out[i] = unicodeToEbcdicSafe(buf[offset + i]);
}
return out;
}
/**
* Get a HODByteToCharConverter instance backed by this CodePage.
*/
default haus.nightmare.lib3270j.converters.HODByteToCharConverter getByteToCharConverter() {
if (isDBCS()) {
return new haus.nightmare.lib3270j.converters.ByteToCharDBCS_EBCDIC(this);
} else {
return new haus.nightmare.lib3270j.converters.ByteToCharSingleByte(this);
}
}
/**
* Get a HODCharToByteConverter instance backed by this CodePage.
*/
default haus.nightmare.lib3270j.converters.HODCharToByteConverter getCharToByteConverter() {
if (isDBCS()) {
return new haus.nightmare.lib3270j.converters.CharToByteDBCS_EBCDIC(this);
} else {
return new haus.nightmare.lib3270j.converters.CharToByteSingleByte(this);
}
}
/**
* Helper to test if a pair of characters forms a Unicode surrogate pair.
*/
static boolean isSurrogate(char high, char low) {
return Character.isSurrogatePair(high, low);
}
/**
* Helper to test if a character is a high surrogate.
*/
static boolean isHighSurrogate(char c) {
return Character.isHighSurrogate(c);
}
/**
* Helper to test if a character is a low surrogate.
*/
static boolean isLowSurrogate(char c) {
return Character.isLowSurrogate(c);
}
/**
* Helper matching IBM HoD CodePage.ComposeChar to combine characters.
*/
static boolean ComposeChar(char[] chars) {
if (chars == null || chars.length < 2) return false;
if (chars[1] >= '\u0300' && chars[1] <= '\u036F') {
String decomposed = new String(chars, 0, 2);
String normalized = java.text.Normalizer.normalize(decomposed, java.text.Normalizer.Form.NFC);
if (normalized.length() == 1) {
chars[0] = normalized.charAt(0);
return true;
}
}
return false;
}
}
@@ -0,0 +1,488 @@
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());
// Register Phase 8 Extended EBCDIC Codepages (Arabic, Hebrew, Thai, Cyrillic, Turkish, Extended DBCS)
registerExtendedCodepages();
// 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();
// Strip package qualifiers if present (e.g. com.ibm.eNetwork.HOD.converters.onea.ByteToCharCp273)
int lastDot = Math.max(s.lastIndexOf('.'), s.lastIndexOf('/'));
if (lastDot >= 0 && lastDot < s.length() - 1) {
s = s.substring(lastDot + 1);
}
// Strip HoD converter class prefixes
for (String pfx : new String[]{
"HODByteToChar", "HODCharToByte", "ByteToChar", "CharToByte",
"ConverterBIDIPrinter", "ConverterFT", "ConverterJDK", "ConverterVT", "PrtConverter"
}) {
if (s.startsWith(pfx)) {
s = s.substring(pfx.length());
break;
}
}
s = s.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, alias, or converter name without fallback to CP037.
* Returns null if unresolvable.
*/
public static CodePage resolveCodePage(String name) {
if (name == null || name.trim().isEmpty()) {
return null;
}
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;
String normTarget = normalizeKey(targetId);
cp = CODE_PAGES.get(normTarget);
if (cp != null) return cp;
}
// Try standard NIO Charset dynamic adapter
try {
if (Charset.isSupported(raw)) {
return new NioCodePageAdapter(raw);
}
if (Charset.isSupported(norm)) {
return new NioCodePageAdapter(norm);
}
String ibmName = "IBM" + norm;
if (Charset.isSupported(ibmName)) {
return new NioCodePageAdapter(ibmName);
}
String ibmDashName = "IBM-" + norm;
if (Charset.isSupported(ibmDashName)) {
return new NioCodePageAdapter(ibmDashName);
}
String cpName = "Cp" + norm;
if (Charset.isSupported(cpName)) {
return new NioCodePageAdapter(cpName);
}
String isoName = "ISO-8859-" + norm.replace("8859", "");
if (Charset.isSupported(isoName)) {
return new NioCodePageAdapter(isoName);
}
String winName = "windows-" + norm;
if (Charset.isSupported(winName)) {
return new NioCodePageAdapter(winName);
}
} catch (Exception e) {
log.fine("Dynamic charset loading failed for " + name + ": " + e.getMessage());
}
return null;
}
/**
* 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 get(String name) {
return getCodePage(name);
}
public static CodePage getDefault() {
return getCodePage("037");
}
public static CodePage getCodePage(String name) {
CodePage cp = resolveCodePage(name);
if (cp != null) {
return cp;
}
log.warning("CodePage not recognized: '" + name + "'; falling back to CP037");
return CODE_PAGES.get("037");
}
/**
* Register extended EBCDIC codepages from HoD v14 converters:
* - Arabic: Cp420, Cp424
* - Hebrew: Cp424, Cp803
* - Thai: Cp838, Cp1160
* - Cyrillic: Cp1025, Cp1123, Cp1154, Cp880
* - Turkish: Cp1155, Cp905
* - Extended Chinese DBCS: Cp1388, Cp1371
*/
public static void registerExtendedCodepages() {
// Arabic & Hebrew
register(new Cp420());
register(new Cp424());
register(new Cp803());
// Thai
register(new Cp838());
register(new Cp1160());
// Cyrillic / Russian / Ukrainian
register(new Cp1025());
register(new Cp1123());
register(new Cp1154());
register(new Cp880());
// Turkish
register(new Cp1155());
register(new Cp905());
// Extended Chinese DBCS
register(new Cp1388());
register(new Cp1371());
// Setup Extended Aliases
addAlias("ar", "420");
addAlias("arabic", "420");
addAlias("ebcdic-cp-ar", "420");
addAlias("he", "424");
addAlias("hebrew", "424");
addAlias("hebrew-lowercase", "424");
addAlias("ebcdic-cp-he", "424");
addAlias("hebrew-old", "803");
addAlias("israel", "803");
addAlias("iw", "803");
addAlias("ebcdic-cp-he-old", "803");
addAlias("th", "838");
addAlias("thai", "838");
addAlias("ebcdic-cp-th", "838");
addAlias("thai-euro", "1160");
addAlias("ru", "1025");
addAlias("russian", "1025");
addAlias("cyrillic", "1025");
addAlias("ebcdic-cp-ru", "1025");
addAlias("ukraine", "1123");
addAlias("ukrainian", "1123");
addAlias("ebcdic-cp-ua", "1123");
addAlias("cyrillic-euro", "1154");
addAlias("ru-euro", "1154");
addAlias("russian-euro", "1154");
addAlias("cyrillic-russian", "880");
addAlias("ru-old", "880");
addAlias("turkish-euro", "1155");
addAlias("tr-euro", "1155");
addAlias("turkish-latin3", "905");
addAlias("tr-latin3", "905");
addAlias("chinese-ext-simplified", "1388");
addAlias("zh-simplified-ext", "1388");
addAlias("zh-ext", "1388");
addAlias("chinese-ext-traditional", "1371");
addAlias("zh-traditional-ext", "1371");
addAlias("zh-tw-ext", "1371");
// HoD Converter aliases mapping all HoD converter names to CodePage / Charset
addAlias("1390", "930");
addAlias("1390jis2004", "930");
addAlias("1399", "939");
addAlias("1399jis2004", "939");
addAlias("937macau", "937");
addAlias("1364", "933");
addAlias("1379", "937");
addAlias("274", "500");
addAlias("275", "037");
addAlias("924", "1047");
addAlias("1153", "870");
addAlias("1156", "1025");
addAlias("1157", "1025");
addAlias("1158", "1025");
addAlias("1166", "1025");
addAlias("1112", "1025");
addAlias("1122", "1025");
addAlias("1137", "037");
addAlias("1008", "420");
addAlias("449", "420");
addAlias("1089", "420");
addAlias("1134", "424");
addAlias("1349", "424");
addAlias("8585", "875");
addAlias("8586", "875");
addAlias("220", "284");
addAlias("big5550", "937");
addAlias("cns", "937");
addAlias("tca", "937");
addAlias("ks25550", "933");
addAlias("jis", "930");
addAlias("euc", "937");
addAlias("singlebyte", "037");
addAlias("dbcsebcdic", "930");
addAlias("dbcsebcdicnibm", "930");
addAlias("dbcsebcdicibm", "930");
addAlias("dbcsascii", "930");
addAlias("encodings", "037");
addAlias("1011", "037");
addAlias("1012", "037");
addAlias("1020", "037");
addAlias("1021", "037");
addAlias("1023", "037");
addAlias("1090", "037");
addAlias("1101", "037");
addAlias("1102", "037");
addAlias("1103", "037");
addAlias("1104", "037");
addAlias("1105", "037");
addAlias("1106", "037");
}
/**
* Check if a code page or converter is registered or resolvable.
*/
public static boolean hasCodePage(String name) {
if (name == null || name.trim().isEmpty()) return false;
return resolveCodePage(name) != null;
}
/**
* Convenience factory method to get a HODByteToCharConverter by name or alias.
*/
public static haus.nightmare.lib3270j.converters.HODByteToCharConverter getByteToCharConverter(String name)
throws haus.nightmare.lib3270j.converters.HODUnsupportedCodepageException {
return haus.nightmare.lib3270j.converters.HODByteToCharConverter.getHODConverter(name);
}
/**
* Convenience factory method to get a HODCharToByteConverter by name or alias.
*/
public static haus.nightmare.lib3270j.converters.HODCharToByteConverter getCharToByteConverter(String name)
throws haus.nightmare.lib3270j.converters.HODUnsupportedCodepageException {
return haus.nightmare.lib3270j.converters.HODCharToByteConverter.getHODConverter(name);
}
/**
* 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. * IBM Code Page 037 (US / Canada / Netherlands / Portugal / Brazil).
* Default: Code Page 037 (US/Canada EBCDIC). * CCSID / CPGID: 37, GCSGID: 697.
*/ */
public class EbcdicTranslator { public class Cp037 extends AbstractCodePage {
/** public static final String ID = "037";
* EBCDIC Code Page 037 to Unicode mapping. public static final String DESCRIPTION = "US / Canada / Brazil - EBCDIC";
* Index is the EBCDIC byte value (0x00-0xFF), value is the Unicode codepoint. public static final int CPGID = 37;
*/ public static final int GCSGID = 697;
private static final int[] CP037_TO_UNICODE = {
public static final int[] MAPPING = {
// 00-0F // 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F, 0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, 0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
@@ -61,82 +62,7 @@ public class EbcdicTranslator {
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F, 0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
}; };
/** public Cp037() {
* Unicode to EBCDIC Code Page 037 mapping (for basic Latin + Latin-1). super(ID, DESCRIPTION, CPGID, GCSGID, "037", MAPPING);
* 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];
}
/**
* Static helper to translate EBCDIC byte to Unicode character.
*/
public static char toUnicode(int ebc) {
return (char) CP037_TO_UNICODE[ebc & 0xFF];
}
/**
* Static helper to translate EBCDIC byte to ASCII character.
*/
public static char ebcdicToAscii(int ebc) {
return toUnicode(ebc);
}
/**
* 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;
} }
} }
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1025 (Cyrillic Multilingual EBCDIC - Russian, Bulgarian, Belarusian, Serbian, Macedonian).
* CCSID / CPGID: 1025, GCSGID: 1150.
*/
public class Cp1025 extends AbstractCodePage {
public static final String ID = "1025";
public static final String DESCRIPTION = "Cyrillic Multilingual EBCDIC";
public static final int CPGID = 1025;
public static final int GCSGID = 1150;
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, 0x044F, 0x044E, 0x044D, 0x044C, 0x044B, 0x044A, 0x0449,
0x0448, 0x0447, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x0446, 0x0445, 0x0444, 0x0443, 0x0442, 0x0441, 0x0440,
0x043F, 0x043E, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x043D, 0x043C, 0x043B, 0x043A, 0x0439, 0x0438,
0x0437, 0x0436, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x0435, 0x0434, 0x0433, 0x0432, 0x0431, 0x0430, 0x0451, 0x0453,
0x0452, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x0454, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x0455, 0x0456, 0x0457, 0x0458, 0x0459, 0x045A,
// 90-9F
0x045B, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x045C, 0x045E, 0x045F, 0x0401, 0x0402, 0x0403,
// A0-AF
0x0404, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x0405, 0x0406, 0x0407, 0x0408, 0x0409, 0x040A,
// B0-BF
0x040B, 0x00A3, 0x040C, 0x00B7, 0x00A9, 0x040E, 0x040F, 0x0410,
0x0411, 0x0412, 0x00AC, 0x007C, 0x0413, 0x0414, 0x0415, 0x0416,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x041D, 0x041E, 0x041F, 0x0420, 0x0421, 0x0422,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E,
};
public Cp1025() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1025", 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 1123 (Cyrillic Ukraine EBCDIC).
* CCSID / CPGID: 1123, GCSGID: 1399.
*/
public class Cp1123 extends AbstractCodePage {
public static final String ID = "1123";
public static final String DESCRIPTION = "Cyrillic Ukraine EBCDIC";
public static final int CPGID = 1123;
public static final int GCSGID = 1399;
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, 0x044F, 0x044E, 0x044D, 0x044C, 0x044B, 0x044A, 0x0449,
0x0448, 0x0447, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x0446, 0x0445, 0x0444, 0x0443, 0x0442, 0x0441, 0x0440,
0x043F, 0x043E, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x043D, 0x043C, 0x043B, 0x043A, 0x0439, 0x0438,
0x0437, 0x0436, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x0435, 0x0434, 0x0433, 0x0432, 0x0431, 0x0430, 0x0451, 0x0491,
0x0454, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x0456, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x0457, 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, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x041D, 0x041E, 0x041F, 0x0420, 0x0421, 0x0422,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E,
};
public Cp1123() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1123", MAPPING);
}
}

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