30 Commits

Author SHA1 Message Date
rudi d8acd713af Hotspot detection debugging
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m29s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m46s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 2m8s
Release j3270 / Build & Publish Release (push) Successful in 1m39s
2026-10-02 13:54:28 +00:00
rudi dde5918da8 Cleaning the UI and organizing
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 43s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 59s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m30s
2026-10-02 12:26:58 +00:00
rudi 03d5ec0fae Fix tests for script changes
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 40s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 52s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m42s
2026-09-28 14:36:49 +00:00
rudi f8d078a23b Cleaning and some optimizations
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m19s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 37s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Failing after 1m27s
2026-09-28 14:16:01 +00:00
rudi cf0c0999bf Fix labeling
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 49s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m33s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m35s
2026-09-28 13:57:22 +00:00
rudi fac6ac8bfb Fix disconnect notifiers
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 53s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m30s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m37s
2026-09-28 13:01:03 +00:00
rudi daac11472d Fix misinterpreted if statement
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 52s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m30s
2026-09-28 04:19:06 +00:00
rudi 8d5672c708 Add tests
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 39s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m43s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m43s
2026-09-28 01:57:27 +00:00
rudi 9918c823c2 Cleaning and some optimizations 2026-09-28 01:56:13 +00:00
rudi 0201c7c277 Fix Read Modified Query from Moshix bug report
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 50s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m24s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m38s
Release j3270 / Build & Publish Release (push) Successful in 1m38s
2026-09-24 17:34:43 +00:00
rudi b61702a57c Fix Read Modified Query from Moshix bug report
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 52s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m42s
Release j3270 / Build & Publish Release (push) Failing after 1m24s
2026-09-24 17:03:38 +00:00
rudi bd4c1f3040 Add Read Modified Query from Moshix bug report
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m19s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m24s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m43s
Release j3270 / Build & Publish Release (push) Successful in 39s
2026-09-24 15:35:08 +00:00
rudi 0d6ab31f0a Connection storage
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 50s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m21s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m34s
2026-09-24 12:44:50 +00:00
rudi 1f0e36c561 Connection hardening and FileTransfer UI changes
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m20s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m32s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 2m7s
2026-09-24 04:15:36 +00:00
rudi 8286af19e6 IND$FILE Browse!
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 36s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 47s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m27s
2026-09-24 01:28:55 +00:00
rudi 3d3a939ece Codepage, ECL, and some UI updates
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 47s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m21s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m29s
2026-09-22 13:38:12 +00:00
rudi d05f1a4c45 Resize debugging
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 39s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 47s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m36s
2026-09-21 16:26:13 +00:00
rudi aa724fbcef Hotspot detection and screen buffer
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) Successful in 1m20s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m28s
Release j3270 / Build & Publish Release (push) Successful in 1m39s
2026-09-18 18:18:53 +00:00
rudi 8a30efe9c0 Add bugfix for ATTN
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 35s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m13s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m36s
2026-09-18 17:33:58 +00:00
rudi 5049d8df74 Add facade classes for 3rd party programs
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 34s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m15s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m18s
2026-09-18 16:48:49 +00:00
rudi badd026055 Cleanup datastream and Graphics
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) Successful in 1m13s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m34s
2026-09-18 00:55:54 +00:00
rudi ef70ea0f5a Bugfixes from zOS testing
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 37s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m16s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m52s
2026-09-17 23:09:43 +00:00
rudi 30b1a77b61 Datastream bug fixes w/ not recognizing bytes
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 33s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m9s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m10s
Release j3270 / Build & Publish Release (push) Successful in 1m11s
2026-09-16 21:08:22 +00:00
rudi 9bd1676ec1 Fix setting storage, and add proxy options like a3270
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 34s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m11s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m17s
2026-09-16 14:43:30 +00:00
rudi cfb32bdca5 Build Gitea release against Java 11 (lowest supported Java)
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 34s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m13s
Release j3270 / Build & Publish Release (push) Successful in 1m12s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m37s
2026-09-15 23:52:21 +00:00
rudi 754b1de7cb Rearrange settings, update sample config
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m8s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m15s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m38s
Release j3270 / Build & Publish Release (push) Successful in 1m13s
2026-09-15 17:59:26 +00:00
rudi 7892229a2c Add icon
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 32s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m9s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m12s
2026-09-14 13:01:41 +00:00
rudi 14bf7ba4b1 Add bugfixes from testing
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 37s
Release j3270 / Build & Publish Release (push) Successful in 43s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m14s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m12s
2026-09-12 17:21:21 -04:00
rudi bdfe6eec2a IMON GRAPHIC!
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 31s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 47s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m9s
Release j3270 / Build & Publish Release (push) Successful in 42s
2026-09-12 15:33:12 +00:00
rudi c28c097e25 Fix bugs reported in Discord
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 42s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m17s
2026-09-12 14:58:48 +00:00
236 changed files with 31080 additions and 2014 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
- name: Make scripts executable
run: |
chmod +x ./build_all.sh ./test_all.sh ./run.sh
chmod +x ./setup.sh ./test_all.sh
- name: Build & Run Unit Tests
run: |
+6 -6
View File
@@ -20,21 +20,21 @@ jobs:
with:
fetch-depth: 0
- name: Set up Java JDK (Temurin 17)
- name: Set up Java JDK (Temurin 11)
uses: actions/setup-java@v3
continue-on-error: true
with:
java-version: '17'
java-version: '11'
distribution: 'temurin'
- name: Ensure Java Available
run: |
if ! command -v javac >/dev/null 2>&1; then
echo "Installing OpenJDK 17 via package manager..."
echo "Installing OpenJDK 11 via package manager..."
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-11-jdk-headless || true
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache openjdk17 || true
apk add --no-cache openjdk11 || true
fi
fi
echo "Java compiler:"
@@ -44,7 +44,7 @@ jobs:
- name: Make scripts executable
run: |
chmod +x ./build_all.sh ./test_all.sh ./run.sh
chmod +x ./setup.sh ./test_all.sh
- name: Build & Run Unit Tests
run: |
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
- name: Make scripts executable
run: |
chmod +x ./build_all.sh ./test_all.sh ./run.sh
chmod +x ./setup.sh ./test_all.sh
- name: Build & Run Unit Tests
run: |
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
- name: Make scripts executable
run: |
chmod +x ./build_all.sh ./test_all.sh ./run.sh
chmod +x ./setup.sh ./test_all.sh
- name: Build & Run Unit Tests
run: |
+12 -14
View File
@@ -3,7 +3,7 @@
[![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)
[![Java](https://img.shields.io/badge/Java-11%20%7C%2017%20%7C%2021%20(LTS)-blue.svg)](https://adoptium.net)
[![Tests](https://img.shields.io/badge/Tests-539%20passed-brightgreen.svg)]()
[![Tests](https://img.shields.io/badge/Tests-851%20passed-brightgreen.svg)]()
[![License](https://img.shields.io/badge/License-Unlicense-blue.svg)](LICENSE)
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.
@@ -12,10 +12,11 @@ An **x3270-aligned and IBM Host On-Demand (HoD) compatible IBM 3270 mainframe te
## 🚀 Quick Download & Run
### Latest Standalone JARs
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:
### Latest Standalone JARs & Maven Artifacts
Ready-to-run executable JARs and standalone library packages 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)**
- **Standalone Protocol Library (`lib3270j.jar`)**: Built by setup/build scripts and published to Maven (`haus.nightmare.j3270:lib3270j:1.2.4`).
- **[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)
@@ -61,8 +62,8 @@ java -jar j3270.jar -c config.ini
# 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
# Launch directly from source setup script
./setup.sh --run
```
---
@@ -105,7 +106,7 @@ java -jar j3270.jar -d mainframe.example.com
- **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).
- **40+ EBCDIC Codepages & Euro Variants**: 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), Japanese SBCS (290, 1027), and 10 full Euro variants (1140–1149).
- **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.
@@ -125,7 +126,7 @@ java -jar j3270.jar -d mainframe.example.com
- **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).
- **`lib3270j`**: Standalone Java protocol and terminal engine with **zero third-party dependencies** (`java.base`, `java.desktop` only). Distributed as standalone JAR (`build/lib3270j.jar`) and published Maven coordinate `haus.nightmare.j3270:lib3270j:1.2.4`.
- **`j3270`**: Standalone desktop Swing terminal application.
---
@@ -159,16 +160,12 @@ Self-contained setup scripts automatically locate an installed Java 11+ JDK (sea
# 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:
### Standalone Testing & CI
Run automated tests directly via the standalone test runner:
```bash
# Direct compile and package without diagnostics:
sh ./build_all.sh
# Run all 539+ automated unit tests (~7s):
# Run all 851 automated unit tests (~25s):
sh ./test_all.sh
```
@@ -186,6 +183,7 @@ All pushes and pull requests are verified across **Java 11, 17, and 21 LTS** run
## 📜 Acknowledgements
- [x3270](https://x3270.miraheze.org/wiki/Main_Page) — Reference C implementation and protocol specifications
- IBM Bob — Architectural evaluation, Host On-Demand parity audit, and efficiency analysis
- [Antigravity](https://antigravity.google/)
- Claude Opus 4.6
- Gemini 3.1 Pro
+146
View File
@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="124 738 320 320"
width="320"
height="320"
xml:space="preserve"
id="svg_j3270_icon"
version="1.1">
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<!-- Pure black background (#000000) for classic 3270 CRT terminal look -->
<rect x="124" y="738" width="320" height="320" fill="#000000" />
<g transform="matrix(1.3333333,0,0,-1.3333333,0,1122.6667)" id="g10">
<g transform="scale(0.1)" id="g12">
<g id="g14">
<!-- Green Terminal Screen Frame -->
<path
id="path28"
style="fill:none;stroke:#00ff00;stroke-width:3.75079012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
<!-- Blue Wave / Exponential Curve -->
<path
id="path30"
style="fill:none;stroke:#0000ff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1172.41,1137.58 79.1,-1.22 -0.21,-0.38 c 1.08,0 2.18,0 3.27,0 762.95,0 1464.86,417.88 1829.53,1089.21" />
<!-- Red Circular Arc -->
<path
id="path32"
style="fill:none;stroke:#ff0000;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2593.86,1620.73 -1.22,-28 -0.08,-0.08 c 0.43,-250.15 202.81,-452.72 452.3,-452.72 0.14,0 0.28,0 0.42,0 l 43.9,2.52" />
<!-- Yellow Smiley Head -->
<path
id="path34"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2976,1363.94 v 0.08 c 44.73,34.29 71.21,87.25 71.8,143.61 l 0.01,-0.15 c -0.06,97.48 -79.11,176.47 -176.59,176.47 -0.36,0 -0.73,0 -1.09,0 l -0.07,0.03 c -0.25,0 -0.51,0 -0.75,0 -37.38,0 -73.79,-11.94 -103.92,-34.08 l 0.02,-0.03 c -44.38,-34.04 -70.82,-86.47 -71.8,-142.39 l 0.06,0.04 c 0.54,-97.43 79.04,-176.47 176.46,-177.69 l 0.08,0.02 c 0.64,-0.01 1.29,-0.01 1.92,-0.01 37.4,0 73.82,11.94 103.96,34.08" />
<!-- 'l' -->
<path
id="path36"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<path
id="path38"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<!-- 'i' -->
<path
id="path40"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<path
id="path42"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<!-- 'b' -->
<path
id="path44"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<path
id="path46"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<!-- '3' -->
<path
id="path48"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<path
id="path50"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<!-- '2' -->
<path
id="path52"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<path
id="path54"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<!-- '7' -->
<path
id="path56"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<path
id="path58"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<!-- '0' -->
<path
id="path60"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<path
id="path62"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<!-- 'j' -->
<path
id="path64"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<path
id="path66"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<!-- Smile -->
<path
id="path68"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2778.85,1478.34 0.04,-0.02 c 21.03,-31.91 56.69,-51.12 94.91,-51.12 0.41,0 0.83,0 1.24,0.01 l -0.02,0.07 c 43.88,0.19 84.11,24.51 104.66,63.28" />
<!-- Left Eye (cyan) -->
<path
id="path70"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2820.22,1595.17 0.01,0.01 c -14.62,-1.91 -25.56,-14.37 -25.56,-29.11 0,-0.03 0,-0.07 0,-0.1 l -0.02,-0.01 c 0,-0.03 0,-0.07 0,-0.1 0,-16.09 13.04,-29.14 29.13,-29.14 0.43,0 0.87,0.01 1.29,0.03 h 3.67 0.01 c 14.61,1.91 25.55,14.37 25.55,29.11 0,0.03 0,0.07 0,0.1 l 0.02,0.01 c 0,0.04 0,0.08 0,0.12 0,16.07 -13.03,29.09 -29.09,29.09 -0.03,0 -0.08,0 -0.12,0 l -4.89,-0.01" />
<!-- Right Eye (cyan) -->
<path
id="path72"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2921.23,1569.61 v -3.65 l -0.01,-0.01 c 0,-0.27 -0.01,-0.54 -0.01,-0.81 0,-18.22 14.66,-33.05 32.87,-33.26 l 0.03,0.01 c 0.28,0 0.57,-0.01 0.84,-0.01 17.29,0 31.72,13.22 33.24,30.44 l -0.02,3.64 0.02,0.02 c 0,0.27 0.01,0.54 0.01,0.81 0,18.37 -14.9,33.28 -33.28,33.28 -0.26,0 -0.54,-0.01 -0.81,-0.02 l -0.02,-0.03 c -17.14,-0.18 -31.36,-13.34 -32.86,-30.42" />
<!-- Nose -->
<path
id="path74"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2892.03,1528.23 -29.21,-20.68" />
<path
id="path76"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2862.82,1507.55 h 32.86" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

+146
View File
@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="144 814 280 169"
width="280"
height="169"
xml:space="preserve"
id="svg_lib3270j_cropped"
version="1.1">
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<!-- Pure black background (#000000) for classic 3270 CRT terminal look -->
<rect x="144" y="814" width="280" height="169" fill="#000000" />
<g transform="matrix(1.3333333,0,0,-1.3333333,0,1122.6667)" id="g10">
<g transform="scale(0.1)" id="g12">
<g id="g14">
<!-- Green Terminal Screen Frame -->
<path
id="path28"
style="fill:none;stroke:#00ff00;stroke-width:3.75079012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
<!-- Blue Wave / Exponential Curve -->
<path
id="path30"
style="fill:none;stroke:#0000ff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1172.41,1137.58 79.1,-1.22 -0.21,-0.38 c 1.08,0 2.18,0 3.27,0 762.95,0 1464.86,417.88 1829.53,1089.21" />
<!-- Red Circular Arc -->
<path
id="path32"
style="fill:none;stroke:#ff0000;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2593.86,1620.73 -1.22,-28 -0.08,-0.08 c 0.43,-250.15 202.81,-452.72 452.3,-452.72 0.14,0 0.28,0 0.42,0 l 43.9,2.52" />
<!-- Yellow Smiley Head -->
<path
id="path34"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2976,1363.94 v 0.08 c 44.73,34.29 71.21,87.25 71.8,143.61 l 0.01,-0.15 c -0.06,97.48 -79.11,176.47 -176.59,176.47 -0.36,0 -0.73,0 -1.09,0 l -0.07,0.03 c -0.25,0 -0.51,0 -0.75,0 -37.38,0 -73.79,-11.94 -103.92,-34.08 l 0.02,-0.03 c -44.38,-34.04 -70.82,-86.47 -71.8,-142.39 l 0.06,0.04 c 0.54,-97.43 79.04,-176.47 176.46,-177.69 l 0.08,0.02 c 0.64,-0.01 1.29,-0.01 1.92,-0.01 37.4,0 73.82,11.94 103.96,34.08" />
<!-- 'l' -->
<path
id="path36"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<path
id="path38"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<!-- 'i' -->
<path
id="path40"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<path
id="path42"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<!-- 'b' -->
<path
id="path44"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<path
id="path46"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<!-- '3' -->
<path
id="path48"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<path
id="path50"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<!-- '2' -->
<path
id="path52"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<path
id="path54"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<!-- '7' -->
<path
id="path56"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<path
id="path58"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<!-- '0' -->
<path
id="path60"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<path
id="path62"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<!-- 'j' -->
<path
id="path64"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<path
id="path66"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<!-- Smile -->
<path
id="path68"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2778.85,1478.34 0.04,-0.02 c 21.03,-31.91 56.69,-51.12 94.91,-51.12 0.41,0 0.83,0 1.24,0.01 l -0.02,0.07 c 43.88,0.19 84.11,24.51 104.66,63.28" />
<!-- Left Eye (cyan) -->
<path
id="path70"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2820.22,1595.17 0.01,0.01 c -14.62,-1.91 -25.56,-14.37 -25.56,-29.11 0,-0.03 0,-0.07 0,-0.1 l -0.02,-0.01 c 0,-0.03 0,-0.07 0,-0.1 0,-16.09 13.04,-29.14 29.13,-29.14 0.43,0 0.87,0.01 1.29,0.03 h 3.67 0.01 c 14.61,1.91 25.55,14.37 25.55,29.11 0,0.03 0,0.07 0,0.1 l 0.02,0.01 c 0,0.04 0,0.08 0,0.12 0,16.07 -13.03,29.09 -29.09,29.09 -0.03,0 -0.08,0 -0.12,0 l -4.89,-0.01" />
<!-- Right Eye (cyan) -->
<path
id="path72"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2921.23,1569.61 v -3.65 l -0.01,-0.01 c 0,-0.27 -0.01,-0.54 -0.01,-0.81 0,-18.22 14.66,-33.05 32.87,-33.26 l 0.03,0.01 c 0.28,0 0.57,-0.01 0.84,-0.01 17.29,0 31.72,13.22 33.24,30.44 l -0.02,3.64 0.02,0.02 c 0,0.27 0.01,0.54 0.01,0.81 0,18.37 -14.9,33.28 -33.28,33.28 -0.26,0 -0.54,-0.01 -0.81,-0.02 l -0.02,-0.03 c -17.14,-0.18 -31.36,-13.34 -32.86,-30.42" />
<!-- Nose -->
<path
id="path74"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2892.03,1528.23 -29.21,-20.68" />
<path
id="path76"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2862.82,1507.55 h 32.86" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

+6 -1
View File
@@ -1,6 +1,6 @@
allprojects {
group = 'haus.nightmare.j3270'
version = '0.1.0'
version = '1.2.4'
repositories {
mavenCentral()
@@ -23,6 +23,11 @@ subprojects {
}
}
tasks.withType(Javadoc).configureEach {
options.addStringOption('Xdoclint:none', '-quiet')
options.encoding = 'UTF-8'
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
-160
View File
@@ -1,160 +0,0 @@
#!/bin/sh
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
echo "=== Building j3270 Terminal Emulator ==="
# 1. Locate Java toolchain
JAVAC_BIN=""
JAR_BIN=""
if [ -n "$JAVA_HOME" ] && [ -x "$JAVA_HOME/bin/javac" ] && [ -x "$JAVA_HOME/bin/jar" ]; then
JAVAC_BIN="$JAVA_HOME/bin/javac"
JAR_BIN="$JAVA_HOME/bin/jar"
elif command -v javac >/dev/null 2>&1 && command -v jar >/dev/null 2>&1; then
if javac -version >/dev/null 2>&1; then
JAVAC_BIN="$(command -v javac)"
JAR_BIN="$(command -v jar)"
fi
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-17-openjdk* \
/usr/lib/jvm/java-11-openjdk* \
/Library/Java/JavaVirtualMachines/*/Contents/Home \
/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
export JAVA_HOME="$h"
JAVAC_BIN="$JAVA_HOME/bin/javac"
JAR_BIN="$JAVA_HOME/bin/jar"
break
fi
done
fi
if [ -z "$JAVAC_BIN" ] || [ -z "$JAR_BIN" ]; then
echo "ERROR: Java development tools (javac, jar) could not be found."
echo "Please install JDK 11+ or set JAVA_HOME."
exit 1
fi
echo "Using Java compiler: $JAVAC_BIN"
echo "Using JAR utility: $JAR_BIN"
BUILD_DIR="$SCRIPT_DIR/build"
rm -rf "$BUILD_DIR/lib3270j" "$BUILD_DIR/j3270" "$BUILD_DIR/MANIFEST.MF"
mkdir -p "$BUILD_DIR/lib3270j" "$BUILD_DIR/j3270"
# 2. Compile lib3270j
echo "Compiling lib3270j..."
find "$SCRIPT_DIR/lib3270j/src/main/java" -name "*.java" > "$BUILD_DIR/lib_sources.txt"
if [ ! -s "$BUILD_DIR/lib_sources.txt" ]; then
echo "ERROR: No sources found in lib3270j/src/main/java"
exit 1
fi
"$JAVAC_BIN" -d "$BUILD_DIR/lib3270j" @"$BUILD_DIR/lib_sources.txt"
# 3. Compile j3270
echo "Compiling j3270 application..."
find "$SCRIPT_DIR/j3270/src/main/java" -name "*.java" > "$BUILD_DIR/app_sources.txt"
if [ ! -s "$BUILD_DIR/app_sources.txt" ]; then
echo "ERROR: No sources found in j3270/src/main/java"
exit 1
fi
"$JAVAC_BIN" -cp "$BUILD_DIR/lib3270j" -d "$BUILD_DIR/j3270" @"$BUILD_DIR/app_sources.txt"
# Clean up temporary source lists
rm -f "$BUILD_DIR/lib_sources.txt" "$BUILD_DIR/app_sources.txt"
# Copy any resources if present
if [ -d "$SCRIPT_DIR/j3270/src/main/resources" ]; then
cp -r "$SCRIPT_DIR/j3270/src/main/resources/"* "$BUILD_DIR/j3270/" 2>/dev/null || true
fi
if [ -d "$SCRIPT_DIR/lib3270j/src/main/resources" ]; then
cp -r "$SCRIPT_DIR/lib3270j/src/main/resources/"* "$BUILD_DIR/lib3270j/" 2>/dev/null || true
fi
# 4. Package executable JAR
echo "Packaging j3270.jar..."
printf "Manifest-Version: 1.0\nMain-Class: haus.nightmare.j3270.J3270App\nImplementation-Title: j3270\nImplementation-Version: 0.1.0\nCreated-By: j3270 build_all.sh\n\n" > "$BUILD_DIR/MANIFEST.MF"
"$JAR_BIN" cvfm "$BUILD_DIR/j3270.jar" "$BUILD_DIR/MANIFEST.MF" \
-C "$BUILD_DIR/lib3270j" . \
-C "$BUILD_DIR/j3270" . > /dev/null
echo "=== Build Complete ==="
echo "Executable JAR created: $BUILD_DIR/j3270.jar"
ls -lh "$BUILD_DIR/j3270.jar"
# 5. Compile and run tests if requested
if [ "$1" = "test" ] || [ "$1" = "check" ]; then
echo "=== Running Tests ==="
TEST_BUILD_DIR="$BUILD_DIR/test-classes"
mkdir -p "$TEST_BUILD_DIR"
JUNIT_CP=$(find "$HOME/.gradle/caches/modules-2/files-2.1/" -name "*.jar" 2>/dev/null | grep -E "junit|opentest|apiguardian" | tr "\n" ":" || true)
FULL_CP="$BUILD_DIR/lib3270j:$BUILD_DIR/j3270:$JUNIT_CP"
find "$SCRIPT_DIR/lib3270j/src/test/java" -name "*.java" > "$BUILD_DIR/test_sources.txt" 2>/dev/null || true
if [ -d "$SCRIPT_DIR/j3270/src/test/java" ]; then
find "$SCRIPT_DIR/j3270/src/test/java" -name "*.java" >> "$BUILD_DIR/test_sources.txt" 2>/dev/null || true
fi
if [ -s "$BUILD_DIR/test_sources.txt" ] && [ -n "$JUNIT_CP" ]; then
echo "Compiling tests..."
"$JAVAC_BIN" -cp "$FULL_CP" -d "$TEST_BUILD_DIR" @"$BUILD_DIR/test_sources.txt"
rm -f "$BUILD_DIR/test_sources.txt"
echo "Executing tests with JUnit Platform..."
cat << 'EOF' > "$BUILD_DIR/TestRunner.java"
import org.junit.platform.launcher.*;
import org.junit.platform.launcher.core.*;
import org.junit.platform.launcher.listeners.*;
import org.junit.platform.engine.discovery.*;
import static org.junit.platform.engine.discovery.DiscoverySelectors.*;
import java.io.PrintWriter;
public class TestRunner {
public static void main(String[] args) {
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(
selectPackage("haus.nightmare")
)
.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
+4 -4
View File
@@ -2,7 +2,7 @@
This repository includes Gitea Actions workflows in `.gitea/workflows/build.yaml` to automatically:
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 standalone executables for each LTS version via `build_all.sh`.
2. Build standalone executables for each LTS version via `setup.sh`.
3. Upload `j3270-java11.jar`, `j3270-java17.jar`, and `j3270-java21.jar` as downloadable artifacts.
4. Report test results and diagnostics per runtime.
@@ -98,12 +98,12 @@ 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:
```bash
# Compile and run all 539 unit tests (~7s):
# Compile and run all 851 unit tests (~25s):
sh ./test_all.sh
# Build executable standalone JAR:
sh ./build_all.sh
./setup.sh
# Run the emulator:
sh ./run.sh
java -jar build/j3270.jar
```
-1
View File
@@ -1,2 +1 @@
android.useAndroidX=true
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
+3 -1
View File
@@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+9 -10
View File
@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 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.
@@ -15,10 +15,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
@@ -27,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
@@ -55,7 +57,7 @@
# 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
# https://github.com/gradle/gradle/blob/<unknown>/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -84,7 +86,7 @@ done
# 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
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -112,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -170,7 +171,6 @@ fi
# 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" )
@@ -203,15 +203,14 @@ fi
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,
# * DEFAULT_JVM_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 \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
Vendored
+24 -34
View File
@@ -13,16 +13,18 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@@ -43,13 +45,13 @@ 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.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -57,36 +59,24 @@ 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.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
: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 %*
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
: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
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
+6 -1
View File
@@ -1,8 +1,12 @@
; j3270 Configuration File
; Load with: java -jar j3270.jar -c j3270.ini
; or: ./run.sh -c j3270.ini
[appearance]
; UI font settings (menus, dialogs, status bar)
; uiFontFamily = SansSerif
; uiFontSize = 13
;
; Terminal font settings (3270 screen display)
; fontFamily = IBM 3270
; fontSize = 18
@@ -10,6 +14,7 @@
; startupBehavior = AUTO_CONNECT | SHOW_CONNECT | DO_NOTHING
; autoConnectHost = mainframe.example.com
; autoConnectPort = 23
; blockPaste = true
[colors]
; Host colors 0-15 (hex RGB)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,37 @@
package haus.nightmare.j3270;
/**
* Version information for the j3270 application.
*/
public final class Version {
/**
* Hardcoded fallback release version matching build.gradle and pom coordinates.
*/
public static final String VERSION = "1.2.4";
/**
* Canonical application name.
*/
public static final String NAME = "j3270";
private Version() {}
/**
* Retrieves the runtime version string of the j3270 application.
* Inspects the package implementation version first (populated when loaded from a JAR),
* and falls back to the compile-time {@link #VERSION} constant if null or blank.
*
* @return the version string (e.g. "1.2.4")
*/
public static String getVersion() {
Package pkg = Version.class.getPackage();
if (pkg != null) {
String implVer = pkg.getImplementationVersion();
if (implVer != null && !implVer.isBlank()) {
return implVer;
}
}
return VERSION;
}
}
@@ -45,6 +45,24 @@ public class Settings {
flushPrefs();
}
public static String getUiFontFamily() {
return prefs.get("uiFontFamily", "SansSerif");
}
public static void setUiFontFamily(String family) {
prefs.put("uiFontFamily", (family != null && !family.trim().isEmpty()) ? family : "SansSerif");
flushPrefs();
}
public static int getUiFontSize() {
return prefs.getInt("uiFontSize", 13);
}
public static void setUiFontSize(int size) {
prefs.putInt("uiFontSize", Math.max(8, Math.min(size, 48)));
flushPrefs();
}
private static void flushPrefs() {
try {
prefs.flush();
@@ -145,6 +163,15 @@ public class Settings {
return getAutoConnectAutoReconnect();
}
public static String getDefaultHostType() {
return prefs.get("defaultHostType", "TSO");
}
public static void setDefaultHostType(String hostType) {
prefs.put("defaultHostType", (hostType != null && !hostType.trim().isEmpty()) ? hostType.trim().toUpperCase() : "TSO");
flushPrefs();
}
public static void setAutoReconnect(boolean autoReconnect) {
setAutoConnectAutoReconnect(autoReconnect);
}
@@ -390,6 +417,17 @@ public class Settings {
prefs.putBoolean("blockSelectMode", block);
}
// ========== Block paste ==========
public static boolean getBlockPaste() {
return prefs.getBoolean("blockPaste", true);
}
public static void setBlockPaste(boolean block) {
prefs.putBoolean("blockPaste", block);
flushPrefs();
}
// ========== Clipboard & Tabular Paste ==========
public static boolean getEnablePasteFromExcel() {
@@ -410,6 +448,33 @@ public class Settings {
flushPrefs();
}
public static boolean getTrimRectSizingHandles() {
return prefs.getBoolean("trimRectSizingHandles", false);
}
public static void setTrimRectSizingHandles(boolean val) {
prefs.putBoolean("trimRectSizingHandles", val);
flushPrefs();
}
public static boolean getTrimRectRemainAfterEdit() {
return prefs.getBoolean("trimRectRemainAfterEdit", false);
}
public static void setTrimRectRemainAfterEdit(boolean val) {
prefs.putBoolean("trimRectRemainAfterEdit", val);
flushPrefs();
}
public static boolean getPasteWordBreak() {
return prefs.getBoolean("pasteWordBreak", true);
}
public static void setPasteWordBreak(boolean val) {
prefs.putBoolean("pasteWordBreak", val);
flushPrefs();
}
// ========== Crosshair Ruler ==========
public static boolean getCrosshairRuler() {
@@ -420,6 +485,71 @@ public class Settings {
prefs.putBoolean("crosshairRuler", enabled);
}
// ========== Phase 25: Hotspots & Mouse Wheel Navigation ==========
public static boolean getHotspotsEnabled() {
return prefs.getBoolean("hotspotsEnabled", true);
}
public static void setHotspotsEnabled(boolean enabled) {
prefs.putBoolean("hotspotsEnabled", enabled);
flushPrefs();
}
public static boolean getHotspotsUrlEnabled() {
return prefs.getBoolean("hotspotsUrlEnabled", true);
}
public static void setHotspotsUrlEnabled(boolean enabled) {
prefs.putBoolean("hotspotsUrlEnabled", enabled);
flushPrefs();
}
public static boolean getHotspotsPfEnabled() {
return prefs.getBoolean("hotspotsPfEnabled", true);
}
public static void setHotspotsPfEnabled(boolean enabled) {
prefs.putBoolean("hotspotsPfEnabled", enabled);
flushPrefs();
}
public static boolean getHotspotsMenuChoicesEnabled() {
return prefs.getBoolean("hotspotsMenuChoicesEnabled", true);
}
public static void setHotspotsMenuChoicesEnabled(boolean enabled) {
prefs.putBoolean("hotspotsMenuChoicesEnabled", enabled);
flushPrefs();
}
public static boolean getMouseWheelEnabled() {
return prefs.getBoolean("mouseWheelEnabled", true);
}
public static void setMouseWheelEnabled(boolean enabled) {
prefs.putBoolean("mouseWheelEnabled", enabled);
flushPrefs();
}
public static String getMouseWheelUpKey() {
return prefs.get("mouseWheelUpKey", "[pf7]");
}
public static void setMouseWheelUpKey(String key) {
prefs.put("mouseWheelUpKey", key != null && !key.trim().isEmpty() ? key.trim() : "[pf7]");
flushPrefs();
}
public static String getMouseWheelDownKey() {
return prefs.get("mouseWheelDownKey", "[pf8]");
}
public static void setMouseWheelDownKey(String key) {
prefs.put("mouseWheelDownKey", key != null && !key.trim().isEmpty() ? key.trim() : "[pf8]");
flushPrefs();
}
// ========== Status Bar Visibility ==========
public static boolean getStatusBarVisible() {
@@ -541,6 +671,16 @@ public class Settings {
switch (key) {
case "fontFamily": setFontFamily(value); break;
case "fontSize": setFontSize(Integer.parseInt(value)); break;
case "uiFontFamily":
case "uifontfamily":
case "uifont":
case "uiFont":
setUiFontFamily(value);
break;
case "uiFontSize":
case "uifontsize":
setUiFontSize(Integer.parseInt(value));
break;
case "javaUiTheme":
case "theme":
case "uiTheme":
@@ -607,7 +747,13 @@ public class Settings {
case "dynamic_cols":
setDynamicCols(Integer.parseInt(value));
break;
case "hostType":
case "hosttype":
case "host_type":
setDefaultHostType(value);
break;
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
case "blockPaste": setBlockPaste(Boolean.parseBoolean(value)); break;
case "autoReconnect":
case "auto_reconnect":
case "autoConnectAutoReconnect":
@@ -632,6 +778,9 @@ public class Settings {
case "auto_sys_unlock":
setAutoSysUnlock(Boolean.parseBoolean(value));
break;
case "blockPasteMode":
setBlockPaste(Boolean.parseBoolean(value));
break;
case "enablePasteFromExcel":
case "pasteFromExcel":
case "excelPaste":
@@ -642,6 +791,18 @@ public class Settings {
case "pasteStopAtProtected":
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
break;
case "trimRectSizingHandles":
case "sizingHandles":
setTrimRectSizingHandles(Boolean.parseBoolean(value));
break;
case "trimRectRemainAfterEdit":
case "remainAfterEdit":
setTrimRectRemainAfterEdit(Boolean.parseBoolean(value));
break;
case "pasteWordBreak":
case "wordBreak":
setPasteWordBreak(Boolean.parseBoolean(value));
break;
default:
log.warning("Unknown behavior/connection key: " + key);
}
@@ -649,6 +810,10 @@ public class Settings {
case "clipboard":
switch (key) {
case "blockPaste":
case "blockPasteMode":
setBlockPaste(Boolean.parseBoolean(value));
break;
case "enablePasteFromExcel":
case "pasteFromExcel":
case "excelPaste":
@@ -659,6 +824,18 @@ public class Settings {
case "pasteStopAtProtected":
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
break;
case "trimRectSizingHandles":
case "sizingHandles":
setTrimRectSizingHandles(Boolean.parseBoolean(value));
break;
case "trimRectRemainAfterEdit":
case "remainAfterEdit":
setTrimRectRemainAfterEdit(Boolean.parseBoolean(value));
break;
case "pasteWordBreak":
case "wordBreak":
setPasteWordBreak(Boolean.parseBoolean(value));
break;
default:
log.warning("Unknown clipboard key: " + key);
}
@@ -761,6 +938,8 @@ public class Settings {
// [appearance]
w.println("[appearance]");
w.println("javaUiTheme = " + getJavaUiTheme().name());
w.println("uiFontFamily = " + getUiFontFamily());
w.println("uiFontSize = " + getUiFontSize());
w.println("fontFamily = " + getFontFamily());
w.println("fontSize = " + getFontSize());
w.println("crosshairRuler = " + getCrosshairRuler());
@@ -780,9 +959,11 @@ public class Settings {
w.println("autoConnectTn3270e = " + getAutoConnectTn3270e());
}
w.println("codePage = " + getCodePage());
w.println("hostType = " + getDefaultHostType());
w.println("dynamicRows = " + getDynamicRows());
w.println("dynamicCols = " + getDynamicCols());
w.println("blockSelectMode = " + getBlockSelectMode());
w.println("blockPaste = " + getBlockPaste());
w.println("autoReconnect = " + getAutoConnectAutoReconnect());
w.println("autoConnectReconnectMaxRetries = " + getAutoConnectReconnectMaxRetries());
w.println("inputMask = " + getInputMask());
@@ -790,6 +971,9 @@ public class Settings {
w.println("autoSysUnlock = " + getAutoSysUnlock());
w.println("enablePasteFromExcel = " + getEnablePasteFromExcel());
w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine());
w.println("trimRectSizingHandles = " + getTrimRectSizingHandles());
w.println("trimRectRemainAfterEdit = " + getTrimRectRemainAfterEdit());
w.println("pasteWordBreak = " + getPasteWordBreak());
w.println();
// [entryassist]
@@ -1,9 +1,11 @@
package haus.nightmare.j3270.ft;
import haus.nightmare.j3270.ui.AppIcon;
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 haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
@@ -27,6 +29,8 @@ public class FileTransferDialog extends JDialog {
public String lrecl = "";
public String blksize = "";
public String space = "";
public FTConfig.AllocationUnit units = FTConfig.AllocationUnit.TRACKS;
public String dirBlocks = "";
public String options = "";
}
@@ -81,6 +85,8 @@ public class FileTransferDialog extends JDialog {
private JTextField lreclField;
private JTextField blksizeField;
private JTextField spaceField;
private JComboBox<FTConfig.AllocationUnit> unitsCombo;
private JTextField dirField;
// VM specific
private JTextField optionsField;
@@ -92,6 +98,7 @@ public class FileTransferDialog extends JDialog {
super(owner, "File Transfer (IND$FILE)", false);
this.owner = owner;
this.coordinator = coordinator;
AppIcon.applyTo(this);
buildUI();
pack();
@@ -218,7 +225,7 @@ public class FileTransferDialog extends JDialog {
formPanel.add(createRow("File Options:", optionsPanel2));
// Host-specific options panel (TSO dataset allocation)
JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5));
JPanel hostOptsPanel = new JPanel(new GridLayout(3, 4, 5, 5));
hostOptsPanel.setOpaque(false);
hostOptsPanel.setBorder(ThemeManager.createTitledBorder("TSO Allocation Options (Send Only)"));
@@ -234,10 +241,24 @@ public class FileTransferDialog extends JDialog {
blksizeField = new JTextField(5);
hostOptsPanel.add(blksizeField);
hostOptsPanel.add(new JLabel("SPACE:"));
hostOptsPanel.add(new JLabel("Units:"));
unitsCombo = new JComboBox<>(new FTConfig.AllocationUnit[]{
FTConfig.AllocationUnit.TRACKS,
FTConfig.AllocationUnit.CYLINDERS,
FTConfig.AllocationUnit.AVBLOCK,
FTConfig.AllocationUnit.DEFAULT
});
unitsCombo.setSelectedItem(FTConfig.AllocationUnit.TRACKS);
hostOptsPanel.add(unitsCombo);
hostOptsPanel.add(new JLabel("SPACE (Pri,Sec):"));
spaceField = new JTextField(15);
hostOptsPanel.add(spaceField);
hostOptsPanel.add(new JLabel("Directory (PDS):"));
dirField = new JTextField(5);
hostOptsPanel.add(dirField);
formPanel.add(Box.createRigidArea(new Dimension(0, 10)));
formPanel.add(hostOptsPanel);
@@ -304,9 +325,25 @@ public class FileTransferDialog extends JDialog {
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.units != null) unitsCombo.setSelectedItem(lastTransferState.units);
if (lastTransferState.space != null) spaceField.setText(lastTransferState.space);
if (lastTransferState.dirBlocks != null) dirField.setText(lastTransferState.dirBlocks);
if (lastTransferState.options != null) optionsField.setText(lastTransferState.options);
}
// When using a saved connection (or when no previous transfer state exists for this connection),
// use the configured host type (CMS/TSO/CICS) for File Transfers
if (coordinator != null && coordinator.getClient() != null) {
haus.nightmare.lib3270j.ConnectionConfig cfg = coordinator.getClient().getConfig();
if (cfg != null) {
FTConfig.HostType configuredType = cfg.getFtHostType();
if (configuredType != null) {
if (cfg.isSavedConnection() || lastTransferState == null) {
hostTypeCombo.setSelectedItem(configuredType);
}
}
}
}
}
private void updateOptionStates() {
@@ -325,7 +362,9 @@ public class FileTransferDialog extends JDialog {
recfmField.setEnabled(isSend && isTso);
lreclField.setEnabled(isSend && isTso);
blksizeField.setEnabled(isSend && isTso);
unitsCombo.setEnabled(isSend && isTso);
spaceField.setEnabled(isSend && isTso);
dirField.setEnabled(isSend && isTso);
optionsField.setEnabled(isCms);
}
@@ -357,11 +396,26 @@ public class FileTransferDialog extends JDialog {
private void browseHostDirectory() {
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
String initialQuery = hostFileField.getText().trim();
if (hostType == FTConfig.HostType.CMS && (initialQuery.isEmpty() || initialQuery.equals("* * *") || initialQuery.equals("*"))) {
initialQuery = "* * A";
}
HostDirectoryDialog dialog = new HostDirectoryDialog(this, coordinator.getClient(), hostType, initialQuery);
dialog.setVisible(true);
if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) {
hostFileField.setText(dialog.getSelectedHostFile());
HostDirectoryEntry entry = dialog.getSelectedEntry();
if (entry != null) {
if (entry.getRecfm() != null && !entry.getRecfm().trim().isEmpty()) {
recfmField.setText(entry.getRecfm().trim());
}
if (entry.getLrecl() > 0) {
lreclField.setText(String.valueOf(entry.getLrecl()));
}
if (entry.getBlksize() > 0) {
blksizeField.setText(String.valueOf(entry.getBlksize()));
}
}
boolean shouldAutofill = localFileField.getText().trim().isEmpty() ||
(lastTransferState != null && localFileField.getText().trim().equals(lastTransferState.localFile));
if (shouldAutofill) {
@@ -408,7 +462,10 @@ public class FileTransferDialog extends JDialog {
config.setRecfm(recfmField.getText().trim());
config.setLrecl(lreclField.getText().trim());
config.setBlksize(blksizeField.getText().trim());
FTConfig.AllocationUnit selectedUnits = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem();
config.setUnits(selectedUnits != null ? selectedUnits : FTConfig.AllocationUnit.DEFAULT);
config.setSpace(spaceField.getText().trim());
config.setDirectoryBlocks(dirField.getText().trim());
config.setOptions(optionsField.getText().trim());
recordSessionTransfer();
@@ -437,7 +494,9 @@ public class FileTransferDialog extends JDialog {
state.recfm = recfmField.getText().trim();
state.lrecl = lreclField.getText().trim();
state.blksize = blksizeField.getText().trim();
state.units = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem();
state.space = spaceField.getText().trim();
state.dirBlocks = dirField.getText().trim();
state.options = optionsField.getText().trim();
lastTransferState = state;
}
@@ -457,7 +516,9 @@ public class FileTransferDialog extends JDialog {
public JTextField getRecfmField() { return recfmField; }
public JTextField getLreclField() { return lreclField; }
public JTextField getBlksizeField() { return blksizeField; }
public JComboBox<FTConfig.AllocationUnit> getUnitsCombo() { return unitsCombo; }
public JTextField getSpaceField() { return spaceField; }
public JTextField getDirField() { return dirField; }
public JTextField getOptionsField() { return optionsField; }
private void applyTheme(Container container) {
@@ -0,0 +1,695 @@
package haus.nightmare.j3270.storage;
import java.io.*;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
/**
* Storage manager for saved host profiles, maintaining 1:1 format compatibility with a3270.
* Persists profiles in Java user preferences with JSON serialization.
*/
public class HostStorage {
private static final Logger log = Logger.getLogger(HostStorage.class.getName());
private static final Preferences prefs = Preferences.userNodeForPackage(HostStorage.class);
public static final String KEY_SAVED_HOSTS = "saved_hosts";
public static synchronized List<SavedHost> getSavedHosts() {
String jsonStr = prefs.get(KEY_SAVED_HOSTS, null);
if (jsonStr == null || jsonStr.trim().isEmpty()) {
return new ArrayList<>();
}
try {
return parseHostsJson(jsonStr);
} catch (Exception e) {
log.log(Level.WARNING, "Failed to parse saved hosts JSON", e);
return new ArrayList<>();
}
}
public static synchronized void saveHost(SavedHost host) {
if (host == null) return;
List<SavedHost> current = new ArrayList<>(getSavedHosts());
int index = -1;
for (int i = 0; i < current.size(); i++) {
if (Objects.equals(current.get(i).getId(), host.getId())) {
index = i;
break;
}
}
// If this host is set to autoConnect, clear autoConnect on all others
SavedHost hostToSave = host.copy();
if (hostToSave.isAutoConnect()) {
for (int i = 0; i < current.size(); i++) {
current.get(i).setAutoConnect(false);
}
}
if (index >= 0) {
current.set(index, hostToSave);
} else {
current.add(hostToSave);
}
saveAll(current);
}
public static synchronized void deleteHost(String hostId) {
if (hostId == null) return;
List<SavedHost> current = new ArrayList<>(getSavedHosts());
current.removeIf(h -> Objects.equals(h.getId(), hostId));
saveAll(current);
}
public static synchronized SavedHost getAutoConnectHost() {
for (SavedHost h : getSavedHosts()) {
if (h.isAutoConnect()) {
return h;
}
}
return null;
}
public static synchronized SavedHost findById(String id) {
if (id == null) return null;
for (SavedHost h : getSavedHosts()) {
if (id.equals(h.getId())) {
return h;
}
}
return null;
}
public static synchronized SavedHost findByName(String name) {
if (name == null) return null;
for (SavedHost h : getSavedHosts()) {
if (name.equalsIgnoreCase(h.getName())) {
return h;
}
}
return null;
}
public static synchronized void saveAll(List<SavedHost> hosts) {
if (hosts == null) return;
String json = serializeHostsJson(hosts);
prefs.put(KEY_SAVED_HOSTS, json);
try {
prefs.flush();
} catch (Exception ignored) {
}
}
public static synchronized void clearAll() {
prefs.remove(KEY_SAVED_HOSTS);
try {
prefs.flush();
} catch (Exception ignored) {
}
}
// ================= INI File Export / Import =================
/**
* Export all currently saved host connections to an INI file.
*/
public static synchronized void exportToIniFile(File file) throws IOException {
List<SavedHost> hosts = getSavedHosts();
exportToIniFile(file, hosts);
}
public static synchronized void exportToIniFile(String path) throws IOException {
exportToIniFile(new File(path));
}
/**
* Export a specified list of host connections to an INI file.
*/
public static void exportToIniFile(File file, List<SavedHost> hosts) throws IOException {
if (file == null) {
throw new IllegalArgumentException("Target file cannot be null");
}
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
try (PrintWriter w = new PrintWriter(new BufferedWriter(new FileWriter(file)))) {
w.println("; j3270 Saved Connections Configuration File");
w.println("; Exported on " + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()));
w.println("; Connections count: " + (hosts != null ? hosts.size() : 0));
w.println();
if (hosts != null) {
for (SavedHost h : hosts) {
String section = (h.getName() != null && !h.getName().trim().isEmpty())
? h.getName().trim()
: (h.getHost() != null && !h.getHost().trim().isEmpty() ? h.getHost().trim() : "Connection");
w.println("[" + section + "]");
if (h.getId() != null && !h.getId().trim().isEmpty()) {
w.println("id = " + h.getId());
}
w.println("name = " + (h.getName() != null ? h.getName() : ""));
w.println("host = " + (h.getHost() != null ? h.getHost() : ""));
w.println("port = " + h.getPort());
w.println("model = " + h.getModel());
w.println("dynamicRows = " + h.getDynamicRows());
w.println("dynamicCols = " + h.getDynamicCols());
w.println("luName = " + (h.getLuName() != null ? h.getLuName() : ""));
w.println("autoConnect = " + h.isAutoConnect());
w.println("hostType = " + (h.getHostType() != null ? h.getHostType() : "TSO"));
w.println("useTls = " + h.isUseTls());
w.println("tlsVerifyCert = " + h.isTlsVerifyCert());
w.println("tn3270e = " + h.isTn3270e());
w.println("graphicsMode = " + (h.getGraphicsMode() != null ? h.getGraphicsMode() : "BOTH"));
w.println("codePage = " + (h.getCodePage() != null ? h.getCodePage() : "037"));
w.println("proxyType = " + (h.getProxyType() != null ? h.getProxyType() : "NONE"));
w.println("proxyHost = " + (h.getProxyHost() != null ? h.getProxyHost() : ""));
w.println("proxyPort = " + h.getProxyPort());
w.println("proxyUsername = " + (h.getProxyUsername() != null ? h.getProxyUsername() : ""));
w.println("proxyPassword = " + (h.getProxyPassword() != null ? h.getProxyPassword() : ""));
w.println();
}
}
}
log.info("Saved connections exported to: " + file.getAbsolutePath());
}
/**
* Load connections from an INI file without saving them to persistent storage.
*/
public static List<SavedHost> loadFromIniFile(File file) throws IOException {
if (file == null || !file.exists()) {
throw new FileNotFoundException("Connections file not found: " + (file != null ? file.getAbsolutePath() : "null"));
}
List<SavedHost> hosts = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
SavedHost current = null;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith(";") || line.startsWith("#")) {
continue;
}
if (line.startsWith("[") && line.endsWith("]")) {
if (current != null) {
hosts.add(current);
}
String section = line.substring(1, line.length() - 1).trim();
if (section.toLowerCase().startsWith("connection:")) {
section = section.substring("connection:".length()).trim();
} else if (section.toLowerCase().startsWith("host:")) {
section = section.substring("host:".length()).trim();
}
current = new SavedHost();
current.setName(section);
continue;
}
int eq = line.indexOf('=');
if (eq < 0) {
continue;
}
String key = line.substring(0, eq).trim();
String value = line.substring(eq + 1).trim();
if (current == null) {
current = new SavedHost();
current.setName("Default");
}
applyIniEntry(current, key, value);
}
if (current != null) {
hosts.add(current);
}
}
return hosts;
}
public static List<SavedHost> loadFromIniFile(String path) throws IOException {
return loadFromIniFile(new File(path));
}
/**
* Import connections from an INI file and merge them into persistent storage.
* Updates existing profiles matching by ID or Name, and appends new profiles.
*/
public static synchronized List<SavedHost> importFromIniFile(File file) throws IOException {
List<SavedHost> imported = loadFromIniFile(file);
if (imported == null || imported.isEmpty()) {
return getSavedHosts();
}
List<SavedHost> current = new ArrayList<>(getSavedHosts());
for (SavedHost host : imported) {
int index = -1;
for (int i = 0; i < current.size(); i++) {
SavedHost existing = current.get(i);
if ((host.getId() != null && !host.getId().trim().isEmpty() && Objects.equals(existing.getId(), host.getId())) ||
(host.getName() != null && !host.getName().trim().isEmpty() &&
host.getName().equalsIgnoreCase(existing.getName()))) {
index = i;
break;
}
}
if (host.isAutoConnect()) {
for (SavedHost c : current) {
c.setAutoConnect(false);
}
}
if (index >= 0) {
if (host.getId() == null || host.getId().trim().isEmpty()) {
host.setId(current.get(index).getId());
}
current.set(index, host);
} else {
current.add(host);
}
}
saveAll(current);
return current;
}
public static synchronized List<SavedHost> importFromIniFile(String path) throws IOException {
return importFromIniFile(new File(path));
}
private static void applyIniEntry(SavedHost h, String key, String value) {
String lowerKey = key.toLowerCase();
switch (lowerKey) {
case "id":
if (!value.isEmpty()) h.setId(value);
break;
case "name":
case "profilename":
case "profile":
if (!value.isEmpty()) h.setName(value);
break;
case "host":
case "hostname":
case "address":
h.setHost(value);
break;
case "port":
h.setPort(parseInt(value, 23));
break;
case "model":
case "terminalmodel":
h.setModel(parseInt(value, 4));
break;
case "dynamicrows":
case "dynamic_rows":
case "rows":
h.setDynamicRows(parseInt(value, 62));
break;
case "dynamiccols":
case "dynamic_cols":
case "cols":
h.setDynamicCols(parseInt(value, 160));
break;
case "luname":
case "lu_name":
case "lu":
h.setLuName(value);
break;
case "autoconnect":
case "auto_connect":
h.setAutoConnect(Boolean.parseBoolean(value));
break;
case "hosttype":
case "host_type":
case "type":
h.setHostType(value.toUpperCase());
break;
case "usetls":
case "use_tls":
case "tls":
case "ssl":
h.setUseTls(Boolean.parseBoolean(value));
break;
case "tlsverifycert":
case "verifycert":
case "tls_verify_cert":
h.setTlsVerifyCert(Boolean.parseBoolean(value));
break;
case "tn3270e":
case "enabletn3270e":
h.setTn3270e(Boolean.parseBoolean(value));
break;
case "graphicsmode":
case "graphics_mode":
case "graphics":
h.setGraphicsMode(value.toUpperCase());
break;
case "codepage":
case "code_page":
case "charset":
h.setCodePage(value);
break;
case "proxytype":
case "proxy_type":
h.setProxyType(value.toUpperCase());
break;
case "proxyhost":
case "proxy_host":
h.setProxyHost(value);
break;
case "proxyport":
case "proxy_port":
h.setProxyPort(parseInt(value, 0));
break;
case "proxyusername":
case "proxy_username":
case "proxyuser":
h.setProxyUsername(value);
break;
case "proxypassword":
case "proxy_password":
case "proxypass":
h.setProxyPassword(value);
break;
default:
break;
}
}
// ================= JSON Serialization =================
public static String serializeHostsJson(List<SavedHost> hosts) {
StringBuilder sb = new StringBuilder();
sb.append("[\n");
for (int i = 0; i < hosts.size(); i++) {
SavedHost h = hosts.get(i);
sb.append(" ").append(serializeHostJson(h));
if (i < hosts.size() - 1) {
sb.append(",");
}
sb.append("\n");
}
sb.append("]");
return sb.toString();
}
public static String serializeHostJson(SavedHost h) {
StringBuilder sb = new StringBuilder();
sb.append("{");
appendField(sb, "id", h.getId(), true);
appendField(sb, "name", h.getName(), true);
appendField(sb, "host", h.getHost(), true);
appendField(sb, "port", h.getPort(), true);
appendField(sb, "model", h.getModel(), true);
appendField(sb, "dynamicRows", h.getDynamicRows(), true);
appendField(sb, "dynamicCols", h.getDynamicCols(), true);
appendField(sb, "luName", h.getLuName(), true);
appendField(sb, "autoConnect", h.isAutoConnect(), true);
appendField(sb, "hostType", h.getHostType(), true);
appendField(sb, "useTls", h.isUseTls(), true);
appendField(sb, "tlsVerifyCert", h.isTlsVerifyCert(), true);
appendField(sb, "tn3270e", h.isTn3270e(), true);
appendField(sb, "graphicsMode", h.getGraphicsMode(), true);
appendField(sb, "codePage", h.getCodePage(), true);
appendField(sb, "proxyType", h.getProxyType(), true);
appendField(sb, "proxyHost", h.getProxyHost(), true);
appendField(sb, "proxyPort", h.getProxyPort(), true);
appendField(sb, "proxyUsername", h.getProxyUsername(), true);
appendField(sb, "proxyPassword", h.getProxyPassword(), false);
sb.append("}");
return sb.toString();
}
private static void appendField(StringBuilder sb, String key, String val, boolean comma) {
sb.append("\"").append(key).append("\":\"").append(escapeJson(val != null ? val : "")).append("\"");
if (comma) sb.append(",");
}
private static void appendField(StringBuilder sb, String key, int val, boolean comma) {
sb.append("\"").append(key).append("\":").append(val);
if (comma) sb.append(",");
}
private static void appendField(StringBuilder sb, String key, boolean val, boolean comma) {
sb.append("\"").append(key).append("\":").append(val);
if (comma) sb.append(",");
}
private static String escapeJson(String s) {
if (s == null) return "";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '"': sb.append("\\\""); break;
case '\\': sb.append("\\\\"); break;
case '\b': sb.append("\\b"); break;
case '\f': sb.append("\\f"); break;
case '\n': sb.append("\\n"); break;
case '\r': sb.append("\\r"); break;
case '\t': sb.append("\\t"); break;
default:
if (c < 32) {
sb.append(String.format("\\u%04x", (int) c));
} else {
sb.append(c);
}
break;
}
}
return sb.toString();
}
// ================= JSON Deserialization =================
public static List<SavedHost> parseHostsJson(String json) {
List<SavedHost> list = new ArrayList<>();
if (json == null || json.trim().isEmpty()) return list;
SimpleJsonParser parser = new SimpleJsonParser(json);
Object root = parser.parseValue();
if (root instanceof List) {
for (Object item : (List<?>) root) {
if (item instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) item;
list.add(fromMap(map));
}
}
} else if (root instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) root;
list.add(fromMap(map));
}
return list;
}
public static SavedHost fromMap(Map<String, Object> map) {
SavedHost h = new SavedHost();
if (map.containsKey("id")) h.setId(String.valueOf(map.get("id")));
if (map.containsKey("name")) h.setName(String.valueOf(map.get("name")));
if (map.containsKey("host")) h.setHost(String.valueOf(map.get("host")));
if (map.containsKey("port")) h.setPort(parseInt(map.get("port"), 23));
if (map.containsKey("model")) h.setModel(parseInt(map.get("model"), 4));
if (map.containsKey("dynamicRows")) h.setDynamicRows(parseInt(map.get("dynamicRows"), 62));
if (map.containsKey("dynamicCols")) h.setDynamicCols(parseInt(map.get("dynamicCols"), 160));
if (map.containsKey("luName")) h.setLuName(String.valueOf(map.get("luName")));
if (map.containsKey("autoConnect")) h.setAutoConnect(parseBool(map.get("autoConnect"), false));
if (map.containsKey("hostType")) h.setHostType(String.valueOf(map.get("hostType")));
if (map.containsKey("useTls")) h.setUseTls(parseBool(map.get("useTls"), false));
if (map.containsKey("tlsVerifyCert")) h.setTlsVerifyCert(parseBool(map.get("tlsVerifyCert"), true));
if (map.containsKey("tn3270e")) h.setTn3270e(parseBool(map.get("tn3270e"), true));
if (map.containsKey("graphicsMode")) {
String gm = String.valueOf(map.get("graphicsMode"));
h.setGraphicsMode(gm);
}
if (map.containsKey("codePage")) h.setCodePage(String.valueOf(map.get("codePage")));
if (map.containsKey("proxyType")) h.setProxyType(String.valueOf(map.get("proxyType")));
if (map.containsKey("proxyHost")) h.setProxyHost(String.valueOf(map.get("proxyHost")));
if (map.containsKey("proxyPort")) h.setProxyPort(parseInt(map.get("proxyPort"), 0));
if (map.containsKey("proxyUsername")) h.setProxyUsername(String.valueOf(map.get("proxyUsername")));
if (map.containsKey("proxyPassword")) h.setProxyPassword(String.valueOf(map.get("proxyPassword")));
return h;
}
private static int parseInt(Object obj, int def) {
if (obj == null) return def;
if (obj instanceof Number) return ((Number) obj).intValue();
try {
return Integer.parseInt(String.valueOf(obj).trim());
} catch (Exception e) {
return def;
}
}
private static boolean parseBool(Object obj, boolean def) {
if (obj == null) return def;
if (obj instanceof Boolean) return (Boolean) obj;
return Boolean.parseBoolean(String.valueOf(obj).trim());
}
/**
* Minimal pure-Java JSON parser for nested Maps, Lists, Strings, Numbers, and Booleans.
*/
static class SimpleJsonParser {
private final String src;
private int pos = 0;
SimpleJsonParser(String src) {
this.src = src;
}
Object parseValue() {
skipWhitespace();
if (pos >= src.length()) return null;
char c = src.charAt(pos);
if (c == '{') return parseObject();
if (c == '[') return parseArray();
if (c == '"') return parseString();
if (c == 't' || c == 'f') return parseBoolean();
if (c == 'n') return parseNull();
if (c == '-' || Character.isDigit(c)) return parseNumber();
throw new IllegalArgumentException("Unexpected char '" + c + "' at position " + pos);
}
private Map<String, Object> parseObject() {
match('{');
Map<String, Object> map = new LinkedHashMap<>();
skipWhitespace();
if (peek() == '}') {
pos++;
return map;
}
while (true) {
skipWhitespace();
String key = parseString();
skipWhitespace();
match(':');
Object val = parseValue();
map.put(key, val);
skipWhitespace();
char c = peek();
if (c == ',') {
pos++;
} else if (c == '}') {
pos++;
break;
} else {
break;
}
}
return map;
}
private List<Object> parseArray() {
match('[');
List<Object> list = new ArrayList<>();
skipWhitespace();
if (peek() == ']') {
pos++;
return list;
}
while (true) {
Object val = parseValue();
list.add(val);
skipWhitespace();
char c = peek();
if (c == ',') {
pos++;
} else if (c == ']') {
pos++;
break;
} else {
break;
}
}
return list;
}
private String parseString() {
match('"');
StringBuilder sb = new StringBuilder();
while (pos < src.length()) {
char c = src.charAt(pos++);
if (c == '"') {
return sb.toString();
} else if (c == '\\') {
if (pos >= src.length()) break;
char esc = src.charAt(pos++);
switch (esc) {
case '"': sb.append('"'); break;
case '\\': sb.append('\\'); break;
case '/': sb.append('/'); break;
case 'b': sb.append('\b'); break;
case 'f': sb.append('\f'); break;
case 'n': sb.append('\n'); break;
case 'r': sb.append('\r'); break;
case 't': sb.append('\t'); break;
case 'u':
if (pos + 4 <= src.length()) {
String hex = src.substring(pos, pos + 4);
pos += 4;
try {
sb.append((char) Integer.parseInt(hex, 16));
} catch (NumberFormatException e) {
sb.append("?");
}
}
break;
default:
sb.append(esc);
break;
}
} else {
sb.append(c);
}
}
return sb.toString();
}
private Boolean parseBoolean() {
if (src.startsWith("true", pos)) {
pos += 4;
return true;
} else if (src.startsWith("false", pos)) {
pos += 5;
return false;
}
throw new IllegalArgumentException("Expected boolean at " + pos);
}
private Object parseNull() {
if (src.startsWith("null", pos)) {
pos += 4;
return null;
}
throw new IllegalArgumentException("Expected null at " + pos);
}
private Number parseNumber() {
int start = pos;
if (src.charAt(pos) == '-') pos++;
while (pos < src.length() && (Character.isDigit(src.charAt(pos)) || src.charAt(pos) == '.' || src.charAt(pos) == 'e' || src.charAt(pos) == 'E' || src.charAt(pos) == '+' || src.charAt(pos) == '-')) {
pos++;
}
String numStr = src.substring(start, pos);
if (numStr.contains(".")) {
return Double.parseDouble(numStr);
}
return Long.parseLong(numStr);
}
private void skipWhitespace() {
while (pos < src.length() && Character.isWhitespace(src.charAt(pos))) {
pos++;
}
}
private char peek() {
return pos < src.length() ? src.charAt(pos) : '\0';
}
private void match(char expected) {
skipWhitespace();
if (pos >= src.length() || src.charAt(pos) != expected) {
throw new IllegalArgumentException("Expected '" + expected + "' at " + pos);
}
pos++;
}
}
}
@@ -0,0 +1,226 @@
package haus.nightmare.j3270.storage;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.graphics.GraphicsMode;
import java.util.Objects;
import java.util.UUID;
/**
* Data model for a saved 3270 connection profile, matching the schema of a3270 SavedHost.
* Includes per-connection host, model, protocol, and proxy configuration.
*/
public class SavedHost {
private String id;
private String name;
private String host;
private int port;
private int model;
private int dynamicRows;
private int dynamicCols;
private String luName;
private boolean autoConnect;
private String hostType;
private boolean useTls;
private boolean tlsVerifyCert;
private boolean tn3270e;
private String graphicsMode;
private String codePage;
// Per-connection proxy configuration
private String proxyType;
private String proxyHost;
private int proxyPort;
private String proxyUsername;
private String proxyPassword;
public SavedHost() {
this.id = UUID.randomUUID().toString();
this.name = "Mainframe";
this.host = "127.0.0.1";
this.port = 23;
this.model = 4;
this.dynamicRows = 62;
this.dynamicCols = 160;
this.luName = "";
this.autoConnect = false;
this.hostType = "TSO";
this.useTls = false;
this.tlsVerifyCert = true;
this.tn3270e = true;
this.graphicsMode = "BOTH";
this.codePage = "037";
this.proxyType = "NONE";
this.proxyHost = "";
this.proxyPort = 0;
this.proxyUsername = "";
this.proxyPassword = "";
}
public SavedHost(String id, String name, String host, int port, int model,
int dynamicRows, int dynamicCols, String luName, boolean autoConnect,
String hostType, boolean useTls, boolean tlsVerifyCert, boolean tn3270e,
String graphicsMode, String codePage, String proxyType, String proxyHost,
int proxyPort, String proxyUsername, String proxyPassword) {
this.id = (id != null && !id.trim().isEmpty()) ? id : UUID.randomUUID().toString();
this.name = name != null ? name : "";
this.host = host != null ? host : "127.0.0.1";
this.port = port > 0 ? port : 23;
this.model = model;
this.dynamicRows = dynamicRows > 0 ? dynamicRows : 62;
this.dynamicCols = dynamicCols > 0 ? dynamicCols : 160;
this.luName = luName != null ? luName : "";
this.autoConnect = autoConnect;
this.hostType = hostType != null ? hostType : "TSO";
this.useTls = useTls;
this.tlsVerifyCert = tlsVerifyCert;
this.tn3270e = tn3270e;
this.graphicsMode = graphicsMode != null ? graphicsMode : "BOTH";
this.codePage = codePage != null ? codePage : "037";
this.proxyType = (proxyType != null && !proxyType.trim().isEmpty()) ? proxyType.toUpperCase() : "NONE";
this.proxyHost = proxyHost != null ? proxyHost : "";
this.proxyPort = proxyPort;
this.proxyUsername = proxyUsername != null ? proxyUsername : "";
this.proxyPassword = proxyPassword != null ? proxyPassword : "";
}
public SavedHost copy() {
return new SavedHost(id, name, host, port, model, dynamicRows, dynamicCols, luName,
autoConnect, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, codePage,
proxyType, proxyHost, proxyPort, proxyUsername, proxyPassword);
}
/**
* Converts this saved host profile into an active ConnectionConfig.
*/
public ConnectionConfig toConnectionConfig() {
TerminalModel tm;
if (model == 0) {
tm = TerminalModel.IBM_DYNAMIC;
} else {
try {
tm = TerminalModel.forModel(model, true);
} catch (Exception e) {
tm = TerminalModel.IBM_3279_4;
}
}
ConnectionConfig config = new ConnectionConfig(host, port, tm, useTls);
config.setSavedConnection(true);
config.setSavedProfileId(this.id);
config.setSavedProfileName(this.name);
config.setHostType(this.hostType != null ? this.hostType : "TSO");
if (model == 0 || tm.isDynamic()) {
config.setDynamicDimensions(dynamicRows, dynamicCols);
}
if (luName != null && !luName.trim().isEmpty()) {
config.setLuName(luName.trim());
}
config.setTlsVerifyCert(tlsVerifyCert);
config.setTn3270eEnabled(tn3270e);
if (graphicsMode != null && !graphicsMode.isEmpty()) {
config.setGraphicsMode(GraphicsMode.fromString(graphicsMode));
}
if (codePage != null && !codePage.isEmpty()) {
config.setCodePage(codePage);
}
// Apply per-connection proxy if configured
if (proxyType != null && !proxyType.equalsIgnoreCase("NONE") && proxyHost != null && !proxyHost.trim().isEmpty()) {
try {
ConnectionConfig.ProxyType pt = ConnectionConfig.ProxyType.valueOf(proxyType.toUpperCase());
int pPort = proxyPort > 0 ? proxyPort : (pt == ConnectionConfig.ProxyType.HTTP ? 8080 : 1080);
config.setProxy(pt, proxyHost.trim(), pPort,
(proxyUsername != null && !proxyUsername.trim().isEmpty()) ? proxyUsername.trim() : null,
(proxyPassword != null && !proxyPassword.isEmpty()) ? proxyPassword : null);
} catch (IllegalArgumentException ignored) {
}
}
return config;
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public int getModel() { return model; }
public void setModel(int model) { this.model = model; }
public int getDynamicRows() { return dynamicRows; }
public void setDynamicRows(int dynamicRows) { this.dynamicRows = dynamicRows; }
public int getDynamicCols() { return dynamicCols; }
public void setDynamicCols(int dynamicCols) { this.dynamicCols = dynamicCols; }
public String getLuName() { return luName; }
public void setLuName(String luName) { this.luName = luName; }
public boolean isAutoConnect() { return autoConnect; }
public void setAutoConnect(boolean autoConnect) { this.autoConnect = autoConnect; }
public String getHostType() { return hostType; }
public void setHostType(String hostType) { this.hostType = hostType; }
public boolean isUseTls() { return useTls; }
public void setUseTls(boolean useTls) { this.useTls = useTls; }
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
public void setTlsVerifyCert(boolean tlsVerifyCert) { this.tlsVerifyCert = tlsVerifyCert; }
public boolean isTn3270e() { return tn3270e; }
public void setTn3270e(boolean tn3270e) { this.tn3270e = tn3270e; }
public String getGraphicsMode() { return graphicsMode; }
public void setGraphicsMode(String graphicsMode) { this.graphicsMode = graphicsMode; }
public String getCodePage() { return codePage; }
public void setCodePage(String codePage) { this.codePage = codePage; }
public String getProxyType() { return proxyType != null ? proxyType : "NONE"; }
public void setProxyType(String proxyType) { this.proxyType = (proxyType != null) ? proxyType.toUpperCase() : "NONE"; }
public String getProxyHost() { return proxyHost != null ? 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 != null ? proxyUsername : ""; }
public void setProxyUsername(String proxyUsername) { this.proxyUsername = proxyUsername; }
public String getProxyPassword() { return proxyPassword != null ? proxyPassword : ""; }
public void setProxyPassword(String proxyPassword) { this.proxyPassword = proxyPassword; }
@Override
public String toString() {
if (name != null && !name.trim().isEmpty()) {
return name;
}
return host + (port > 0 ? ":" + port : "");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SavedHost savedHost = (SavedHost) o;
return Objects.equals(id, savedHost.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
@@ -0,0 +1,138 @@
package haus.nightmare.j3270.ui;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Utility for loading and applying application window and taskbar icons.
* Loads multi-resolution lib3270j icons from resources.
*/
public final class AppIcon {
private static final Logger log = Logger.getLogger(AppIcon.class.getName());
private static final int[] ICON_SIZES = {16, 24, 32, 48, 64, 128, 256, 512};
private static List<Image> cachedIcons = null;
private static Image defaultIcon = null;
private AppIcon() {}
/**
* Retrieves the list of available application icon images in various resolutions.
*
* @return an unmodifiable list of Image objects, or empty list if none could be loaded.
*/
public static synchronized List<Image> getAppIcons() {
if (cachedIcons == null) {
loadIcons();
}
return cachedIcons;
}
/**
* Retrieves the default application icon image (256x256 or best available).
*
* @return the default Image, or null if no icon is available.
*/
public static synchronized Image getDefaultIcon() {
if (cachedIcons == null) {
loadIcons();
}
return defaultIcon;
}
/**
* Applies the application icons to a target Window (JFrame, JDialog, etc.)
* and updates the desktop Taskbar icon if supported.
*
* @param window the Window to set icon images for.
*/
public static void applyTo(Window window) {
if (GraphicsEnvironment.isHeadless()) {
return;
}
List<Image> icons = getAppIcons();
if (icons.isEmpty()) {
return;
}
if (window != null) {
window.setIconImages(icons);
}
// Apply to OS Taskbar / Dock if supported (Java 9+)
try {
if (Taskbar.isTaskbarSupported()) {
Taskbar taskbar = Taskbar.getTaskbar();
if (taskbar.isSupported(Taskbar.Feature.ICON_IMAGE)) {
Image bestIcon = defaultIcon != null ? defaultIcon : icons.get(icons.size() - 1);
taskbar.setIconImage(bestIcon);
}
}
} catch (Throwable t) {
log.log(Level.FINE, "Taskbar icon could not be set: {0}", t.getMessage());
}
}
private static void loadIcons() {
List<Image> icons = new ArrayList<>();
for (int size : ICON_SIZES) {
String path = "/icons/lib3270j_" + size + ".png";
Image img = loadImage(path);
if (img != null) {
icons.add(img);
}
}
if (icons.isEmpty()) {
Image img = loadImage("/icons/lib3270j.png");
if (img != null) {
icons.add(img);
}
}
if (!icons.isEmpty()) {
for (Image img : icons) {
if (img.getWidth(null) == 256) {
defaultIcon = img;
break;
}
}
if (defaultIcon == null) {
defaultIcon = icons.get(icons.size() - 1);
}
}
cachedIcons = Collections.unmodifiableList(icons);
}
private static Image loadImage(String resourcePath) {
try {
URL url = AppIcon.class.getResource(resourcePath);
if (url != null) {
BufferedImage img = ImageIO.read(url);
if (img != null) {
return img;
}
}
try (InputStream in = AppIcon.class.getResourceAsStream(resourcePath)) {
if (in != null) {
return ImageIO.read(in);
}
}
} catch (Exception e) {
log.log(Level.FINE, "Failed to load icon from " + resourcePath, e);
}
return null;
}
}
@@ -0,0 +1,146 @@
package haus.nightmare.j3270.ui;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.KeyEvent;
/**
* Dialogue shown when a connection is initiated while j3270 is already connected.
* Asks the user whether the connection should be opened in a new window or
* if the current connection should be aborted.
*/
public class ConnectChoiceDialog extends JDialog {
public enum Choice {
NEW_WINDOW,
ABORT_CURRENT,
CANCEL
}
private Choice result = Choice.CANCEL;
private final JButton newWindowBtn;
private final JButton abortBtn;
private final JButton cancelBtn;
private final JLabel titleLabel;
private final JLabel line1;
private final JLabel line2;
public ConnectChoiceDialog(Window owner, String currentTarget) {
super(owner, "Active Connection", ModalityType.APPLICATION_MODAL);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setResizable(false);
JPanel mainPanel = new JPanel(new BorderLayout(16, 16));
mainPanel.setBorder(new EmptyBorder(20, 24, 20, 24));
// Message Panel
JPanel messagePanel = new JPanel();
messagePanel.setLayout(new BoxLayout(messagePanel, BoxLayout.Y_AXIS));
messagePanel.setOpaque(false);
titleLabel = new JLabel("Connection Already Active");
titleLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 2));
messagePanel.add(titleLabel);
messagePanel.add(Box.createVerticalStrut(10));
String targetText = (currentTarget != null && !currentTarget.trim().isEmpty())
? currentTarget.trim()
: "the host";
line1 = new JLabel("j3270 is currently connected to " + targetText + ".");
line1.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
messagePanel.add(line1);
messagePanel.add(Box.createVerticalStrut(6));
line2 = new JLabel("Would you like to open the connection in a new window or abort the current connection?");
line2.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
messagePanel.add(line2);
mainPanel.add(messagePanel, BorderLayout.CENTER);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
buttonPanel.setOpaque(false);
cancelBtn = new JButton("Cancel");
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
cancelBtn.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
cancelBtn.addActionListener(e -> {
result = Choice.CANCEL;
dispose();
});
abortBtn = new JButton("Abort Current Connection");
ThemeManager.styleButton(abortBtn, ThemeManager.ButtonVariant.DANGER);
abortBtn.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
abortBtn.addActionListener(e -> {
result = Choice.ABORT_CURRENT;
dispose();
});
newWindowBtn = new JButton("New Window");
ThemeManager.styleButton(newWindowBtn, ThemeManager.ButtonVariant.PRIMARY);
newWindowBtn.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
newWindowBtn.addActionListener(e -> {
result = Choice.NEW_WINDOW;
dispose();
});
buttonPanel.add(cancelBtn);
buttonPanel.add(abortBtn);
buttonPanel.add(newWindowBtn);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
// ESC key cancels
getRootPane().registerKeyboardAction(e -> {
result = Choice.CANCEL;
dispose();
}, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
getRootPane().setDefaultButton(newWindowBtn);
pack();
if (owner != null) {
setLocationRelativeTo(owner);
} else {
setLocationRelativeTo(null);
}
}
public Choice getChoice() {
return result;
}
public JButton getNewWindowButton() {
return newWindowBtn;
}
public JButton getAbortButton() {
return abortBtn;
}
public JButton getCancelButton() {
return cancelBtn;
}
public JLabel getTitleLabel() {
return titleLabel;
}
public JLabel getLine1() {
return line1;
}
public JLabel getLine2() {
return line2;
}
public static Choice showPrompt(Window owner, String currentTarget) {
ConnectChoiceDialog dialog = new ConnectChoiceDialog(owner, currentTarget);
dialog.setVisible(true);
return dialog.getChoice();
}
}
@@ -1,16 +1,32 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.storage.HostStorage;
import haus.nightmare.j3270.storage.SavedHost;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.CodePageRegistry;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.util.List;
/**
* Connection dialog for entering host, port, model, and LU name.
* Connection dialog for entering host, port, model, LU name, and per-connection proxy configuration.
* Supports loading and saving host profiles matching the a3270 schema.
*/
public class ConnectDialog extends JDialog {
private static final String NEW_PROFILE_LABEL = "<New / Custom Connection>";
// Profile selector
private JComboBox<Object> profileCombo;
private JButton saveProfileBtn;
private JButton deleteProfileBtn;
private String selectedProfileId = null;
// Connection parameters
private JTextField hostField;
private JTextField portField;
private JComboBox<TerminalModel> modelCombo;
@@ -21,77 +37,137 @@ public class ConnectDialog extends JDialog {
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsCombo;
private JComboBox<String> codePageCombo;
private JTextField luField;
private JComboBox<String> hostTypeCombo;
private JCheckBox tlsCheckBox;
private JCheckBox verifyCertCheckBox;
private JCheckBox tn3270eCheckBox;
private JCheckBox keepAliveCheckBox;
private JCheckBox autoReconnectCheckBox;
// Per-connection proxy configuration
private JComboBox<ConnectionConfig.ProxyType> proxyTypeCombo;
private JTextField proxyHostField;
private JTextField proxyPortField;
private JTextField proxyUserField;
private JPasswordField proxyPassField;
private boolean confirmed;
private ConnectionConfig result;
public ConnectDialog(Frame parent) {
super(parent, "Connect to Host", true);
AppIcon.applyTo(this);
buildUI();
pack();
setLocationRelativeTo(parent);
setResizable(false);
setResizable(true);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new GridBagLayout());
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
JPanel contentPanel = new JPanel(new GridBagLayout());
contentPanel.setBorder(BorderFactory.createEmptyBorder(12, 14, 12, 14));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 6, 5, 6);
gbc.insets = new Insets(4, 5, 4, 5);
gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
Font labelFont = ThemeManager.getUiFont();
// Host
int gridy = 0;
// 0. Profile Row
gbc.gridx = 0;
gbc.gridy = 0;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel profileLabel = new JLabel("Profile:");
profileLabel.setFont(labelFont);
contentPanel.add(profileLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
JPanel profileBar = new JPanel(new BorderLayout(6, 0));
profileBar.setOpaque(false);
profileCombo = new JComboBox<>();
profileCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(profileCombo);
refreshProfileCombo();
profileCombo.addActionListener(e -> onProfileSelectionChanged());
profileBar.add(profileCombo, BorderLayout.CENTER);
JPanel profileBtnBar = new JPanel(new FlowLayout(FlowLayout.RIGHT, 4, 0));
profileBtnBar.setOpaque(false);
saveProfileBtn = new JButton("Save Profile...");
ThemeManager.styleButton(saveProfileBtn, ThemeManager.ButtonVariant.DEFAULT);
saveProfileBtn.setFont(labelFont);
saveProfileBtn.addActionListener(e -> onSaveProfile());
deleteProfileBtn = new JButton("Delete");
ThemeManager.styleButton(deleteProfileBtn, ThemeManager.ButtonVariant.CANCEL);
deleteProfileBtn.setFont(labelFont);
deleteProfileBtn.setEnabled(false);
deleteProfileBtn.addActionListener(e -> onDeleteProfile());
profileBtnBar.add(saveProfileBtn);
profileBtnBar.add(deleteProfileBtn);
profileBar.add(profileBtnBar, BorderLayout.EAST);
contentPanel.add(profileBar, gbc);
gridy++;
// 1. Host
gbc.gridx = 0;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel hostLabel = new JLabel("Host:");
hostLabel.setFont(labelFont);
mainPanel.add(hostLabel, gbc);
contentPanel.add(hostLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
hostField = createField(20);
mainPanel.add(hostField, gbc);
contentPanel.add(hostField, gbc);
// Port
gridy++;
// 2. Port
gbc.gridx = 0;
gbc.gridy = 1;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel portLabel = new JLabel("Port:");
portLabel.setFont(labelFont);
mainPanel.add(portLabel, gbc);
contentPanel.add(portLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
portField = createField(6);
portField.setText("23");
mainPanel.add(portField, gbc);
contentPanel.add(portField, gbc);
// Model
gridy++;
// 3. Model
gbc.gridx = 0;
gbc.gridy = 2;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel modelLabel = new JLabel("Model:");
modelLabel.setFont(labelFont);
mainPanel.add(modelLabel, gbc);
contentPanel.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));
modelCombo.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleComboBox(modelCombo);
mainPanel.add(modelCombo, gbc);
contentPanel.add(modelCombo, gbc);
// Dynamic Dimensions (shown when IBM-DYNAMIC is selected)
gridy++;
// 4. Dynamic Dimensions
gbc.gridx = 0;
gbc.gridy = 3;
gbc.gridy = gridy;
gbc.weightx = 0;
dynamicDimLabel = new JLabel("Screen Size:");
dynamicDimLabel.setFont(labelFont);
mainPanel.add(dynamicDimLabel, gbc);
contentPanel.add(dynamicDimLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
@@ -112,7 +188,7 @@ public class ConnectDialog extends JDialog {
dynamicDimPanel.add(dynamicRowsSpinner);
dynamicDimPanel.add(colsLabel);
dynamicDimPanel.add(dynamicColsSpinner);
mainPanel.add(dynamicDimPanel, gbc);
contentPanel.add(dynamicDimPanel, gbc);
Runnable updateDynamicVisibility = () -> {
TerminalModel m = (TerminalModel) modelCombo.getSelectedItem();
@@ -124,97 +200,97 @@ public class ConnectDialog extends JDialog {
modelCombo.addActionListener(e -> updateDynamicVisibility.run());
updateDynamicVisibility.run();
// LU Name
gridy++;
// 5. LU Name
gbc.gridx = 0;
gbc.gridy = 4;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel luLabel = new JLabel("LU Name:");
luLabel.setFont(labelFont);
mainPanel.add(luLabel, gbc);
contentPanel.add(luLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
luField = createField(12);
mainPanel.add(luField, gbc);
contentPanel.add(luField, gbc);
// Graphics Mode
gridy++;
// Host Type (CMS/TSO/CICS)
gbc.gridx = 0;
gbc.gridy = 5;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel hostTypeLabel = new JLabel("Host Type:");
hostTypeLabel.setFont(labelFont);
contentPanel.add(hostTypeLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
String[] hostTypes = {"TSO", "CMS", "CICS"};
hostTypeCombo = new JComboBox<>(hostTypes);
hostTypeCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(hostTypeCombo);
setInitialHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
contentPanel.add(hostTypeCombo, gbc);
gridy++;
// 6. Graphics Mode
gbc.gridx = 0;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel graphicsLabel = new JLabel("Graphics:");
graphicsLabel.setFont(labelFont);
mainPanel.add(graphicsLabel, gbc);
contentPanel.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));
graphicsCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(graphicsCombo);
mainPanel.add(graphicsCombo, gbc);
graphicsCombo.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 (value instanceof haus.nightmare.lib3270j.graphics.GraphicsMode) {
setText(((haus.nightmare.lib3270j.graphics.GraphicsMode) value).getDescription());
}
if (isSelected) {
setBackground(ThemeManager.getSelectionBg());
setForeground(ThemeManager.getSelectionFg());
} else {
setBackground(ThemeManager.getBgComponent());
setForeground(ThemeManager.getFgMain());
}
return this;
}
});
contentPanel.add(graphicsCombo, gbc);
// Code Page
gridy++;
// 7. Code Page
gbc.gridx = 0;
gbc.gridy = 6;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel cpLabel = new JLabel("Code Page:");
cpLabel.setFont(labelFont);
mainPanel.add(cpLabel, gbc);
contentPanel.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));
codePageCombo = new JComboBox<>(CodePageRegistry.COMMON_CODE_PAGES);
codePageCombo.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleComboBox(codePageCombo);
mainPanel.add(codePageCombo, gbc);
setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
contentPanel.add(codePageCombo, gbc);
// TLS / SSL Checkbox
gridy++;
// 8. TLS Checkbox
gbc.gridx = 1;
gbc.gridy = 7;
gbc.weightx = 1.0;
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
gbc.gridy = gridy;
tlsCheckBox = new JCheckBox("Use TLS / SSL Encryption");
ThemeManager.styleCheckBox(tlsCheckBox);
tlsCheckBox.setFont(labelFont);
tlsCheckBox.addActionListener(e -> {
@@ -227,46 +303,169 @@ public class ConnectDialog extends JDialog {
portField.setText("23");
}
});
mainPanel.add(tlsCheckBox, gbc);
contentPanel.add(tlsCheckBox, gbc);
// Verify Certificate Checkbox
gridy++;
// 9. Verify Cert Checkbox
gbc.gridx = 1;
gbc.gridy = 8;
gbc.gridy = gridy;
verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
ThemeManager.styleCheckBox(verifyCertCheckBox);
verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
verifyCertCheckBox.setFont(ThemeManager.getUiFont());
verifyCertCheckBox.setSelected(true);
verifyCertCheckBox.setEnabled(false);
mainPanel.add(verifyCertCheckBox, gbc);
contentPanel.add(verifyCertCheckBox, gbc);
// TN3270E Checkbox
gridy++;
// 10. TN3270E Checkbox
gbc.gridx = 1;
gbc.gridy = 9;
gbc.gridy = gridy;
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
ThemeManager.styleCheckBox(tn3270eCheckBox);
tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
tn3270eCheckBox.setFont(ThemeManager.getUiFont());
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
mainPanel.add(tn3270eCheckBox, gbc);
contentPanel.add(tn3270eCheckBox, gbc);
// Keep-Alive Checkbox
gridy++;
// 11. Keep-Alive Checkbox
gbc.gridx = 1;
gbc.gridy = 10;
gbc.gridy = gridy;
keepAliveCheckBox = new JCheckBox("Enable Keep-Alive Heartbeat (NOP)");
ThemeManager.styleCheckBox(keepAliveCheckBox);
keepAliveCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
keepAliveCheckBox.setFont(ThemeManager.getUiFont());
keepAliveCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectKeepAlive());
mainPanel.add(keepAliveCheckBox, gbc);
contentPanel.add(keepAliveCheckBox, gbc);
// Auto-Reconnect Checkbox
gridy++;
// 12. Auto-Reconnect Checkbox
gbc.gridx = 1;
gbc.gridy = 11;
gbc.gridy = gridy;
autoReconnectCheckBox = new JCheckBox("Auto-Reconnect on Disconnect");
ThemeManager.styleCheckBox(autoReconnectCheckBox);
autoReconnectCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
autoReconnectCheckBox.setFont(ThemeManager.getUiFont());
autoReconnectCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
mainPanel.add(autoReconnectCheckBox, gbc);
contentPanel.add(autoReconnectCheckBox, gbc);
gridy++;
// 13. PER-CONNECTION PROXY CONFIGURATION PANEL
gbc.gridx = 0;
gbc.gridy = gridy;
gbc.gridwidth = 2;
gbc.weightx = 1.0;
JPanel proxyGroup = new JPanel(new GridBagLayout());
proxyGroup.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(),
"Proxy Configuration (This Connection)",
TitledBorder.LEFT,
TitledBorder.TOP,
labelFont
));
GridBagConstraints pgbc = new GridBagConstraints();
pgbc.insets = new Insets(3, 6, 3, 6);
pgbc.fill = GridBagConstraints.HORIZONTAL;
// Proxy Type
pgbc.gridx = 0;
pgbc.gridy = 0;
pgbc.weightx = 0;
JLabel ptLabel = new JLabel("Proxy Type:");
ptLabel.setFont(labelFont);
proxyGroup.add(ptLabel, pgbc);
pgbc.gridx = 1;
pgbc.gridwidth = 3;
pgbc.weightx = 1.0;
proxyTypeCombo = new JComboBox<>(ConnectionConfig.ProxyType.values());
proxyTypeCombo.setSelectedItem(ConnectionConfig.ProxyType.NONE);
proxyTypeCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(proxyTypeCombo);
proxyGroup.add(proxyTypeCombo, pgbc);
// Proxy Host & Port
pgbc.gridx = 0;
pgbc.gridy = 1;
pgbc.gridwidth = 1;
pgbc.weightx = 0;
JLabel phLabel = new JLabel("Proxy Host:");
phLabel.setFont(labelFont);
proxyGroup.add(phLabel, pgbc);
pgbc.gridx = 1;
pgbc.weightx = 1.0;
proxyHostField = createField(14);
proxyGroup.add(proxyHostField, pgbc);
pgbc.gridx = 2;
pgbc.weightx = 0;
JLabel ppLabel = new JLabel("Port:");
ppLabel.setFont(labelFont);
proxyGroup.add(ppLabel, pgbc);
pgbc.gridx = 3;
pgbc.weightx = 0.4;
proxyPortField = createField(5);
proxyGroup.add(proxyPortField, pgbc);
// Proxy User & Password
pgbc.gridx = 0;
pgbc.gridy = 2;
pgbc.gridwidth = 1;
pgbc.weightx = 0;
JLabel puLabel = new JLabel("Username:");
puLabel.setFont(labelFont);
proxyGroup.add(puLabel, pgbc);
pgbc.gridx = 1;
pgbc.weightx = 1.0;
proxyUserField = createField(12);
proxyGroup.add(proxyUserField, pgbc);
pgbc.gridx = 2;
pgbc.weightx = 0;
JLabel pwLabel = new JLabel("Password:");
pwLabel.setFont(labelFont);
proxyGroup.add(pwLabel, pgbc);
pgbc.gridx = 3;
pgbc.weightx = 0.4;
proxyPassField = createPasswordField(10);
proxyGroup.add(proxyPassField, pgbc);
// Dynamic enable/disable based on Proxy Type
Runnable updateProxyFields = () -> {
ConnectionConfig.ProxyType pt = (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
boolean active = (pt != null && pt != ConnectionConfig.ProxyType.NONE);
proxyHostField.setEnabled(active);
proxyPortField.setEnabled(active);
proxyUserField.setEnabled(active);
proxyPassField.setEnabled(active && pt != ConnectionConfig.ProxyType.SOCKS4);
if (active) {
String currentPPort = proxyPortField.getText().trim();
if (currentPPort.isEmpty() || "0".equals(currentPPort)) {
proxyPortField.setText(pt == ConnectionConfig.ProxyType.HTTP ? "8080" : "1080");
}
}
};
proxyTypeCombo.addActionListener(e -> updateProxyFields.run());
updateProxyFields.run();
contentPanel.add(proxyGroup, gbc);
gridy++;
// 14. Action Buttons
gbc.gridx = 0;
gbc.gridy = gridy;
gbc.gridwidth = 2;
gbc.weightx = 1.0;
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
JButton connectBtn = new JButton("Connect");
@@ -284,13 +483,14 @@ public class ConnectDialog extends JDialog {
buttonPanel.add(cancelBtn);
buttonPanel.add(connectBtn);
contentPanel.add(buttonPanel, gbc);
gbc.gridx = 0;
gbc.gridy = 12;
gbc.gridwidth = 2;
mainPanel.add(buttonPanel, gbc);
JScrollPane scrollPane = new JScrollPane(contentPanel);
scrollPane.setBorder(BorderFactory.createEmptyBorder());
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.getVerticalScrollBar().setUnitIncrement(14);
setContentPane(scrollPane);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
// Enter key triggers connect
@@ -299,11 +499,197 @@ public class ConnectDialog extends JDialog {
private JTextField createField(int cols) {
JTextField field = new JTextField(cols);
field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
field.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(field);
return field;
}
private JPasswordField createPasswordField(int cols) {
JPasswordField field = new JPasswordField(cols);
field.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(field);
return field;
}
// ================= Profile Management =================
private void refreshProfileCombo() {
profileCombo.removeAllItems();
profileCombo.addItem(NEW_PROFILE_LABEL);
List<SavedHost> saved = HostStorage.getSavedHosts();
for (SavedHost h : saved) {
profileCombo.addItem(h);
}
if (selectedProfileId != null) {
for (int i = 0; i < profileCombo.getItemCount(); i++) {
Object item = profileCombo.getItemAt(i);
if (item instanceof SavedHost && ((SavedHost) item).getId().equals(selectedProfileId)) {
profileCombo.setSelectedIndex(i);
return;
}
}
}
profileCombo.setSelectedIndex(0);
}
private void onProfileSelectionChanged() {
Object sel = profileCombo.getSelectedItem();
if (sel instanceof SavedHost) {
SavedHost h = (SavedHost) sel;
selectedProfileId = h.getId();
deleteProfileBtn.setEnabled(true);
loadProfileFields(h);
} else {
selectedProfileId = null;
deleteProfileBtn.setEnabled(false);
setInitialHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
}
}
public void loadProfile(SavedHost h) {
if (h == null) return;
selectedProfileId = h.getId();
refreshProfileCombo();
loadProfileFields(h);
}
private void loadProfileFields(SavedHost h) {
if (h == null) return;
hostField.setText(h.getHost());
portField.setText(String.valueOf(h.getPort()));
if (h.getModel() == 0) {
modelCombo.setSelectedItem(TerminalModel.IBM_DYNAMIC);
} else {
try {
modelCombo.setSelectedItem(TerminalModel.forModel(h.getModel(), true));
} catch (Exception ignored) {
}
}
dynamicRowsSpinner.setValue(h.getDynamicRows() > 0 ? h.getDynamicRows() : 62);
dynamicColsSpinner.setValue(h.getDynamicCols() > 0 ? h.getDynamicCols() : 160);
luField.setText(h.getLuName() != null ? h.getLuName() : "");
setInitialHostType(h.getHostType() != null ? h.getHostType() : "TSO");
tlsCheckBox.setSelected(h.isUseTls());
verifyCertCheckBox.setEnabled(h.isUseTls());
verifyCertCheckBox.setSelected(h.isTlsVerifyCert());
tn3270eCheckBox.setSelected(h.isTn3270e());
setInitialCodePage(h.getCodePage());
if (h.getGraphicsMode() != null) {
graphicsCombo.setSelectedItem(haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(h.getGraphicsMode()));
}
// Per-connection proxy parameters
String ptStr = h.getProxyType();
try {
ConnectionConfig.ProxyType pt = (ptStr != null && !ptStr.trim().isEmpty())
? ConnectionConfig.ProxyType.valueOf(ptStr.toUpperCase())
: ConnectionConfig.ProxyType.NONE;
proxyTypeCombo.setSelectedItem(pt);
} catch (IllegalArgumentException e) {
proxyTypeCombo.setSelectedItem(ConnectionConfig.ProxyType.NONE);
}
proxyHostField.setText(h.getProxyHost() != null ? h.getProxyHost() : "");
proxyPortField.setText(h.getProxyPort() > 0 ? String.valueOf(h.getProxyPort()) : "");
proxyUserField.setText(h.getProxyUsername() != null ? h.getProxyUsername() : "");
proxyPassField.setText(h.getProxyPassword() != null ? h.getProxyPassword() : "");
}
private void onSaveProfile() {
String hStr = hostField.getText().trim();
if (hStr.isEmpty()) {
hostField.requestFocus();
JOptionPane.showMessageDialog(this, "Please specify a host before saving profile.", "Missing Host", JOptionPane.WARNING_MESSAGE);
return;
}
String defaultName = hStr;
Object currentSel = profileCombo.getSelectedItem();
if (currentSel instanceof SavedHost) {
defaultName = ((SavedHost) currentSel).getName();
}
String profileName = (String) JOptionPane.showInputDialog(
this,
"Enter Host Profile Name:",
"Save Profile",
JOptionPane.PLAIN_MESSAGE,
null,
null,
defaultName
);
if (profileName == null || profileName.trim().isEmpty()) {
return;
}
profileName = profileName.trim();
SavedHost hostToSave = buildCurrentSavedHost(profileName);
HostStorage.saveHost(hostToSave);
selectedProfileId = hostToSave.getId();
refreshProfileCombo();
}
private void onDeleteProfile() {
if (selectedProfileId == null) return;
int opt = JOptionPane.showConfirmDialog(this,
"Are you sure you want to delete this host profile?",
"Delete Profile",
JOptionPane.YES_NO_OPTION);
if (opt == JOptionPane.YES_OPTION) {
HostStorage.deleteHost(selectedProfileId);
selectedProfileId = null;
refreshProfileCombo();
}
}
private SavedHost buildCurrentSavedHost(String profileName) {
String id = selectedProfileId != null ? selectedProfileId : java.util.UUID.randomUUID().toString();
String h = hostField.getText().trim();
int port = 23;
try {
port = Integer.parseInt(portField.getText().trim());
} catch (NumberFormatException ignored) {}
TerminalModel m = (TerminalModel) modelCombo.getSelectedItem();
int mNum = (m != null) ? m.getModelNumber() : 4;
int dRows = (Integer) dynamicRowsSpinner.getValue();
int dCols = (Integer) dynamicColsSpinner.getValue();
String lu = luField.getText().trim();
String hostType = getSelectedHostType();
boolean tls = tlsCheckBox.isSelected();
boolean verify = verifyCertCheckBox.isSelected();
boolean tn3270e = tn3270eCheckBox.isSelected();
String gfx = "BOTH";
if (graphicsCombo.getSelectedItem() != null) {
gfx = ((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem()).name();
}
String cp = "037";
Object cpSel = codePageCombo.getSelectedItem();
if (cpSel != null) {
String s = cpSel.toString().trim();
int dash = s.indexOf(" -");
cp = (dash > 0) ? s.substring(0, dash).trim() : s;
}
ConnectionConfig.ProxyType pt = (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
String ptStr = (pt != null) ? pt.name() : "NONE";
String pHost = proxyHostField.getText().trim();
int pPort = 0;
try {
pPort = Integer.parseInt(proxyPortField.getText().trim());
} catch (NumberFormatException ignored) {}
String pUser = proxyUserField.getText().trim();
String pPass = new String(proxyPassField.getPassword());
return new SavedHost(id, profileName, h, port, mNum, dRows, dCols, lu, false,
hostType, tls, verify, tn3270e, gfx, cp, ptStr, pHost, pPort, pUser, pPass);
}
// ================= Connect Action =================
private void onConnect() {
String host = hostField.getText().trim();
if (host.isEmpty()) {
@@ -320,20 +706,35 @@ public class ConnectDialog extends JDialog {
}
TerminalModel selectedModel = (TerminalModel) modelCombo.getSelectedItem();
result = new ConnectionConfig(host, port, selectedModel);
if (selectedModel != null && selectedModel.isDynamic()) {
if (selectedModel == null) {
selectedModel = TerminalModel.IBM_3279_4;
}
// Support full host string parsing if the user pasted flags or prefixes into host field
if (host.contains(" ") || host.startsWith("L:") || host.startsWith("P:") || host.startsWith("--proxy=") || host.startsWith("-proxy=")) {
result = ConnectionConfig.parseHostString(host, port, selectedModel);
} else {
result = new ConnectionConfig(host, port, selectedModel);
}
if (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());
if (graphicsCombo.getSelectedItem() != null) {
haus.nightmare.lib3270j.graphics.GraphicsMode gm = (haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem();
result.setGraphicsMode(gm);
haus.nightmare.j3270.config.Settings.setGraphicsMode(gm);
}
Object cpSelection = codePageCombo.getSelectedItem();
if (cpSelection != null) {
String cpStr = cpSelection.toString().trim();
@@ -360,6 +761,34 @@ public class ConnectDialog extends JDialog {
result.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
haus.nightmare.j3270.config.Settings.setAutoConnectAutoReconnect(ar);
// Apply per-connection proxy parameters strictly to this connection
ConnectionConfig.ProxyType pType = (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
String pHost = proxyHostField.getText().trim();
if (pType != null && pType != ConnectionConfig.ProxyType.NONE && !pHost.isEmpty()) {
int pPort = 0;
try {
pPort = Integer.parseInt(proxyPortField.getText().trim());
} catch (NumberFormatException ignored) {}
if (pPort <= 0) {
pPort = (pType == ConnectionConfig.ProxyType.HTTP) ? 8080 : 1080;
}
String pUser = proxyUserField.getText().trim();
String pPass = new String(proxyPassField.getPassword());
result.setProxy(pType, pHost, pPort, pUser.isEmpty() ? null : pUser, pPass.isEmpty() ? null : pPass);
}
result.setHostType(getSelectedHostType());
if (selectedProfileId != null) {
result.setSavedConnection(true);
result.setSavedProfileId(selectedProfileId);
SavedHost existing = HostStorage.findById(selectedProfileId);
String pName = (existing != null && existing.getName() != null && !existing.getName().trim().isEmpty())
? existing.getName() : host;
result.setSavedProfileName(pName);
SavedHost updated = buildCurrentSavedHost(pName);
HostStorage.saveHost(updated);
}
confirmed = true;
dispose();
}
@@ -394,6 +823,12 @@ public class ConnectDialog extends JDialog {
tn3270eCheckBox.setSelected(tn3270e);
}
public void setInitialGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode mode) {
if (mode != null && selectedProfileId == null && graphicsCombo != null) {
graphicsCombo.setSelectedItem(mode);
}
}
public void setInitialCodePage(String cp) {
if (cp != null && codePageCombo != null) {
for (int i = 0; i < codePageCombo.getItemCount(); i++) {
@@ -406,4 +841,89 @@ public class ConnectDialog extends JDialog {
codePageCombo.setSelectedItem(cp);
}
}
/**
* Set initial proxy settings for this connection.
*/
public void setInitialProxy(ConnectionConfig.ProxyType type, String host, int port, String user, String pass) {
if (proxyTypeCombo != null) {
proxyTypeCombo.setSelectedItem(type != null ? type : ConnectionConfig.ProxyType.NONE);
}
if (proxyHostField != null) {
proxyHostField.setText(host != null ? host : "");
}
if (proxyPortField != null) {
proxyPortField.setText(port > 0 ? String.valueOf(port) : "");
}
if (proxyUserField != null) {
proxyUserField.setText(user != null ? user : "");
}
if (proxyPassField != null) {
proxyPassField.setText(pass != null ? pass : "");
}
}
public ConnectionConfig.ProxyType getSelectedProxyType() {
return (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
}
public String getProxyHost() {
return proxyHostField.getText().trim();
}
public int getProxyPort() {
try {
return Integer.parseInt(proxyPortField.getText().trim());
} catch (Exception e) {
return 0;
}
}
public String getProxyUsername() {
return proxyUserField.getText().trim();
}
public String getProxyPassword() {
return new String(proxyPassField.getPassword());
}
public void setInitialHostType(String hostType) {
if (hostType != null && hostTypeCombo != null) {
for (int i = 0; i < hostTypeCombo.getItemCount(); i++) {
if (hostTypeCombo.getItemAt(i).equalsIgnoreCase(hostType.trim())) {
hostTypeCombo.setSelectedIndex(i);
return;
}
}
}
}
public void setInitialHostType(haus.nightmare.lib3270j.ft.FTConfig.HostType hostType) {
if (hostType != null) {
setInitialHostType(hostType.name());
}
}
public String getSelectedHostType() {
if (hostTypeCombo != null && hostTypeCombo.getSelectedItem() != null) {
return hostTypeCombo.getSelectedItem().toString().trim().toUpperCase();
}
return "TSO";
}
public haus.nightmare.lib3270j.ft.FTConfig.HostType getSelectedFtHostType() {
try {
return haus.nightmare.lib3270j.ft.FTConfig.HostType.valueOf(getSelectedHostType());
} catch (Exception e) {
return haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO;
}
}
public JComboBox<String> getHostTypeCombo() {
return hostTypeCombo;
}
public String getSelectedProfileId() {
return selectedProfileId;
}
}
@@ -21,7 +21,7 @@ import java.util.List;
*/
public class FieldInspectorDialog extends JDialog implements ScreenUpdateListener {
private final Telnet3270Client client;
private Telnet3270Client client;
private final TerminalPanel terminalPanel;
private DefaultTableModel tableModel;
@@ -32,6 +32,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
super(parent, "3270 Presentation Space Field Inspector", false);
this.client = client;
this.terminalPanel = terminalPanel;
AppIcon.applyTo(this);
buildUI();
setSize(850, 420);
@@ -45,13 +46,39 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
if (client != null) {
// Screen update listener removed on close
if (FieldInspectorDialog.this.client != null) {
FieldInspectorDialog.this.client.removeScreenUpdateListener(FieldInspectorDialog.this);
}
}
});
}
public void setClient(Telnet3270Client newClient) {
if (this.client != null) {
this.client.removeScreenUpdateListener(this);
}
this.client = newClient;
if (this.client != null) {
this.client.addScreenUpdateListener(this);
refreshFields();
} else if (tableModel != null && countLabel != null) {
tableModel.setRowCount(0);
countLabel.setText("Not connected");
}
}
public Telnet3270Client getClient() {
return this.client;
}
@Override
public void dispose() {
if (client != null) {
client.removeScreenUpdateListener(this);
}
super.dispose();
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(8, 8));
mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
@@ -60,7 +87,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
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));
countLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
topPanel.add(countLabel, BorderLayout.WEST);
JButton refreshBtn = new JButton("Refresh");
@@ -83,12 +110,12 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
table = new JTable(tableModel);
ThemeManager.styleTable(table);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
table.setFont(ThemeManager.getMonospacedUiFont());
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20);
table.setRowHeight(Math.max(22, ThemeManager.getMonospacedUiFont().getSize() + 8));
JTableHeader header = table.getTableHeader();
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
header.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
// Column widths
int[] widths = {35, 45, 60, 60, 40, 45, 45, 45, 50, 55, 60, 260};
@@ -15,7 +15,7 @@ import java.awt.event.KeyEvent;
*/
public class FindDialog extends JDialog {
private final Telnet3270Client client;
private Telnet3270Client client;
private final TerminalPanel terminalPanel;
private JTextField searchField;
@@ -32,6 +32,7 @@ public class FindDialog extends JDialog {
super(parent, "Find on Screen", false);
this.client = client;
this.terminalPanel = terminalPanel;
AppIcon.applyTo(this);
buildUI();
pack();
@@ -39,6 +40,24 @@ public class FindDialog extends JDialog {
setResizable(false);
}
public void setClient(Telnet3270Client client) {
this.client = client;
}
public Telnet3270Client getClient() {
return this.client;
}
public void setSearchText(String text) {
if (searchField != null) {
searchField.setText(text);
}
}
public String getSearchText() {
return searchField != null ? searchField.getText() : "";
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
@@ -50,7 +69,7 @@ public class FindDialog extends JDialog {
gbc.insets = new Insets(4, 4, 4, 4);
gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
Font labelFont = ThemeManager.getUiFont();
// Search text
gbc.gridx = 0;
@@ -63,7 +82,7 @@ public class FindDialog extends JDialog {
gbc.weightx = 1.0;
searchField = new JTextField(20);
searchField.setText(lastSearchText);
searchField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
searchField.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(searchField);
formPanel.add(searchField, gbc);
@@ -102,7 +121,7 @@ public class FindDialog extends JDialog {
gbc.gridwidth = 2;
statusLabel = new JLabel(" ");
statusLabel.setForeground(ThemeManager.getOiaFgAlert());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
statusLabel.setFont(labelFont);
formPanel.add(statusLabel, gbc);
mainPanel.add(formPanel, BorderLayout.CENTER);
@@ -3,15 +3,19 @@ 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.CMSDirectoryParser;
import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.TSODirectoryParser;
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.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
@@ -19,6 +23,7 @@ import java.util.List;
/**
* Host Directory and Dataset Catalog browser dialog for TSO and VM/CMS environments.
* Aligned with IBM Host On-Demand presentation space queries and clipboard parsing.
*/
public class HostDirectoryDialog extends JDialog {
@@ -27,22 +32,26 @@ public class HostDirectoryDialog extends JDialog {
private JComboBox<FTConfig.HostType> hostTypeCombo;
private JTextField queryField;
private JButton queryHostBtn;
private JTable table;
private DefaultTableModel tableModel;
private JLabel statusLabel;
private String selectedHostFile = null;
private HostDirectoryEntry selectedEntry = null;
private boolean confirmed = false;
private final List<HostDirectoryEntry> allEntries = new ArrayList<>();
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;
AppIcon.applyTo(this);
buildUI(initialQuery);
setSize(700, 420);
setSize(840, 480);
setLocationRelativeTo(parent);
}
@@ -50,7 +59,7 @@ public class HostDirectoryDialog extends JDialog {
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);
Font font = ThemeManager.getUiFont();
// Top Query bar
JPanel topPanel = new JPanel(new GridBagLayout());
@@ -68,40 +77,75 @@ public class HostDirectoryDialog extends JDialog {
if (initialHostType == FTConfig.HostType.CMS) hostTypeCombo.setSelectedItem(FTConfig.HostType.CMS);
else hostTypeCombo.setSelectedItem(FTConfig.HostType.TSO);
ThemeManager.styleComboBox(hostTypeCombo);
hostTypeCombo.addActionListener(e -> {
setupTableColumns();
FTConfig.HostType ht = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
if (ht == FTConfig.HostType.CMS) {
String cur = queryField.getText().trim();
if (cur.isEmpty() || cur.equals("* * *") || cur.equals("*")) {
queryField.setText("* * A");
}
} else if (ht == FTConfig.HostType.TSO) {
String cur = queryField.getText().trim();
if (cur.equals("* * A") || cur.equals("* * *")) {
queryField.setText("");
}
}
if (client != null && client.getConnectionState().isFullSession()) {
queryHost();
} else {
parseCurrentScreen();
}
});
topPanel.add(hostTypeCombo, gbc);
gbc.gridx = 2;
JLabel queryLabel = new JLabel("Query Pattern / Text:");
JLabel queryLabel = new JLabel("Filter / Pattern:");
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);
String defaultQuery = resolveDefaultQuery(initialHostType, initialQuery);
queryField = new JTextField(defaultQuery, 16);
queryField.setFont(ThemeManager.getMonospacedUiFont());
queryField.addActionListener(e -> onFilterOrParse());
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);
JButton filterBtn = new JButton("Filter / Parse");
ThemeManager.styleButton(filterBtn, ThemeManager.ButtonVariant.DEFAULT);
filterBtn.setFont(font);
filterBtn.addActionListener(e -> onFilterOrParse());
topPanel.add(filterBtn, gbc);
gbc.gridx = 5;
queryHostBtn = new JButton("Query Host");
ThemeManager.styleButton(queryHostBtn, ThemeManager.ButtonVariant.DEFAULT);
queryHostBtn.setFont(font);
queryHostBtn.setToolTipText("Send LISTCAT or LISTFILE query to active host session");
queryHostBtn.addActionListener(e -> queryHost());
topPanel.add(queryHostBtn, gbc);
mainPanel.add(topPanel, BorderLayout.NORTH);
// Table
tableModel = new DefaultTableModel();
tableModel = new DefaultTableModel() {
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
};
table = new JTable(tableModel);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
ThemeManager.styleTable(table);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
table.setFont(ThemeManager.getMonospacedUiFont());
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20);
table.setRowHeight(Math.max(22, ThemeManager.getMonospacedUiFont().getSize() + 8));
JTableHeader header = table.getTableHeader();
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
header.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
table.addMouseListener(new MouseAdapter() {
@Override
@@ -113,26 +157,42 @@ public class HostDirectoryDialog extends JDialog {
});
JScrollPane scrollPane = new JScrollPane(table);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
ThemeManager.styleScrollPane(scrollPane);
mainPanel.add(scrollPane, BorderLayout.CENTER);
// Bottom
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
JPanel bottomPanel = new JPanel(new BorderLayout(5, 6));
bottomPanel.setOpaque(false);
bottomPanel.setBorder(BorderFactory.createEmptyBorder(6, 4, 4, 4));
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);
statusLabel.setFont(font);
bottomPanel.add(statusLabel, BorderLayout.NORTH);
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
btnPanel.setOpaque(false);
JButton pasteScreenBtn = new JButton("Parse Current Screen");
JButton inspectBtn = new JButton("Inspect (LISTDS)");
ThemeManager.styleButton(inspectBtn, ThemeManager.ButtonVariant.DEFAULT);
inspectBtn.setFont(font);
inspectBtn.setToolTipText("Retrieve VTOC attributes (RECFM, LRECL, BLKSIZE) for the selected dataset via TSO LISTDS");
inspectBtn.addActionListener(e -> inspectSelectedDataset());
JButton pasteScreenBtn = new JButton("Parse Screen");
ThemeManager.styleButton(pasteScreenBtn, ThemeManager.ButtonVariant.DEFAULT);
pasteScreenBtn.setFont(font);
pasteScreenBtn.setToolTipText("Parse items currently visible on the 3270 presentation space");
pasteScreenBtn.addActionListener(e -> parseCurrentScreen());
JButton pasteClipBtn = new JButton("Paste Clipboard");
ThemeManager.styleButton(pasteClipBtn, ThemeManager.ButtonVariant.DEFAULT);
pasteClipBtn.setFont(font);
pasteClipBtn.setToolTipText("Parse host filenames copied to the system clipboard (HoD compatible)");
pasteClipBtn.addActionListener(e -> parseClipboard());
JButton selectBtn = new JButton("Select Dataset");
ThemeManager.styleButton(selectBtn, ThemeManager.ButtonVariant.PRIMARY);
selectBtn.setFont(font);
@@ -146,23 +206,28 @@ public class HostDirectoryDialog extends JDialog {
dispose();
});
btnPanel.add(inspectBtn);
btnPanel.add(pasteScreenBtn);
btnPanel.add(pasteClipBtn);
btnPanel.add(Box.createHorizontalStrut(10));
btnPanel.add(selectBtn);
btnPanel.add(cancelBtn);
bottomPanel.add(btnPanel, BorderLayout.EAST);
bottomPanel.add(btnPanel, BorderLayout.SOUTH);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
// Initial setup
// Initial setup: columns and screen parse or host query
setupTableColumns();
if (initialQuery != null && !initialQuery.isEmpty()) {
runQuery();
if (client != null && client.getConnectionState().isFullSession()) {
queryHost();
} else {
parseCurrentScreen();
if (!queryField.getText().trim().isEmpty()) {
applyFilter();
}
}
}
@@ -173,76 +238,665 @@ public class HostDirectoryDialog extends JDialog {
tableModel.setColumnIdentifiers(new Object[]{
"Filename", "Filetype", "Filemode", "Format", "Lrecl", "Records", "Date / Time"
});
int[] widths = {120, 100, 80, 70, 70, 90, 170};
for (int i = 0; i < widths.length && i < table.getColumnCount(); i++) {
table.getColumnModel().getColumn(i).setPreferredWidth(widths[i]);
}
} else {
tableModel.setColumnIdentifiers(new Object[]{
"Dataset Name", "Volume", "Recfm", "Lrecl", "Blksize", "Tracks", "Extents"
"Dataset Name", "Volume"
});
}
}
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()
});
int[] widths = {620, 160};
for (int i = 0; i < widths.length && i < table.getColumnCount(); i++) {
table.getColumnModel().getColumn(i).setPreferredWidth(widths[i]);
if (i == 0) {
table.getColumnModel().getColumn(0).setMinWidth(250);
}
}
}
}
statusLabel.setText(currentEntries.size() + " entries found.");
private void onFilterOrParse() {
String query = queryField.getText().trim();
// If text contains multiple lines, treat it as pasted raw text to parse
if (query.contains("\n") || query.contains("\r") || query.toUpperCase().contains("NONVSAM")
|| query.toUpperCase().contains("IN-CAT") || query.toUpperCase().contains("FILENAME")) {
parseText(query, "query text");
} else if (allEntries.isEmpty() && client != null && client.getConnectionState().isFullSession()) {
queryHost();
} else {
applyFilter();
}
}
private void queryHost() {
if (client == null || !client.getConnectionState().isFullSession()) {
statusLabel.setText("No active session to query.");
return;
}
String pattern = queryField.getText().trim();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
statusLabel.setText("Querying host (" + hostType + ")...");
if (queryHostBtn != null) queryHostBtn.setEnabled(false);
SwingWorker<String, Void> worker = new SwingWorker<>() {
@Override
protected String doInBackground() throws Exception {
if (client == null || client.getPS() == null) return "";
StringBuilder allPages = new StringBuilder();
int maxPages = 500;
if (hostType == FTConfig.HostType.CMS) {
// CMS Query:
// 1. Clear presentation space first to ensure clean state
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 3000);
// 2. Format CMS LISTFILE query command (default to minidisk A unless specified)
String cmsFilter = formatCMSFilter(pattern);
client.getPS().SendKeys("LISTFILE " + cmsFilter + " ( nohead alloc date[enter]");
Thread.sleep(100);
// 3. Multi-page collection loop:
// CMS pagination displays MORE... or HOLDING when a screen is full.
// When advanced with [clear], CMS flashes RUNNING while generating the next screen.
// We poll up to 10 seconds per page for MORE... / HOLDING or Ready;
for (int page = 0; page < maxPages; page++) {
long startWait = System.currentTimeMillis();
long timeoutMs = 10000; // 10 seconds per page
String screen = "";
boolean isMore = false;
while (System.currentTimeMillis() - startWait < timeoutMs) {
waitForKeyboardUnlock(client, 500);
int rows = client.getPS().getRows();
int cols = client.getPS().getCols();
screen = client.getPS().copyString(0, 0, rows - 1, cols - 1);
String upper = screen.toUpperCase();
if (upper.contains("MORE...") || upper.contains("HOLDING")) {
isMore = true;
break;
}
if (upper.contains("READY;") || upper.contains("READY(")) {
break;
}
// Still RUNNING or transitioning, wait and re-check
Thread.sleep(150);
}
allPages.append(screen).append("\n");
if (isMore) {
// In CMS, sending [clear] advances to the next page of output
client.getPS().SendKeys("[clear]");
Thread.sleep(100);
waitForKeyboardUnlock(client, 3000);
} else {
// Ready; encountered or timed out -> listing complete
break;
}
}
// Automatically clear the listing from the terminal presentation space
// so the terminal screen is left clean for the user while keeping the dialog populated
if (client.getPS() != null) {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 3000);
}
} else {
// TSO Query:
// Check if current screen is in ISPF (formatted screen or containing ===> or ISPF)
int rows = client.getPS().getRows();
int cols = client.getPS().getCols();
String initialScreen = client.getPS().copyString(0, 0, rows - 1, cols - 1);
boolean isFormatted = client.getScreenBuffer() != null && client.getScreenBuffer().isFormatted();
boolean hasCmdArrow = initialScreen.contains("===>");
boolean isIspf = isFormatted || hasCmdArrow || initialScreen.contains("ISPF");
String tsoCmd = "TSO LISTCAT ALL";
if (!pattern.isEmpty()) {
String dsnPattern = pattern.trim();
if (dsnPattern.startsWith("'") && dsnPattern.endsWith("'") && dsnPattern.length() > 2) {
dsnPattern = dsnPattern.substring(1, dsnPattern.length() - 1);
}
int paren = dsnPattern.indexOf('(');
if (paren > 0) {
dsnPattern = dsnPattern.substring(0, paren);
}
if (dsnPattern.equals("*") || dsnPattern.equals("* * *") || dsnPattern.equals("*.*")) {
tsoCmd = "TSO LISTCAT ALL";
} else {
String level = dsnPattern;
if (level.endsWith(".*")) {
level = level.substring(0, level.length() - 2);
} else if (level.endsWith("*")) {
level = level.substring(0, level.length() - 1);
}
if (level.isEmpty()) {
tsoCmd = "TSO LISTCAT ALL";
} else {
tsoCmd = "TSO LISTCAT LEVEL(" + level + ") ALL";
}
}
}
if (isIspf) {
// In ISPF, do NOT send [clear] because [clear] destroys the ISPF panel.
// Locate the command/option line (indicated by ===>) or home to first input field.
int arrowPos = initialScreen.indexOf("===>");
if (arrowPos >= 0 && client.getScreenBuffer() != null) {
int inputAddr = client.getScreenBuffer().findNextUnprotected(arrowPos);
if (inputAddr > 0) {
client.getScreenBuffer().setCursorAddress(inputAddr);
client.getScreenBuffer().updateDisplaySnapshot();
} else {
client.getPS().SendKeys("[home]");
}
} else {
client.getPS().SendKeys("[home]");
}
client.getPS().SendKeys("[erase_eof]");
client.getPS().SendKeys(tsoCmd + "[enter]");
} else {
// In native TSO (ISPF closed, line mode READY prompt)
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 3000);
client.getPS().SendKeys(tsoCmd + "[enter]");
}
Thread.sleep(100);
// Multi-page collection loop for TSO:
// 1. Wait for output to start appearing after sending command
long startCmd = System.currentTimeMillis();
while (System.currentTimeMillis() - startCmd < 4000) {
waitForKeyboardUnlock(client, 200);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String s = client.getPS().copyString(0, 0, r - 1, c - 1);
String u = s.toUpperCase();
if (u.contains("NONVSAM") || u.contains("CLUSTER") || u.contains("GDG")
|| u.contains("ALIAS") || u.contains("IN-CAT") || u.contains("IDCAMS")
|| u.contains("***") || u.contains("READY") || u.contains("NOT FOUND")
|| u.contains("NO ENTRY") || s.contains(" --- ")
|| u.contains("--RECFM-") || u.contains("--VOLUMES--")) {
break;
}
Thread.sleep(80);
}
// 2. Multi-page collection
String prevScreen = "";
for (int page = 0; page < maxPages; page++) {
waitForKeyboardUnlock(client, 300);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String screen = client.getPS().copyString(0, 0, r - 1, c - 1);
String upper = screen.toUpperCase();
if (screen.equals(prevScreen) || screen.trim().isEmpty()) {
break;
}
prevScreen = screen;
allPages.append(screen).append("\n");
if (upper.contains("***")) {
client.getPS().SendKeys("[enter]");
Thread.sleep(100);
waitForKeyboardUnlock(client, 2000);
} else if (upper.contains("NONVSAM") || upper.contains("CLUSTER") || upper.contains("GDG")
|| upper.contains("ALIAS") || upper.contains("IDCAMS") || screen.contains(" --- ")
|| upper.contains("--RECFM-") || upper.contains("--VOLUMES--")) {
// If screen shows dataset data without ***, send [enter] once to see if *** or more output is triggered
client.getPS().SendKeys("[enter]");
Thread.sleep(100);
waitForKeyboardUnlock(client, 1000);
int r2 = client.getPS().getRows();
int c2 = client.getPS().getCols();
String s2 = client.getPS().copyString(0, 0, r2 - 1, c2 - 1);
if (s2.toUpperCase().contains("***")) {
prevScreen = s2;
allPages.append(s2).append("\n");
client.getPS().SendKeys("[enter]");
Thread.sleep(100);
waitForKeyboardUnlock(client, 2000);
continue;
} else {
if (!s2.equals(screen) && !s2.trim().isEmpty()) {
allPages.append(s2).append("\n");
}
break;
}
} else {
break;
}
}
// 3. Terminal clean-up: clear the list from the terminal presentation space
if (client.getPS() != null) {
if (isIspf) {
client.getPS().SendKeys("[enter]");
Thread.sleep(150);
waitForKeyboardUnlock(client, 1500);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String currentScr = client.getPS().copyString(0, 0, r - 1, c - 1);
if (currentScr.toUpperCase().contains("NONVSAM") || currentScr.toUpperCase().contains("IDCAMS")
|| currentScr.contains(" --- ") || currentScr.toUpperCase().contains("--RECFM-")
|| (client.getScreenBuffer() != null && !client.getScreenBuffer().isFormatted())) {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 2000);
}
} else {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 2000);
}
}
}
return allPages.toString();
}
@Override
protected void done() {
if (queryHostBtn != null) queryHostBtn.setEnabled(true);
try {
String screenText = get();
parseText(screenText, "host query");
if (!pattern.isEmpty() && !allEntries.isEmpty()) {
applyFilter();
}
} catch (Exception ex) {
statusLabel.setText("Host query failed: " + ex.getMessage());
}
}
};
worker.execute();
}
private void inspectSelectedDataset() {
int row = table.getSelectedRow();
if (row < 0 || row >= currentEntries.size()) {
JOptionPane.showMessageDialog(this, "Please select an entry from the table to inspect.",
"No Selection", JOptionPane.INFORMATION_MESSAGE);
return;
}
HostDirectoryEntry entry = currentEntries.get(row);
if (!(entry instanceof TSODirectoryEntry)) {
JOptionPane.showMessageDialog(this, "Inspection is only supported for TSO datasets.",
"Not Supported", JOptionPane.INFORMATION_MESSAGE);
return;
}
if (client == null || !client.getConnectionState().isFullSession()) {
JOptionPane.showMessageDialog(this, "No active full 3270 session to query host.",
"Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
TSODirectoryEntry tso = (TSODirectoryEntry) entry;
String dsn = tso.getDatasetName();
if (dsn == null || dsn.trim().isEmpty()) {
return;
}
statusLabel.setText("Inspecting " + dsn + " via TSO LISTDS...");
SwingWorker<String, Void> worker = new SwingWorker<String, Void>() {
@Override
protected String doInBackground() throws Exception {
StringBuilder output = new StringBuilder();
int rows = client.getPS().getRows();
int cols = client.getPS().getCols();
String initialScreen = client.getPS().copyString(0, 0, rows - 1, cols - 1);
boolean isFormatted = client.getScreenBuffer() != null && client.getScreenBuffer().isFormatted();
boolean hasCmdArrow = initialScreen.contains("===>");
boolean isIspf = isFormatted || hasCmdArrow || initialScreen.contains("ISPF");
String tsoCmd = "TSO LISTDS '" + dsn + "' STATUS";
if (isIspf) {
int arrowPos = initialScreen.indexOf("===>");
if (arrowPos >= 0 && client.getScreenBuffer() != null) {
int inputAddr = client.getScreenBuffer().findNextUnprotected(arrowPos);
if (inputAddr > 0) {
client.getScreenBuffer().setCursorAddress(inputAddr);
client.getScreenBuffer().updateDisplaySnapshot();
} else {
client.getPS().SendKeys("[home]");
}
} else {
client.getPS().SendKeys("[home]");
}
client.getPS().SendKeys("[erase_eof]");
client.getPS().SendKeys(tsoCmd + "[enter]");
} else {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 3000);
client.getPS().SendKeys(tsoCmd + "[enter]");
}
Thread.sleep(100);
// Wait for output to start appearing
long startCmd = System.currentTimeMillis();
while (System.currentTimeMillis() - startCmd < 4000) {
waitForKeyboardUnlock(client, 200);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String s = client.getPS().copyString(0, 0, r - 1, c - 1);
String u = s.toUpperCase();
if (u.contains("--RECFM-") || u.contains("--VOLUMES--") || u.contains("***")
|| u.contains("READY") || u.contains("NOT IN CATALOG") || u.contains("NOT FOUND")) {
break;
}
Thread.sleep(80);
}
String prevScreen = "";
for (int page = 0; page < 5; page++) {
waitForKeyboardUnlock(client, 300);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String screen = client.getPS().copyString(0, 0, r - 1, c - 1);
String upper = screen.toUpperCase();
if (screen.equals(prevScreen) || screen.trim().isEmpty()) {
break;
}
prevScreen = screen;
output.append(screen).append("\n");
if (upper.contains("***")) {
client.getPS().SendKeys("[enter]");
Thread.sleep(100);
waitForKeyboardUnlock(client, 2000);
} else {
break;
}
}
// Terminal cleanup
if (client.getPS() != null) {
if (isIspf) {
client.getPS().SendKeys("[enter]");
Thread.sleep(150);
waitForKeyboardUnlock(client, 1500);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String currentScr = client.getPS().copyString(0, 0, r - 1, c - 1);
if (currentScr.toUpperCase().contains("--RECFM-") || (client.getScreenBuffer() != null && !client.getScreenBuffer().isFormatted())) {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 2000);
}
} else {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 2000);
}
}
return output.toString();
}
@Override
protected void done() {
try {
String outText = get();
List<TSODirectoryEntry> parsed = TSODirectoryParser.parse(outText);
if (!parsed.isEmpty()) {
TSODirectoryEntry updated = parsed.get(0);
if (updated.getRecfm() != null) tso.setRecfm(updated.getRecfm());
if (updated.getLrecl() > 0) tso.setLrecl(updated.getLrecl());
if (updated.getBlksize() > 0) tso.setBlksize(updated.getBlksize());
if (updated.getDsorg() != null) tso.setDsorg(updated.getDsorg());
if (updated.getVolume() != null && (tso.getVolume() == null || tso.getVolume().equals("******") || tso.getVolume().isEmpty())) {
tso.setVolume(updated.getVolume());
}
// Update table row in view
int modelRow = table.convertRowIndexToModel(row);
if (modelRow >= 0 && modelRow < tableModel.getRowCount()) {
tableModel.setValueAt(tso.getVolume() != null ? tso.getVolume() : "", modelRow, 1);
}
statusLabel.setText("Attributes for " + dsn + ": RECFM=" + tso.getRecfm() + ", LRECL=" + tso.getLrecl() + ", BLKSIZE=" + tso.getBlksize() + ", VOL=" + tso.getVolume());
StringBuilder msg = new StringBuilder();
msg.append("Dataset: ").append(dsn).append("\n\n");
msg.append("Volume: ").append(tso.getVolume() != null ? tso.getVolume() : "N/A").append("\n");
msg.append("Record Format (RECFM): ").append(tso.getRecfm() != null ? tso.getRecfm() : "N/A").append("\n");
msg.append("Logical Record Length (LRECL): ").append(tso.getLrecl() > 0 ? String.valueOf(tso.getLrecl()) : "N/A").append("\n");
msg.append("Block Size (BLKSIZE): ").append(tso.getBlksize() > 0 ? String.valueOf(tso.getBlksize()) : "N/A").append("\n");
msg.append("Dataset Organization (DSORG): ").append(tso.getDsorg() != null ? tso.getDsorg() : "N/A").append("\n");
if (tso.getTracksAllocated() > 0) {
msg.append("Allocated Tracks: ").append(tso.getTracksAllocated()).append("\n");
}
if (tso.getExtents() > 0) {
msg.append("Extents: ").append(tso.getExtents()).append("\n");
}
JOptionPane.showMessageDialog(HostDirectoryDialog.this, msg.toString(), "Dataset Attributes (LISTDS)", JOptionPane.INFORMATION_MESSAGE);
} else {
statusLabel.setText("No VTOC attributes found for " + dsn);
}
} catch (Exception ex) {
statusLabel.setText("Inspection failed: " + ex.getMessage());
}
}
};
worker.execute();
}
private static boolean waitForKeyboardUnlock(Telnet3270Client client, long timeoutMs) {
if (client == null) return false;
long deadline = System.currentTimeMillis() + timeoutMs;
while (System.currentTimeMillis() < deadline) {
boolean locked = false;
if (client.getInputProcessor() != null && client.getInputProcessor().isKeyboardLocked()) {
locked = true;
}
if (client.getOIA() != null && (client.getOIA().isKeyLocked() || client.getOIA().isXSystem())) {
locked = true;
}
if (!locked) {
return true;
}
try {
Thread.sleep(40);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
private void parseCurrentScreen() {
if (client == null || !client.getConnectionState().isFullSession()) {
if (client == null || !client.getConnectionState().isFullSession() || client.getPS() == null) {
statusLabel.setText("No active session to parse.");
return;
}
int rows = client.getPS().getRows();
int cols = client.getPS().getCols();
String screenText = client.getPS().copyString(0, 0, rows - 1, cols - 1);
parseText(screenText, "presentation space");
}
private void parseClipboard() {
try {
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
Transferable contents = clipboard.getContents(null);
if (contents != null && contents.isDataFlavorSupported(DataFlavor.stringFlavor)) {
String clipText = (String) contents.getTransferData(DataFlavor.stringFlavor);
if (clipText != null && !clipText.trim().isEmpty()) {
parseText(clipText, "clipboard");
return;
}
}
statusLabel.setText("Clipboard contains no text.");
} catch (Exception ex) {
statusLabel.setText("Error reading clipboard: " + ex.getMessage());
}
}
private void parseText(String text, String source) {
setupTableColumns();
currentEntries.clear();
allEntries.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()
});
}
List<CMSDirectoryEntry> cmsEntries = CMSDirectoryParser.parse(text);
allEntries.addAll(cmsEntries);
} else {
List<TSODirectoryEntry> tsoEntries = client.getXfer().getTsoDirectory(screenText);
currentEntries.addAll(tsoEntries);
for (TSODirectoryEntry entry : tsoEntries) {
List<TSODirectoryEntry> tsoEntries = TSODirectoryParser.parse(text);
allEntries.addAll(tsoEntries);
}
applyFilter();
if (allEntries.isEmpty()) {
statusLabel.setText("0 entries found from " + source + ". Use 'Query Host' or 'Paste Clipboard'.");
} else {
statusLabel.setText("Parsed " + allEntries.size() + " entries from " + source + ".");
}
}
private void applyFilter() {
tableModel.setRowCount(0);
currentEntries.clear();
String filter = queryField.getText().trim();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
for (HostDirectoryEntry entry : allEntries) {
if (!matchesFilter(entry, filter)) {
continue;
}
currentEntries.add(entry);
if (hostType == FTConfig.HostType.CMS && entry instanceof CMSDirectoryEntry) {
CMSDirectoryEntry cms = (CMSDirectoryEntry) entry;
tableModel.addRow(new Object[]{
entry.getDatasetName(), entry.getVolume(), entry.getRecfm(),
entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents()
cms.getFilename() != null ? cms.getFilename() : "",
cms.getFiletype() != null ? cms.getFiletype() : "",
cms.getFilemode() != null ? cms.getFilemode() : "",
cms.getRecfm() != null ? cms.getRecfm() : "",
cms.getLrecl() > 0 ? String.valueOf(cms.getLrecl()) : "",
cms.getNumRecords() > 0 ? String.valueOf(cms.getNumRecords()) : "",
(cms.getDate() != null ? cms.getDate() : "") + " " + (cms.getTime() != null ? cms.getTime() : "")
});
} else if (entry instanceof TSODirectoryEntry) {
TSODirectoryEntry tso = (TSODirectoryEntry) entry;
tableModel.addRow(new Object[]{
tso.getDatasetName() != null ? tso.getDatasetName() : "",
tso.getVolume() != null ? tso.getVolume() : ""
});
}
}
statusLabel.setText("Parsed " + currentEntries.size() + " entries from presentation space.");
if (!allEntries.isEmpty()) {
statusLabel.setText(currentEntries.size() + " entries shown (" + allEntries.size() + " total).");
}
}
public static boolean matchesFilter(HostDirectoryEntry entry, String filter) {
if (filter == null || filter.trim().isEmpty()) {
return true;
}
String cleanFilter = filter.replace("'", "").replace("\"", "").trim();
if (cleanFilter.isEmpty() || cleanFilter.equals("*") || cleanFilter.equals("* * *") || cleanFilter.equals("*.*")) {
return true;
}
if (entry instanceof CMSDirectoryEntry) {
return matchesCmsFilter((CMSDirectoryEntry) entry, cleanFilter);
} else if (entry instanceof TSODirectoryEntry) {
return matchesTsoFilter((TSODirectoryEntry) entry, cleanFilter);
} else {
return matchesWildcard(entry.getName(), cleanFilter);
}
}
public static boolean matchesCmsFilter(CMSDirectoryEntry cms, String filter) {
String fn = cms.getFilename() != null ? cms.getFilename().trim() : "";
String ft = cms.getFiletype() != null ? cms.getFiletype().trim() : "";
String fm = cms.getFilemode() != null ? cms.getFilemode().trim() : "";
String[] tokens = filter.split("\\s+");
if (tokens.length >= 3) {
String fnPat = tokens[0];
String ftPat = tokens[1];
String fmPat = tokens[2];
return matchesWildcard(fn, fnPat)
&& matchesWildcard(ft, ftPat)
&& matchesCmsFilemode(fm, fmPat);
} else if (tokens.length == 2) {
String fnPat = tokens[0];
String ftPat = tokens[1];
return matchesWildcard(fn, fnPat) && matchesWildcard(ft, ftPat);
} else {
String tok = tokens[0];
if (tok.contains(".")) {
int dot = tok.indexOf('.');
String fnPat = tok.substring(0, dot);
String ftPat = tok.substring(dot + 1);
return matchesWildcard(fn, fnPat) && matchesWildcard(ft, ftPat);
}
if (matchesWildcard(fn, tok) || matchesWildcard(ft, tok) || matchesCmsFilemode(fm, tok)) {
return true;
}
return matchesWildcard(cms.getName(), tok);
}
}
public static boolean matchesCmsFilemode(String actualFm, String fmPattern) {
if (fmPattern == null || fmPattern.isEmpty() || fmPattern.equals("*")) {
return true;
}
if (actualFm == null || actualFm.isEmpty()) {
return false;
}
if (fmPattern.length() == 1 && Character.isLetter(fmPattern.charAt(0))) {
return actualFm.toUpperCase().startsWith(fmPattern.toUpperCase());
}
return matchesWildcard(actualFm, fmPattern);
}
public static boolean matchesTsoFilter(TSODirectoryEntry tso, String filter) {
String dsn = tso.getDatasetName() != null ? tso.getDatasetName().trim() : "";
if (filter.equals("*") || filter.equals("* * *") || filter.equals("*.*")) {
return true;
}
if (filter.contains("*") || filter.contains("?")) {
return matchesWildcard(dsn, filter);
}
return dsn.toUpperCase().contains(filter.toUpperCase());
}
public static boolean matchesWildcard(String text, String pattern) {
if (text == null) text = "";
if (pattern == null || pattern.isEmpty() || pattern.equals("*")) return true;
StringBuilder regex = new StringBuilder("^");
for (int i = 0; i < pattern.length(); i++) {
char c = pattern.charAt(i);
if (c == '*') {
regex.append(".*");
} else if (c == '?') {
regex.append(".");
} else if ("\\.[]{}()+^$|".indexOf(c) >= 0) {
regex.append("\\").append(c);
} else {
regex.append(Character.toUpperCase(c));
}
}
regex.append("$");
return text.toUpperCase().matches(regex.toString());
}
private void onConfirmSelection() {
@@ -255,6 +909,7 @@ public class HostDirectoryDialog extends JDialog {
HostDirectoryEntry entry = currentEntries.get(row);
selectedHostFile = entry.getName();
selectedEntry = entry;
confirmed = true;
dispose();
}
@@ -266,4 +921,50 @@ public class HostDirectoryDialog extends JDialog {
public String getSelectedHostFile() {
return selectedHostFile;
}
public HostDirectoryEntry getSelectedEntry() {
return selectedEntry;
}
public List<HostDirectoryEntry> getAllEntries() {
return allEntries;
}
public List<HostDirectoryEntry> getCurrentEntries() {
return currentEntries;
}
public JTextField getQueryField() {
return queryField;
}
public static String resolveDefaultQuery(FTConfig.HostType hostType, String initialQuery) {
String defaultQuery = initialQuery != null ? initialQuery.trim() : "";
if (hostType == FTConfig.HostType.CMS && (defaultQuery.isEmpty() || defaultQuery.equals("* * *") || defaultQuery.equals("*"))) {
defaultQuery = "* * A";
}
return defaultQuery;
}
public static String formatCMSFilter(String pattern) {
if (pattern == null || pattern.trim().isEmpty()) {
return "* * A";
}
String clean = pattern.replace("'", "").replace("\"", "").trim();
if (clean.equals("* * *") || clean.equals("*")) {
return "* * A";
} else if (clean.contains(".")) {
int dot = clean.indexOf('.');
return clean.substring(0, dot).trim() + " " + clean.substring(dot + 1).trim() + " A";
} else {
String[] parts = clean.split("\\s+");
if (parts.length == 1) {
return parts[0] + " * A";
} else if (parts.length == 2) {
return parts[0] + " " + parts[1] + " A";
} else {
return parts[0] + " " + parts[1] + " " + parts[2];
}
}
}
}
@@ -0,0 +1,440 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Real-time hotspot and URL auto-detection engine matching IBM Host On-Demand
* ShowURLOptions and ScreenText parity.
*
* Scans screen text using regular expressions for:
* 1. URLs (http://, https://, ftp://, file://, mailto:, www.)
* 2. Function keys (PF1..PF24, F1..F24, PA1..PA3, [ENTER], ENTER)
* 3. Menu choices ("1.", "2.", "===> 3", "=> 4")
*
* Provides hit-testing for mouse hover/clicks and generates keystroke actions.
*/
public class HotspotEngine {
public enum HotspotType {
URL,
PF_KEY,
ENTER,
MENU_CHOICE
}
/**
* Represents an active interactive hotspot region on the terminal screen.
*/
public static class Hotspot {
private final HotspotType type;
private final int startRow;
private final int startCol;
private final int endRow;
private final int endCol;
private final String displayText;
private final String action;
private final String tooltip;
public Hotspot(HotspotType type, int startRow, int startCol, int endRow, int endCol,
String displayText, String action, String tooltip) {
this.type = type;
this.startRow = startRow;
this.startCol = startCol;
this.endRow = endRow;
this.endCol = endCol;
this.displayText = displayText;
this.action = action;
this.tooltip = tooltip;
}
public HotspotType getType() {
return type;
}
public int getStartRow() {
return startRow;
}
public int getStartCol() {
return startCol;
}
public int getEndRow() {
return endRow;
}
public int getEndCol() {
return endCol;
}
public String getDisplayText() {
return displayText;
}
public String getAction() {
return action;
}
public String getTooltip() {
return tooltip;
}
/**
* Test whether the given row/col coordinate falls within this hotspot.
*/
public boolean contains(int row, int col) {
if (row < startRow || row > endRow) {
return false;
}
if (startRow == endRow) {
return col >= startCol && col <= endCol;
}
if (row == startRow) {
return col >= startCol;
}
if (row == endRow) {
return col <= endCol;
}
return true; // Intermediate rows
}
@Override
public String toString() {
return "Hotspot{" + type + " [" + startRow + "," + startCol + "]-[" + endRow + "," + endCol + "] '" + displayText + "'}";
}
}
// Configurable scan enablement
private boolean urlEnabled = true;
private boolean pfKeyEnabled = true;
private boolean menuChoiceEnabled = true;
// Regular expression patterns
// 1. URLs
private static final Pattern URL_PATTERN = Pattern.compile(
"(?:https?|ftp|file)://[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+|www\\.[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+|mailto:[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+",
Pattern.CASE_INSENSITIVE
);
// 2. Function keys (PF1..PF24, F1..F24, PA1..PA3, [ENTER], ENTER)
private static final Pattern PF_KEY_PATTERN = Pattern.compile(
"(?:(?<=\\W)|^)(PF(?:[1-9]|1[0-9]|2[0-4])|F(?:[1-9]|1[0-9]|2[0-4])|PA[1-3]|\\[ENTER\\]|ENTER)(?:(?=\\W)|$)",
Pattern.CASE_INSENSITIVE
);
// 3. Menu choices: "1.", "2.", "===> 3", "=> 4", "--> 5", ISPF (" 0 Settings", " 1 View", "10 SCLM", " X Exit")
private static final Pattern MENU_NUMBER_PATTERN = Pattern.compile(
"(?:(?<=\\s)|^)(\\d{1,2})\\.(?:(?=\\s)|$)"
);
private static final Pattern MENU_PROMPT_PATTERN = Pattern.compile(
"(?:===>|=>|-->)\\s*(\\d{1,2})(?:(?=\\s)|$)"
);
private static final Pattern ISPF_MENU_PATTERN = Pattern.compile(
"(?:^\\s*|(?<=\\s{3,}))(?:(\\d{1,2}(?:\\.\\d{1,2})?)(?:\\s*[-–—]\\s*|\\s+)|([A-Za-z])(?:\\s*[-–—]\\s*|\\s{2,}))([A-Za-z][A-Za-z0-9/_-]*)"
);
// Caching state to avoid rescanning unchanged screens
private String cachedScreenText = "";
private int cachedRows = 0;
private int cachedCols = 0;
private List<Hotspot> cachedHotspots = Collections.emptyList();
private ScreenBuffer lastScreenBuffer;
private long lastSnapshotVersion = -1;
private long scanCount = 0;
public HotspotEngine() {
}
public boolean isUrlEnabled() {
return urlEnabled;
}
public void setUrlEnabled(boolean urlEnabled) {
this.urlEnabled = urlEnabled;
invalidateCache();
}
public boolean isPfKeyEnabled() {
return pfKeyEnabled;
}
public void setPfKeyEnabled(boolean pfKeyEnabled) {
this.pfKeyEnabled = pfKeyEnabled;
invalidateCache();
}
public boolean isMenuChoiceEnabled() {
return menuChoiceEnabled;
}
public void setMenuChoiceEnabled(boolean menuChoiceEnabled) {
this.menuChoiceEnabled = menuChoiceEnabled;
invalidateCache();
}
public synchronized void invalidateCache() {
cachedScreenText = "";
cachedHotspots = Collections.emptyList();
lastScreenBuffer = null;
lastSnapshotVersion = -1;
}
public synchronized void markDirty() {
invalidateCache();
}
public synchronized long getScanCount() {
return scanCount;
}
/**
* Scan the given ScreenBuffer for interactive hotspots.
*/
public synchronized List<Hotspot> scan(ScreenBuffer sb) {
if (sb == null) return Collections.emptyList();
int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols();
long version = sb.getSnapshotVersion();
if (sb == lastScreenBuffer && version == lastSnapshotVersion && rows == cachedRows && cols == cachedCols) {
return cachedHotspots;
}
scanCount++;
// Build plain text representation
StringBuilder fullText = new StringBuilder(rows * (cols + 1));
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int baddr = r * cols + c;
ExtendedAttribute ea = sb.getDisplayCell(baddr);
if (ea == null || ea.isFieldAttribute() || ea.ucs4 <= 0x20 || ea.ucs4 == 0xFF) {
fullText.append(' ');
} else {
fullText.append((char) ea.ucs4);
}
}
if (r < rows - 1) {
fullText.append('\n');
}
}
List<Hotspot> results = scanText(fullText.toString(), rows, cols);
lastScreenBuffer = sb;
lastSnapshotVersion = version;
return results;
}
/**
* Scan formatted screen text (lines separated by '\n') for hotspots.
*/
public synchronized List<Hotspot> scanText(String screenText, int rows, int cols) {
if (screenText == null || rows <= 0 || cols <= 0) {
return Collections.emptyList();
}
if (screenText.equals(cachedScreenText) && rows == cachedRows && cols == cachedCols) {
return cachedHotspots;
}
List<Hotspot> hotspots = new ArrayList<>();
String[] lines = screenText.split("\n", -1);
int actualRows = Math.min(rows, lines.length);
// Scan line by line for precise row/column tracking
for (int r = 0; r < actualRows; r++) {
String line = lines[r];
boolean[] occupied = new boolean[line.length() + 1];
// 1. Scan URLs
if (urlEnabled) {
Matcher mUrl = URL_PATTERN.matcher(line);
while (mUrl.find()) {
int startCol = mUrl.start();
int endCol = mUrl.end();
String match = mUrl.group();
// Trim trailing punctuation like . , ; : ) ]
while (match.length() > 1) {
char last = match.charAt(match.length() - 1);
if (last == '.' || last == ',' || last == ';' || last == ':' || last == ')' || last == ']' || last == '>') {
match = match.substring(0, match.length() - 1);
endCol--;
} else {
break;
}
}
if (match.isEmpty()) continue;
String actionUrl = match;
if (match.toLowerCase().startsWith("www.")) {
actionUrl = "http://" + match;
}
Hotspot h = new Hotspot(
HotspotType.URL,
r, startCol, r, endCol - 1,
match, actionUrl, "Open URL: " + actionUrl
);
hotspots.add(h);
markOccupied(occupied, startCol, endCol);
}
}
// 2. Scan PF / Function keys
if (pfKeyEnabled) {
Matcher mPf = PF_KEY_PATTERN.matcher(line);
while (mPf.find()) {
int startCol = mPf.start(1);
int endCol = mPf.end(1);
if (isRangeOccupied(occupied, startCol, endCol)) continue;
String keyText = mPf.group(1).toUpperCase();
String action;
String tooltip;
if (keyText.equals("ENTER") || keyText.equals("[ENTER]")) {
action = "[enter]";
tooltip = "Send [ENTER]";
} else if (keyText.startsWith("PF")) {
String numStr = keyText.substring(2);
action = "[pf" + numStr + "]";
tooltip = "Send " + action.toUpperCase();
} else if (keyText.startsWith("F")) {
String numStr = keyText.substring(1);
action = "[pf" + numStr + "]";
tooltip = "Send " + action.toUpperCase();
} else if (keyText.startsWith("PA")) {
String numStr = keyText.substring(2);
action = "[pa" + numStr + "]";
tooltip = "Send " + action.toUpperCase();
} else {
continue;
}
Hotspot h = new Hotspot(
keyText.contains("ENTER") ? HotspotType.ENTER : HotspotType.PF_KEY,
r, startCol, r, endCol - 1,
mPf.group(1), action, tooltip
);
hotspots.add(h);
markOccupied(occupied, startCol, endCol);
}
}
// 3. Scan Menu choices ("1.", "2.", "===> 3", "=> 4")
if (menuChoiceEnabled) {
// Pattern 3a: Number followed by dot (e.g. " 1. ")
Matcher mNum = MENU_NUMBER_PATTERN.matcher(line);
while (mNum.find()) {
int startCol = mNum.start(1);
int endCol = mNum.end();
if (isRangeOccupied(occupied, startCol, endCol)) continue;
String choiceNum = mNum.group(1);
String action = choiceNum + "[enter]";
String tooltip = "Select option " + choiceNum;
Hotspot h = new Hotspot(
HotspotType.MENU_CHOICE,
r, startCol, r, endCol - 1,
choiceNum + ".", action, tooltip
);
hotspots.add(h);
markOccupied(occupied, startCol, endCol);
}
// Pattern 3b: Prompt followed by number (e.g. "===> 3")
Matcher mPrompt = MENU_PROMPT_PATTERN.matcher(line);
while (mPrompt.find()) {
int startCol = mPrompt.start();
int endCol = mPrompt.end();
if (isRangeOccupied(occupied, startCol, endCol)) continue;
String choiceNum = mPrompt.group(1);
String action = choiceNum + "[enter]";
String tooltip = "Execute choice " + choiceNum;
Hotspot h = new Hotspot(
HotspotType.MENU_CHOICE,
r, startCol, r, endCol - 1,
mPrompt.group(), action, tooltip
);
hotspots.add(h);
markOccupied(occupied, startCol, endCol);
}
// Pattern 3c: ISPF menu choices (e.g. " 1 View", " 2 Edit", "10 SCLM", " X Exit", " M More", " 1 - View", " 3.1 Library")
Matcher mIspf = ISPF_MENU_PATTERN.matcher(line);
while (mIspf.find()) {
String choiceStr = mIspf.group(1) != null ? mIspf.group(1) : mIspf.group(2).toUpperCase();
String labelStr = mIspf.group(3);
int startCol = mIspf.group(1) != null ? mIspf.start(1) : mIspf.start(2);
int endCol = mIspf.end(3);
if (isRangeOccupied(occupied, startCol, endCol)) continue;
String action = choiceStr + "[enter]";
String tooltip = "Select option " + choiceStr + " (" + labelStr + ")";
String display = line.substring(startCol, endCol);
Hotspot h = new Hotspot(
HotspotType.MENU_CHOICE,
r, startCol, r, endCol - 1,
display, action, tooltip
);
hotspots.add(h);
markOccupied(occupied, startCol, endCol);
}
}
}
this.cachedScreenText = screenText;
this.cachedRows = rows;
this.cachedCols = cols;
this.cachedHotspots = Collections.unmodifiableList(hotspots);
return cachedHotspots;
}
/**
* Hit-test for a hotspot at the specified terminal row and column.
*/
public Hotspot findHotspot(int row, int col) {
for (Hotspot h : cachedHotspots) {
if (h.contains(row, col)) {
return h;
}
}
return null;
}
/**
* Query currently active cached hotspots.
*/
public List<Hotspot> getHotspots() {
return cachedHotspots;
}
private static void markOccupied(boolean[] arr, int start, int end) {
for (int i = Math.max(0, start); i < Math.min(arr.length, end); i++) {
arr[i] = true;
}
}
private static boolean isRangeOccupied(boolean[] arr, int start, int end) {
for (int i = Math.max(0, start); i < Math.min(arr.length, end); i++) {
if (arr[i]) return true;
}
return false;
}
}
@@ -206,7 +206,8 @@ public class KeyBindings {
/**
* Parse a .kmp file from a Reader.
* Supports both INI-style KEY<n>=action formats and bind statements.
* Supports both IBM Host On-Demand (HoD) .kmp format (hashKey prefixes)
* and IBM Personal Communications (PCOMM) INI-style and bind statements.
*
* @param reader the reader to parse from
* @return Map of j3270 action names to key stroke bindings
@@ -214,11 +215,38 @@ public class KeyBindings {
*/
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<>();
List<String> rawLines = new ArrayList<>();
boolean hasHodKeyRemap = false;
boolean hasHodHashKey = false;
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
rawLines.add(line);
String trimmed = line.trim();
if (trimmed.equalsIgnoreCase("[KeyRemap]")) {
hasHodKeyRemap = true;
}
int eq = trimmed.indexOf('=');
if (eq > 0) {
String left = trimmed.substring(0, eq).trim();
if (left.matches("^[A-Za-z][0-9]+$")) {
hasHodHashKey = true;
}
}
}
if (hasHodKeyRemap || hasHodHashKey) {
Map<String, String> hodMap = haus.nightmare.lib3270j.input.HodKeyMap.parseHodKmp(
new StringReader(String.join("\n", rawLines))
);
if (hodMap != null && !hodMap.isEmpty()) {
return hodMap;
}
}
Map<String, List<String>> actionToBindings = new LinkedHashMap<>();
for (String raw : rawLines) {
line = raw.trim();
// Strip comments
int commentIdx = line.indexOf(';');
@@ -262,6 +290,22 @@ public class KeyBindings {
return result;
}
/**
* Export key bindings to a .kmp file using IBM HoD format.
*/
public static void exportKmp(File file, Map<String, String> keyBindings) throws IOException {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
exportKmp(bw, keyBindings);
}
}
/**
* Export key bindings to a Writer using IBM HoD format.
*/
public static void exportKmp(Writer writer, Map<String, String> keyBindings) throws IOException {
haus.nightmare.lib3270j.input.HodKeyMap.exportHodKmp(writer, keyBindings);
}
private static void parseBindStatement(String stmt, Map<String, List<String>> actionToBindings) {
String[] parts = stmt.split("\\s+", 2);
if (parts.length < 2) return;
@@ -288,6 +332,19 @@ public class KeyBindings {
}
private static void parseKeyAssignment(String left, String right, Map<String, List<String>> actionToBindings) {
if (left.matches("^[A-Za-z][0-9]+$")) {
haus.nightmare.lib3270j.input.HodKeyMap.KeyEntry entry =
haus.nightmare.lib3270j.input.HodKeyMap.parseHashKey(left, right);
if (entry != null) {
String stroke = entry.toKeyStrokeString();
String act = haus.nightmare.lib3270j.input.HodKeyMap.resolveMnemonicToAction(right);
if (act != null && stroke != null && !stroke.isEmpty()) {
addBinding(actionToBindings, act, stroke);
return;
}
}
}
// Case A: left is KEY<n> or Modifier-KEY<n>, right is mnemonic
String action = resolveMnemonic(right);
String keyStroke = null;
@@ -0,0 +1,392 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ecl.ECLSession;
import haus.nightmare.lib3270j.macro.Macro;
import haus.nightmare.lib3270j.macro.MacroScreen;
import haus.nightmare.lib3270j.macro.MacroScreens;
import haus.nightmare.lib3270j.macro.MacroXmlParser;
import haus.nightmare.lib3270j.macro.action.MacroAction;
import haus.nightmare.lib3270j.macro.action.MacroActionPrompt;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
/**
* Interactive Swing dialog for loading, editing, running, and debugging
* IBM Host On-Demand XML Macros (<HASCRIPT> / .mac files).
*/
public class MacroDialog extends JDialog implements Macro.MacroListener, Macro.MacroPromptHandler {
private final Telnet3270Client client;
private final TerminalPanel terminalPanel;
private JTextArea xmlArea;
private JTextArea logArea;
private JLabel statusLabel;
private JButton playBtn;
private JButton pauseBtn;
private JButton stopBtn;
private Macro macro;
public MacroDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
super(parent, "IBM HoD XML Macro Runtime (<HASCRIPT>)", false);
this.client = client;
this.terminalPanel = terminalPanel;
AppIcon.applyTo(this);
this.macro = new Macro(client);
this.macro.addMacroListener(this);
this.macro.setPromptHandler(this);
buildUI();
setSize(780, 580);
setLocationRelativeTo(parent);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
Font labelFont = ThemeManager.getUiFont();
// Top info label
JPanel topPanel = new JPanel(new BorderLayout(5, 5));
topPanel.setOpaque(false);
JLabel descLabel = new JLabel("<html>Load, edit, and execute IBM Host On-Demand XML Macro files (<code>.mac</code> / <code>&lt;HASCRIPT&gt;</code>).<br>"
+ "Automates host flows via screen descriptors, actions, variables, and state transitions.</html>");
descLabel.setFont(labelFont);
topPanel.add(descLabel, BorderLayout.CENTER);
mainPanel.add(topPanel, BorderLayout.NORTH);
// Center split: XML editor and Execution Log
xmlArea = new JTextArea();
xmlArea.setFont(ThemeManager.getMonospacedUiFont());
xmlArea.setLineWrap(false);
xmlArea.setText(getDefaultMacroXml());
ThemeManager.styleTextArea(xmlArea);
JScrollPane xmlScroll = new JScrollPane(xmlArea);
ThemeManager.styleScrollPane(xmlScroll);
xmlScroll.setBorder(ThemeManager.createTitledBorder("Macro XML (<HASCRIPT>)"));
logArea = new JTextArea();
logArea.setFont(ThemeManager.getMonospacedUiFont(Font.PLAIN, -1));
logArea.setEditable(false);
logArea.setLineWrap(true);
ThemeManager.styleTextArea(logArea);
JScrollPane logScroll = new JScrollPane(logArea);
ThemeManager.styleScrollPane(logScroll);
logScroll.setBorder(ThemeManager.createTitledBorder("Execution Log"));
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, xmlScroll, logScroll);
splitPane.setResizeWeight(0.65);
splitPane.setOpaque(false);
mainPanel.add(splitPane, BorderLayout.CENTER);
// Bottom control panel
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
bottomPanel.setOpaque(false);
statusLabel = new JLabel("Status: Stopped");
statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(labelFont);
bottomPanel.add(statusLabel, BorderLayout.WEST);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
buttonPanel.setOpaque(false);
JButton loadBtn = new JButton("Load .mac...");
ThemeManager.styleButton(loadBtn, ThemeManager.ButtonVariant.DEFAULT);
loadBtn.setFont(labelFont);
loadBtn.addActionListener(e -> loadMacroFile());
JButton saveBtn = new JButton("Save .mac...");
ThemeManager.styleButton(saveBtn, ThemeManager.ButtonVariant.DEFAULT);
saveBtn.setFont(labelFont);
saveBtn.addActionListener(e -> saveMacroFile());
playBtn = new JButton("Play");
ThemeManager.styleButton(playBtn, ThemeManager.ButtonVariant.PRIMARY);
playBtn.setFont(labelFont);
playBtn.addActionListener(e -> playMacro());
pauseBtn = new JButton("Pause");
ThemeManager.styleButton(pauseBtn, ThemeManager.ButtonVariant.DEFAULT);
pauseBtn.setFont(labelFont);
pauseBtn.setEnabled(false);
pauseBtn.addActionListener(e -> togglePause());
stopBtn = new JButton("Stop");
ThemeManager.styleButton(stopBtn, ThemeManager.ButtonVariant.DEFAULT);
stopBtn.setFont(labelFont);
stopBtn.setEnabled(false);
stopBtn.addActionListener(e -> stopMacro());
JButton clearLogBtn = new JButton("Clear Log");
ThemeManager.styleButton(clearLogBtn, ThemeManager.ButtonVariant.DEFAULT);
clearLogBtn.setFont(labelFont);
clearLogBtn.addActionListener(e -> logArea.setText(""));
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(8));
buttonPanel.add(playBtn);
buttonPanel.add(pauseBtn);
buttonPanel.add(stopBtn);
buttonPanel.add(clearLogBtn);
buttonPanel.add(Box.createHorizontalStrut(8));
buttonPanel.add(closeBtn);
bottomPanel.add(buttonPanel, BorderLayout.EAST);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
}
private void loadMacroFile() {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("Open IBM HoD Macro");
chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml"));
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
File f = chooser.getSelectedFile();
try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = r.readLine()) != null) {
sb.append(line).append("\n");
}
xmlArea.setText(sb.toString());
log("Loaded macro file: " + f.getName());
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Failed to load macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
}
private void saveMacroFile() {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("Save IBM HoD Macro");
chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml"));
if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
File f = chooser.getSelectedFile();
if (!f.getName().contains(".")) {
f = new File(f.getParentFile(), f.getName() + ".mac");
}
try (Writer w = new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)) {
w.write(xmlArea.getText());
log("Saved macro file: " + f.getName());
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Failed to save macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
}
public void playMacro() {
String xml = xmlArea.getText();
if (xml == null || xml.trim().isEmpty()) {
JOptionPane.showMessageDialog(this, "Macro XML is empty.", "Warning", JOptionPane.WARNING_MESSAGE);
return;
}
if (client == null || !client.getConnectionState().isFullSession()) {
int opt = JOptionPane.showConfirmDialog(this,
"Session is not currently connected to a host.\nDo you want to run macro anyway (descriptors will wait for connection)?",
"Not Connected", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
if (opt != JOptionPane.YES_OPTION) {
return;
}
}
try {
MacroScreens screens = MacroXmlParser.parse(xml);
macro.setScreens(screens);
macro.setClient(client);
log("Starting macro: " + (screens.getName().isEmpty() ? "Untitled" : screens.getName()));
macro.play();
} catch (Exception ex) {
log("Parse / Launch Error: " + ex.getMessage());
JOptionPane.showMessageDialog(this, "Macro parsing error: " + ex.getMessage(), "Macro Error", JOptionPane.ERROR_MESSAGE);
}
}
public void togglePause() {
if (macro.getState() == Macro.STATE_PLAYING) {
macro.pause();
pauseBtn.setText("Resume");
log("Macro paused.");
} else if (macro.getState() == Macro.STATE_PAUSED) {
macro.resume();
pauseBtn.setText("Pause");
log("Macro resumed.");
}
}
public void stopMacro() {
macro.stop();
log("Macro stopped by user.");
}
private void log(String msg) {
SwingUtilities.invokeLater(() -> {
logArea.append(msg + "\n");
logArea.setCaretPosition(logArea.getDocument().getLength());
});
}
// ========== MacroListener Callbacks ==========
@Override
public void macroStateChanged(Macro macro, int oldState, int newState) {
SwingUtilities.invokeLater(() -> {
switch (newState) {
case Macro.STATE_PLAYING:
statusLabel.setText("Status: Playing");
statusLabel.setForeground(ThemeManager.getFgMain());
playBtn.setEnabled(false);
pauseBtn.setEnabled(true);
pauseBtn.setText("Pause");
stopBtn.setEnabled(true);
break;
case Macro.STATE_PAUSED:
statusLabel.setText("Status: Paused");
statusLabel.setForeground(ThemeManager.getFgMuted());
pauseBtn.setText("Resume");
break;
case Macro.STATE_ERROR:
statusLabel.setText("Status: Error");
statusLabel.setForeground(Color.RED);
playBtn.setEnabled(true);
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
break;
case Macro.STATE_STOPPED:
default:
statusLabel.setText("Status: Stopped");
statusLabel.setForeground(ThemeManager.getFgMuted());
playBtn.setEnabled(true);
pauseBtn.setEnabled(false);
pauseBtn.setText("Pause");
stopBtn.setEnabled(false);
break;
}
});
}
@Override
public void macroScreenMatching(MacroScreen screen) {
log("Matched screen: [" + screen.getName() + "]");
if (terminalPanel != null) {
terminalPanel.repaint();
}
}
@Override
public void macroActionExecuting(MacroAction action) {
log(" -> Executing: " + action.getClass().getSimpleName());
if (terminalPanel != null) {
terminalPanel.repaint();
}
}
@Override
public void macroMessage(String title, String message) {
log("Message [" + title + "]: " + message);
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(this, message, title.isEmpty() ? "Macro Message" : title, JOptionPane.INFORMATION_MESSAGE);
});
}
@Override
public void macroError(String error) {
log("ERROR: " + error);
}
// ========== MacroPromptHandler Callback ==========
@Override
public String prompt(MacroActionPrompt prompt) {
AtomicReference<String> result = new AtomicReference<>(null);
if (SwingUtilities.isEventDispatchThread()) {
result.set(showPromptModal(prompt));
} else {
try {
SwingUtilities.invokeAndWait(() -> {
result.set(showPromptModal(prompt));
});
} catch (Exception e) {
return null;
}
}
return result.get();
}
private String showPromptModal(MacroActionPrompt prompt) {
JPanel panel = new JPanel(new BorderLayout(5, 5));
JLabel msgLabel = new JLabel(prompt.getDescription().isEmpty() ? "Enter parameter:" : prompt.getDescription());
panel.add(msgLabel, BorderLayout.NORTH);
JTextField textField;
if (prompt.isEncrypted()) {
textField = new JPasswordField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15);
} else {
textField = new JTextField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15);
}
panel.add(textField, BorderLayout.CENTER);
int opt = JOptionPane.showConfirmDialog(this, panel,
prompt.getTitle().isEmpty() ? "Macro Input Prompt" : prompt.getTitle(),
JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
if (opt == JOptionPane.OK_OPTION) {
if (textField instanceof JPasswordField) {
return new String(((JPasswordField) textField).getPassword());
}
return textField.getText();
}
return null;
}
private String getDefaultMacroXml() {
return "<HASCRIPT name=\"SampleLogon\" timeout=\"15000\" usevars=\"true\">\n"
+ " <vars>\n"
+ " <create name=\"$userId\" type=\"string\" value=\"USER01\" />\n"
+ " <create name=\"$count\" type=\"integer\" value=\"0\" />\n"
+ " </vars>\n\n"
+ " <screen name=\"LogonScreen\" entryscreen=\"true\">\n"
+ " <description>\n"
+ " <oia status=\"NOTINHIBITED\" />\n"
+ " </description>\n"
+ " <actions>\n"
+ " <input value=\"$userId + '[enter]'\" movecursor=\"true\" xlatehostkeys=\"true\" />\n"
+ " </actions>\n"
+ " <nextscreens timeout=\"10000\">\n"
+ " <nextscreen name=\"ReadyScreen\" />\n"
+ " </nextscreens>\n"
+ " </screen>\n\n"
+ " <screen name=\"ReadyScreen\" exitscreen=\"true\">\n"
+ " <description>\n"
+ " <oia status=\"NOTINHIBITED\" />\n"
+ " </description>\n"
+ " <actions>\n"
+ " <message title=\"Success\" value=\"Logon macro completed successfully!\" />\n"
+ " </actions>\n"
+ " </screen>\n"
+ "</HASCRIPT>";
}
}
@@ -48,6 +48,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) {
super(parent, "IBM 3287 Printer Session Manager", false);
this.parent = parent;
AppIcon.applyTo(this);
config = new PrinterConfig(defaultHost, defaultPort);
config.setUseTls(defaultTls);
@@ -71,7 +72,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
gbc.insets = new Insets(3, 4, 3, 4);
gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
Font labelFont = ThemeManager.getUiFont();
// Host / Port
gbc.gridx = 0; gbc.gridy = 0;
@@ -205,7 +206,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Spool text area
spoolArea = new JTextArea();
spoolArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
spoolArea.setFont(ThemeManager.getMonospacedUiFont());
spoolArea.setEditable(false);
ThemeManager.styleTextArea(spoolArea);
@@ -256,7 +257,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
private JTextField createField(String text, int cols) {
JTextField tf = new JTextField(text, cols);
tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
tf.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(tf);
return tf;
}
@@ -0,0 +1,323 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer;
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer.ScreenHistoryListener;
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer.ScreenSnapshot;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.*;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Screen History viewer dialog matching IBM Host On-Demand ScreenHistory UI parity.
*
* Provides:
* - Interactive JSlider scrubber to review past 64 screen buffer frames
* - Previous / Next navigation buttons
* - Exact monospace terminal frame preview with selection capability
* - Copy Screen, Copy Selection, and Copy as Table actions
* - Live update synchronization as new host screens arrive
*/
public class ScreenHistoryDialog extends JDialog implements ScreenHistoryListener {
private final J3270App parentApp;
private final Telnet3270Client client;
private final ScreenHistoryBuffer historyBuffer;
private JSlider scrubSlider;
private JButton btnPrev;
private JButton btnNext;
private JLabel infoLabel;
private JTextArea previewArea;
private JButton btnCopyScreen;
private JButton btnCopySelection;
private JButton btnCopyTable;
private JButton btnClose;
private int currentIndex = -1; // 0-based index into historyBuffer
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public ScreenHistoryDialog(J3270App parent, Telnet3270Client client) {
super(parent, "Screen History", false);
this.parentApp = parent;
this.client = client;
this.historyBuffer = client != null ? client.getScreenHistoryBuffer() : null;
AppIcon.applyTo(this);
initComponents();
setupKeyNavigation();
if (historyBuffer != null) {
historyBuffer.addListener(this);
refreshHistoryView(historyBuffer.size() - 1);
} else {
updateEmptyState();
}
addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
if (historyBuffer != null) {
historyBuffer.removeListener(ScreenHistoryDialog.this);
}
}
});
setSize(780, 560);
setLocationRelativeTo(parent);
}
private void initComponents() {
setLayout(new BorderLayout(6, 6));
JPanel contentPane = new JPanel(new BorderLayout(8, 8));
contentPane.setBorder(new EmptyBorder(10, 10, 10, 10));
setContentPane(contentPane);
// Top Navigation Bar
JPanel topPanel = new JPanel(new BorderLayout(8, 8));
topPanel.setBorder(BorderFactory.createTitledBorder("History Scrub Navigation"));
JPanel sliderPanel = new JPanel(new BorderLayout(6, 6));
btnPrev = new JButton("◄ Prev");
ThemeManager.styleButton(btnPrev, ThemeManager.ButtonVariant.DEFAULT);
btnPrev.addActionListener(e -> stepHistory(-1));
btnNext = new JButton("Next ►");
ThemeManager.styleButton(btnNext, ThemeManager.ButtonVariant.DEFAULT);
btnNext.addActionListener(e -> stepHistory(1));
scrubSlider = new JSlider(1, 1, 1);
scrubSlider.setMajorTickSpacing(10);
scrubSlider.setMinorTickSpacing(1);
scrubSlider.setPaintTicks(true);
scrubSlider.setSnapToTicks(true);
scrubSlider.addChangeListener(e -> {
if (!scrubSlider.getValueIsAdjusting()) {
int selected = scrubSlider.getValue() - 1;
displayScreen(selected);
}
});
sliderPanel.add(btnPrev, BorderLayout.WEST);
sliderPanel.add(scrubSlider, BorderLayout.CENTER);
sliderPanel.add(btnNext, BorderLayout.EAST);
infoLabel = new JLabel("Screen 0 of 0", SwingConstants.CENTER);
infoLabel.setFont(infoLabel.getFont().deriveFont(Font.BOLD, 13f));
topPanel.add(sliderPanel, BorderLayout.CENTER);
topPanel.add(infoLabel, BorderLayout.SOUTH);
contentPane.add(topPanel, BorderLayout.NORTH);
// Center Terminal Preview Pane
previewArea = new JTextArea();
previewArea.setEditable(false);
previewArea.setFont(new Font("Monospaced", Font.PLAIN, 14));
previewArea.setBackground(Color.BLACK);
previewArea.setForeground(new Color(0, 255, 65)); // 3270 Green Phosphor
previewArea.setCaretColor(Color.WHITE);
previewArea.setLineWrap(false);
previewArea.setMargin(new Insets(6, 6, 6, 6));
JScrollPane scrollPane = new JScrollPane(previewArea);
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60)));
contentPane.add(scrollPane, BorderLayout.CENTER);
// Bottom Action Bar
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
btnCopyScreen = new JButton("Copy Screen");
ThemeManager.styleButton(btnCopyScreen, ThemeManager.ButtonVariant.DEFAULT);
btnCopyScreen.setToolTipText("Copy entire historical screen to clipboard");
btnCopyScreen.addActionListener(e -> copyCurrentScreen());
btnCopySelection = new JButton("Copy Selection");
ThemeManager.styleButton(btnCopySelection, ThemeManager.ButtonVariant.DEFAULT);
btnCopySelection.setToolTipText("Copy selected text to clipboard");
btnCopySelection.addActionListener(e -> copySelectedText());
btnCopyTable = new JButton("Copy as Table");
ThemeManager.styleButton(btnCopyTable, ThemeManager.ButtonVariant.DEFAULT);
btnCopyTable.setToolTipText("Copy screen text with trimmed trailing spaces per row");
btnCopyTable.addActionListener(e -> copyAsTable());
btnClose = new JButton("Close");
ThemeManager.styleButton(btnClose, ThemeManager.ButtonVariant.DEFAULT);
btnClose.addActionListener(e -> setVisible(false));
bottomPanel.add(btnCopyScreen);
bottomPanel.add(btnCopySelection);
bottomPanel.add(btnCopyTable);
bottomPanel.add(Box.createHorizontalStrut(12));
bottomPanel.add(btnClose);
contentPane.add(bottomPanel, BorderLayout.SOUTH);
}
private void setupKeyNavigation() {
// Keyboard shortcuts for rapid scrub navigation
Action prevAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { stepHistory(-1); }
};
Action nextAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { stepHistory(1); }
};
Action homeAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { displayScreen(0); }
};
Action endAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) {
if (historyBuffer != null) displayScreen(historyBuffer.size() - 1);
}
};
Action closeAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { setVisible(false); }
};
JRootPane root = getRootPane();
InputMap im = root.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
ActionMap am = root.getActionMap();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.ALT_DOWN_MASK), "prevScreen");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), "prevScreen");
am.put("prevScreen", prevAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.ALT_DOWN_MASK), "nextScreen");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), "nextScreen");
am.put("nextScreen", nextAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, InputEvent.ALT_DOWN_MASK), "homeScreen");
am.put("homeScreen", homeAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, InputEvent.ALT_DOWN_MASK), "endScreen");
am.put("endScreen", endAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeDialog");
am.put("closeDialog", closeAction);
}
private void stepHistory(int delta) {
if (historyBuffer == null || historyBuffer.isEmpty()) return;
int target = currentIndex + delta;
target = Math.max(0, Math.min(target, historyBuffer.size() - 1));
displayScreen(target);
}
private void displayScreen(int index) {
if (historyBuffer == null || historyBuffer.isEmpty()) {
updateEmptyState();
return;
}
int total = historyBuffer.size();
index = Math.max(0, Math.min(index, total - 1));
currentIndex = index;
ScreenSnapshot snap = historyBuffer.get(index);
if (snap != null) {
previewArea.setText(snap.getText());
previewArea.setCaretPosition(0);
String timeStr = dateFormat.format(new Date(snap.getTimestamp()));
infoLabel.setText("Screen " + (index + 1) + " of " + total + " (" + timeStr + ") — "
+ snap.getRows() + "x" + snap.getCols() + ", Cursor: ["
+ (snap.getCursorRow() + 1) + "," + (snap.getCursorCol() + 1) + "]");
scrubSlider.setValue(index + 1);
}
btnPrev.setEnabled(index > 0);
btnNext.setEnabled(index < total - 1);
btnCopyScreen.setEnabled(true);
btnCopySelection.setEnabled(true);
btnCopyTable.setEnabled(true);
}
private void updateEmptyState() {
currentIndex = -1;
previewArea.setText("No screen history captured yet.\n\nScreens are automatically recorded as the host session receives updates or unlocks the keyboard.");
infoLabel.setText("Screen 0 of 0 — Empty History");
scrubSlider.setEnabled(false);
btnPrev.setEnabled(false);
btnNext.setEnabled(false);
btnCopyScreen.setEnabled(false);
btnCopySelection.setEnabled(false);
btnCopyTable.setEnabled(false);
}
/**
* Refresh slider bounds and select target index.
*/
public synchronized void refreshHistoryView(int targetIndex) {
if (historyBuffer == null || historyBuffer.isEmpty()) {
updateEmptyState();
return;
}
int total = historyBuffer.size();
scrubSlider.setEnabled(true);
scrubSlider.setMinimum(1);
scrubSlider.setMaximum(total);
int select = targetIndex >= 0 ? Math.min(targetIndex, total - 1) : total - 1;
displayScreen(select);
}
@Override
public void onHistoryUpdated(ScreenHistoryBuffer buffer, ScreenSnapshot newSnapshot) {
SwingUtilities.invokeLater(() -> {
boolean wasAtLatest = (currentIndex >= 0 && currentIndex == buffer.size() - 2);
int newTotal = buffer.size();
scrubSlider.setMaximum(newTotal);
if (wasAtLatest || currentIndex < 0) {
// Keep tracked with latest frame
displayScreen(newTotal - 1);
} else {
// Keep operator's active scrub position, update label
displayScreen(currentIndex);
}
});
}
private void copyCurrentScreen() {
if (historyBuffer == null || currentIndex < 0) return;
ScreenSnapshot snap = historyBuffer.get(currentIndex);
if (snap != null) {
setClipboardText(snap.getText());
}
}
private void copySelectedText() {
String selected = previewArea.getSelectedText();
if (selected != null && !selected.isEmpty()) {
setClipboardText(selected);
} else {
copyCurrentScreen();
}
}
private void copyAsTable() {
if (historyBuffer == null || currentIndex < 0) return;
ScreenSnapshot snap = historyBuffer.get(currentIndex);
if (snap != null) {
setClipboardText(snap.getFormattedTable());
}
}
private void setClipboardText(String text) {
try {
Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
cb.setContents(new StringSelection(text), null);
} catch (Exception e) {
Toolkit.getDefaultToolkit().beep();
}
}
}
@@ -15,7 +15,7 @@ import java.nio.charset.StandardCharsets;
*/
public class ScriptDialog extends JDialog {
private final Telnet3270Client client;
private Telnet3270Client client;
private final TerminalPanel terminalPanel;
private JTextArea scriptArea;
@@ -25,17 +25,36 @@ public class ScriptDialog extends JDialog {
super(parent, "Run Keystrokes / ECL Script", false);
this.client = client;
this.terminalPanel = terminalPanel;
AppIcon.applyTo(this);
buildUI();
setSize(600, 480);
setLocationRelativeTo(parent);
}
public void setClient(Telnet3270Client client) {
this.client = client;
}
public Telnet3270Client getClient() {
return this.client;
}
public void setScriptText(String text) {
if (scriptArea != null) {
scriptArea.setText(text);
}
}
public String getScriptText() {
return scriptArea != null ? scriptArea.getText() : "";
}
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);
Font labelFont = ThemeManager.getUiFont();
// Header
JPanel topPanel = new JPanel(new BorderLayout(5, 5));
@@ -48,7 +67,7 @@ public class ScriptDialog extends JDialog {
// Script area
scriptArea = new JTextArea();
scriptArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
scriptArea.setFont(ThemeManager.getMonospacedUiFont());
scriptArea.setLineWrap(true);
scriptArea.setWrapStyleWord(false);
ThemeManager.styleTextArea(scriptArea);
@@ -74,7 +93,7 @@ public class ScriptDialog extends JDialog {
for (String token : tokens) {
JButton btn = new JButton(token);
btn.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11));
btn.setFont(ThemeManager.getMonospacedUiFont(Font.PLAIN, -2));
ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.DEFAULT);
btn.setFocusable(false);
btn.setMargin(new Insets(2, 4, 2, 4));
@@ -94,7 +113,7 @@ public class ScriptDialog extends JDialog {
statusLabel = new JLabel("Ready");
statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
statusLabel.setFont(labelFont);
bottomPanel.add(statusLabel, BorderLayout.WEST);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
File diff suppressed because it is too large Load Diff
@@ -29,14 +29,15 @@ public class StatusBar extends JPanel {
private Telnet3270Client client;
private TerminalPanel terminalPanel;
private haus.nightmare.lib3270j.ecl.ECLOIANotify oiaNotifyListener;
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);
Font oiaFont = ThemeManager.getMonospacedUiFont();
updateBarHeight(oiaFont);
connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim());
tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
@@ -131,8 +132,15 @@ public class StatusBar extends JPanel {
}
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
if (this.client != null && this.client.getOIA() != null && this.oiaNotifyListener != null) {
this.client.getOIA().unregisterOIAEvent(this.oiaNotifyListener);
}
this.client = client;
this.terminalPanel = terminalPanel;
if (this.client != null && this.client.getOIA() != null) {
this.oiaNotifyListener = event -> updateStatus();
this.client.getOIA().registerOIAEvent(this.oiaNotifyListener);
}
updateStatus();
}
@@ -143,9 +151,36 @@ public class StatusBar extends JPanel {
}
setBackground(ThemeManager.getStatusBarBg(theme));
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme)));
Font oiaFont = ThemeManager.getMonospacedUiFont();
updateBarHeight(oiaFont);
applyFontRecursively(this, oiaFont);
revalidate();
repaint();
updateStatus();
}
private void updateBarHeight(Font font) {
int fontHeight = 16;
try {
FontMetrics fm = getFontMetrics(font);
if (fm != null) fontHeight = fm.getHeight();
} catch (Exception ignored) {
fontHeight = font.getSize() + 4;
}
int barHeight = Math.max(22, fontHeight + 6);
setPreferredSize(new Dimension(800, barHeight));
}
private void applyFontRecursively(Component comp, Font font) {
if (comp instanceof JLabel) {
comp.setFont(font);
} else if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
applyFontRecursively(child, font);
}
}
}
public void updateStatus() {
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(this::updateStatus);
@@ -322,9 +357,12 @@ public class StatusBar extends JPanel {
wordWrapStatus.setText("");
}
// Field status: Numeric vs Alphanumeric
// Field status: Numeric vs Alphanumeric vs DBCS
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
if (client.getOIA().isNumeric()) {
if (client.getOIA().isDBCS()) {
fieldTypeStatus.setText("DBCS");
fieldTypeStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
} else if (client.getOIA().isNumeric()) {
fieldTypeStatus.setText("NUM");
fieldTypeStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
} else {
@@ -361,4 +399,8 @@ public class StatusBar extends JPanel {
cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr));
cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme));
}
public String getLockStatusText() {
return lockStatus != null ? lockStatus.getText() : "";
}
}
File diff suppressed because it is too large Load Diff
@@ -20,6 +20,7 @@ import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import haus.nightmare.j3270.config.Settings;
/**
* Centralized theme manager for j3270 Java desktop UI.
@@ -33,6 +34,25 @@ public final class ThemeManager {
private ThemeManager() {}
public static Font getUiFont() {
return getUiFont(Font.PLAIN, 0);
}
public static Font getUiFont(int style, int sizeDelta) {
String family = Settings.getUiFontFamily();
int size = Math.max(8, Settings.getUiFontSize() + sizeDelta);
return new Font(family, style, size);
}
public static Font getMonospacedUiFont() {
return getMonospacedUiFont(Font.PLAIN, 0);
}
public static Font getMonospacedUiFont(int style, int sizeDelta) {
int size = Math.max(8, Settings.getUiFontSize() + sizeDelta);
return new Font(Font.MONOSPACED, style, size);
}
public static UITheme getTheme() {
return currentTheme;
}
@@ -433,6 +453,47 @@ public final class ThemeManager {
UIManager.put("TitledBorder.titleColor", fgMain);
UIManager.put("OptionPane.background", bgPanel);
UIManager.put("OptionPane.messageForeground", fgMain);
// UI Font Defaults
Font uiFont = getUiFont();
Font uiFontBold = getUiFont(Font.BOLD, 0);
Font monoUiFont = getMonospacedUiFont();
UIManager.put("Button.font", uiFont);
UIManager.put("ToggleButton.font", uiFont);
UIManager.put("RadioButton.font", uiFont);
UIManager.put("CheckBox.font", uiFont);
UIManager.put("ColorChooser.font", uiFont);
UIManager.put("ComboBox.font", uiFont);
UIManager.put("Label.font", uiFont);
UIManager.put("List.font", uiFont);
UIManager.put("MenuBar.font", uiFont);
UIManager.put("MenuItem.font", uiFont);
UIManager.put("RadioButtonMenuItem.font", uiFont);
UIManager.put("CheckBoxMenuItem.font", uiFont);
UIManager.put("Menu.font", uiFont);
UIManager.put("PopupMenu.font", uiFont);
UIManager.put("OptionPane.font", uiFont);
UIManager.put("OptionPane.messageFont", uiFont);
UIManager.put("OptionPane.buttonFont", uiFont);
UIManager.put("Panel.font", uiFont);
UIManager.put("ProgressBar.font", uiFont);
UIManager.put("ScrollPane.font", uiFont);
UIManager.put("Viewport.font", uiFont);
UIManager.put("TabbedPane.font", uiFont);
UIManager.put("Table.font", uiFont);
UIManager.put("TableHeader.font", uiFontBold);
UIManager.put("TextField.font", uiFont);
UIManager.put("PasswordField.font", uiFont);
UIManager.put("TextArea.font", monoUiFont);
UIManager.put("TextPane.font", uiFont);
UIManager.put("EditorPane.font", uiFont);
UIManager.put("TitledBorder.font", uiFontBold);
UIManager.put("ToolBar.font", uiFont);
UIManager.put("ToolTip.font", uiFont);
UIManager.put("Tree.font", uiFont);
UIManager.put("Spinner.font", uiFont);
UIManager.put("FormattedTextField.font", uiFont);
}
// =========================================================================
@@ -446,6 +507,7 @@ public final class ThemeManager {
public static JButton styleButton(JButton button, ButtonVariant variant) {
if (button == null) return null;
button.setUI(new StyledButtonUI(variant));
button.setFont(getUiFont());
button.setFocusPainted(false);
button.setOpaque(false);
button.setContentAreaFilled(false);
@@ -465,6 +527,7 @@ public final class ThemeManager {
public static JTextField styleTextField(JTextField field) {
if (field == null) return null;
field.setFont(getUiFont());
field.setBackground(getBgComponent());
field.setForeground(getFgMain());
field.setCaretColor(getFgMain());
@@ -478,6 +541,7 @@ public final class ThemeManager {
public static JTextArea styleTextArea(JTextArea area) {
if (area == null) return null;
area.setFont(getMonospacedUiFont());
area.setBackground(getCodeAreaBg());
area.setForeground(getCodeAreaFg());
area.setCaretColor(getFgMain());
@@ -488,6 +552,7 @@ public final class ThemeManager {
public static JComboBox<?> styleComboBox(JComboBox<?> box) {
if (box == null) return null;
box.setFont(getUiFont());
box.setBackground(getBgComponent());
box.setForeground(getFgMain());
box.setRenderer(new DefaultListCellRenderer() {
@@ -510,12 +575,14 @@ public final class ThemeManager {
public static JTable styleTable(JTable table) {
if (table == null) return null;
table.setFont(getUiFont());
table.setBackground(getBgComponent());
table.setForeground(getFgMain());
table.setSelectionBackground(getSelectionBg());
table.setSelectionForeground(getSelectionFg());
table.setGridColor(getTableGrid());
table.setRowHeight(24);
int rowHeight = Math.max(24, getUiFont().getSize() + 10);
table.setRowHeight(rowHeight);
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
@Override
@@ -537,14 +604,14 @@ public final class ThemeManager {
if (header != null) {
header.setBackground(getTableHeaderBg());
header.setForeground(getTableHeaderFg());
header.setFont(header.getFont().deriveFont(Font.BOLD));
header.setFont(getUiFont(Font.BOLD, 0));
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));
setFont(getUiFont(Font.BOLD, 0));
setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 1, 1, getBorderColor()),
new EmptyBorder(4, 6, 4, 6)));
@@ -558,6 +625,7 @@ public final class ThemeManager {
public static JTabbedPane styleTabbedPane(JTabbedPane tp) {
if (tp == null) return null;
tp.setUI(new StyledTabbedPaneUI());
tp.setFont(getUiFont());
tp.setBackground(getBgMain());
tp.setForeground(getFgMain());
return tp;
@@ -566,6 +634,7 @@ public final class ThemeManager {
public static JMenuBar styleMenuBar(JMenuBar bar) {
if (bar == null) return null;
bar.setUI(new StyledMenuBarUI());
bar.setFont(getUiFont());
bar.setBackground(getMenuBarBg());
bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, getBorder()));
return bar;
@@ -574,6 +643,7 @@ public final class ThemeManager {
public static JMenu styleMenu(JMenu menu) {
if (menu == null) return null;
menu.setUI(new StyledMenuUI());
menu.setFont(getUiFont());
menu.setForeground(getMenuBarFg());
menu.setBackground(getMenuBarBg());
menu.setOpaque(false);
@@ -583,6 +653,7 @@ public final class ThemeManager {
public static JMenuItem styleMenuItem(JMenuItem item) {
if (item == null) return null;
item.setUI(new StyledMenuItemUI());
item.setFont(getUiFont());
item.setBackground(getMenuPopupBg());
item.setForeground(getMenuItemFg());
return item;
@@ -591,6 +662,7 @@ public final class ThemeManager {
public static JPopupMenu stylePopupMenu(JPopupMenu popup) {
if (popup == null) return null;
popup.setUI(new StyledPopupMenuUI());
popup.setFont(getUiFont());
popup.setBackground(getMenuPopupBg());
popup.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(getBorder(), 1),
@@ -610,12 +682,14 @@ public final class ThemeManager {
public static JSpinner styleSpinner(JSpinner sp) {
if (sp == null) return null;
sp.setFont(getUiFont());
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.setFont(getUiFont());
tf.setBackground(getBgComponent());
tf.setForeground(getFgMain());
tf.setCaretColor(getFgMain());
@@ -626,6 +700,7 @@ public final class ThemeManager {
public static JCheckBox styleCheckBox(JCheckBox cb) {
if (cb == null) return null;
cb.setFont(getUiFont());
cb.setOpaque(false);
cb.setForeground(getFgMain());
cb.setFocusPainted(false);
@@ -634,6 +709,7 @@ public final class ThemeManager {
public static JRadioButton styleRadioButton(JRadioButton rb) {
if (rb == null) return null;
rb.setFont(getUiFont());
rb.setOpaque(false);
rb.setForeground(getFgMain());
rb.setFocusPainted(false);
@@ -646,7 +722,7 @@ public final class ThemeManager {
title,
TitledBorder.DEFAULT_JUSTIFICATION,
TitledBorder.DEFAULT_POSITION,
new Font(Font.SANS_SERIF, Font.BOLD, 12),
getUiFont(Font.BOLD, 0),
getFgMain());
}
@@ -732,6 +808,7 @@ public final class ThemeManager {
if (comp instanceof JButton) {
JButton btn = (JButton) comp;
btn.setFont(getUiFont());
if (!(btn.getUI() instanceof StyledButtonUI)) {
styleButton(btn, ButtonVariant.DEFAULT);
}
@@ -770,6 +847,21 @@ public final class ThemeManager {
if (comp instanceof JLabel) {
comp.setForeground(getFgMain(theme));
JLabel label = (JLabel) comp;
Font cur = label.getFont();
int style = (cur != null && cur.isBold()) ? Font.BOLD : Font.PLAIN;
Object deltaProp = label.getClientProperty("uiFontSizeDelta");
int delta = 0;
if (deltaProp instanceof Integer) {
delta = (Integer) deltaProp;
} else {
int baseSize = Settings.getUiFontSize();
if (cur != null && cur.getSize() > baseSize) {
delta = cur.getSize() - baseSize;
}
label.putClientProperty("uiFontSizeDelta", delta);
}
label.setFont(getUiFont(style, delta));
return;
}
@@ -783,6 +875,7 @@ public final class ThemeManager {
if (b instanceof TitledBorder) {
TitledBorder tb = (TitledBorder) b;
tb.setTitleColor(getFgMain(theme));
tb.setTitleFont(getUiFont(Font.BOLD, 0));
tb.setBorder(new LineBorder(getBorder(theme), 1));
}
} else if (comp instanceof Container) {
@@ -17,6 +17,7 @@ public class UntrustedCertificateDialog extends JDialog {
public UntrustedCertificateDialog(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) {
super(parent, "Untrusted SSL/TLS Certificate", true);
AppIcon.applyTo(this);
buildUI(host, port, chain, exception);
pack();
setLocationRelativeTo(parent);
@@ -31,18 +32,20 @@ public class UntrustedCertificateDialog extends JDialog {
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
headerPanel.setOpaque(false);
JLabel iconLabel = new JLabel("⚠️");
iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28));
iconLabel.putClientProperty("uiFontSizeDelta", 15);
iconLabel.setFont(ThemeManager.getUiFont(Font.PLAIN, 15));
headerPanel.add(iconLabel);
JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2));
titleBox.setOpaque(false);
JLabel titleLabel = new JLabel("Untrusted SSL Certificate");
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
titleLabel.putClientProperty("uiFontSizeDelta", 3);
titleLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 3));
titleLabel.setForeground(ThemeManager.getOiaFgWarn());
titleBox.add(titleLabel);
JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified.");
subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
subtitleLabel.setFont(ThemeManager.getUiFont());
subtitleLabel.setForeground(ThemeManager.getFgMuted());
titleBox.add(subtitleLabel);
headerPanel.add(titleBox);
@@ -69,7 +72,7 @@ public class UntrustedCertificateDialog extends JDialog {
JTextArea detailsArea = new JTextArea(sb.toString());
detailsArea.setEditable(false);
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
detailsArea.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextArea(detailsArea);
detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
@@ -91,7 +94,7 @@ public class UntrustedCertificateDialog extends JDialog {
JButton trustBtn = new JButton("Connect Anyway");
ThemeManager.styleButton(trustBtn, ThemeManager.ButtonVariant.DANGER);
trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
trustBtn.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
trustBtn.addActionListener(e -> {
accepted = true;
dispose();
@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="124 738 320 320"
width="320"
height="320"
xml:space="preserve"
id="svg_j3270_icon"
version="1.1">
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<!-- Pure black background (#000000) for classic 3270 CRT terminal look -->
<rect x="124" y="738" width="320" height="320" fill="#000000" />
<g transform="matrix(1.3333333,0,0,-1.3333333,0,1122.6667)" id="g10">
<g transform="scale(0.1)" id="g12">
<g id="g14">
<!-- Green Terminal Screen Frame -->
<path
id="path28"
style="fill:none;stroke:#00ff00;stroke-width:3.75079012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
<!-- Blue Wave / Exponential Curve -->
<path
id="path30"
style="fill:none;stroke:#0000ff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1172.41,1137.58 79.1,-1.22 -0.21,-0.38 c 1.08,0 2.18,0 3.27,0 762.95,0 1464.86,417.88 1829.53,1089.21" />
<!-- Red Circular Arc -->
<path
id="path32"
style="fill:none;stroke:#ff0000;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2593.86,1620.73 -1.22,-28 -0.08,-0.08 c 0.43,-250.15 202.81,-452.72 452.3,-452.72 0.14,0 0.28,0 0.42,0 l 43.9,2.52" />
<!-- Yellow Smiley Head -->
<path
id="path34"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2976,1363.94 v 0.08 c 44.73,34.29 71.21,87.25 71.8,143.61 l 0.01,-0.15 c -0.06,97.48 -79.11,176.47 -176.59,176.47 -0.36,0 -0.73,0 -1.09,0 l -0.07,0.03 c -0.25,0 -0.51,0 -0.75,0 -37.38,0 -73.79,-11.94 -103.92,-34.08 l 0.02,-0.03 c -44.38,-34.04 -70.82,-86.47 -71.8,-142.39 l 0.06,0.04 c 0.54,-97.43 79.04,-176.47 176.46,-177.69 l 0.08,0.02 c 0.64,-0.01 1.29,-0.01 1.92,-0.01 37.4,0 73.82,11.94 103.96,34.08" />
<!-- 'l' -->
<path
id="path36"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<path
id="path38"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<!-- 'i' -->
<path
id="path40"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<path
id="path42"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<!-- 'b' -->
<path
id="path44"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<path
id="path46"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<!-- '3' -->
<path
id="path48"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<path
id="path50"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<!-- '2' -->
<path
id="path52"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<path
id="path54"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<!-- '7' -->
<path
id="path56"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<path
id="path58"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<!-- '0' -->
<path
id="path60"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<path
id="path62"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<!-- 'j' -->
<path
id="path64"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<path
id="path66"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<!-- Smile -->
<path
id="path68"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2778.85,1478.34 0.04,-0.02 c 21.03,-31.91 56.69,-51.12 94.91,-51.12 0.41,0 0.83,0 1.24,0.01 l -0.02,0.07 c 43.88,0.19 84.11,24.51 104.66,63.28" />
<!-- Left Eye (cyan) -->
<path
id="path70"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2820.22,1595.17 0.01,0.01 c -14.62,-1.91 -25.56,-14.37 -25.56,-29.11 0,-0.03 0,-0.07 0,-0.1 l -0.02,-0.01 c 0,-0.03 0,-0.07 0,-0.1 0,-16.09 13.04,-29.14 29.13,-29.14 0.43,0 0.87,0.01 1.29,0.03 h 3.67 0.01 c 14.61,1.91 25.55,14.37 25.55,29.11 0,0.03 0,0.07 0,0.1 l 0.02,0.01 c 0,0.04 0,0.08 0,0.12 0,16.07 -13.03,29.09 -29.09,29.09 -0.03,0 -0.08,0 -0.12,0 l -4.89,-0.01" />
<!-- Right Eye (cyan) -->
<path
id="path72"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2921.23,1569.61 v -3.65 l -0.01,-0.01 c 0,-0.27 -0.01,-0.54 -0.01,-0.81 0,-18.22 14.66,-33.05 32.87,-33.26 l 0.03,0.01 c 0.28,0 0.57,-0.01 0.84,-0.01 17.29,0 31.72,13.22 33.24,30.44 l -0.02,3.64 0.02,0.02 c 0,0.27 0.01,0.54 0.01,0.81 0,18.37 -14.9,33.28 -33.28,33.28 -0.26,0 -0.54,-0.01 -0.81,-0.02 l -0.02,-0.03 c -17.14,-0.18 -31.36,-13.34 -32.86,-30.42" />
<!-- Nose -->
<path
id="path74"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2892.03,1528.23 -29.21,-20.68" />
<path
id="path76"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2862.82,1507.55 h 32.86" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

+146
View File
@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="144 814 280 169"
width="280"
height="169"
xml:space="preserve"
id="svg_lib3270j_cropped"
version="1.1">
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<!-- Pure black background (#000000) for classic 3270 CRT terminal look -->
<rect x="144" y="814" width="280" height="169" fill="#000000" />
<g transform="matrix(1.3333333,0,0,-1.3333333,0,1122.6667)" id="g10">
<g transform="scale(0.1)" id="g12">
<g id="g14">
<!-- Green Terminal Screen Frame -->
<path
id="path28"
style="fill:none;stroke:#00ff00;stroke-width:3.75079012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
<!-- Blue Wave / Exponential Curve -->
<path
id="path30"
style="fill:none;stroke:#0000ff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1172.41,1137.58 79.1,-1.22 -0.21,-0.38 c 1.08,0 2.18,0 3.27,0 762.95,0 1464.86,417.88 1829.53,1089.21" />
<!-- Red Circular Arc -->
<path
id="path32"
style="fill:none;stroke:#ff0000;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2593.86,1620.73 -1.22,-28 -0.08,-0.08 c 0.43,-250.15 202.81,-452.72 452.3,-452.72 0.14,0 0.28,0 0.42,0 l 43.9,2.52" />
<!-- Yellow Smiley Head -->
<path
id="path34"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2976,1363.94 v 0.08 c 44.73,34.29 71.21,87.25 71.8,143.61 l 0.01,-0.15 c -0.06,97.48 -79.11,176.47 -176.59,176.47 -0.36,0 -0.73,0 -1.09,0 l -0.07,0.03 c -0.25,0 -0.51,0 -0.75,0 -37.38,0 -73.79,-11.94 -103.92,-34.08 l 0.02,-0.03 c -44.38,-34.04 -70.82,-86.47 -71.8,-142.39 l 0.06,0.04 c 0.54,-97.43 79.04,-176.47 176.46,-177.69 l 0.08,0.02 c 0.64,-0.01 1.29,-0.01 1.92,-0.01 37.4,0 73.82,11.94 103.96,34.08" />
<!-- 'l' -->
<path
id="path36"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<path
id="path38"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<!-- 'i' -->
<path
id="path40"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<path
id="path42"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<!-- 'b' -->
<path
id="path44"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<path
id="path46"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<!-- '3' -->
<path
id="path48"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<path
id="path50"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<!-- '2' -->
<path
id="path52"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<path
id="path54"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<!-- '7' -->
<path
id="path56"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<path
id="path58"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<!-- '0' -->
<path
id="path60"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<path
id="path62"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<!-- 'j' -->
<path
id="path64"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<path
id="path66"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<!-- Smile -->
<path
id="path68"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2778.85,1478.34 0.04,-0.02 c 21.03,-31.91 56.69,-51.12 94.91,-51.12 0.41,0 0.83,0 1.24,0.01 l -0.02,0.07 c 43.88,0.19 84.11,24.51 104.66,63.28" />
<!-- Left Eye (cyan) -->
<path
id="path70"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2820.22,1595.17 0.01,0.01 c -14.62,-1.91 -25.56,-14.37 -25.56,-29.11 0,-0.03 0,-0.07 0,-0.1 l -0.02,-0.01 c 0,-0.03 0,-0.07 0,-0.1 0,-16.09 13.04,-29.14 29.13,-29.14 0.43,0 0.87,0.01 1.29,0.03 h 3.67 0.01 c 14.61,1.91 25.55,14.37 25.55,29.11 0,0.03 0,0.07 0,0.1 l 0.02,0.01 c 0,0.04 0,0.08 0,0.12 0,16.07 -13.03,29.09 -29.09,29.09 -0.03,0 -0.08,0 -0.12,0 l -4.89,-0.01" />
<!-- Right Eye (cyan) -->
<path
id="path72"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2921.23,1569.61 v -3.65 l -0.01,-0.01 c 0,-0.27 -0.01,-0.54 -0.01,-0.81 0,-18.22 14.66,-33.05 32.87,-33.26 l 0.03,0.01 c 0.28,0 0.57,-0.01 0.84,-0.01 17.29,0 31.72,13.22 33.24,30.44 l -0.02,3.64 0.02,0.02 c 0,0.27 0.01,0.54 0.01,0.81 0,18.37 -14.9,33.28 -33.28,33.28 -0.26,0 -0.54,-0.01 -0.81,-0.02 l -0.02,-0.03 c -17.14,-0.18 -31.36,-13.34 -32.86,-30.42" />
<!-- Nose -->
<path
id="path74"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2892.03,1528.23 -29.21,-20.68" />
<path
id="path76"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2862.82,1507.55 h 32.86" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

@@ -0,0 +1,57 @@
package haus.nightmare.j3270;
import haus.nightmare.j3270.ui.AppIcon;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import java.net.URL;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class AppIconTest {
@Test
public void testIconsAreLoaded() {
List<Image> icons = AppIcon.getAppIcons();
assertNotNull(icons, "Icons list must not be null");
assertFalse(icons.isEmpty(), "At least one icon image must be loaded");
// Verify standard sizes are present
int[] expectedSizes = {16, 24, 32, 48, 64, 128, 256, 512};
for (int expectedSize : expectedSizes) {
boolean found = icons.stream().anyMatch(img -> img.getWidth(null) == expectedSize && img.getHeight(null) == expectedSize);
assertTrue(found, "Expected icon of size " + expectedSize + "x" + expectedSize + " was not found");
}
}
@Test
public void testDefaultIcon() {
Image defaultIcon = AppIcon.getDefaultIcon();
assertNotNull(defaultIcon, "Default icon must not be null");
assertEquals(256, defaultIcon.getWidth(null), "Default icon width should be 256");
assertEquals(256, defaultIcon.getHeight(null), "Default icon height should be 256");
}
@Test
public void testApplyToInHeadless() {
// Must execute without exception even in headless environment
assertDoesNotThrow(() -> AppIcon.applyTo(null));
if (!GraphicsEnvironment.isHeadless()) {
JFrame frame = new JFrame();
assertDoesNotThrow(() -> AppIcon.applyTo(frame));
frame.dispose();
}
}
@Test
public void testSvgResourcesExist() {
URL svgUrl = getClass().getResource("/icons/lib3270j.svg");
assertNotNull(svgUrl, "lib3270j.svg must exist in /icons/");
URL iconSvgUrl = getClass().getResource("/icons/j3270_icon.svg");
assertNotNull(iconSvgUrl, "j3270_icon.svg must exist in /icons/");
}
}
@@ -0,0 +1,328 @@
package haus.nightmare.j3270;
import haus.nightmare.j3270.ui.ConnectChoiceDialog;
import haus.nightmare.j3270.ui.ThemeManager;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class ConnectionConflictTest {
private final List<J3270App> createdApps = new ArrayList<>();
@BeforeEach
public void setUp() {
for (J3270App app : new ArrayList<>(J3270App.getOpenWindows())) {
try {
app.dispose();
} catch (Exception ignored) {}
}
}
@AfterEach
public void tearDown() {
for (J3270App app : createdApps) {
try {
app.dispose();
} catch (Exception ignored) {}
}
for (J3270App app : new ArrayList<>(J3270App.getOpenWindows())) {
try {
app.dispose();
} catch (Exception ignored) {}
}
}
private J3270App createApp() {
try {
J3270App app = new J3270App();
createdApps.add(app);
return app;
} catch (HeadlessException e) {
return null;
}
}
@Test
@DisplayName("ConnectChoiceDialog components, styling, and action results")
public void testConnectChoiceDialogComponents() {
try {
ConnectChoiceDialog dialog = new ConnectChoiceDialog(null, "mvs.corp.local:23");
assertEquals("Active Connection", dialog.getTitle());
assertNotNull(dialog.getNewWindowButton());
assertNotNull(dialog.getAbortButton());
assertNotNull(dialog.getCancelButton());
assertEquals("New Window", dialog.getNewWindowButton().getText());
assertEquals("Abort Current Connection", dialog.getAbortButton().getText());
assertEquals("Cancel", dialog.getCancelButton().getText());
assertTrue(dialog.getLine1().getText().contains("mvs.corp.local:23"));
assertTrue(dialog.getLine2().getText().contains("new window"));
assertTrue(dialog.getLine2().getText().contains("abort"));
// Test button click choices
dialog.getNewWindowButton().doClick();
assertEquals(ConnectChoiceDialog.Choice.NEW_WINDOW, dialog.getChoice());
ConnectChoiceDialog dialog2 = new ConnectChoiceDialog(null, "cms.test:992");
dialog2.getAbortButton().doClick();
assertEquals(ConnectChoiceDialog.Choice.ABORT_CURRENT, dialog2.getChoice());
ConnectChoiceDialog dialog3 = new ConnectChoiceDialog(null, "cics.test:23");
dialog3.getCancelButton().doClick();
assertEquals(ConnectChoiceDialog.Choice.CANCEL, dialog3.getChoice());
} catch (HeadlessException ignored) {
// Expected in headless CI environments without X11
}
}
@Test
@DisplayName("When connected, Connect with NEW_WINDOW choice opens in a new window and preserves current connection")
public void testConnectChoiceNewWindow() {
J3270App app = createApp();
if (app == null) return;
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
config1.setSavedProfileName("Host 1");
app.connect(config1, false);
assertTrue(app.getConnectedTarget().contains("host1.test") || app.getConnectedTarget().contains("Host 1"));
// Configure handler to return NEW_WINDOW
boolean[] handlerCalled = new boolean[]{false};
app.setConnectChoiceHandler((owner, target) -> {
handlerCalled[0] = true;
assertTrue(target.contains("host1.test") || target.contains("Host 1"));
return ConnectChoiceDialog.Choice.NEW_WINDOW;
});
int initialWindows = J3270App.getOpenWindows().size();
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
config2.setSavedProfileName("Host 2");
app.connect(config2, true);
// Handler was invoked
assertTrue(handlerCalled[0], "Connect choice handler must be called when already connected");
// A new window was opened
assertEquals(initialWindows + 1, J3270App.getOpenWindows().size(),
"A new window should have been added to openWindows");
// Original app remains on config1
assertEquals("host1.test", app.getClient().getConfig().getHost());
// Find the new window
J3270App newWindow = null;
for (J3270App w : J3270App.getOpenWindows()) {
if (w != app) {
newWindow = w;
createdApps.add(w);
break;
}
}
assertNotNull(newWindow, "New window must exist");
assertNotNull(newWindow.getClient(), "New window should have a client");
assertEquals("host2.test", newWindow.getClient().getConfig().getHost(),
"New window must be connected to host2.test");
}
@Test
@DisplayName("When connected, Connect with ABORT_CURRENT choice aborts existing connection and connects in same window")
public void testConnectChoiceAbortCurrent() {
J3270App app = createApp();
if (app == null) return;
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
app.connect(config1, false);
int initialWindows = J3270App.getOpenWindows().size();
boolean[] handlerCalled = new boolean[]{false};
app.setConnectChoiceHandler((owner, target) -> {
handlerCalled[0] = true;
return ConnectChoiceDialog.Choice.ABORT_CURRENT;
});
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
app.connect(config2, true);
assertTrue(handlerCalled[0], "Connect choice handler must be called");
assertEquals(initialWindows, J3270App.getOpenWindows().size(),
"No new window should be created when choosing ABORT_CURRENT");
assertEquals("host2.test", app.getClient().getConfig().getHost(),
"Same window should now be connected to host2.test");
}
@Test
@DisplayName("When connected, Connect with CANCEL choice keeps existing connection and creates no new window")
public void testConnectChoiceCancel() {
J3270App app = createApp();
if (app == null) return;
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
app.connect(config1, false);
int initialWindows = J3270App.getOpenWindows().size();
boolean[] handlerCalled = new boolean[]{false};
app.setConnectChoiceHandler((owner, target) -> {
handlerCalled[0] = true;
return ConnectChoiceDialog.Choice.CANCEL;
});
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
app.connect(config2, true);
assertTrue(handlerCalled[0], "Connect choice handler must be called");
assertEquals(initialWindows, J3270App.getOpenWindows().size(),
"No new window should be created when choosing CANCEL");
assertEquals("host1.test", app.getClient().getConfig().getHost(),
"Existing window must remain connected to host1.test");
}
@Test
@DisplayName("File menu contains Close Window item with VK_W accelerator")
public void testCloseWindowMenuItem() {
J3270App app = createApp();
if (app == null) return;
JMenuBar mb = app.getJMenuBar();
assertNotNull(mb);
JMenu fileMenu = null;
for (int i = 0; i < mb.getMenuCount(); i++) {
if ("File".equals(mb.getMenu(i).getText())) {
fileMenu = mb.getMenu(i);
break;
}
}
assertNotNull(fileMenu, "File menu must exist");
JMenuItem closeWindowItem = null;
for (int i = 0; i < fileMenu.getItemCount(); i++) {
JMenuItem item = fileMenu.getItem(i);
if (item != null && "Close Window".equals(item.getText())) {
closeWindowItem = item;
break;
}
}
assertNotNull(closeWindowItem, "File menu must contain Close Window item");
KeyStroke ks = closeWindowItem.getAccelerator();
assertNotNull(ks, "Close Window item must have an accelerator");
assertEquals(KeyEvent.VK_W, ks.getKeyCode());
}
@Test
@DisplayName("Closing one window when multiple windows are open leaves remaining window open")
public void testMultiWindowCloseIndependent() {
J3270App app1 = createApp();
if (app1 == null) return;
J3270App app2 = createApp();
if (app2 == null) return;
assertTrue(J3270App.getOpenWindows().contains(app1));
assertTrue(J3270App.getOpenWindows().contains(app2));
assertEquals(2, J3270App.getOpenWindows().size());
// Close app1
app1.closeWindow();
assertFalse(J3270App.getOpenWindows().contains(app1), "app1 should be removed from open windows");
assertTrue(J3270App.getOpenWindows().contains(app2), "app2 should remain open");
assertEquals(1, J3270App.getOpenWindows().size());
}
@Test
@DisplayName("Abort current connection cleanly tears down old client and connects new client in same window")
public void testAbortCurrentConnectionGracefullyDisconnects() throws Exception {
J3270App app = createApp();
if (app == null) return;
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
app.connect(config1, false);
Telnet3270Client oldClient = app.getClient();
assertNotNull(oldClient);
app.setConnectChoiceHandler((owner, target) -> ConnectChoiceDialog.Choice.ABORT_CURRENT);
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
app.connect(config2, true);
// Client on app is immediately replaced with new client
Telnet3270Client newClient = app.getClient();
assertNotNull(newClient);
assertNotSame(oldClient, newClient);
assertEquals("host2.test", newClient.getConfig().getHost());
// Old client should be disconnected
Thread.sleep(100);
assertEquals(haus.nightmare.lib3270j.ConnectionState.NOT_CONNECTED, oldClient.getConnectionState());
assertFalse(oldClient.isConnected());
}
@Test
@DisplayName("Explicit app.disconnect() tears down client and clears UI state")
public void testExplicitDisconnectResetsUI() throws Exception {
J3270App app = createApp();
if (app == null) return;
ConnectionConfig config = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
app.connect(config, false);
Telnet3270Client client = app.getClient();
assertNotNull(client);
app.disconnect();
assertNull(app.getClient());
assertFalse(app.isConnected());
Thread.sleep(100);
assertEquals(haus.nightmare.lib3270j.ConnectionState.NOT_CONNECTED, client.getConnectionState());
assertFalse(client.isConnected());
}
@Test
@DisplayName("When not connected, connect does not trigger abort connection dialog for previously closed connection")
public void testNotConnectedDoesNotTriggerAbortDialog() throws Exception {
J3270App app = createApp();
if (app == null) return;
// Simulate a previously closed connection
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
app.connect(config1, false);
Telnet3270Client client = app.getClient();
assertNotNull(client);
// Force the client's FSM state to NOT_CONNECTED, simulating connection closure
client.getTelnetFSM().setConnectionState(haus.nightmare.lib3270j.ConnectionState.NOT_CONNECTED);
assertFalse(app.isConnected(), "app.isConnected() must return false when client is NOT_CONNECTED");
boolean[] handlerCalled = new boolean[]{false};
app.setConnectChoiceHandler((owner, target) -> {
handlerCalled[0] = true;
return ConnectChoiceDialog.Choice.CANCEL;
});
// Initiate a new connection
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
app.connect(config2, true);
assertFalse(handlerCalled[0], "Connect choice handler must NOT be called when j3270 is not connected");
assertEquals("host2.test", app.getClient().getConfig().getHost(), "Must proceed to connect to host2.test in same window");
}
}
@@ -0,0 +1,94 @@
package haus.nightmare.j3270;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.junit.jupiter.api.Assertions.*;
public class LoggingConfigurationTest {
private PrintStream originalOut;
private PrintStream originalErr;
@BeforeEach
public void setUp() {
originalOut = System.out;
originalErr = System.err;
}
@AfterEach
public void tearDown() {
System.setOut(originalOut);
System.setErr(originalErr);
// Ensure all handlers are closed and logging reset
J3270App.configureLogging(false);
}
@Test
public void testLoggingDisabledWhenDebugFalse() {
File logFile = new File("j3270.log");
if (logFile.exists()) {
logFile.delete();
}
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
ByteArrayOutputStream errContent = new ByteArrayOutputStream();
System.setOut(new PrintStream(outContent));
System.setErr(new PrintStream(errContent));
J3270App.configureLogging(false);
// Root logger should have no handlers attached
Logger rootLogger = Logger.getLogger("");
assertEquals(0, rootLogger.getHandlers().length, "Root logger should have no handlers when debug is disabled");
assertEquals(Level.OFF, rootLogger.getLevel(), "Root logger level should be OFF when debug is disabled");
// Application loggers should be OFF
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare").getLevel());
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare.j3270").getLevel());
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare.lib3270j").getLevel());
// Emit log records at all levels
Logger appLogger = Logger.getLogger("haus.nightmare.j3270.J3270App");
appLogger.severe("Test SEVERE message");
appLogger.warning("Test WARNING message");
appLogger.info("Test INFO message");
appLogger.fine("Test FINE message");
// Verify nothing was written to stdout or stderr
assertEquals(0, outContent.size(), "Standard output should be empty when debug is disabled");
assertEquals(0, errContent.size(), "Standard error should be empty when debug is disabled");
// Verify j3270.log was NOT created
assertFalse(logFile.exists(), "j3270.log should NOT be created when debug is disabled");
}
@Test
public void testLoggingEnabledWhenDebugTrue() {
J3270App.configureLogging(true);
Logger rootLogger = Logger.getLogger("");
Handler[] handlers = rootLogger.getHandlers();
assertTrue(handlers.length >= 2, "Root logger should have at least ConsoleHandler and FileHandler when debug is enabled");
assertEquals(Level.ALL, rootLogger.getLevel());
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare").getLevel());
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.j3270").getLevel());
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.lib3270j").getLevel());
File logFile = new File("j3270.log");
File logFile1 = new File("j3270.log.1");
assertTrue(logFile.exists() || logFile1.exists(), "j3270.log should be created when debug is enabled");
// Clean up
J3270App.configureLogging(false);
}
}
@@ -0,0 +1,189 @@
package haus.nightmare.j3270;
import haus.nightmare.j3270.ui.TerminalPanel;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.nvt.NvtProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.telnet.TelnetFSM;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
/**
* Verification test suite for Phase 27:
* Build Hygiene, Release Versioning & Zero-Risk Pruning (v1.2.4)
*/
public class Phase27BuildAndRenderTest {
@Test
@DisplayName("Phase 27.1 & 27.2: Release version 1.2.4 synchronized across build files and android.useAndroidX removed")
public void testVersionSynchronizationAndGradleProperties() throws Exception {
File projectRoot = new File(".").getCanonicalFile();
if (!new File(projectRoot, "settings.gradle").exists() && projectRoot.getParentFile() != null) {
projectRoot = projectRoot.getParentFile();
}
// 1. Verify build.gradle version = '1.2.4'
File buildGradle = new File(projectRoot, "build.gradle");
assertTrue(buildGradle.exists(), "build.gradle must exist");
String buildGradleContent = Files.readString(buildGradle.toPath());
assertTrue(buildGradleContent.contains("version = '1.2.4'"), "build.gradle must define version = '1.2.4'");
assertFalse(buildGradleContent.contains("0.1.0"), "build.gradle must not contain 0.1.0");
// 3. Verify setup.ps1 Implementation-Version: 1.2.4
File setupPs1 = new File(projectRoot, "setup.ps1");
assertTrue(setupPs1.exists(), "setup.ps1 must exist");
String setupPs1Content = Files.readString(setupPs1.toPath());
assertTrue(setupPs1Content.contains("Implementation-Version: 1.2.4"), "setup.ps1 must specify Implementation-Version: 1.2.4");
assertFalse(setupPs1Content.contains("Implementation-Version: 0.1.0"), "setup.ps1 must not contain 0.1.0");
// 4. Verify setup.sh Implementation-Version: 1.2.4
File setupSh = new File(projectRoot, "setup.sh");
assertTrue(setupSh.exists(), "setup.sh must exist");
String setupShContent = Files.readString(setupSh.toPath());
assertTrue(setupShContent.contains("Implementation-Version: 1.2.4"), "setup.sh must specify Implementation-Version: 1.2.4");
assertFalse(setupShContent.contains("Implementation-Version: 0.1.0"), "setup.sh must not contain 0.1.0");
// 5. Verify gradle.properties android.useAndroidX removed
File gradleProps = new File(projectRoot, "gradle.properties");
assertTrue(gradleProps.exists(), "gradle.properties must exist");
String gradlePropsContent = Files.readString(gradleProps.toPath());
assertFalse(gradlePropsContent.contains("android.useAndroidX"), "gradle.properties must not contain android.useAndroidX");
}
@Test
@DisplayName("Phase 27.3: T1-B Static Color & BasicStroke constants promoted in TerminalPanel")
public void testHotspotHoverStaticConstants() throws Exception {
Field colorField = TerminalPanel.class.getDeclaredField("HOTSPOT_HOVER_COLOR");
colorField.setAccessible(true);
assertTrue(Modifier.isStatic(colorField.getModifiers()), "HOTSPOT_HOVER_COLOR must be static");
assertTrue(Modifier.isFinal(colorField.getModifiers()), "HOTSPOT_HOVER_COLOR must be final");
Color hoverColor = (Color) colorField.get(null);
assertEquals(new Color(0, 220, 255), hoverColor, "HOTSPOT_HOVER_COLOR must match bright cyan (0, 220, 255)");
Field strokeField = TerminalPanel.class.getDeclaredField("HOTSPOT_HOVER_STROKE");
strokeField.setAccessible(true);
assertTrue(Modifier.isStatic(strokeField.getModifiers()), "HOTSPOT_HOVER_STROKE must be static");
assertTrue(Modifier.isFinal(strokeField.getModifiers()), "HOTSPOT_HOVER_STROKE must be final");
Stroke hoverStroke = (Stroke) strokeField.get(null);
assertNotNull(hoverStroke, "HOTSPOT_HOVER_STROKE must not be null");
assertTrue(hoverStroke instanceof BasicStroke, "HOTSPOT_HOVER_STROKE must be BasicStroke");
assertEquals(2.0f, ((BasicStroke) hoverStroke).getLineWidth(), 0.001f, "HOTSPOT_HOVER_STROKE line width must be 2.0f");
}
@Test
@DisplayName("Phase 27.4: T1-A charScratch Buffer exists and non-ASCII render renders without exceptions")
public void testCharScratchAndNonAsciiRendering() throws Exception {
Field scratchField = TerminalPanel.class.getDeclaredField("charScratch");
scratchField.setAccessible(true);
assertFalse(Modifier.isStatic(scratchField.getModifiers()), "charScratch should be an instance field confined to EDT");
assertTrue(Modifier.isFinal(scratchField.getModifiers()), "charScratch should be final");
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
char[] scratch = (char[]) scratchField.get(panel);
assertNotNull(scratch, "charScratch must not be null");
assertEquals(1, scratch.length, "charScratch must be length 1");
// Set up client with non-ASCII characters (e.g. U+2500 box drawing, U+00E9 acute e, U+2550 double box)
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Put non-ASCII characters on screen
sb.getCell(0).ucs4 = 0x2500; // Box drawing horizontal line
sb.getCell(1).ucs4 = 0x00E9; // e with acute
sb.getCell(2).ucs4 = 0x2550; // Box drawing double horizontal
sb.getCell(3).ucs4 = 0x2308; // APL left ceiling
sb.updateDisplaySnapshot();
panel.setClient(client);
BufferedImage img = new BufferedImage(800, 600, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = img.createGraphics();
assertDoesNotThrow(() -> panel.paint(g2), "paint must render non-ASCII cells via drawChars without error");
g2.dispose();
// Check that charScratch was used
assertTrue(scratch[0] > 0, "charScratch should have held the last drawn non-ASCII character");
}
@Test
@DisplayName("Phase 27.5: T1-C Reusable NVT Single-Byte Buffer dispatches without allocations")
public void testReusableNvtSingleByteBuffer() throws Exception {
Field scratchField = TelnetFSM.class.getDeclaredField("nvtSingleByteScratch");
scratchField.setAccessible(true);
assertFalse(Modifier.isStatic(scratchField.getModifiers()), "nvtSingleByteScratch must be an instance field");
assertTrue(Modifier.isFinal(scratchField.getModifiers()), "nvtSingleByteScratch must be final");
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
TelnetFSM fsm = client.getTelnetFSM();
byte[] expectedScratch = (byte[]) scratchField.get(fsm);
assertNotNull(expectedScratch, "nvtSingleByteScratch must not be null");
assertEquals(1, expectedScratch.length, "nvtSingleByteScratch must have length 1");
fsm.setConnectionState(ConnectionState.CONNECTED_NVT);
AtomicBoolean dispatched = new AtomicBoolean(false);
AtomicReference<byte[]> receivedBuffer = new AtomicReference<>();
NvtProcessor customNvtProcessor = new NvtProcessor(client.getScreenBuffer(), new EbcdicTranslator()) {
@Override
public void processNVTData(byte[] buffer, int offset, int length) {
dispatched.set(true);
receivedBuffer.set(buffer);
assertEquals(0, offset);
assertEquals(1, length);
assertEquals((byte) 'Z', buffer[offset]);
super.processNVTData(buffer, offset, length);
}
};
Field nvtProcField = TelnetFSM.class.getDeclaredField("nvtProcessor");
nvtProcField.setAccessible(true);
nvtProcField.set(fsm, customNvtProcessor);
// Feed single byte 'Z' into FSM
fsm.feedByte('Z');
assertTrue(dispatched.get(), "NvtProcessor.processNVTData should have been called");
assertSame(expectedScratch, receivedBuffer.get(), "Buffer passed to processNVTData must be the reusable nvtSingleByteScratch array instance");
}
@Test
@DisplayName("Phase 27.6: J3270App has Settings import and no in-line FQCN occurrences")
public void testJ3270AppSettingsImport() throws Exception {
File projectRoot = new File(".").getCanonicalFile();
if (!new File(projectRoot, "j3270").exists() && projectRoot.getParentFile() != null) {
projectRoot = projectRoot.getParentFile();
}
File appFile = new File(projectRoot, "j3270/src/main/java/haus/nightmare/j3270/J3270App.java");
assertTrue(appFile.exists(), "J3270App.java must exist");
String content = Files.readString(appFile.toPath());
assertTrue(content.contains("import haus.nightmare.j3270.config.Settings;"),
"J3270App.java must import haus.nightmare.j3270.config.Settings");
// The only occurrence of haus.nightmare.j3270.config.Settings should be in the import
int firstIndex = content.indexOf("haus.nightmare.j3270.config.Settings");
assertTrue(firstIndex >= 0, "Must contain import");
int secondIndex = content.indexOf("haus.nightmare.j3270.config.Settings", firstIndex + 1);
assertEquals(-1, secondIndex, "There should be no inline occurrences of haus.nightmare.j3270.config.Settings in J3270App");
}
}
@@ -0,0 +1,341 @@
package haus.nightmare.j3270;
import haus.nightmare.j3270.ui.FieldInspectorDialog;
import haus.nightmare.j3270.ui.FindDialog;
import haus.nightmare.j3270.ui.HotspotEngine;
import haus.nightmare.j3270.ui.ScriptDialog;
import haus.nightmare.j3270.ui.TerminalPanel;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* Verification test suite for Phase 28:
* UI Responsiveness, Dialog Lifecycle & Event Optimization
*/
public class Phase28UiOptimizationTest {
private void setField(Object obj, String fieldName, Object val) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, val);
}
private void writeString(ScreenBuffer sb, int row, int col, String text) {
int cols = sb.getDisplayCols();
int base = row * cols + col;
for (int i = 0; i < text.length(); i++) {
sb.getCell(base + i).ucs4 = text.charAt(i);
}
sb.updateDisplaySnapshot();
}
@Test
@DisplayName("Phase 28.1: T2-A Hotspot Dirty Flagging & Lazy Text Scraping eliminates redundant scans on mouseMoved")
public void testRapidMouseMoveEventsScanOnlyOnce() throws Exception {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Put hotspots on screen: PF keys and URL
writeString(sb, 0, 0, "APPLICATION SELECTION MENU");
writeString(sb, 2, 0, "PF1=Help PF3=Exit PF12=Cancel");
writeString(sb, 4, 0, "Visit http://example.com/3270 for assistance");
sb.updateDisplaySnapshot();
panel.setClient(client);
HotspotEngine engine = panel.getHotspotEngine();
engine.setUrlEnabled(true);
engine.setPfKeyEnabled(true);
engine.setMenuChoiceEnabled(true);
assertTrue(panel.isHotspotsDirty(), "Hotspots should initially be dirty on panel setup");
long initialScanCount = engine.getScanCount();
MouseMotionListener[] motionListeners = panel.getMouseMotionListeners();
assertTrue(motionListeners.length > 0, "TerminalPanel must have at least one MouseMotionListener");
MouseMotionListener mml = motionListeners[0];
// Simulate 1,000 rapid mouse move events across the terminal grid
for (int i = 0; i < 1000; i++) {
int x = 20 + (i % 200);
int y = 30 + (i % 150);
MouseEvent me = new MouseEvent(panel, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, x, y, 0, false);
mml.mouseMoved(me);
}
// After 1,000 moves, hotspotEngine.scan() should only have executed ONCE
assertEquals(initialScanCount + 1, engine.getScanCount(),
"hotspotEngine.scan() should execute exactly once across 1,000 rapid mouse move events on a clean screen");
assertFalse(panel.isHotspotsDirty(), "Hotspots must be marked clean after scanning");
// Verify hotspots were actually discovered
List<HotspotEngine.Hotspot> hotspots = engine.getHotspots();
assertFalse(hotspots.isEmpty(), "Hotspots on screen must be found");
assertNotNull(engine.findHotspot(2, 0), "Hotspot at row 2 col 0 (PF1) must be found");
// Direct calls to HotspotEngine.scan(sb) without snapshot version changes must also bypass StringBuilder
long beforeDirectCalls = engine.getScanCount();
for (int i = 0; i < 1000; i++) {
List<HotspotEngine.Hotspot> res = engine.scan(sb);
assertFalse(res.isEmpty());
}
assertEquals(beforeDirectCalls, engine.getScanCount(),
"Direct calls to HotspotEngine.scan(sb) without snapshot version change must return cachedHotspots");
// Now simulate screen update
writeString(sb, 6, 0, "NEW UPDATE: PF7=Up PF8=Down");
panel.onScreenUpdated();
assertTrue(panel.isHotspotsDirty(), "onScreenUpdated() must flag hotspotsDirty = true");
// Simulate another burst of 1,000 rapid mouse moves
for (int i = 0; i < 1000; i++) {
int x = 30 + (i % 200);
int y = 40 + (i % 150);
MouseEvent me = new MouseEvent(panel, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, x, y, 0, false);
mml.mouseMoved(me);
}
assertEquals(initialScanCount + 2, engine.getScanCount(),
"After screen update, exactly one more scan should run across 1,000 mouse moves");
assertFalse(panel.isHotspotsDirty(), "Hotspots must be marked clean again after rescan");
}
@Test
@DisplayName("Phase 28.2: T2-B Precomputed Selection Scratch Lookup matches isCellSelected exactly across all modes")
public void testSelectionBoundsMatchingPrecomputedScratchVsLegacy() throws Exception {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
panel.setClient(client);
int rows = 24;
int cols = 80;
// Configuration 1: No selection
panel.clearSelection();
panel.precomputeSelectionScratch(rows, cols);
boolean[] scratch = panel.getSelectedCellsScratch();
assertEquals(rows * cols, scratch.length);
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int baddr = r * cols + c;
assertFalse(scratch[baddr], "No cell should be selected");
assertEquals(panel.isCellSelected(r, c), scratch[baddr]);
}
}
// Configuration 2: Linear / Stream mode forward selection (spanning multiple rows)
setField(panel, "blockSelectMode", false);
setField(panel, "trimRectSizingHandles", false);
setField(panel, "selectionStartRow", 2);
setField(panel, "selectionStartCol", 15);
setField(panel, "selectionEndRow", 5);
setField(panel, "selectionEndCol", 42);
panel.precomputeSelectionScratch(rows, cols);
scratch = panel.getSelectedCellsScratch();
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int baddr = r * cols + c;
boolean expected = panel.isCellSelected(r, c);
assertEquals(expected, scratch[baddr], "Mismatch in linear forward mode at row " + r + ", col " + c);
}
}
// Configuration 3: Linear / Stream mode backward selection (drag upwards)
setField(panel, "selectionStartRow", 8);
setField(panel, "selectionStartCol", 30);
setField(panel, "selectionEndRow", 4);
setField(panel, "selectionEndCol", 10);
panel.precomputeSelectionScratch(rows, cols);
scratch = panel.getSelectedCellsScratch();
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int baddr = r * cols + c;
boolean expected = panel.isCellSelected(r, c);
assertEquals(expected, scratch[baddr], "Mismatch in linear backward mode at row " + r + ", col " + c);
}
}
// Configuration 4: Block select mode
setField(panel, "blockSelectMode", true);
setField(panel, "trimRectSizingHandles", false);
setField(panel, "selectionStartRow", 3);
setField(panel, "selectionStartCol", 20);
setField(panel, "selectionEndRow", 7);
setField(panel, "selectionEndCol", 60);
panel.precomputeSelectionScratch(rows, cols);
scratch = panel.getSelectedCellsScratch();
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int baddr = r * cols + c;
boolean expected = panel.isCellSelected(r, c);
assertEquals(expected, scratch[baddr], "Mismatch in block select mode at row " + r + ", col " + c);
}
}
// Configuration 5: Trim Rect Sizing Handles mode (block geometry)
setField(panel, "blockSelectMode", false);
setField(panel, "trimRectSizingHandles", true);
setField(panel, "selectionStartRow", 10);
setField(panel, "selectionStartCol", 5);
setField(panel, "selectionEndRow", 15);
setField(panel, "selectionEndCol", 25);
panel.precomputeSelectionScratch(rows, cols);
scratch = panel.getSelectedCellsScratch();
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int baddr = r * cols + c;
boolean expected = panel.isCellSelected(r, c);
assertEquals(expected, scratch[baddr], "Mismatch in trim handles mode at row " + r + ", col " + c);
}
}
// Configuration 6: Model 5 Dynamic Geometry (27 rows x 132 cols)
rows = 27;
cols = 132;
client.getScreenBuffer().setDimensions(rows, cols);
client.getScreenBuffer().updateDisplaySnapshot();
setField(panel, "selectionStartRow", 1);
setField(panel, "selectionStartCol", 1);
setField(panel, "selectionEndRow", 25);
setField(panel, "selectionEndCol", 130);
panel.precomputeSelectionScratch(rows, cols);
scratch = panel.getSelectedCellsScratch();
assertEquals(rows * cols, scratch.length);
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int baddr = r * cols + c;
boolean expected = panel.isCellSelected(r, c);
assertEquals(expected, scratch[baddr], "Mismatch in Model 5 27x132 at row " + r + ", col " + c);
}
}
}
@Test
@DisplayName("Phase 28.3: Dialog Lifecycle & Reconnection Hardening updates cached dialogs with active client")
public void testReconnectCycleFindDialogInteractsWithNewClient() throws Exception {
// 1. Verify API contract and reflection for dialog lifecycle methods
Method findSetClient = FindDialog.class.getMethod("setClient", Telnet3270Client.class);
assertNotNull(findSetClient);
Method findGetClient = FindDialog.class.getMethod("getClient");
assertEquals(Telnet3270Client.class, findGetClient.getReturnType());
Method scriptSetClient = ScriptDialog.class.getMethod("setClient", Telnet3270Client.class);
assertNotNull(scriptSetClient);
Method scriptGetClient = ScriptDialog.class.getMethod("getClient");
assertEquals(Telnet3270Client.class, scriptGetClient.getReturnType());
Method fieldSetClient = FieldInspectorDialog.class.getMethod("setClient", Telnet3270Client.class);
assertNotNull(fieldSetClient);
Method fieldGetClient = FieldInspectorDialog.class.getMethod("getClient");
assertEquals(Telnet3270Client.class, fieldGetClient.getReturnType());
Method appGetFind = J3270App.class.getMethod("getFindDialog");
assertEquals(FindDialog.class, appGetFind.getReturnType());
Method appGetScript = J3270App.class.getMethod("getScriptDialog");
assertEquals(ScriptDialog.class, appGetScript.getReturnType());
Method appGetField = J3270App.class.getMethod("getFieldInspectorDialog");
assertEquals(FieldInspectorDialog.class, appGetField.getReturnType());
// 2. If non-headless environment, perform end-to-end interactive GUI verification
try {
J3270App app = new J3270App();
TerminalPanel panel = app.getTerminalPanel();
// Session 1: Client 1
ConnectionConfig config1 = new ConnectionConfig("host1.example.com", 23);
Telnet3270Client client1 = new Telnet3270Client(config1);
client1.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
writeString(client1.getScreenBuffer(), 1, 0, "SYSTEM_SESSION_ALPHA_123");
panel.setClient(client1);
setField(app, "client", client1);
// Open FindDialog on session 1
FindDialog findDialog = new FindDialog(app, client1, panel);
assertSame(client1, findDialog.getClient());
// Search for string on Client 1
findDialog.setSearchText("SYSTEM_SESSION_ALPHA_123");
findDialog.findNext();
int expectedPos1 = 1 * client1.getScreenBuffer().getDisplayCols();
assertEquals(expectedPos1, client1.getScreenBuffer().getCursorAddress(),
"FindDialog must move cursor to match on client1");
// Simulate Session Disconnect
setField(app, "findDialog", findDialog);
app.disconnect(false);
assertNull(findDialog.getClient(), "Disconnect must nullify client in cached dialog");
// Subsequent find attempt while disconnected should report disconnected without errors
findDialog.findNext();
assertEquals(expectedPos1, client1.getScreenBuffer().getCursorAddress());
// Session 2: Reconnection to Client 2
ConnectionConfig config2 = new ConnectionConfig("host2.example.com", 23);
Telnet3270Client client2 = new Telnet3270Client(config2);
client2.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
writeString(client2.getScreenBuffer(), 3, 10, "MAINFRAME_SESSION_BETA_999");
// Simulate reconnect hook in J3270App
findDialog.setClient(client2);
assertSame(client2, findDialog.getClient(), "FindDialog must now hold client2");
// Search for string on Client 2
findDialog.setSearchText("MAINFRAME_SESSION_BETA_999");
findDialog.findNext();
int expectedPos2 = 3 * client2.getScreenBuffer().getDisplayCols() + 10;
assertEquals(expectedPos2, client2.getScreenBuffer().getCursorAddress(),
"FindDialog must move cursor to match on active client2 after reconnect");
// Verify search for old client1 string fails on client2
int oldCursorPos = client2.getScreenBuffer().getCursorAddress();
findDialog.setSearchText("SYSTEM_SESSION_ALPHA_123");
findDialog.findNext();
assertEquals(oldCursorPos, client2.getScreenBuffer().getCursorAddress(),
"Search for stale text should not match on client2");
// Verify ScriptDialog and FieldInspectorDialog client lifecycle
ScriptDialog scriptDialog = new ScriptDialog(app, client1, panel);
assertSame(client1, scriptDialog.getClient());
scriptDialog.setClient(client2);
assertSame(client2, scriptDialog.getClient());
FieldInspectorDialog fieldInspector = new FieldInspectorDialog(app, client1, panel);
assertSame(client1, fieldInspector.getClient());
fieldInspector.setClient(client2);
assertSame(client2, fieldInspector.getClient());
// Cleanup
fieldInspector.dispose();
findDialog.dispose();
scriptDialog.dispose();
app.closeWindow();
} catch (java.awt.HeadlessException ignored) {
// Running in headless mode: GUI instantiation safely bypassed
}
}
}
@@ -0,0 +1,213 @@
package haus.nightmare.j3270;
import haus.nightmare.lib3270j.charset.CodePage;
import haus.nightmare.lib3270j.charset.CodePageRegistry;
import haus.nightmare.lib3270j.graphics.GraphicsMode;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.HeadlessException;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* Verification test suite for Phase 31:
* Help > About Dialogue Version Reporting and j3270 Interface Inconsistency Fixes (v1.2.4).
*/
public class Phase31AboutAndInterfaceTest {
@Test
@DisplayName("Phase 31.1: Dedicated Version classes report v1.2.4 single source of truth")
public void testVersionClasses() {
// lib3270j
assertEquals("1.2.4", haus.nightmare.lib3270j.Version.VERSION);
assertEquals("lib3270j", haus.nightmare.lib3270j.Version.NAME);
assertEquals("1.2.4", haus.nightmare.lib3270j.Version.getVersion());
// j3270
assertEquals("1.2.4", haus.nightmare.j3270.Version.VERSION);
assertEquals("j3270", haus.nightmare.j3270.Version.NAME);
assertEquals("1.2.4", haus.nightmare.j3270.Version.getVersion());
}
@Test
@DisplayName("Phase 31.2: Help > About dialogue text properly reports j3270 v1.2.4 and lib3270j v1.2.4")
public void testAboutTextContent() {
String aboutText = J3270App.getAboutText();
assertNotNull(aboutText, "About text must not be null");
// Verify version reports
assertTrue(aboutText.contains("j3270 v1.2.4"), "About text must report j3270 v1.2.4");
assertTrue(aboutText.contains("lib3270j v1.2.4"), "About text must report lib3270j v1.2.4");
// Verify stale pre-release versions are eradicated
assertFalse(aboutText.contains("0.2.0"), "About text must not contain stale version 0.2.0");
assertFalse(aboutText.contains("0.1.0"), "About text must not contain stale version 0.1.0");
// Verify 40+ codepages reporting
assertTrue(aboutText.contains("40+ Pluggable EBCDIC Codepages & DBCS Support"),
"About text must report 40+ Pluggable EBCDIC Codepages");
assertFalse(aboutText.contains("22+"), "About text must not report stale 22+ codepages");
}
@Test
@DisplayName("Phase 31.3: CodePageRegistry.COMMON_CODE_PAGES defines 40+ supported codepages that all resolve")
public void testCommonCodePagesRegistration() {
String[] common = CodePageRegistry.COMMON_CODE_PAGES;
assertNotNull(common, "COMMON_CODE_PAGES must not be null");
assertTrue(common.length >= 40, "COMMON_CODE_PAGES must define at least 40 entries, found: " + common.length);
for (String entry : common) {
String cpId = entry.split(" -")[0].trim();
CodePage cp = CodePageRegistry.resolveCodePage(cpId);
assertNotNull(cp, "Codepage identifier '" + cpId + "' from entry '" + entry + "' must resolve in CodePageRegistry");
}
}
@Test
@DisplayName("Phase 31.4: Menu bar consistency: Code Page & Graphics Mode in Connection menu, Screen History in View")
public void testMenuBarConsistency() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
// Headless environment without X11/display
return;
}
try {
JMenuBar mb = app.getJMenuBar();
assertNotNull(mb, "JMenuBar must exist");
JMenu viewMenu = null;
JMenu actionsMenu = null;
JMenu fileMenu = null;
JMenu connectionMenu = 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 ("Connection".equals(m.getText())) connectionMenu = m;
}
}
assertNotNull(viewMenu, "View menu must exist");
assertNotNull(actionsMenu, "Actions menu must exist");
assertNotNull(fileMenu, "File menu must exist");
assertNotNull(connectionMenu, "Connection menu must exist");
// Verify Connect... and Disconnect are in Connection menu and NOT in File menu
boolean connectInConnection = false;
boolean disconnectInConnection = false;
for (int i = 0; i < connectionMenu.getItemCount(); i++) {
JMenuItem item = connectionMenu.getItem(i);
if (item != null) {
if ("Connect...".equals(item.getText())) connectInConnection = true;
if ("Disconnect".equals(item.getText())) disconnectInConnection = true;
}
}
assertTrue(connectInConnection, "Connect... must be in Connection menu");
assertTrue(disconnectInConnection, "Disconnect must be in Connection menu");
boolean connectInFile = false;
boolean disconnectInFile = false;
for (int i = 0; i < fileMenu.getItemCount(); i++) {
JMenuItem item = fileMenu.getItem(i);
if (item != null) {
if ("Connect...".equals(item.getText())) connectInFile = true;
if ("Disconnect".equals(item.getText())) disconnectInFile = true;
}
}
assertFalse(connectInFile, "Connect... must NOT be in File menu");
assertFalse(disconnectInFile, "Disconnect must NOT be in File menu");
// Verify Screen History is in View menu
boolean screenHistoryInView = false;
for (int i = 0; i < viewMenu.getItemCount(); i++) {
JMenuItem item = viewMenu.getItem(i);
if (item != null && "Screen History...".equals(item.getText())) {
screenHistoryInView = true;
}
}
assertTrue(screenHistoryInView, "Screen History... must be present in View menu");
// Verify Screen History is NOT duplicated in Actions menu
boolean screenHistoryInActions = false;
for (int i = 0; i < actionsMenu.getItemCount(); i++) {
JMenuItem item = actionsMenu.getItem(i);
if (item != null && "Screen History...".equals(item.getText())) {
screenHistoryInActions = true;
}
}
assertFalse(screenHistoryInActions, "Screen History... must NOT be duplicated in Actions menu");
// Verify Code Page and Graphics Mode are in Connection menu, NOT View menu
JMenu codePageSubmenu = null;
JMenu graphicsModeSubmenu = null;
for (int i = 0; i < connectionMenu.getItemCount(); i++) {
JMenuItem item = connectionMenu.getItem(i);
if (item != null) {
if ("Code Page".equals(item.getText()) && item instanceof JMenu) {
codePageSubmenu = (JMenu) item;
}
if ("Graphics Mode".equals(item.getText()) && item instanceof JMenu) {
graphicsModeSubmenu = (JMenu) item;
}
}
}
assertNotNull(codePageSubmenu, "Connection > Code Page submenu must exist");
assertNotNull(graphicsModeSubmenu, "Connection > Graphics Mode submenu must exist");
assertEquals(CodePageRegistry.COMMON_CODE_PAGES.length, codePageSubmenu.getItemCount(),
"Connection > Code Page submenu must contain all " + CodePageRegistry.COMMON_CODE_PAGES.length + " common codepages");
assertEquals(GraphicsMode.values().length, graphicsModeSubmenu.getItemCount(),
"Connection > Graphics Mode submenu must contain all " + GraphicsMode.values().length + " graphics modes");
// Graphics Mode must be enabled when disconnected
assertFalse(app.isConnected(), "App should not be connected by default");
assertTrue(graphicsModeSubmenu.isEnabled(), "Graphics Mode submenu should be enabled when disconnected");
// Verify Code Page and Graphics Mode are NOT in View menu
for (int i = 0; i < viewMenu.getItemCount(); i++) {
JMenuItem item = viewMenu.getItem(i);
if (item != null) {
assertNotEquals("Code Page", item.getText(), "Code Page must NOT be in View menu");
assertNotEquals("Graphics Mode", item.getText(), "Graphics Mode must NOT be in View menu");
}
}
// Verify Base 4-Color Override is in View menu
boolean base4InView = false;
for (int i = 0; i < viewMenu.getItemCount(); i++) {
JMenuItem item = viewMenu.getItem(i);
if (item != null && "Base 4-Color Override".equals(item.getText())) {
base4InView = true;
}
}
assertTrue(base4InView, "Base 4-Color Override must be present in View menu");
// Verify Modes menu contains strictly the 4 interactive operator modes
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 must exist");
assertEquals(4, modesMenu.getItemCount(), "Modes menu should contain exactly 4 interactive operator modes");
assertEquals("Document Mode (DOC)", modesMenu.getItem(0).getText());
assertEquals("Word Wrap Mode", modesMenu.getItem(1).getText());
assertEquals("APL Keyboard Mode", modesMenu.getItem(2).getText());
assertEquals("Insert Mode", modesMenu.getItem(3).getText());
} finally {
app.dispose();
}
}
}
@@ -0,0 +1,271 @@
package haus.nightmare.j3270.ft;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import haus.nightmare.j3270.storage.HostStorage;
import haus.nightmare.j3270.storage.SavedHost;
import haus.nightmare.j3270.ui.ConnectDialog;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ft.FTConfig;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.Frame;
import java.awt.HeadlessException;
import java.io.File;
import java.io.PrintWriter;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
public class ConnectionHostTypeConfigTest {
private List<SavedHost> originalSavedHosts;
private String originalDefaultHostType;
@BeforeEach
public void setUp() {
FileTransferDialog.resetSessionState();
originalSavedHosts = HostStorage.getSavedHosts();
originalDefaultHostType = Settings.getDefaultHostType();
HostStorage.clearAll();
}
@AfterEach
public void tearDown() {
FileTransferDialog.resetSessionState();
HostStorage.clearAll();
if (originalSavedHosts != null) {
HostStorage.saveAll(originalSavedHosts);
}
Settings.setDefaultHostType(originalDefaultHostType);
}
@Test
@DisplayName("SavedHost converts to ConnectionConfig preserving hostType and saved connection metadata")
public void testSavedHostToConnectionConfigPreservesHostType() {
SavedHost cmsHost = new SavedHost(
"cms-uuid-1", "VM/CMS System", "vm370.corp.local", 23, 4, 62, 160, "",
false, "CMS", false, true, true, "BOTH", "037",
"NONE", "", 0, "", ""
);
ConnectionConfig cfgCms = cmsHost.toConnectionConfig();
assertTrue(cfgCms.isSavedConnection(), "Config from SavedHost must be marked as saved connection");
assertEquals("cms-uuid-1", cfgCms.getSavedProfileId());
assertEquals("VM/CMS System", cfgCms.getSavedProfileName());
assertEquals("CMS", cfgCms.getHostType());
assertEquals(FTConfig.HostType.CMS, cfgCms.getFtHostType());
SavedHost cicsHost = new SavedHost(
"cics-uuid-2", "CICS Gateway", "cics.corp.local", 23, 2, 24, 80, "LU1",
false, "CICS", false, true, true, "BOTH", "037",
"NONE", "", 0, "", ""
);
ConnectionConfig cfgCics = cicsHost.toConnectionConfig();
assertTrue(cfgCics.isSavedConnection());
assertEquals("CICS", cfgCics.getHostType());
assertEquals(FTConfig.HostType.CICS, cfgCics.getFtHostType());
SavedHost tsoHost = new SavedHost(
"tso-uuid-3", "MVS TSO", "tso.corp.local", 23, 4, 43, 80, "",
false, "TSO", false, true, true, "BOTH", "037",
"NONE", "", 0, "", ""
);
ConnectionConfig cfgTso = tsoHost.toConnectionConfig();
assertTrue(cfgTso.isSavedConnection());
assertEquals("TSO", cfgTso.getHostType());
assertEquals(FTConfig.HostType.TSO, cfgTso.getFtHostType());
}
@Test
@DisplayName("FileTransferDialog preselects CMS when using a saved connection configured as CMS")
public void testFileTransferDialogPrefillsFromSavedConnectionCms() {
ConnectionConfig config = new ConnectionConfig("vm.example.com", 23);
config.setSavedConnection(true);
config.setSavedProfileId("vm-1");
config.setSavedProfileName("VM370");
config.setHostType("CMS");
Telnet3270Client client = new Telnet3270Client(config);
FileTransfer ft = new FileTransfer(client, null);
try {
FileTransferDialog dialog = new FileTransferDialog(null, ft);
assertEquals(FTConfig.HostType.CMS, dialog.getHostTypeCombo().getSelectedItem(),
"FileTransferDialog must preselect CMS when saved connection is configured as CMS");
dialog.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("FileTransferDialog preselects CICS when using a saved connection configured as CICS")
public void testFileTransferDialogPrefillsFromSavedConnectionCics() {
ConnectionConfig config = new ConnectionConfig("cics.example.com", 23);
config.setSavedConnection(true);
config.setSavedProfileId("cics-1");
config.setSavedProfileName("CICS Region");
config.setHostType("CICS");
Telnet3270Client client = new Telnet3270Client(config);
FileTransfer ft = new FileTransfer(client, null);
try {
FileTransferDialog dialog = new FileTransferDialog(null, ft);
assertEquals(FTConfig.HostType.CICS, dialog.getHostTypeCombo().getSelectedItem(),
"FileTransferDialog must preselect CICS when saved connection is configured as CICS");
dialog.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("FileTransferDialog overrides stale prior transfer state with configured host type from saved connection")
public void testFileTransferDialogOverridesOldTransferStateWhenSavedConnectionUsed() {
// Simulate a prior transfer performed on a TSO system
FileTransferDialog.TransferSessionState oldState = new FileTransferDialog.TransferSessionState();
oldState.hostType = FTConfig.HostType.TSO;
oldState.localFile = "test.txt";
oldState.hostFile = "USER.DATA";
FileTransferDialog.setLastTransferState(oldState);
// Now connect to a CMS saved connection
ConnectionConfig cmsConfig = new ConnectionConfig("vm.corp.internal", 23);
cmsConfig.setSavedConnection(true);
cmsConfig.setSavedProfileName("VM Prod");
cmsConfig.setHostType("CMS");
Telnet3270Client client = new Telnet3270Client(cmsConfig);
FileTransfer ft = new FileTransfer(client, null);
try {
FileTransferDialog dialog = new FileTransferDialog(null, ft);
assertEquals(FTConfig.HostType.CMS, dialog.getHostTypeCombo().getSelectedItem(),
"FileTransferDialog must use the configured host type (CMS) of the saved connection rather than prior session's TSO");
dialog.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("ConnectDialog allows host type configuration and saves it to profile")
public void testConnectDialogHostTypeConfigurationAndProfile() {
try {
ConnectDialog dialog = new ConnectDialog((Frame) null);
assertNotNull(dialog.getHostTypeCombo(), "ConnectDialog must provide a Host Type combo box");
assertEquals("TSO", dialog.getSelectedHostType());
// Switch to CMS
dialog.setInitialHostType("CMS");
assertEquals("CMS", dialog.getSelectedHostType());
assertEquals(FTConfig.HostType.CMS, dialog.getSelectedFtHostType());
// Switch to CICS
dialog.setInitialHostType("CICS");
assertEquals("CICS", dialog.getSelectedHostType());
assertEquals(FTConfig.HostType.CICS, dialog.getSelectedFtHostType());
// Loading a SavedHost profile updates host type in ConnectDialog
SavedHost cmsProfile = new SavedHost(
"cms-prof-1", "CMS Dev", "cms.dev.local", 23, 4, 62, 160, "",
false, "CMS", false, true, true, "BOTH", "037",
"NONE", "", 0, "", ""
);
dialog.loadProfile(cmsProfile);
assertEquals("CMS", dialog.getSelectedHostType());
dialog.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("Settings persists and exports defaultHostType")
public void testSettingsDefaultHostTypePersistenceAndIni() throws Exception {
Settings.setDefaultHostType("CMS");
assertEquals("CMS", Settings.getDefaultHostType());
File tmpIni = File.createTempFile("j3270_test_cfg", ".ini");
tmpIni.deleteOnExit();
try (PrintWriter writer = new PrintWriter(tmpIni)) {
writer.println("[connection]");
writer.println("hostType = CICS");
}
Settings.loadFromIniFile(tmpIni.getAbsolutePath());
assertEquals("CICS", Settings.getDefaultHostType());
// Test export
File exportFile = File.createTempFile("j3270_export", ".ini");
exportFile.deleteOnExit();
Settings.exportToIniFile(exportFile.getAbsolutePath());
String content = new String(java.nio.file.Files.readAllBytes(exportFile.toPath()));
assertTrue(content.contains("hostType = CICS"), "Exported INI must contain hostType = CICS");
}
@Test
@DisplayName("J3270App setConnectionHostType updates connection config and syncs with profile in HostStorage")
public void testSetConnectionHostTypeUpdatesActiveConnectionAndStorage() {
SavedHost saved = new SavedHost(
"saved-prof-1", "Prod CMS", "vm.prod", 23, 4, 62, 160, "",
false, "TSO", false, true, true, "BOTH", "037",
"NONE", "", 0, "", ""
);
HostStorage.saveHost(saved);
ConnectionConfig config = saved.toConnectionConfig();
assertEquals("TSO", config.getHostType());
try {
J3270App app = new J3270App();
try {
// Verify Connection menu exists in JMenuBar
JMenuBar mb = app.getJMenuBar();
assertNotNull(mb);
boolean hasConnectionMenu = false;
for (int i = 0; i < mb.getMenuCount(); i++) {
JMenu m = mb.getMenu(i);
if (m != null && "Connection".equals(m.getText())) {
hasConnectionMenu = true;
break;
}
}
assertTrue(hasConnectionMenu, "JMenuBar must include a Connection menu");
// Simulate connecting with this config
app.connect(config);
// Change host type via setConnectionHostType
app.setConnectionHostType(FTConfig.HostType.CMS);
assertEquals(FTConfig.HostType.CMS, app.getClient().getConfig().getFtHostType());
// Profile in storage should also be updated
SavedHost reloaded = HostStorage.findById("saved-prof-1");
assertNotNull(reloaded);
assertEquals("CMS", reloaded.getHostType());
} finally {
app.dispose();
}
} catch (HeadlessException ignored) {
// In headless environments where JFrame cannot be initialized, test model updates directly
Telnet3270Client client = new Telnet3270Client(config);
client.getConfig().setFtHostType(FTConfig.HostType.CMS);
saved.setHostType(FTConfig.HostType.CMS.name());
HostStorage.saveHost(saved);
SavedHost reloaded = HostStorage.findById("saved-prof-1");
assertNotNull(reloaded);
assertEquals("CMS", reloaded.getHostType());
}
}
}
@@ -1,6 +1,9 @@
package haus.nightmare.j3270.ft;
import haus.nightmare.j3270.ui.HostDirectoryDialog;
import haus.nightmare.lib3270j.ft.FTConfig;
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -99,4 +102,114 @@ public class FileTransferSessionAndCmsTest {
// Headless environment; verified through state retention
}
}
@Test
public void testPdsSessionStatePersistence() {
FileTransferDialog.TransferSessionState state = new FileTransferDialog.TransferSessionState();
state.hostType = FTConfig.HostType.TSO;
state.isSend = true;
state.hostFile = "'USER.PDS.DATA'";
state.units = FTConfig.AllocationUnit.CYLINDERS;
state.space = "10,5";
state.dirBlocks = "20";
FileTransferDialog.setLastTransferState(state);
FileTransferDialog.TransferSessionState retrieved = FileTransferDialog.getLastTransferState();
assertNotNull(retrieved);
assertEquals(FTConfig.AllocationUnit.CYLINDERS, retrieved.units);
assertEquals("10,5", retrieved.space);
assertEquals("20", retrieved.dirBlocks);
try {
FileTransferDialog dialog = new FileTransferDialog(null, null);
assertEquals(FTConfig.AllocationUnit.CYLINDERS, dialog.getUnitsCombo().getSelectedItem());
assertEquals("10,5", dialog.getSpaceField().getText());
assertEquals("20", dialog.getDirField().getText());
dialog.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
public void testHostDirectoryDialogStructureAndFilter() {
// Test default query resolution
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, null));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, ""));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, " "));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "* * *"));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "*"));
assertEquals("TEST EXEC", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "TEST EXEC"));
assertEquals("", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, null));
assertEquals("", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, ""));
assertEquals("'MY.DATASET'", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, "'MY.DATASET'"));
// Test CMS filter formatting
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter(null));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter(""));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("* * *"));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("*"));
assertEquals("TEST * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST"));
assertEquals("TEST EXEC A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST EXEC"));
assertEquals("TEST EXEC B", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST EXEC B"));
assertEquals("TEST EXEC A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST.EXEC"));
try {
haus.nightmare.j3270.ui.HostDirectoryDialog dialog =
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "");
assertNotNull(dialog);
assertFalse(dialog.isConfirmed());
assertNull(dialog.getSelectedHostFile());
assertEquals("* * A", dialog.getQueryField().getText());
dialog.dispose();
haus.nightmare.j3270.ui.HostDirectoryDialog dialog2 =
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "* * *");
assertEquals("* * A", dialog2.getQueryField().getText());
dialog2.dispose();
haus.nightmare.j3270.ui.HostDirectoryDialog dialog3 =
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "TEST EXEC");
assertEquals("TEST EXEC", dialog3.getQueryField().getText());
dialog3.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
public void testMatchesFilterWildcards() {
CMSDirectoryEntry cmsA = new CMSDirectoryEntry("PROFILE", "EXEC", "A1");
CMSDirectoryEntry cmsB = new CMSDirectoryEntry("PROFILE", "EXEC", "B1");
CMSDirectoryEntry cmsSys = new CMSDirectoryEntry("SYSTEM", "CONFIG", "S2");
// * * A matches A disk files (A1, A0, etc.), excludes B and S
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "* * A"));
assertFalse(HostDirectoryDialog.matchesFilter(cmsB, "* * A"));
assertFalse(HostDirectoryDialog.matchesFilter(cmsSys, "* * A"));
// Blank, *, * * * matches all
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, ""));
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "*"));
assertTrue(HostDirectoryDialog.matchesFilter(cmsB, "* * *"));
assertTrue(HostDirectoryDialog.matchesFilter(cmsSys, "* * *"));
// Two tokens (fn ft) and dot tokens
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "* EXEC"));
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "*.EXEC"));
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "PROFILE *"));
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "PROF*.*"));
assertFalse(HostDirectoryDialog.matchesFilter(cmsSys, "* EXEC"));
// TSO Wildcards
TSODirectoryEntry tso = new TSODirectoryEntry("TSOUSER.TEST.DATA");
assertTrue(HostDirectoryDialog.matchesFilter(tso, ""));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "*"));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "* * *"));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "*.DATA"));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "TEST"));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "TSOUSER.*"));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "'TSOUSER.TEST.*'"));
assertFalse(HostDirectoryDialog.matchesFilter(tso, "*.CNTL"));
}
}
@@ -0,0 +1,342 @@
package haus.nightmare.j3270.storage;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import haus.nightmare.j3270.ui.SettingsDialog;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class ConnectionIniStorageTest {
private List<SavedHost> originalSavedHosts;
private String origHost;
private int origPort;
@BeforeEach
public void setUp() {
originalSavedHosts = HostStorage.getSavedHosts();
origHost = Settings.getAutoConnectHost();
origPort = Settings.getAutoConnectPort();
}
@AfterEach
public void tearDown() {
if (originalSavedHosts != null) {
HostStorage.saveAll(originalSavedHosts);
} else {
HostStorage.clearAll();
}
Settings.setAutoConnectHost(origHost);
Settings.setAutoConnectPort(origPort);
}
@Test
@DisplayName("HostStorage exports to and loads from connections.ini with full fidelity")
public void testExportAndLoadConnectionsIniRoundtrip() throws IOException {
List<SavedHost> hosts = new ArrayList<>();
SavedHost h1 = new SavedHost("id-mainframe-1", "Production TSO", "mainframe.corp.com", 23, 4, 62, 160,
"LU01", true, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
SavedHost h2 = new SavedHost("id-vm-2", "VM CMS Test", "vmcms.lab.net", 992, 2, 24, 80,
"LUCMS", false, "CMS", true, false, true, "GDDM", "1047", "SOCKS5", "10.0.0.1", 1080, "proxyuser", "proxypass");
hosts.add(h1);
hosts.add(h2);
File tmpIni = File.createTempFile("connections_test", ".ini");
tmpIni.deleteOnExit();
try {
HostStorage.exportToIniFile(tmpIni, hosts);
assertTrue(tmpIni.exists());
String content = new String(Files.readAllBytes(tmpIni.toPath()));
assertTrue(content.contains("[Production TSO]"), "Must contain section header [Production TSO]");
assertTrue(content.contains("[VM CMS Test]"), "Must contain section header [VM CMS Test]");
assertTrue(content.contains("host = mainframe.corp.com"));
assertTrue(content.contains("proxyHost = 10.0.0.1"));
assertTrue(content.contains("proxyType = SOCKS5"));
List<SavedHost> loaded = HostStorage.loadFromIniFile(tmpIni);
assertEquals(2, loaded.size(), "Should load 2 connections from INI");
SavedHost loaded1 = loaded.get(0);
assertEquals("id-mainframe-1", loaded1.getId());
assertEquals("Production TSO", loaded1.getName());
assertEquals("mainframe.corp.com", loaded1.getHost());
assertEquals(23, loaded1.getPort());
assertEquals(4, loaded1.getModel());
assertEquals(62, loaded1.getDynamicRows());
assertEquals(160, loaded1.getDynamicCols());
assertEquals("LU01", loaded1.getLuName());
assertTrue(loaded1.isAutoConnect());
assertEquals("TSO", loaded1.getHostType());
assertFalse(loaded1.isUseTls());
assertTrue(loaded1.isTlsVerifyCert());
SavedHost loaded2 = loaded.get(1);
assertEquals("id-vm-2", loaded2.getId());
assertEquals("VM CMS Test", loaded2.getName());
assertEquals("vmcms.lab.net", loaded2.getHost());
assertEquals(992, loaded2.getPort());
assertEquals(2, loaded2.getModel());
assertEquals(24, loaded2.getDynamicRows());
assertEquals(80, loaded2.getDynamicCols());
assertEquals("LUCMS", loaded2.getLuName());
assertFalse(loaded2.isAutoConnect());
assertEquals("CMS", loaded2.getHostType());
assertTrue(loaded2.isUseTls());
assertFalse(loaded2.isTlsVerifyCert());
assertEquals("SOCKS5", loaded2.getProxyType());
assertEquals("10.0.0.1", loaded2.getProxyHost());
assertEquals(1080, loaded2.getProxyPort());
assertEquals("proxyuser", loaded2.getProxyUsername());
assertEquals("proxypass", loaded2.getProxyPassword());
} finally {
if (tmpIni.exists()) tmpIni.delete();
}
}
@Test
@DisplayName("HostStorage importFromIniFile merges and updates existing hosts by name or id")
public void testImportFromIniFileMergesAndUpdates() throws IOException {
SavedHost initial = new SavedHost("id-100", "Mainframe Prod", "old.host.org", 23, 4, 62, 160,
"", false, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
List<SavedHost> initList = new ArrayList<>();
initList.add(initial);
HostStorage.saveAll(initList);
File tmpIni = File.createTempFile("connections_merge", ".ini");
tmpIni.deleteOnExit();
try {
try (FileWriter w = new FileWriter(tmpIni)) {
// Update Mainframe Prod with new host address
w.write("[Mainframe Prod]\n");
w.write("id = id-100\n");
w.write("host = new.host.org\n");
w.write("port = 2323\n");
w.write("\n");
// Add a second connection
w.write("[Development CICS]\n");
w.write("host = cics.dev.org\n");
w.write("port = 992\n");
w.write("hostType = CICS\n");
w.write("useTls = true\n");
}
List<SavedHost> result = HostStorage.importFromIniFile(tmpIni);
assertEquals(2, result.size(), "Import should result in 2 saved hosts (1 updated, 1 added)");
SavedHost updated = HostStorage.findById("id-100");
assertNotNull(updated);
assertEquals("new.host.org", updated.getHost());
assertEquals(2323, updated.getPort());
SavedHost added = HostStorage.findByName("Development CICS");
assertNotNull(added);
assertEquals("cics.dev.org", added.getHost());
assertEquals(992, added.getPort());
assertEquals("CICS", added.getHostType());
assertTrue(added.isUseTls());
} finally {
if (tmpIni.exists()) tmpIni.delete();
}
}
@Test
@DisplayName("HostStorage parses INI section prefixes and aliases properly")
public void testImportHandlesMissingFieldsAndAliases() throws IOException {
File tmpIni = File.createTempFile("connections_alias", ".ini");
tmpIni.deleteOnExit();
try {
try (FileWriter w = new FileWriter(tmpIni)) {
w.write("[connection:Aliased Host]\n");
w.write("hostname = test.alias.net\n");
w.write("port = 1023\n");
w.write("terminalModel = 2\n");
w.write("type = CMS\n");
w.write("tls = true\n");
w.write("verifyCert = false\n");
w.write("charset = 1047\n");
w.write("rows = 24\n");
w.write("cols = 80\n");
w.write("lu = LUVM\n");
}
List<SavedHost> parsed = HostStorage.loadFromIniFile(tmpIni);
assertEquals(1, parsed.size());
SavedHost h = parsed.get(0);
assertEquals("Aliased Host", h.getName());
assertEquals("test.alias.net", h.getHost());
assertEquals(1023, h.getPort());
assertEquals(2, h.getModel());
assertEquals("CMS", h.getHostType());
assertTrue(h.isUseTls());
assertFalse(h.isTlsVerifyCert());
assertEquals("1047", h.getCodePage());
assertEquals(24, h.getDynamicRows());
assertEquals(80, h.getDynamicCols());
assertEquals("LUVM", h.getLuName());
} finally {
if (tmpIni.exists()) tmpIni.delete();
}
}
@Test
@DisplayName("SettingsDialog contains Import Connections and Export Connections buttons and is taller")
public void testSettingsDialogConnectionButtonsAndHeight() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
JButton btnImportConn = dialog.getBtnImportConnections();
JButton btnExportConn = dialog.getBtnExportConnections();
assertNotNull(btnImportConn, "Import Connections button must exist in SettingsDialog");
assertNotNull(btnExportConn, "Export Connections button must exist in SettingsDialog");
assertEquals("Import Connections...", btnImportConn.getText());
assertEquals("Export Connections...", btnExportConn.getText());
// Check window height: must be taller than previous 540 to accommodate extra buttons
assertTrue(dialog.getHeight() > 540,
"Settings window height (" + dialog.getHeight() + ") must be taller than 540 to accommodate the extra buttons");
// Verify config buttons share container row1, and connection buttons share container row2
JButton btnImport = dialog.getBtnImport();
JButton btnExport = dialog.getBtnExport();
assertNotNull(btnImport);
assertNotNull(btnExport);
Container configRow = btnImport.getParent();
assertNotNull(configRow);
assertSame(configRow, btnExport.getParent(), "btnImport and btnExport must share the same parent row");
Container connRow = btnImportConn.getParent();
assertNotNull(connRow);
assertSame(connRow, btnExportConn.getParent(), "btnImportConn and btnExportConn must share the same parent row");
assertNotSame(configRow, connRow, "Connection buttons must be below and NOT in line with config buttons");
// Verify connRow is positioned below configRow in buttonPanel
Container buttonPanel = configRow.getParent();
assertNotNull(buttonPanel);
assertSame(buttonPanel, connRow.getParent(), "Both rows must be in buttonPanel");
int configRowIdx = -1;
int connRowIdx = -1;
Component[] rows = buttonPanel.getComponents();
for (int i = 0; i < rows.length; i++) {
if (rows[i] == configRow) configRowIdx = i;
if (rows[i] == connRow) connRowIdx = i;
}
assertTrue(configRowIdx >= 0, "configRow must be in buttonPanel");
assertTrue(connRowIdx >= 0, "connRow must be in buttonPanel");
assertTrue(connRowIdx > configRowIdx, "connRow must be below configRow in buttonPanel (" + connRowIdx + " > " + configRowIdx + ")");
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("SettingsDialog importConnectionsFile updates savedHostBox and Settings")
public void testSettingsDialogImportConnectionsFileUpdatesDropdownAndSettings() throws IOException {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
File tmpIni = File.createTempFile("settings_dialog_import_test", ".ini");
tmpIni.deleteOnExit();
try {
try (FileWriter w = new FileWriter(tmpIni)) {
w.write("[Auto Mainframe]\n");
w.write("host = auto.mainframe.org\n");
w.write("port = 992\n");
w.write("autoConnect = true\n");
w.write("useTls = true\n");
}
SettingsDialog dialog = new SettingsDialog(app);
dialog.importConnectionsFile(tmpIni);
// Check savedHostBox updated
JComboBox<SavedHost> box = dialog.getSavedHostBox();
assertNotNull(box);
assertTrue(box.getItemCount() >= 1);
SavedHost sel = (SavedHost) box.getSelectedItem();
assertNotNull(sel);
assertEquals("auto.mainframe.org", sel.getHost());
assertTrue(sel.isAutoConnect());
// Check Settings updated for autoConnect host
assertEquals("auto.mainframe.org", Settings.getAutoConnectHost());
assertEquals(992, Settings.getAutoConnectPort());
assertTrue(Settings.getAutoConnectTls());
// Test exportConnectionsFile
File exportTarget = File.createTempFile("settings_dialog_export_test", ".ini");
exportTarget.deleteOnExit();
dialog.exportConnectionsFile(exportTarget);
assertTrue(exportTarget.exists());
String exported = new String(Files.readAllBytes(exportTarget.toPath()));
assertTrue(exported.contains("auto.mainframe.org"));
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
} finally {
if (tmpIni.exists()) tmpIni.delete();
}
}
@Test
@DisplayName("SettingsDialog importConnectionsFile throws FileNotFoundException for nonexistent file")
public void testImportNonExistentConnectionsFileThrows() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
File nonExistent = new File("/path/to/nonexistent/connections_" + System.currentTimeMillis() + ".ini");
assertThrows(FileNotFoundException.class, () -> {
dialog.importConnectionsFile(nonExistent);
});
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
}
@@ -0,0 +1,343 @@
package haus.nightmare.j3270.storage;
import haus.nightmare.j3270.ui.ConnectDialog;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.Frame;
import java.awt.GraphicsEnvironment;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
public class HostStorageAndProxyTest {
private List<SavedHost> originalHosts;
@BeforeEach
public void setUp() {
originalHosts = HostStorage.getSavedHosts();
HostStorage.clearAll();
}
@AfterEach
public void tearDown() {
HostStorage.clearAll();
HostStorage.saveAll(originalHosts);
}
@Test
@DisplayName("SavedHost correctly models per-connection proxy and converts to ConnectionConfig")
public void testSavedHostProxyToConnectionConfig() {
// 1. Host with SOCKS5 proxy and auth
SavedHost host1 = new SavedHost(
UUID.randomUUID().toString(),
"Prod Mainframe",
"mvs.corp.local",
2323,
4,
43,
80,
"TSO01",
false,
"TSO",
true,
true,
true,
"BOTH",
"037",
"SOCKS5",
"proxy.internal.corp",
1080,
"admin",
"secret123"
);
ConnectionConfig cfg1 = host1.toConnectionConfig();
assertEquals("mvs.corp.local", cfg1.getHost());
assertEquals(2323, cfg1.getPort());
assertTrue(cfg1.isUseTls());
assertEquals("TSO01", cfg1.getLuName());
assertEquals(ConnectionConfig.ProxyType.SOCKS5, cfg1.getProxyType());
assertEquals("proxy.internal.corp", cfg1.getProxyHost());
assertEquals(1080, cfg1.getProxyPort());
assertEquals("admin", cfg1.getProxyUsername());
assertEquals("secret123", cfg1.getProxyPassword());
// 2. Host with direct connection (NONE proxy)
SavedHost host2 = new SavedHost(
UUID.randomUUID().toString(),
"Local VM370",
"127.0.0.1",
23,
2,
24,
80,
"",
false,
"CMS",
false,
false,
false,
"BOTH",
"037",
"NONE",
"",
0,
"",
""
);
ConnectionConfig cfg2 = host2.toConnectionConfig();
assertEquals("127.0.0.1", cfg2.getHost());
assertEquals(23, cfg2.getPort());
assertFalse(cfg2.isUseTls());
assertEquals(ConnectionConfig.ProxyType.NONE, cfg2.getProxyType());
assertNull(cfg2.getProxyHost());
}
@Test
@DisplayName("HostStorage persists and restores multiple host profiles with isolated per-connection proxies")
public void testHostStoragePersistenceAndIsolation() {
SavedHost h1 = new SavedHost(
"id-1",
"System A (SOCKS5)",
"sysa.corp",
23,
4,
62,
160,
"",
false,
"TSO",
false,
true,
true,
"BOTH",
"037",
"SOCKS5",
"socks.gateway.net",
1080,
"alice",
"passA"
);
SavedHost h2 = new SavedHost(
"id-2",
"System B (HTTP CONNECT)",
"sysb.dmz",
992,
5,
27,
132,
"LU99",
false,
"TSO",
true,
true,
true,
"BOTH",
"1047",
"HTTP",
"squid.proxy.org",
3128,
"bob",
"passB"
);
SavedHost h3 = new SavedHost(
"id-3",
"System C (Direct)",
"sysc.lan",
23,
2,
24,
80,
"",
true, // autoConnect
"CMS",
false,
true,
true,
"BOTH",
"500",
"NONE",
"",
0,
"",
""
);
HostStorage.saveHost(h1);
HostStorage.saveHost(h2);
HostStorage.saveHost(h3);
List<SavedHost> loaded = HostStorage.getSavedHosts();
assertEquals(3, loaded.size());
SavedHost loadedA = HostStorage.findById("id-1");
assertNotNull(loadedA);
assertEquals("System A (SOCKS5)", loadedA.getName());
assertEquals("SOCKS5", loadedA.getProxyType());
assertEquals("socks.gateway.net", loadedA.getProxyHost());
assertEquals(1080, loadedA.getProxyPort());
assertEquals("alice", loadedA.getProxyUsername());
assertEquals("passA", loadedA.getProxyPassword());
SavedHost loadedB = HostStorage.findById("id-2");
assertNotNull(loadedB);
assertEquals("HTTP", loadedB.getProxyType());
assertEquals("squid.proxy.org", loadedB.getProxyHost());
assertEquals(3128, loadedB.getProxyPort());
assertEquals("bob", loadedB.getProxyUsername());
assertEquals("passB", loadedB.getProxyPassword());
SavedHost loadedC = HostStorage.findById("id-3");
assertNotNull(loadedC);
assertEquals("NONE", loadedC.getProxyType());
assertEquals("", loadedC.getProxyHost());
assertTrue(loadedC.isAutoConnect());
// Verify getAutoConnectHost returns System C with its own connection settings
SavedHost auto = HostStorage.getAutoConnectHost();
assertNotNull(auto);
assertEquals("id-3", auto.getId());
assertEquals(ConnectionConfig.ProxyType.NONE, auto.toConnectionConfig().getProxyType());
// Verify deleteHost
HostStorage.deleteHost("id-2");
assertEquals(2, HostStorage.getSavedHosts().size());
assertNull(HostStorage.findById("id-2"));
}
@Test
@DisplayName("HostStorage JSON matches a3270 format and parses a3270 JSON strings")
public void testA3270JsonFormatCompatibility() {
// String representative of a3270 saved_hosts preference JSON
String a3270Json = "[\n" +
" {\n" +
" \"id\": \"android-uuid-1\",\n" +
" \"name\": \"Dallas Mainframe\",\n" +
" \"host\": \"dallas.ibm.com\",\n" +
" \"port\": 2323,\n" +
" \"model\": 4,\n" +
" \"dynamicRows\": 62,\n" +
" \"dynamicCols\": 160,\n" +
" \"luName\": \"DAL01\",\n" +
" \"autoConnect\": false,\n" +
" \"hostType\": \"TSO\",\n" +
" \"useTls\": true,\n" +
" \"tlsVerifyCert\": false,\n" +
" \"tn3270e\": true,\n" +
" \"graphicsMode\": \"BOTH\",\n" +
" \"codePage\": \"037\",\n" +
" \"proxyType\": \"SOCKS5\",\n" +
" \"proxyHost\": \"10.20.30.40\",\n" +
" \"proxyPort\": 9050,\n" +
" \"proxyUsername\": \"dallasuser\",\n" +
" \"proxyPassword\": \"dallaspass\"\n" +
" }\n" +
"]";
List<SavedHost> parsed = HostStorage.parseHostsJson(a3270Json);
assertEquals(1, parsed.size());
SavedHost h = parsed.get(0);
assertEquals("android-uuid-1", h.getId());
assertEquals("Dallas Mainframe", h.getName());
assertEquals("dallas.ibm.com", h.getHost());
assertEquals(2323, h.getPort());
assertEquals(4, h.getModel());
assertTrue(h.isUseTls());
assertFalse(h.isTlsVerifyCert());
assertEquals("SOCKS5", h.getProxyType());
assertEquals("10.20.30.40", h.getProxyHost());
assertEquals(9050, h.getProxyPort());
assertEquals("dallasuser", h.getProxyUsername());
assertEquals("dallaspass", h.getProxyPassword());
// Roundtrip serialization
String serialized = HostStorage.serializeHostsJson(parsed);
List<SavedHost> roundtripped = HostStorage.parseHostsJson(serialized);
assertEquals(1, roundtripped.size());
SavedHost r = roundtripped.get(0);
assertEquals(h.getId(), r.getId());
assertEquals(h.getProxyType(), r.getProxyType());
assertEquals(h.getProxyHost(), r.getProxyHost());
assertEquals(h.getProxyPort(), r.getProxyPort());
assertEquals(h.getProxyUsername(), r.getProxyUsername());
assertEquals(h.getProxyPassword(), r.getProxyPassword());
}
@Test
@DisplayName("ConnectDialog loads profile proxy and applies per-connection proxy to ConnectionConfig")
public void testConnectDialogProxyHandling() {
if (!GraphicsEnvironment.isHeadless()) {
ConnectDialog dialog = new ConnectDialog((Frame) null);
dialog.setInitialHost("test.mainframe.com");
dialog.setInitialPort(23);
dialog.setInitialProxy(ConnectionConfig.ProxyType.SOCKS5, "127.0.0.1", 1080, "testuser", "testpass");
assertEquals(ConnectionConfig.ProxyType.SOCKS5, dialog.getSelectedProxyType());
assertEquals("127.0.0.1", dialog.getProxyHost());
assertEquals(1080, dialog.getProxyPort());
assertEquals("testuser", dialog.getProxyUsername());
assertEquals("testpass", dialog.getProxyPassword());
// Loading a profile updates dialog proxy fields
SavedHost profile = new SavedHost(
"p1", "Profile 1", "prod.ibm.net", 992, 4, 43, 80, "", false,
"TSO", true, true, true, "BOTH", "037",
"HTTP", "proxy.corp.net", 8080, "corpuser", "corppass"
);
dialog.loadProfile(profile);
assertEquals(ConnectionConfig.ProxyType.HTTP, dialog.getSelectedProxyType());
assertEquals("proxy.corp.net", dialog.getProxyHost());
assertEquals(8080, dialog.getProxyPort());
assertEquals("corpuser", dialog.getProxyUsername());
assertEquals("corppass", dialog.getProxyPassword());
dialog.dispose();
}
}
@Test
@DisplayName("SavedHost preserves all GraphicsMode settings across JSON serialization without clobbering to BOTH")
public void testSavedHostGraphicsModeSerializationRoundtrip() {
for (haus.nightmare.lib3270j.graphics.GraphicsMode gm : haus.nightmare.lib3270j.graphics.GraphicsMode.values()) {
SavedHost host = new SavedHost(
UUID.randomUUID().toString(),
"Gfx " + gm.name(),
"test.host.com",
23,
4,
24,
80,
"",
false,
"TSO",
false,
false,
true,
gm.name(),
"037",
"NONE",
"",
0,
"",
""
);
String json = HostStorage.serializeHostsJson(List.of(host));
List<SavedHost> parsed = HostStorage.parseHostsJson(json);
assertEquals(1, parsed.size());
assertEquals(gm.name(), parsed.get(0).getGraphicsMode(),
"Graphics mode " + gm.name() + " must be preserved and not coerced to BOTH");
assertEquals(gm, parsed.get(0).toConnectionConfig().getGraphicsMode(),
"ConnectionConfig must reflect GraphicsMode." + gm.name());
}
}
}
@@ -0,0 +1,242 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import haus.nightmare.j3270.storage.HostStorage;
import haus.nightmare.j3270.storage.SavedHost;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class AutoConnectSettingsAndTerminalCenteringTest {
private List<SavedHost> originalSavedHosts;
private Settings.StartupBehavior originalStartupBehavior;
private String originalAutoConnectHost;
private int originalAutoConnectPort;
@BeforeEach
public void setUp() {
originalSavedHosts = new ArrayList<>(HostStorage.getSavedHosts());
originalStartupBehavior = Settings.getStartupBehavior();
originalAutoConnectHost = Settings.getAutoConnectHost();
originalAutoConnectPort = Settings.getAutoConnectPort();
}
@AfterEach
public void tearDown() {
HostStorage.saveAll(originalSavedHosts);
Settings.setStartupBehavior(originalStartupBehavior);
Settings.setAutoConnectHost(originalAutoConnectHost);
Settings.setAutoConnectPort(originalAutoConnectPort);
}
@Test
@DisplayName("TerminalPanel centering: left bound is flush when constrained, centered with left <= right when too big")
public void testTerminalPanelCenteringLogic() {
TerminalPanel panel = new TerminalPanel();
panel.cellWidth = 10;
panel.cellHeight = 20;
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
panel.setClient(client);
int cols = sb.getDisplayCols(); // 80
int rows = sb.getDisplayRows(); // 24
int gridW = cols * panel.cellWidth; // 800
int gridH = rows * panel.cellHeight; // 480
// 1. Panel is smaller than grid: left bound must be 0 (flush left cells), vertical must be 0
panel.setSize(gridW - 100, gridH - 50);
assertEquals(0, panel.getRenderOffsetX(), "Left bound should be 0 when window width is constrained");
assertEquals(0, panel.getRenderOffsetY(), "Top bound should be 0 when window height is constrained");
// 2. Panel exactly matches grid: offset must be 0
panel.setSize(gridW, gridH);
assertEquals(0, panel.getRenderOffsetX(), "Offset X must be 0 when width exactly matches grid");
assertEquals(0, panel.getRenderOffsetY(), "Offset Y must be 0 when height exactly matches grid");
// 3. Panel is larger with odd extra (extra = 7)
panel.setSize(gridW + 7, gridH + 11);
int ox = panel.getRenderOffsetX();
int leftSpace = ox;
int rightSpace = (gridW + 7) - (ox + gridW);
assertTrue(leftSpace <= rightSpace, "Left blank space (" + leftSpace + ") must be <= right space (" + rightSpace + ")");
assertEquals(3, leftSpace);
assertEquals(4, rightSpace);
int oy = panel.getRenderOffsetY();
int topSpace = oy;
int bottomSpace = (gridH + 11) - (oy + gridH);
assertTrue(topSpace <= bottomSpace, "Top space must be <= bottom space");
assertEquals(5, topSpace);
assertEquals(6, bottomSpace);
// 4. Panel is larger with even extra (extra = 6)
panel.setSize(gridW + 6, gridH + 8);
assertEquals(3, panel.getRenderOffsetX());
assertEquals(4, panel.getRenderOffsetY());
panel.dispose();
}
@Test
@DisplayName("guardedPack does not resize window when window is too small")
public void testGuardedPackDoesNotResizeWhenTooSmall() {
try {
JFrame frame = new JFrame();
TerminalPanel panel = new TerminalPanel();
panel.cellWidth = 10;
panel.cellHeight = 20;
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
panel.setClient(client);
frame.getContentPane().add(panel);
frame.setSize(400, 300);
frame.addNotify(); // makes frame displayable
// Set size explicitly smaller than preferred size (800+ x 480+)
frame.setSize(400, 300);
Dimension sizeBefore = frame.getSize();
// Run guardedPack
panel.guardedPack();
// Window should not have expanded
Dimension sizeAfter = frame.getSize();
assertEquals(sizeBefore.width, sizeAfter.width, "Window width should NOT be enlarged when too small");
assertEquals(sizeBefore.height, sizeAfter.height, "Window height should NOT be enlarged when too small");
frame.dispose();
panel.dispose();
} catch (HeadlessException ignored) {
// Environment without X display
}
}
@Test
@DisplayName("SettingsDialog Startup & Auto-Connect presents saved connections dropdown and updates auto-connect host")
public void testSettingsDialogSavedConnectionsDropdown() throws Exception {
SavedHost host1 = new SavedHost("id-test-1", "Host Alpha", "alpha.mainframe.test", 23, 2, 24, 80, "LU1", true, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
SavedHost host2 = new SavedHost("id-test-2", "Host Beta", "beta.mainframe.test", 992, 4, 43, 80, "LU2", false, "VM", true, false, true, "BOTH", "037", "NONE", "", 0, "", "");
List<SavedHost> testHosts = new ArrayList<>();
testHosts.add(host1);
testHosts.add(host2);
HostStorage.saveAll(testHosts);
Settings.setStartupBehavior(Settings.StartupBehavior.AUTO_CONNECT);
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
JComboBox<SavedHost> combo = dialog.getSavedHostBox();
assertNotNull(combo, "savedHostBox should not be null");
assertEquals(2, combo.getItemCount(), "Dropdown should contain all saved hosts");
// Host Alpha was autoConnect=true, so it should be initially selected
SavedHost initialSel = (SavedHost) combo.getSelectedItem();
assertNotNull(initialSel);
assertEquals("id-test-1", initialSel.getId());
// Switch to Host Beta
combo.setSelectedIndex(1);
// Trigger applySettings via reflection
java.lang.reflect.Method applyMethod = SettingsDialog.class.getDeclaredMethod("applySettings");
applyMethod.setAccessible(true);
applyMethod.invoke(dialog);
// Verify Host Beta is now the auto-connect host
SavedHost auto = HostStorage.getAutoConnectHost();
assertNotNull(auto, "Auto-connect host should exist");
assertEquals("id-test-2", auto.getId());
assertEquals("Host Beta", auto.getName());
assertEquals("beta.mainframe.test", auto.getHost());
assertEquals(992, auto.getPort());
SavedHost loadedAlpha = HostStorage.findById("id-test-1");
assertNotNull(loadedAlpha);
assertFalse(loadedAlpha.isAutoConnect(), "Host Alpha should no longer have autoConnect = true");
// Verify Settings autoConnectHost/Port synchronized
assertEquals("beta.mainframe.test", Settings.getAutoConnectHost());
assertEquals(992, Settings.getAutoConnectPort());
// Switch startup behavior to SHOW_CONNECT and apply
Field sbField = SettingsDialog.class.getDeclaredField("startupBehaviorBox");
sbField.setAccessible(true);
@SuppressWarnings("unchecked")
JComboBox<Settings.StartupBehavior> sbBox = (JComboBox<Settings.StartupBehavior>) sbField.get(dialog);
sbBox.setSelectedItem(Settings.StartupBehavior.SHOW_CONNECT);
applyMethod.invoke(dialog);
assertNull(HostStorage.getAutoConnectHost(), "When startup behavior is SHOW_CONNECT, auto-connect host should be cleared");
dialog.dispose();
app.dispose();
} finally {
HostStorage.saveAll(originalSavedHosts);
}
}
@Test
@DisplayName("Window size is adjusted once on connect for selected model and left untouched on onScreenSizeChanged")
public void testWindowSizedOnceForModelAndNotOnScreenSizeChanged() {
try {
JFrame frame = new JFrame();
TerminalPanel panel = new TerminalPanel();
panel.cellWidth = 10;
panel.cellHeight = 20;
frame.getContentPane().add(panel);
frame.addNotify();
// Pack for Model 4 (43 rows, 80 cols)
panel.packForModel(43, 80);
Dimension model4Size = frame.getSize();
assertTrue(model4Size.height >= 43 * 20, "Height should accommodate 43 rows");
assertTrue(model4Size.width >= 80 * 10, "Width should accommodate 80 cols");
// Pack for Model 5 (27 rows, 132 cols)
panel.packForModel(27, 132);
Dimension model5Size = frame.getSize();
assertTrue(model5Size.width >= 132 * 10, "Width should accommodate 132 cols");
// Set custom size and verify guardedPack leaves the window alone
frame.setSize(600, 400);
Dimension fixedSize = frame.getSize();
panel.guardedPack();
assertEquals(fixedSize.width, frame.getWidth());
assertEquals(fixedSize.height, frame.getHeight());
frame.dispose();
panel.dispose();
} catch (HeadlessException ignored) {}
}
}
@@ -20,6 +20,7 @@ public class BehaviorSettingsTest {
private int origReconnectMaxRetries;
private boolean origInputMask;
private String origInputMaskChar;
private boolean origBlockPaste;
@BeforeEach
public void setUp() {
@@ -27,6 +28,7 @@ public class BehaviorSettingsTest {
origReconnectMaxRetries = Settings.getAutoConnectReconnectMaxRetries();
origInputMask = Settings.getInputMask();
origInputMaskChar = Settings.getInputMaskChar();
origBlockPaste = Settings.getBlockPaste();
}
@AfterEach
@@ -35,6 +37,7 @@ public class BehaviorSettingsTest {
Settings.setAutoConnectReconnectMaxRetries(origReconnectMaxRetries);
Settings.setInputMask(origInputMask);
Settings.setInputMaskChar(origInputMaskChar);
Settings.setBlockPaste(origBlockPaste);
}
@Test
@@ -74,6 +77,39 @@ public class BehaviorSettingsTest {
assertEquals("*", Settings.getInputMaskChar());
}
@Test
@DisplayName("Block paste setting defaults to true and binds to SettingsDialog")
public void testBlockPasteSettingAndDialogBinding() throws Exception {
// Default must be true
assertTrue(Settings.getBlockPaste(), "Settings.getBlockPaste() should default to true");
Settings.setBlockPaste(false);
assertFalse(Settings.getBlockPaste());
Settings.setBlockPaste(true);
assertTrue(Settings.getBlockPaste());
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
Field blockPasteField = SettingsDialog.class.getDeclaredField("blockPasteCheck");
blockPasteField.setAccessible(true);
JCheckBox blockPasteCheck = (JCheckBox) blockPasteField.get(dialog);
assertNotNull(blockPasteCheck, "blockPasteCheck must exist in SettingsDialog");
assertEquals("Block paste", blockPasteCheck.getText());
assertEquals(Settings.getBlockPaste(), blockPasteCheck.isSelected());
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("INI export and load preserves autoReconnect and inputMask")
public void testIniExportAndLoad() throws Exception {
@@ -173,4 +209,90 @@ public class BehaviorSettingsTest {
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("SettingsDialog tabs are organized into Appearance, Behavior, Connection, and Entry Assist")
public void testSettingsDialogReorganizedTabsAndGrouping() throws Exception {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
// Find top-level JTabbedPane
JTabbedPane mainTabs = null;
for (Component c : dialog.getContentPane().getComponents()) {
if (c instanceof JTabbedPane) {
mainTabs = (JTabbedPane) c;
break;
}
}
assertNotNull(mainTabs, "Main JTabbedPane must exist");
assertEquals(4, mainTabs.getTabCount(), "Must have 4 main tabs");
assertEquals("Appearance", mainTabs.getTitleAt(0));
assertEquals("Behavior", mainTabs.getTitleAt(1));
assertEquals("Connection", mainTabs.getTitleAt(2));
assertEquals("Entry Assist", mainTabs.getTitleAt(3));
// Verify Appearance subtabs: Display & Font, Colors
Component appComp = mainTabs.getComponentAt(0);
assertTrue(appComp instanceof JPanel);
JTabbedPane appTabs = findTabbedPane((JPanel) appComp);
assertNotNull(appTabs, "Appearance panel must have subtabs");
assertEquals(2, appTabs.getTabCount());
assertEquals("Display & Font", appTabs.getTitleAt(0));
assertEquals("Colors", appTabs.getTitleAt(1));
// Verify Behavior subtabs: General, Keymap
Component behComp = mainTabs.getComponentAt(1);
assertTrue(behComp instanceof JPanel);
JTabbedPane behTabs = findTabbedPane((JPanel) behComp);
assertNotNull(behTabs, "Behavior panel must have subtabs");
assertEquals(2, behTabs.getTabCount());
assertEquals("General", behTabs.getTitleAt(0));
assertEquals("Keymap", behTabs.getTitleAt(1));
// Verify controls exist in SettingsDialog
Field cursorStyleField = SettingsDialog.class.getDeclaredField("cursorStyleBox");
cursorStyleField.setAccessible(true);
assertNotNull(cursorStyleField.get(dialog), "cursorStyleBox must exist");
Field crosshairField = SettingsDialog.class.getDeclaredField("crosshairRulerCheck");
crosshairField.setAccessible(true);
assertNotNull(crosshairField.get(dialog), "crosshairRulerCheck must exist");
Field fourColorField = SettingsDialog.class.getDeclaredField("fourColorOverrideCheck");
fourColorField.setAccessible(true);
assertNotNull(fourColorField.get(dialog), "fourColorOverrideCheck must exist");
Field numericLockField = SettingsDialog.class.getDeclaredField("numericFieldLockCheck");
numericLockField.setAccessible(true);
assertNotNull(numericLockField.get(dialog), "numericFieldLockCheck must exist");
Field autoSkipField = SettingsDialog.class.getDeclaredField("autoSkipCheck");
autoSkipField.setAccessible(true);
assertNotNull(autoSkipField.get(dialog), "autoSkipCheck must exist");
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
private static JTabbedPane findTabbedPane(Container container) {
for (Component c : container.getComponents()) {
if (c instanceof JTabbedPane) {
return (JTabbedPane) c;
}
if (c instanceof Container) {
JTabbedPane nested = findTabbedPane((Container) c);
if (nested != null) return nested;
}
}
return null;
}
}
@@ -0,0 +1,153 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.*;
import java.awt.event.InputMethodEvent;
import java.text.AttributedString;
import static org.junit.jupiter.api.Assertions.*;
public class DBCSInputAndClipboardTest {
@Test
@DisplayName("Clipboard selection skips DB_RIGHT cells to avoid duplicating wide characters")
public void testDBCSClipboardCopyNoDoubling() {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
ConnectionConfig config = new ConnectionConfig("localhost", 23);
config.setCodePage("930");
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Put a 2-cell DBCS character at cells 0 and 1, followed by SBCS 'A' at cell 2
ExtendedAttribute cell0 = sb.getCell(0);
cell0.db = ExtendedAttribute.DB_LEFT;
cell0.ucs4 = '\u3000';
ExtendedAttribute cell1 = sb.getCell(1);
cell1.db = ExtendedAttribute.DB_RIGHT;
cell1.ucs4 = '\u3000';
ExtendedAttribute cell2 = sb.getCell(2);
cell2.db = ExtendedAttribute.DB_NONE;
cell2.ucs4 = 'A';
panel.setClient(client);
// Stream selection covering cells 0..2 (row 0, col 0..2)
panel.select(0, 0, 0, 2);
String selected = panel.getSelectedText();
assertEquals("\u3000A", selected, "Stream selection must not duplicate the DBCS character");
}
@Test
@DisplayName("Block selection skips DB_RIGHT cells to avoid duplicating wide characters")
public void testDBCSBlockSelectionNoDoubling() {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
panel.setBlockSelectMode(true);
ConnectionConfig config = new ConnectionConfig("localhost", 23);
config.setCodePage("930");
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Put a 2-cell DBCS character at cells 0 and 1, followed by SBCS 'B' at cell 2
ExtendedAttribute cell0 = sb.getCell(0);
cell0.db = ExtendedAttribute.DB_LEFT;
cell0.ucs4 = '\u3000';
ExtendedAttribute cell1 = sb.getCell(1);
cell1.db = ExtendedAttribute.DB_RIGHT;
cell1.ucs4 = '\u3000';
ExtendedAttribute cell2 = sb.getCell(2);
cell2.db = ExtendedAttribute.DB_NONE;
cell2.ucs4 = 'B';
panel.setClient(client);
panel.select(0, 0, 0, 2);
String selected = panel.getSelectedText();
assertEquals("\u3000B", selected, "Block selection must not duplicate the DBCS character");
}
@Test
@DisplayName("InputMethodRequests returns cursor bounds and processes committed text")
public void testDBCSInputMethodRequestsAndEvents() {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
ConnectionConfig config = new ConnectionConfig("localhost", 23);
config.setCodePage("930");
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
panel.setClient(client);
assertNotNull(panel.getInputMethodRequests(), "getInputMethodRequests must return this");
Rectangle loc = panel.getTextLocation(null);
assertNotNull(loc, "Text location must not be null");
assertTrue(loc.width > 0, "Cursor width must be positive");
assertTrue(loc.height > 0, "Cursor height must be positive");
// Simulate committed IME text '\u3000'
AttributedString as = new AttributedString("\u3000");
InputMethodEvent imeEvent = new InputMethodEvent(panel, InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
as.getIterator(), 1, null, null);
panel.inputMethodTextChanged(imeEvent);
ScreenBuffer sb = client.getScreenBuffer();
assertEquals(ExtendedAttribute.DB_LEFT, sb.getCell(0).db);
assertEquals('\u3000', sb.getCell(0).ucs4);
assertEquals(ExtendedAttribute.DB_RIGHT, sb.getCell(1).db);
assertEquals('\u3000', sb.getCell(1).ucs4);
assertEquals(2, sb.getCursorAddress(), "Cursor should advance by 2 after typing 2-cell DBCS");
}
@Test
@DisplayName("InputProcessor backspace over DBCS character deletes both cells")
public void testInputProcessorDBCSTypingAndBackspace() {
ConnectionConfig config = new ConnectionConfig("localhost", 23);
config.setCodePage("930");
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
InputProcessor input = client.getInputProcessor();
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
sb.setCursorAddress(0);
// Type DBCS character
input.typeCharacter('\u3000');
assertEquals(2, sb.getCursorAddress());
assertEquals(ExtendedAttribute.DB_LEFT, sb.getCell(0).db);
assertEquals(ExtendedAttribute.DB_RIGHT, sb.getCell(1).db);
// Backspace over DBCS character
input.backspace();
assertEquals(0, sb.getCursorAddress(), "Cursor should back up 2 cells");
assertEquals(0, sb.getCell(0).ucs4, "Cell 0 should be deleted (null)");
assertEquals(0, sb.getCell(1).ucs4, "Cell 1 should be deleted (null)");
assertEquals(ExtendedAttribute.DB_NONE, sb.getCell(0).db);
assertEquals(ExtendedAttribute.DB_NONE, sb.getCell(1).db);
}
}
@@ -15,17 +15,23 @@ public class FontSettingsPersistenceTest {
private int originalFontSize;
private String originalFontFamily;
private int originalUiFontSize;
private String originalUiFontFamily;
@BeforeEach
public void setup() {
originalFontSize = Settings.getFontSize();
originalFontFamily = Settings.getFontFamily();
originalUiFontSize = Settings.getUiFontSize();
originalUiFontFamily = Settings.getUiFontFamily();
}
@AfterEach
public void tearDown() {
Settings.setFontSize(originalFontSize);
Settings.setFontFamily(originalFontFamily);
Settings.setUiFontSize(originalUiFontSize);
Settings.setUiFontFamily(originalUiFontFamily);
}
@Test
@@ -39,6 +45,76 @@ public class FontSettingsPersistenceTest {
assertEquals(18, Settings.getFontSize());
}
@Test
public void testUiFontSettingsIndependence() {
// Configure terminal fonts
Settings.setFontFamily("Courier");
Settings.setFontSize(20);
// Configure UI fonts
Settings.setUiFontFamily("Dialog");
Settings.setUiFontSize(15);
// Verify independent getters
assertEquals("Courier", Settings.getFontFamily());
assertEquals(20, Settings.getFontSize());
assertEquals("Dialog", Settings.getUiFontFamily());
assertEquals(15, Settings.getUiFontSize());
// Change UI font and ensure terminal font is untouched
Settings.setUiFontSize(18);
Settings.setUiFontFamily("SansSerif");
assertEquals("Courier", Settings.getFontFamily(), "Terminal font family must not change when UI font changes");
assertEquals(20, Settings.getFontSize(), "Terminal font size must not change when UI font changes");
assertEquals("SansSerif", Settings.getUiFontFamily());
assertEquals(18, Settings.getUiFontSize());
// Change terminal font and ensure UI font is untouched
Settings.setFontSize(24);
Settings.setFontFamily("IBM 3270");
assertEquals("SansSerif", Settings.getUiFontFamily(), "UI font family must not change when terminal font changes");
assertEquals(18, Settings.getUiFontSize(), "UI font size must not change when terminal font changes");
assertEquals("IBM 3270", Settings.getFontFamily());
assertEquals(24, Settings.getFontSize());
}
@Test
public void testUiFontIniImportExport() throws Exception {
java.io.File tempIni = java.io.File.createTempFile("j3270_ui_font_test", ".ini");
tempIni.deleteOnExit();
try {
try (java.io.PrintWriter pw = new java.io.PrintWriter(tempIni)) {
pw.println("[appearance]");
pw.println("uiFontFamily = Dialog");
pw.println("uiFontSize = 16");
pw.println("fontFamily = Monospaced");
pw.println("fontSize = 22");
}
Settings.loadFromIniFile(tempIni.getAbsolutePath());
assertEquals("Dialog", Settings.getUiFontFamily());
assertEquals(16, Settings.getUiFontSize());
assertEquals("Monospaced", Settings.getFontFamily());
assertEquals(22, Settings.getFontSize());
// Export to another file and check contents
java.io.File exportIni = java.io.File.createTempFile("j3270_ui_font_export", ".ini");
exportIni.deleteOnExit();
Settings.exportToIniFile(exportIni.getAbsolutePath());
String exported = new String(java.nio.file.Files.readAllBytes(exportIni.toPath()));
assertTrue(exported.contains("uiFontFamily = Dialog"), "Exported INI must contain uiFontFamily");
assertTrue(exported.contains("uiFontSize = 16"), "Exported INI must contain uiFontSize");
assertTrue(exported.contains("fontFamily = Monospaced"), "Exported INI must contain terminal fontFamily");
assertTrue(exported.contains("fontSize = 22"), "Exported INI must contain terminal fontSize");
} finally {
tempIni.delete();
}
}
@Test
public void testTerminalPanelResizeDoesNotResetFontSize() {
try {
@@ -77,4 +153,57 @@ public class FontSettingsPersistenceTest {
// In headless environment without display, Settings persistence is verified above
}
}
@Test
public void testSettingsDialogApplyDoesNotIncrementFontSize() throws Exception {
try {
Settings.setUiFontSize(16);
Settings.setFontSize(22);
Settings.setUiFontFamily("Dialog");
Settings.setFontFamily("Monospaced");
ThemeManager.applyUIManagerDefaults(ThemeManager.getTheme());
SettingsDialog dialog = new SettingsDialog(null);
java.lang.reflect.Method applyMethod = SettingsDialog.class.getDeclaredMethod("applySettings");
applyMethod.setAccessible(true);
// Find all JLabels in SettingsDialog
java.util.List<javax.swing.JLabel> labels = new java.util.ArrayList<>();
findLabelsRecursively(dialog.getContentPane(), labels);
assertFalse(labels.isEmpty(), "SettingsDialog must contain labels");
for (javax.swing.JLabel label : labels) {
assertEquals(16, label.getFont().getSize(),
"Label '" + label.getText() + "' must initially have size 16");
}
// Click Apply multiple times
for (int click = 1; click <= 5; click++) {
applyMethod.invoke(dialog);
assertEquals(16, Settings.getUiFontSize(), "Settings uiFontSize must stay 16");
assertEquals(22, Settings.getFontSize(), "Settings fontSize must stay 22");
for (javax.swing.JLabel label : labels) {
assertEquals(16, label.getFont().getSize(),
"Label '" + label.getText() + "' must remain at size 16 after click #" + click);
}
}
dialog.dispose();
} catch (HeadlessException e) {
// Ignored in headless environment if dialog creation fails
}
}
private void findLabelsRecursively(java.awt.Container container, java.util.List<javax.swing.JLabel> labels) {
for (java.awt.Component c : container.getComponents()) {
if (c instanceof javax.swing.JLabel) {
labels.add((javax.swing.JLabel) c);
} else if (c instanceof java.awt.Container) {
findLabelsRecursively((java.awt.Container) c, labels);
}
}
}
}
@@ -0,0 +1,294 @@
package haus.nightmare.j3270.ui;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for HotspotEngine (Phase 25.2).
* Verifies regex detection of URLs, PF keys, Enter, and menu choices,
* hit-testing, action strings, and word boundary isolation.
*/
public class HotspotEngineTest {
private HotspotEngine engine;
@BeforeEach
public void setUp() {
engine = new HotspotEngine();
}
@Test
@DisplayName("Detects HTTP, HTTPS, FTP, and WWW URLs with action strings")
public void testUrlDetection() {
String screen = "Visit https://ibm.com/support or http://example.org:8080/path?q=1 for help.\n" +
"Download from ftp://ftp.archive.org/pub/files or browse www.google.com.";
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 2, 80);
assertEquals(4, hotspots.size());
HotspotEngine.Hotspot h0 = hotspots.get(0);
assertEquals(HotspotEngine.HotspotType.URL, h0.getType());
assertEquals("https://ibm.com/support", h0.getDisplayText());
assertEquals("https://ibm.com/support", h0.getAction());
assertEquals(0, h0.getStartRow());
assertEquals(6, h0.getStartCol());
assertTrue(h0.contains(0, 10));
assertFalse(h0.contains(0, 5));
HotspotEngine.Hotspot h1 = hotspots.get(1);
assertEquals("http://example.org:8080/path?q=1", h1.getDisplayText());
assertEquals("http://example.org:8080/path?q=1", h1.getAction());
HotspotEngine.Hotspot h2 = hotspots.get(2);
assertEquals("ftp://ftp.archive.org/pub/files", h2.getDisplayText());
assertEquals("ftp://ftp.archive.org/pub/files", h2.getAction());
HotspotEngine.Hotspot h3 = hotspots.get(3);
assertEquals("www.google.com", h3.getDisplayText());
assertEquals("http://www.google.com", h3.getAction(), "www. URLs should have http:// prepended for action");
}
@Test
@DisplayName("Detects PF1..PF24, F1..F24, PA1..PA3, and [ENTER] function keys")
public void testFunctionKeyDetection() {
String screen = "PF1=Help PF3=Exit PF7=Up PF8=Down PF12=Cancel\n" +
"F1=Help F12=Cancel PA1=Abort [ENTER]=Continue";
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 2, 80);
assertEquals(9, hotspots.size());
// PF1
assertEquals(HotspotEngine.HotspotType.PF_KEY, hotspots.get(0).getType());
assertEquals("PF1", hotspots.get(0).getDisplayText());
assertEquals("[pf1]", hotspots.get(0).getAction());
// PF3
assertEquals("PF3", hotspots.get(1).getDisplayText());
assertEquals("[pf3]", hotspots.get(1).getAction());
// PF7
assertEquals("PF7", hotspots.get(2).getDisplayText());
assertEquals("[pf7]", hotspots.get(2).getAction());
// PF8
assertEquals("PF8", hotspots.get(3).getDisplayText());
assertEquals("[pf8]", hotspots.get(3).getAction());
// PF12
assertEquals("PF12", hotspots.get(4).getDisplayText());
assertEquals("[pf12]", hotspots.get(4).getAction());
// F1 -> [pf1]
assertEquals("F1", hotspots.get(5).getDisplayText());
assertEquals("[pf1]", hotspots.get(5).getAction());
// F12 -> [pf12]
assertEquals("F12", hotspots.get(6).getDisplayText());
assertEquals("[pf12]", hotspots.get(6).getAction());
// PA1 -> [pa1]
assertEquals(HotspotEngine.HotspotType.PF_KEY, hotspots.get(7).getType());
assertEquals("PA1", hotspots.get(7).getDisplayText());
assertEquals("[pa1]", hotspots.get(7).getAction());
// [ENTER] -> [enter]
assertEquals(HotspotEngine.HotspotType.ENTER, hotspots.get(8).getType());
assertEquals("[ENTER]", hotspots.get(8).getDisplayText());
assertEquals("[enter]", hotspots.get(8).getAction());
}
@Test
@DisplayName("Isolates function keys with strict word boundaries to avoid false positives")
public void testWordBoundaryIsolation() {
String screen = "STAFF1 MEMBER SPF12 FILE TRANSFER PROMPT";
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 1, 80);
// Neither STAFF1 nor SPF12 nor TRANSFER should be detected as PF1, F1, or F12
assertEquals(0, hotspots.size(), "Embedded letters in words should not trigger PF key hotspots");
}
@Test
@DisplayName("Detects numbered menu choices and arrow prompts")
public void testMenuChoiceDetection() {
String screen = " 1. User Settings\n" +
" 2. Session Profiles\n" +
"===> 3";
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 3, 80);
assertEquals(3, hotspots.size());
HotspotEngine.Hotspot m1 = hotspots.get(0);
assertEquals(HotspotEngine.HotspotType.MENU_CHOICE, m1.getType());
assertEquals("1.", m1.getDisplayText());
assertEquals("1[enter]", m1.getAction());
assertEquals(0, m1.getStartRow());
HotspotEngine.Hotspot m2 = hotspots.get(1);
assertEquals("2.", m2.getDisplayText());
assertEquals("2[enter]", m2.getAction());
assertEquals(1, m2.getStartRow());
HotspotEngine.Hotspot m3 = hotspots.get(2);
assertEquals("===> 3", m3.getDisplayText());
assertEquals("3[enter]", m3.getAction());
assertEquals(2, m3.getStartRow());
}
@Test
@DisplayName("Hit-testing finds hotspot under specific coordinate")
public void testFindHotspot() {
String screen = "PF3=Exit https://github.com ===> 1";
engine.scanText(screen, 1, 80);
// Under PF3 (cols 0..2)
HotspotEngine.Hotspot hPf = engine.findHotspot(0, 1);
assertNotNull(hPf);
assertEquals("PF3", hPf.getDisplayText());
// Space between hotspots
assertNull(engine.findHotspot(0, 5));
// Under URL (cols 11..28)
HotspotEngine.Hotspot hUrl = engine.findHotspot(0, 15);
assertNotNull(hUrl);
assertEquals("https://github.com", hUrl.getDisplayText());
// Under prompt (cols 32..37)
HotspotEngine.Hotspot hPrompt = engine.findHotspot(0, 34);
assertNotNull(hPrompt);
assertEquals("===> 1", hPrompt.getDisplayText());
}
@Test
@DisplayName("Toggling engine options enables or disables specific hotspot categories")
public void testTogglingCategories() {
String screen = "PF3=Exit https://example.com 1. Options";
engine.scanText(screen, 1, 80);
assertEquals(3, engine.getHotspots().size());
// Disable URLs
engine.setUrlEnabled(false);
List<HotspotEngine.Hotspot> noUrls = engine.scanText(screen, 1, 80);
assertEquals(2, noUrls.size());
assertFalse(noUrls.stream().anyMatch(h -> h.getType() == HotspotEngine.HotspotType.URL));
// Disable PF keys
engine.setPfKeyEnabled(false);
List<HotspotEngine.Hotspot> menuOnly = engine.scanText(screen, 1, 80);
assertEquals(1, menuOnly.size());
assertEquals(HotspotEngine.HotspotType.MENU_CHOICE, menuOnly.get(0).getType());
// Disable menu choices
engine.setMenuChoiceEnabled(false);
List<HotspotEngine.Hotspot> empty = engine.scanText(screen, 1, 80);
assertEquals(0, empty.size());
}
@Test
@DisplayName("Detects ISPF primary option menu choices (numbers, two-digit, and letters)")
public void testIspfPrimaryOptionMenuDetection() {
String screen =
" Menu Utilities Compilers Options Status Help\n" +
" ──────────────────────────────────────────────────────────────────────────────\n" +
" ISPF Primary Option Menu\n" +
" Option ===>\n" +
" \n" +
" 0 Settings Terminal and user parameters User ID . : RUDI\n" +
" 1 View Display source data or listings Time. . . : 12:31\n" +
" 2 Edit Create or change source data Terminal. : 3278\n" +
" 3 Utilities Perform utility functions Screen. . : 1\n" +
" 4 Foreground Interactive language processing Language. : ENGLISH\n" +
" 5 Batch Submit job for language processing Appl ID . : ISR\n" +
" 6 Command Enter TSO or Workstation commands TSO logon : DEVPROC\n" +
" 7 Dialog Test Perform dialog testing TSO prefix: RUDI\n" +
" 9 IBM Products IBM program development products System ID : VS01\n" +
" 10 SCLM SW Configuration Library Manager Release . : ISPF 7.5\n" +
" 11 Workplace ISPF Object/Action Workplace\n" +
" M More Additional workplace options\n" +
" X Exit Terminate ISPF using log and list defaults";
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 18, 80);
// 14 total hotspots: 13 ISPF menu choices (0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, M, X) + 1 [ENTER] key on row 11
assertEquals(14, hotspots.size());
List<HotspotEngine.Hotspot> menuChoices = hotspots.stream()
.filter(h -> h.getType() == HotspotEngine.HotspotType.MENU_CHOICE)
.collect(java.util.stream.Collectors.toList());
assertEquals(13, menuChoices.size());
// Option 0
HotspotEngine.Hotspot h0 = menuChoices.get(0);
assertEquals(HotspotEngine.HotspotType.MENU_CHOICE, h0.getType());
assertEquals("0 Settings", h0.getDisplayText());
assertEquals("0[enter]", h0.getAction());
assertEquals("Select option 0 (Settings)", h0.getTooltip());
assertEquals(5, h0.getStartRow());
// Option 1
HotspotEngine.Hotspot h1 = menuChoices.get(1);
assertEquals("1 View", h1.getDisplayText());
assertEquals("1[enter]", h1.getAction());
assertEquals("Select option 1 (View)", h1.getTooltip());
assertEquals(6, h1.getStartRow());
// Option 2
HotspotEngine.Hotspot h2 = menuChoices.get(2);
assertEquals("2 Edit", h2.getDisplayText());
assertEquals("2[enter]", h2.getAction());
assertEquals("Select option 2 (Edit)", h2.getTooltip());
// Option 10 (two-digit, index 9 after 0,1,2,3,4,5,6,7,9)
HotspotEngine.Hotspot h10 = menuChoices.get(9);
assertEquals("10 SCLM", h10.getDisplayText());
assertEquals("10[enter]", h10.getAction());
assertEquals("Select option 10 (SCLM)", h10.getTooltip());
// Option M (letter, index 11)
HotspotEngine.Hotspot hM = menuChoices.get(11);
assertEquals("M More", hM.getDisplayText());
assertEquals("M[enter]", hM.getAction());
assertEquals("Select option M (More)", hM.getTooltip());
// Option X (exit, index 12)
HotspotEngine.Hotspot hX = menuChoices.get(12);
assertEquals("X Exit", hX.getDisplayText());
assertEquals("X[enter]", hX.getAction());
assertEquals("Select option X (Exit)", hX.getTooltip());
}
@Test
@DisplayName("Detects ISPF dotted sub-options, dashes, and multi-column menus")
public void testIspfSubOptionsAndMultiColumn() {
String screen =
" 3.1 Library - Compress or print data set\n" +
" 3.2 DataSet - Allocate, rename, or delete\n" +
" 1 - View 2 - Edit";
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 3, 80);
assertEquals(4, hotspots.size());
HotspotEngine.Hotspot h31 = hotspots.get(0);
assertEquals("3.1 Library", h31.getDisplayText());
assertEquals("3.1[enter]", h31.getAction());
assertEquals("Select option 3.1 (Library)", h31.getTooltip());
HotspotEngine.Hotspot h32 = hotspots.get(1);
assertEquals("3.2 DataSet", h32.getDisplayText());
assertEquals("3.2[enter]", h32.getAction());
HotspotEngine.Hotspot h1 = hotspots.get(2);
assertEquals("1 - View", h1.getDisplayText());
assertEquals("1[enter]", h1.getAction());
HotspotEngine.Hotspot h2 = hotspots.get(3);
assertEquals("2 - Edit", h2.getDisplayText());
assertEquals("2[enter]", h2.getAction());
}
}
@@ -0,0 +1,157 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.*;
public class ImportConfigTest {
private String origFontFamily;
private int origFontSize;
private String origHost;
private int origPort;
private boolean origAutoReconnect;
@BeforeEach
public void setUp() {
origFontFamily = Settings.getFontFamily();
origFontSize = Settings.getFontSize();
origHost = Settings.getAutoConnectHost();
origPort = Settings.getAutoConnectPort();
origAutoReconnect = Settings.getAutoConnectAutoReconnect();
}
@AfterEach
public void tearDown() {
Settings.setFontFamily(origFontFamily);
Settings.setFontSize(origFontSize);
Settings.setAutoConnectHost(origHost);
Settings.setAutoConnectPort(origPort);
Settings.setAutoConnectAutoReconnect(origAutoReconnect);
}
@Test
@DisplayName("SettingsDialog contains Import Config button positioned to the left of Export Config")
public void testImportButtonPlacementAndProperties() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
JButton btnImport = dialog.getBtnImport();
JButton btnExport = dialog.getBtnExport();
assertNotNull(btnImport, "Import Config button must exist");
assertNotNull(btnExport, "Export Config button must exist");
assertEquals("Import Config...", btnImport.getText());
assertEquals("Export Config...", btnExport.getText());
// Verify btnImport is in the same container and positioned before (to the left of) btnExport
Container parent = btnImport.getParent();
assertNotNull(parent, "Import button must have a parent container");
assertSame(parent, btnExport.getParent(), "Import and Export buttons must share the same parent panel");
Component[] comps = parent.getComponents();
int importIdx = -1;
int exportIdx = -1;
for (int i = 0; i < comps.length; i++) {
if (comps[i] == btnImport) importIdx = i;
if (comps[i] == btnExport) exportIdx = i;
}
assertTrue(importIdx >= 0, "btnImport must be in parent container");
assertTrue(exportIdx >= 0, "btnExport must be in parent container");
assertTrue(importIdx < exportIdx, "btnImport must be placed to the left of btnExport (index " + importIdx + " < " + exportIdx + ")");
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("importConfigFile updates Settings and refreshes SettingsDialog controls")
public void testImportConfigFileUpdatesSettingsAndDialog() throws IOException {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
File tempIni = File.createTempFile("j3270_test_import", ".ini");
try {
try (FileWriter writer = new FileWriter(tempIni)) {
writer.write("[appearance]\n");
writer.write("fontSize = 24\n");
writer.write("fontFamily = Monospaced\n");
writer.write("\n[behavior]\n");
writer.write("startupBehavior = AUTO_CONNECT\n");
writer.write("autoConnectHost = testhost.example.org\n");
writer.write("autoConnectPort = 2323\n");
writer.write("autoConnectAutoReconnect = true\n");
}
SettingsDialog dialog = new SettingsDialog(app);
dialog.importConfigFile(tempIni);
// Verify Settings updated
assertEquals(24, Settings.getFontSize());
assertEquals("testhost.example.org", Settings.getAutoConnectHost());
assertEquals(2323, Settings.getAutoConnectPort());
assertTrue(Settings.getAutoConnectAutoReconnect());
// Verify dialog state reflects the imported values
assertEquals(Settings.StartupBehavior.AUTO_CONNECT, Settings.getStartupBehavior());
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
} finally {
if (tempIni.exists()) {
tempIni.delete();
}
}
}
@Test
@DisplayName("importConfigFile throws FileNotFoundException for nonexistent file")
public void testImportNonExistentFileThrows() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
File nonExistent = new File("/path/to/nonexistent/file_" + System.currentTimeMillis() + ".ini");
assertThrows(FileNotFoundException.class, () -> {
dialog.importConfigFile(nonExistent);
});
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
}
@@ -0,0 +1,265 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.config.Settings;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.Cursor;
import java.awt.HeadlessException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.LinkedHashMap;
import java.util.Map;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class SelectionHandlesAndTrimTest {
private boolean origRemainAfterEdit;
private boolean origSizingHandles;
@BeforeEach
public void setUp() {
origRemainAfterEdit = Settings.getTrimRectRemainAfterEdit();
origSizingHandles = Settings.getTrimRectSizingHandles();
}
@AfterEach
public void tearDown() {
Settings.setTrimRectRemainAfterEdit(origRemainAfterEdit);
Settings.setTrimRectSizingHandles(origSizingHandles);
}
private TerminalPanel createTestPanel() {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
panel.cellWidth = 10;
panel.cellHeight = 20;
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
panel.setClient(client);
return panel;
}
@Test
@DisplayName("Trailing whitespace is trimmed on copied lines in block selection mode")
public void testBlockSelectionTrailingWhitespaceTrim() {
try {
TerminalPanel panel = createTestPanel();
ScreenBuffer sb = panel.getClient().getScreenBuffer();
int cols = sb.getCols();
// Populate row 0: "HELLO "
String text0 = "HELLO";
for (int i = 0; i < text0.length(); i++) {
sb.getCell(i).ucs4 = text0.charAt(i);
}
// Populate row 1: "WORLD 123 "
String text1 = "WORLD 123";
for (int i = 0; i < text1.length(); i++) {
sb.getCell(cols + i).ucs4 = text1.charAt(i);
}
// Select rectangle from (row 0, col 0) to (row 1, col 20)
panel.setBlockSelectMode(true);
panel.setSelectionRange(0, cols + 20);
String copied = panel.getSelectedText();
// Should be "HELLO\nWORLD 123" without the trailing spaces up to col 20
assertEquals("HELLO\nWORLD 123", copied);
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("Trailing whitespace is trimmed on copied lines in stream selection mode")
public void testStreamSelectionTrailingWhitespaceTrim() {
try {
TerminalPanel panel = createTestPanel();
ScreenBuffer sb = panel.getClient().getScreenBuffer();
int cols = sb.getCols();
// Populate row 0: "LINE1 "
String text0 = "LINE1";
for (int i = 0; i < text0.length(); i++) {
sb.getCell(i).ucs4 = text0.charAt(i);
}
// Populate row 1: "LINE2"
String text1 = "LINE2";
for (int i = 0; i < text1.length(); i++) {
sb.getCell(cols + i).ucs4 = text1.charAt(i);
}
panel.setBlockSelectMode(false);
panel.setTrimRectSizingHandles(false);
// Select from row 0, col 0 to row 1, col 10
panel.setSelectionRange(0, cols + 10);
String copied = panel.getSelectedText();
assertEquals("LINE1\nLINE2", copied);
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("Drag handle detection identifies 8 resize handles, MOVE inside box, and NONE outside")
public void testDragHandleDetection() {
try {
TerminalPanel panel = createTestPanel();
panel.setBlockSelectMode(true);
panel.setTrimRectSizingHandles(true);
// Without selection, should return NONE
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(100, 100));
// Select rectangle: rows 2 to 5, cols 10 to 20
ScreenBuffer sb = panel.getClient().getScreenBuffer();
int cols = sb.getCols();
panel.setSelectionRange(2 * cols + 10, 5 * cols + 20);
int ox = panel.getRenderOffsetX();
int oy = panel.getRenderOffsetY();
int cw = panel.cellWidth;
int ch = panel.cellHeight;
int x1 = ox + 10 * cw;
int y1 = oy + 2 * ch;
int x2 = ox + 21 * cw;
int y2 = oy + 6 * ch;
int xm = (x1 + x2) / 2;
int ym = (y1 + y2) / 2;
// Test 8 handles
assertEquals(TerminalPanel.DragHandle.NW, panel.getDragHandleAt(x1, y1));
assertEquals(TerminalPanel.DragHandle.N, panel.getDragHandleAt(xm, y1));
assertEquals(TerminalPanel.DragHandle.NE, panel.getDragHandleAt(x2, y1));
assertEquals(TerminalPanel.DragHandle.E, panel.getDragHandleAt(x2, ym));
assertEquals(TerminalPanel.DragHandle.SE, panel.getDragHandleAt(x2, y2));
assertEquals(TerminalPanel.DragHandle.S, panel.getDragHandleAt(xm, y2));
assertEquals(TerminalPanel.DragHandle.SW, panel.getDragHandleAt(x1, y2));
assertEquals(TerminalPanel.DragHandle.W, panel.getDragHandleAt(x1, ym));
// Test inside box -> MOVE
assertEquals(TerminalPanel.DragHandle.MOVE, panel.getDragHandleAt(xm, ym));
// Test far outside -> NONE
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(0, 0));
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(x2 + 50, y2 + 50));
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("Cursor mapping for all drag handles")
public void testCursorForDragHandles() {
try {
TerminalPanel panel = createTestPanel();
assertEquals(Cursor.NW_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NW).getType());
assertEquals(Cursor.N_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.N).getType());
assertEquals(Cursor.NE_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NE).getType());
assertEquals(Cursor.E_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.E).getType());
assertEquals(Cursor.SE_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.SE).getType());
assertEquals(Cursor.S_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.S).getType());
assertEquals(Cursor.SW_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.SW).getType());
assertEquals(Cursor.W_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.W).getType());
assertEquals(Cursor.MOVE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.MOVE).getType());
assertEquals(Cursor.DEFAULT_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NONE).getType());
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("trimRectRemainAfterEdit controls whether selection is retained after copy")
public void testTrimRectRemainAfterEdit() {
try {
TerminalPanel panel = createTestPanel();
ScreenBuffer sb = panel.getClient().getScreenBuffer();
sb.getCell(0).ucs4 = 'A';
panel.setSelectionRange(0, 5);
assertTrue(panel.hasSelection());
// By default (false), copySelection() should clear the selection
Settings.setTrimRectRemainAfterEdit(false);
panel.copySelection();
assertFalse(panel.hasSelection(), "Selection should be cleared after copy when remainAfterEdit is false");
// When set to true, copySelection() should preserve the selection
Settings.setTrimRectRemainAfterEdit(true);
panel.setSelectionRange(0, 5);
assertTrue(panel.hasSelection());
panel.copySelection();
assertTrue(panel.hasSelection(), "Selection should remain active after copy when remainAfterEdit is true");
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("cutSelection clears unprotected cells, sets FA_MODIFY, and respects remainAfterEdit")
public void testCutSelectionBehavior() {
try {
TerminalPanel panel = createTestPanel();
ScreenBuffer sb = panel.getClient().getScreenBuffer();
// Field at 0: Unprotected field
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
String data = "TESTDATA";
for (int i = 0; i < data.length(); i++) {
sb.getCell(1 + i).ucs4 = data.charAt(i);
}
// Select the field contents
panel.setSelectionRange(1, 8);
assertTrue(panel.hasSelection());
Settings.setTrimRectRemainAfterEdit(false);
panel.cutSelection();
// Characters should be replaced with spaces
for (int i = 1; i <= 8; i++) {
assertEquals(' ', sb.getCell(i).ucs4, "Cell " + i + " should be erased to space");
}
// FA_MODIFY should remain or be set
assertTrue((sb.getCell(0).ec & FA_MODIFY) != 0, "FA_MODIFY should be set on field attribute");
assertFalse(panel.hasSelection(), "Selection should be cleared when remainAfterEdit is false");
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("KeyBindings exportKmp generates HoD format and round-trips through parseKmp")
public void testKeyBindingsExportAndParseRoundTrip() throws Exception {
Map<String, String> keymap = new LinkedHashMap<>();
keymap.put("ENTER", "ENTER");
keymap.put("shift TAB", "TAB");
keymap.put("CLEAR", "PAUSE");
keymap.put("PF1", "F1");
keymap.put("PF13", "shift F1");
keymap.put("ESCAPE", "ESCAPE");
StringWriter writer = new StringWriter();
KeyBindings.exportKmp(writer, keymap);
String exported = writer.toString();
assertTrue(exported.contains("[KeyRemap]"), "Exported KMP must contain [KeyRemap] section header");
assertTrue(exported.contains("[enter]"), "Exported KMP must contain HoD mnemonics");
assertTrue(exported.contains("[backtab]"), "Exported KMP must contain [backtab]");
// Roundtrip parse
Map<String, String> parsed = KeyBindings.parseKmp(new StringReader(exported));
assertNotNull(parsed);
assertEquals("ENTER", parsed.get("ENTER"));
assertEquals("TAB", parsed.get("shift TAB"));
assertEquals("PAUSE", parsed.get("CLEAR"));
assertEquals("F1", parsed.get("PF1"));
assertEquals("shift F1", parsed.get("PF13"));
assertEquals("ESCAPE", parsed.get("ESCAPE"));
}
}
@@ -59,4 +59,49 @@ public class StatusBarTest {
// Ignored in headless environments
}
}
@Test
public void testStatusBarXSystemAndXProt() {
try {
ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_2, false);
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(haus.nightmare.lib3270j.ConnectionState.CONNECTED_3270);
StatusBar statusBar = new StatusBar();
statusBar.setClient(client, null);
statusBar.updateStatus();
// Initially not locked
assertEquals("", statusBar.getLockStatusText());
// 1. Setup protected field and type into it -> triggers X PROT
client.getScreenBuffer().setFieldAttribute(0, (byte) haus.nightmare.lib3270j.protocol.DS3270Constants.FA_PROTECT);
client.getScreenBuffer().setCursorAddress(1);
client.typeCharacter('A');
statusBar.updateStatus();
assertEquals("X PROT", statusBar.getLockStatusText());
// 2. Reset -> clears X PROT
client.reset();
statusBar.updateStatus();
assertEquals("", statusBar.getLockStatusText());
// 3. Send Enter -> triggers X SYSTEM
client.sendEnter();
statusBar.updateStatus();
assertEquals("X SYSTEM", statusBar.getLockStatusText());
// 4. Host unlocks keyboard
client.getInputProcessor().setKeyboardLocked(false);
statusBar.updateStatus();
assertEquals("", statusBar.getLockStatusText());
// 5. Subsequent Enter -> triggers X SYSTEM again
client.sendEnter();
statusBar.updateStatus();
assertEquals("X SYSTEM", statusBar.getLockStatusText());
} catch (HeadlessException e) {
// Ignored in headless environments
}
}
}
@@ -0,0 +1,335 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.HeadlessException;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class TerminalPanelSelectionTest {
@Test
@DisplayName("Selection drag over protected/intensified fields is preserved on mouse release (Bugs 3 & 4)")
public void testSelectionOverProtectedFieldsAndReleasePreservation() {
try {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Put a protected, intensified selectable field at pos 0 (ISPF header simulation)
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_PROTECT | FA_INT_HIGH_SEL));
String headerText = " ISPF PRIMARY OPTION MENU";
for (int i = 0; i < headerText.length(); i++) {
sb.getCell(1 + i).ucs4 = headerText.charAt(i);
sb.getCell(1 + i).ec = (byte) (0x40 + i);
}
panel.setClient(client);
// Cell dimensions in panel
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
int ox = panel.getRenderOffsetX();
int oy = panel.getRenderOffsetY();
MouseListener[] mouseListeners = panel.getMouseListeners();
MouseMotionListener[] motionListeners = panel.getMouseMotionListeners();
assertTrue(mouseListeners.length > 0);
assertTrue(motionListeners.length > 0);
MouseListener ml = mouseListeners[0];
MouseMotionListener mml = motionListeners[0];
// 1. Mouse Pressed at cell (row 0, col 2)
int startX = ox + 2 * cellW + cellW / 2;
int startY = oy + cellH / 2;
MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
0, startX, startY, 1, false, MouseEvent.BUTTON1);
ml.mousePressed(pressEvent);
// 2. Drag to cell (row 0, col 15)
int dragX = ox + 15 * cellW + cellW / 2;
int dragY = oy + cellH / 2;
MouseEvent dragEvent = new MouseEvent(panel, MouseEvent.MOUSE_DRAGGED, System.currentTimeMillis(),
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
mml.mouseDragged(dragEvent);
assertTrue(panel.hasSelection(), "Drag over protected field must create active selection (Bug 3)");
// 3. Mouse Released at cell (row 0, col 15)
MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
ml.mouseReleased(releaseEvent);
// Selection MUST NOT be dropped upon mouse release! (Bug 4)
assertTrue(panel.hasSelection(), "Selection must remain active after mouse release (Bug 4)");
String selected = panel.getSelectedText();
assertNotNull(selected);
assertTrue(selected.length() > 0, "Selected text must not be empty");
// 4. A subsequent single click without drag should clear the selection and move the cursor
int clickX = ox + 5 * cellW + cellW / 2;
int clickY = oy + cellH / 2;
MouseEvent clickPress = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
ml.mousePressed(clickPress);
MouseEvent clickRelease = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
ml.mouseReleased(clickRelease);
assertFalse(panel.hasSelection(), "Single click must clear previous selection");
assertEquals(5, sb.getCursorAddress(), "Single click must position cursor at clicked cell");
} catch (HeadlessException e) {
// Handled gracefully in headless environments
}
}
@Test
@DisplayName("Mouse press and release on TerminalPanel requests focus")
public void testTerminalPanelMousePressRequestsFocus() {
try {
TerminalPanel panel = new TerminalPanel();
assertTrue(panel.isFocusable());
MouseListener[] mouseListeners = panel.getMouseListeners();
assertTrue(mouseListeners.length > 0);
MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
0, 50, 50, 1, false, MouseEvent.BUTTON1);
mouseListeners[0].mousePressed(pressEvent);
MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
0, 50, 50, 1, false, MouseEvent.BUTTON1);
mouseListeners[0].mouseReleased(releaseEvent);
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("Clicking an unprotected, intensified line (e.g. XEDIT current line) does not transmit AID_SELECT")
public void testClickOnUnprotectedIntensifiedFieldDoesNotTriggerAidSelect() {
try {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Row 3 (address 240): Unprotected, intensified line with spaces (XEDIT located line)
int row3Start = 3 * 80;
sb.setCellFA(row3Start, (byte) (FA_PRINTABLE | FA_INT_HIGH_SEL)); // Unprotected, high intensity (detectable)
// Space designator and COBOL text
String lineText = " IDENTIFICATION DIVISION.";
for (int i = 0; i < lineText.length(); i++) {
sb.getCell(row3Start + 1 + i).ucs4 = lineText.charAt(i);
sb.getCell(row3Start + 1 + i).ec = (byte) (lineText.charAt(i) == ' ' ? 0x40 : 0xC1 + i);
}
panel.setClient(client);
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
int ox = panel.getRenderOffsetX();
int oy = panel.getRenderOffsetY();
MouseListener[] mouseListeners = panel.getMouseListeners();
assertTrue(mouseListeners.length > 0);
MouseListener ml = mouseListeners[0];
// Click at row 3, col 28 (like in j3270.log)
int clickX = ox + 28 * cellW + cellW / 2;
int clickY = oy + 3 * cellH + cellH / 2;
MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
ml.mousePressed(pressEvent);
MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
ml.mouseReleased(releaseEvent);
// Cursor must move to clicked address without sending AID_SELECT (AID 0x7E)
assertEquals(row3Start + 28, sb.getCursorAddress(), "Cursor must be placed at clicked position");
assertEquals(AID_NO, client.getInputProcessor().getLastAid(), "No AID must be transmitted on single click");
// Text on line must NOT have been modified or cleared
assertEquals('I', (char) sb.getCell(row3Start + 8).ucs4);
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("Clicking a toggle field (? -> >) toggles the designator without transmitting AID")
public void testClickOnToggleField() {
try {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Field with ? designator
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL));
sb.getCell(1).ucs4 = '?';
sb.getCell(1).ec = (byte) 0x6F;
panel.setClient(client);
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
int ox = panel.getRenderOffsetX();
int oy = panel.getRenderOffsetY();
MouseListener ml = panel.getMouseListeners()[0];
int clickX = ox + 1 * cellW + cellW / 2;
int clickY = oy + cellH / 2;
ml.mousePressed(new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1));
ml.mouseReleased(new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1));
assertEquals('>', (char) sb.getCell(1).ucs4, "Toggle field ? should toggle to > on click");
assertEquals(AID_NO, client.getInputProcessor().getLastAid(), "No AID transmitted for deferred toggle field");
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("Clicking an unprotected space-designator field when lightPenMode is enabled transmits AID_SELECT")
public void testClickInLightPenModeTransmitsAidSelect() {
try {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Unprotected, high-intensity field with space designator
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_HIGH_SEL));
sb.getCell(1).ucs4 = ' ';
sb.getCell(1).ec = (byte) 0x40;
panel.setClient(client);
panel.toggleLightPen();
assertTrue(panel.isLightPenMode());
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
int ox = panel.getRenderOffsetX();
int oy = panel.getRenderOffsetY();
MouseListener ml = panel.getMouseListeners()[0];
int clickX = ox + 1 * cellW + cellW / 2;
int clickY = oy + cellH / 2;
ml.mousePressed(new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1));
ml.mouseReleased(new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1));
assertEquals(AID_SELECT, client.getInputProcessor().getLastAid(), "AID_SELECT must be transmitted in lightPenMode");
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("Clicking an ISPF menu choice navigates to home/option line, types choice, and sends AID_ENTER")
public void testClickOnIspfMenuChoiceSubmitsViaHomeAndEnter() {
try {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Row 3 (col 0): Protected label "Option ===> "
int row3Start = 3 * 80;
sb.setCellFA(row3Start, (byte) (FA_PRINTABLE | FA_PROTECT));
String promptText = "Option ===> ";
for (int i = 0; i < promptText.length(); i++) {
sb.getCell(row3Start + 1 + i).ucs4 = promptText.charAt(i);
sb.getCell(row3Start + 1 + i).ec = (byte) (0xC1 + i);
}
// Unprotected command input field after "Option ===> " at address 253 (row 3 col 13)
int inputFieldAddr = row3Start + 13;
sb.setCellFA(inputFieldAddr, (byte) FA_PRINTABLE); // Unprotected!
for (int i = 0; i < 20; i++) {
sb.getCell(inputFieldAddr + 1 + i).ucs4 = ' ';
sb.getCell(inputFieldAddr + 1 + i).ec = (byte) 0x40;
}
// Row 6 (address 480): Protected field with ISPF menu option " 1 View"
int row6Start = 6 * 80;
sb.setCellFA(row6Start, (byte) (FA_PRINTABLE | FA_PROTECT));
String menuText = " 1 View Display source data or listings";
for (int i = 0; i < menuText.length(); i++) {
sb.getCell(row6Start + 1 + i).ucs4 = menuText.charAt(i);
sb.getCell(row6Start + 1 + i).ec = (byte) (menuText.charAt(i) == ' ' ? 0x40 : 0xC1 + i);
}
sb.updateFormatted();
sb.updateDisplaySnapshot();
panel.setClient(client);
panel.setHotspotsDirty(true);
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
int ox = panel.getRenderOffsetX();
int oy = panel.getRenderOffsetY();
MouseListener ml = panel.getMouseListeners()[0];
// Click at row 6, col 2 (on the "1" in " 1 View")
int clickX = ox + 2 * cellW + cellW / 2;
int clickY = oy + 6 * cellH + cellH / 2;
ml.mousePressed(new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1));
ml.mouseReleased(new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1));
// AID_ENTER must have been transmitted
assertEquals(AID_ENTER, client.getInputProcessor().getLastAid(), "Clicking menu choice must transmit AID_ENTER");
// Keyboard lock should be standard system lock awaiting host response, NOT protected field error
assertNotEquals(com.ibm.eNetwork.ECL.ECLConstants.INHIBIT_PROTECTED_FIELD, client.getOIA().getInputInhibited(),
"Keyboard must not be locked with protected field violation");
// Option "1" must have been typed into the unprotected command input field (address inputFieldAddr + 1)
assertEquals('1', (char) sb.getCell(inputFieldAddr + 1).ucs4, "Option '1' must be placed on the command/option line");
} catch (HeadlessException ignored) {
}
}
}
@@ -262,4 +262,81 @@ public class ThemeManagerTest {
// Handled in headless CI
}
}
@Test
public void testUiFontRetrievalAndDefaults() {
String prevFamily = Settings.getUiFontFamily();
int prevSize = Settings.getUiFontSize();
try {
Settings.setUiFontFamily("Dialog");
Settings.setUiFontSize(16);
Font uiFont = ThemeManager.getUiFont();
assertEquals("Dialog", uiFont.getFamily());
assertEquals(16, uiFont.getSize());
assertEquals(Font.PLAIN, uiFont.getStyle());
Font boldOffset = ThemeManager.getUiFont(Font.BOLD, 2);
assertEquals(18, boldOffset.getSize());
assertEquals(Font.BOLD, boldOffset.getStyle());
Font monoFont = ThemeManager.getMonospacedUiFont();
assertEquals(Font.MONOSPACED, monoFont.getName());
assertEquals(16, monoFont.getSize());
// Test applyUIManagerDefaults sets font properties
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
assertEquals(uiFont, UIManager.get("Button.font"));
assertEquals(uiFont, UIManager.get("Label.font"));
assertEquals(uiFont, UIManager.get("Menu.font"));
assertEquals(uiFont, UIManager.get("Table.font"));
assertEquals(monoFont, UIManager.get("TextArea.font"));
} finally {
Settings.setUiFontFamily(prevFamily);
Settings.setUiFontSize(prevSize);
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
}
}
@Test
public void testLabelFontNotIncrementedOnRepeatedThemeApplication() {
String prevFamily = Settings.getUiFontFamily();
int prevSize = Settings.getUiFontSize();
try {
Settings.setUiFontFamily("Dialog");
Settings.setUiFontSize(16);
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
JLabel label = new JLabel("Settings Label");
ThemeManager.applyTheme(label);
assertEquals(16, label.getFont().getSize(), "Initial label font size must match configured uiFontSize");
// Repeated theme applications (e.g. subsequent clicks of 'Apply' in Settings dialog)
// must never increment font size
for (int i = 1; i <= 5; i++) {
ThemeManager.applyTheme(label);
assertEquals(16, label.getFont().getSize(),
"Label font size must remain 16 on subsequent application #" + i);
}
// Test label with custom delta
JLabel headerLabel = new JLabel("Header Label");
headerLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 3));
headerLabel.putClientProperty("uiFontSizeDelta", 3);
ThemeManager.applyTheme(headerLabel);
assertEquals(19, headerLabel.getFont().getSize(), "Custom header label should have base + 3 size");
for (int i = 1; i <= 5; i++) {
ThemeManager.applyTheme(headerLabel);
assertEquals(19, headerLabel.getFont().getSize(),
"Header label font size must remain 19 on subsequent application #" + i);
}
} finally {
Settings.setUiFontFamily(prevFamily);
Settings.setUiFontSize(prevSize);
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
}
}
}
+42
View File
@@ -1,5 +1,11 @@
plugins {
id 'java-library'
id 'maven-publish'
}
java {
withSourcesJar()
withJavadocJar()
}
dependencies {
@@ -17,3 +23,39 @@ jar {
'Implementation-Version': archiveVersion
}
}
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
groupId = 'haus.nightmare.j3270'
artifactId = 'lib3270j'
version = project.version
pom {
name = 'lib3270j'
description = 'Pure Java IBM 3270 terminal emulation and protocol library with Host On-Demand (HOD) parity'
url = 'https://github.com/rudi/j3270'
licenses {
license {
name = 'The Unlicense'
url = 'https://unlicense.org/'
distribution = 'repo'
}
}
developers {
developer {
id = 'rudi'
name = 'Rudi'
}
}
scm {
connection = 'scm:git:git://github.com/rudi/j3270.git'
developerConnection = 'scm:git:ssh://github.com/rudi/j3270.git'
url = 'https://github.com/rudi/j3270'
}
}
}
}
}
@@ -0,0 +1,45 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLEvent;
/**
* Communications event matching IBM Host On-Demand ECLCommEvent.
*/
public class ECLCommEvent extends ECLEvent {
private final boolean status;
private final int eventType;
public ECLCommEvent(Object object, boolean status) {
super(object);
this.status = status;
this.eventType = status ? 1 : 2;
}
public ECLCommEvent(Object object, int eventType, boolean status) {
super(object);
this.eventType = eventType;
this.status = status;
}
public boolean connected() {
return this.status;
}
public boolean isConnected() {
return this.status;
}
public int getEventType() {
return this.eventType;
}
public ECLConnection GetConnection() {
Object src = GetSource();
if (src instanceof ECLConnection) return (ECLConnection) src;
if (src instanceof haus.nightmare.lib3270j.ecl.ECLConnection) {
return new ECLConnection((haus.nightmare.lib3270j.ecl.ECLConnection) src);
}
return null;
}
}
@@ -0,0 +1,10 @@
package com.ibm.eNetwork.ECL;
/**
* Communications notification interface matching IBM Host On-Demand ECLCommNotify.
*/
public interface ECLCommNotify {
void NotifyEvent(ECLConnection conn, boolean connected);
default void NotifyStop(ECLConnection conn, int reason) {}
default void NotifyError(ECLConnection conn, ECLErr err) {}
}
@@ -0,0 +1,79 @@
package com.ibm.eNetwork.ECL;
import java.io.IOException;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Communications and connection management facade matching IBM Host On-Demand ECLConnection.
*/
public class ECLConnection extends haus.nightmare.lib3270j.ecl.ECLConnection implements ECLConstants {
private final List<ECLCommNotify> commNotifies = new CopyOnWriteArrayList<>();
public ECLConnection() {
super();
setupListener();
}
public ECLConnection(Properties props) {
super(props);
setupListener();
}
public ECLConnection(String host, int port) {
super(host, port);
setupListener();
}
public ECLConnection(haus.nightmare.lib3270j.ecl.ECLSession session) {
super(session);
setupListener();
}
public ECLConnection(haus.nightmare.lib3270j.ecl.ECLConnection delegate) {
super(delegate != null ? delegate.getSession() : null);
setupListener();
}
private void setupListener() {
RegisterCommEvent(new haus.nightmare.lib3270j.ecl.ECLCommListener() {
@Override
public void CommNotifyEvent(haus.nightmare.lib3270j.ecl.ECLCommEvent event) {
boolean conn = event != null && event.isConnected();
for (ECLCommNotify n : commNotifies) {
try {
n.NotifyEvent(ECLConnection.this, conn);
} catch (Exception ignored) {}
}
}
});
}
public void RegisterCommEvent(ECLCommNotify notify) {
if (notify != null && !commNotifies.contains(notify)) {
commNotifies.add(notify);
}
}
public void registerCommEvent(ECLCommNotify notify) { RegisterCommEvent(notify); }
public void UnregisterCommEvent(ECLCommNotify notify) {
if (notify != null) {
commNotifies.remove(notify);
}
}
public void unregisterCommEvent(ECLCommNotify notify) { UnregisterCommEvent(notify); }
@Override
public void StartCommunication() throws IOException {
super.StartCommunication();
}
@Override
public void StopCommunication() {
super.StopCommunication();
}
}
@@ -0,0 +1,138 @@
package com.ibm.eNetwork.ECL;
/**
* IBM Host Access Class Library (HACL) common constants.
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.ECLConstants.
*/
public interface ECLConstants extends haus.nightmare.lib3270j.ecl.ECLConstants {
// Presentation space logical planes (HoD bitmask values)
int TEXT_PLANE = 1;
int COLOR_PLANE = 2;
int DBCS_PLANE = 4;
int GRID_PLANE = 8;
int FIELD_PLANE = 16;
int EXFIELD_PLANE = 32;
int NLS_PLANE = 64;
int HOST_PLANE = 128;
int RAWTEXT_PLANE = 256;
int COLOR_ATTRIBUTES_PLANE = 512;
int ALL_PLANES = 319;
// Search directions
int SEARCH_FORWARD = 1;
int SEARCH_BACKWARD = 2;
// Field filters
int ALL_FIELDS = 0;
int MODIFIED_FIELD = 1;
int ALPHANUM_FIELD = 2;
int HILIGHT_FIELD = 16;
int PROTECTED_FIELD = 32;
int DISPLAYABLE_FIELD = 64;
int PENDETECTABLE_FIELD = 128;
int UNMODIFIED_FIELD = 256;
int NUMERIC_FIELD = 512;
int LOLIGHT_FIELD = 4096;
int UNPROTECTED_FIELD = 8192;
int NON_DISPLAYABLE_FIELD = 16384;
int NON_PENDETECTABLE_FIELD = 32768;
// Standard Mnemonics Constants
String ENTER_STR = "[enter]";
String CLEAR_STR = "[clear]";
String RESET_STR = "[reset]";
String TAB_STR = "[tab]";
String FWDTAB_STR = "[tab]";
String BACKTAB_STR = "[backtab]";
String BACKSP_STR = "[backspace]";
String DELCHAR_STR = "[delete]";
String INSERT_STR = "[insert]";
String HOME_STR = "[home]";
String NEWLINE_STR = "[newline]";
String ENDLINE_STR = "[eof]";
String ERASEEOF_STR = "[eraseeof]";
String ERASEFLD_STR = "[erasefld]";
String ERASEINPUT_STR = "[erinp]";
String ATTN_STR = "[attn]";
String SYSREQ_STR = "[sysreq]";
String DUP_STR = "[dup]";
String FLDMRK_STR = "[fieldmark]";
String PA1_STR = "[pa1]";
String PA2_STR = "[pa2]";
String PA3_STR = "[pa3]";
String CURUP_STR = "[up]";
String CURDOWN_STR = "[down]";
String CURLEFT_STR = "[left]";
String CURRIGHT_STR = "[right]";
String PAGEUP_STR = "[pageup]";
String PAGEDWN_STR = "[pagedn]";
String COPY_STR = "[copy]";
String PASTE_STR = "[paste]";
String CUT_STR = "[cut]";
String SELECTALL_STR = "[selectall]";
// Function keys F1 - F24
String F1_STR = "[pf1]";
String F2_STR = "[pf2]";
String F3_STR = "[pf3]";
String F4_STR = "[pf4]";
String F5_STR = "[pf5]";
String F6_STR = "[pf6]";
String F7_STR = "[pf7]";
String F8_STR = "[pf8]";
String F9_STR = "[pf9]";
String F10_STR = "[pf10]";
String F11_STR = "[pf11]";
String F12_STR = "[pf12]";
String F13_STR = "[pf13]";
String F14_STR = "[pf14]";
String F15_STR = "[pf15]";
String F16_STR = "[pf16]";
String F17_STR = "[pf17]";
String F18_STR = "[pf18]";
String F19_STR = "[pf19]";
String F20_STR = "[pf20]";
String F21_STR = "[pf21]";
String F22_STR = "[pf22]";
String F23_STR = "[pf23]";
String F24_STR = "[pf24]";
// Shift F1 - F24
String SHIFT_F1_STR = "[Spf1]";
String SHIFT_F2_STR = "[Spf2]";
String SHIFT_F3_STR = "[Spf3]";
String SHIFT_F4_STR = "[Spf4]";
String SHIFT_F5_STR = "[Spf5]";
String SHIFT_F6_STR = "[Spf6]";
String SHIFT_F7_STR = "[Spf7]";
String SHIFT_F8_STR = "[Spf8]";
String SHIFT_F9_STR = "[Spf9]";
String SHIFT_F10_STR = "[Spf10]";
String SHIFT_F11_STR = "[Spf11]";
String SHIFT_F12_STR = "[Spf12]";
String SHIFT_F13_STR = "[Spf13]";
String SHIFT_F14_STR = "[Spf14]";
String SHIFT_F15_STR = "[Spf15]";
String SHIFT_F16_STR = "[Spf16]";
String SHIFT_F17_STR = "[Spf17]";
String SHIFT_F18_STR = "[Spf18]";
String SHIFT_F19_STR = "[Spf19]";
String SHIFT_F20_STR = "[Spf20]";
// Field attributes
short ATTR_PROTECTED = 32;
short ATTR_ALPHANUM = 16;
short ATTR_DISPLAY1 = 8;
short ATTR_DISPLAY2 = 4;
short ATTR_MDT = 1;
// Extended attributes
short EXT_ATTR_3270_UNDERLINE = 192;
short EXT_ATTR_3270_REVERSE = 128;
short EXT_ATTR_3270_BLINK = 64;
short EXT_ATTR_3270_EBCDIC = 0;
// Timeout
int NO_TIMEOUT = -1;
}
@@ -0,0 +1,54 @@
package com.ibm.eNetwork.ECL;
/**
* IBM Host Access Class Library (HACL) ECLErr checked exception facade.
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.ECLErr.
*/
public class ECLErr extends haus.nightmare.lib3270j.ecl.ECLErr {
private static final long serialVersionUID = 1L;
public ECLErr() {
super();
}
public ECLErr(String text) {
super(text);
}
public ECLErr(String location, String msgNum) {
super(location, msgNum);
}
public ECLErr(String location, String msgNum, String insert) {
super(location, msgNum, insert);
}
public ECLErr(String location, String msgNum, String insert1, String insert2) {
super(location, msgNum, insert1, insert2);
}
public ECLErr(String location, String msgNum, String insert1, String insert2, boolean log) {
super(location, msgNum, insert1, insert2, log);
}
public ECLErr(String location, String msgNum, String insert1, String insert2, String insert3) {
super(location, msgNum, insert1, insert2, insert3);
}
public ECLErr(String location, String msgNum, int reasonCode) {
super(location, msgNum, reasonCode);
}
public ECLErr(String location, String msgNum, String insert1, int reasonCode) {
super(location, msgNum, insert1, reasonCode);
}
public ECLErr(Throwable cause) {
super(cause);
}
public ECLErr(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,148 @@
package com.ibm.eNetwork.ECL;
/**
* Discrete 3270 field facade matching IBM Host On-Demand ECLField.
*/
public class ECLField extends haus.nightmare.lib3270j.ecl.ECLField implements ECLConstants {
private final haus.nightmare.lib3270j.ecl.ECLField delegate;
public ECLField(haus.nightmare.lib3270j.ecl.ECLField delegate) {
super(delegate != null ? delegate.getPS() : null,
delegate != null ? delegate.getStart() : 0,
delegate != null ? delegate.getDataStart() : 0,
delegate != null ? delegate.getEnd() : 0,
delegate != null ? delegate.getLength() : 0,
(byte) (delegate != null ? delegate.getAttribute() : 0));
this.delegate = delegate;
}
public int GetStart() {
return delegate != null ? delegate.GetStart() + 1 : 1;
}
public int getStart() { return GetStart(); }
public int GetStartRow() {
return delegate != null ? delegate.GetStartRow() + 1 : 1;
}
public int getStartRow() { return GetStartRow(); }
public int GetStartCol() {
return delegate != null ? delegate.GetStartCol() + 1 : 1;
}
public int getStartCol() { return GetStartCol(); }
public int GetEnd() {
return delegate != null ? delegate.GetEnd() + 1 : 1;
}
public int getEnd() { return GetEnd(); }
public int GetEndRow() {
return delegate != null ? delegate.GetEndRow() + 1 : 1;
}
public int getEndRow() { return GetEndRow(); }
public int GetEndCol() {
return delegate != null ? delegate.GetEndCol() + 1 : 1;
}
public int getEndCol() { return GetEndCol(); }
public int GetLength() {
return delegate != null ? delegate.GetLength() : 0;
}
public int getLength() { return GetLength(); }
public String GetString() {
return delegate != null ? delegate.getString() : "";
}
public String getString() { return GetString(); }
public int GetString(char[] buffer, int len) throws ECLErr {
if (buffer == null) throw new ECLErr("ECLField:GetString", "ECL0011", "1");
String text = GetString();
int count = Math.min(len, Math.min(buffer.length, text.length()));
for (int i = 0; i < count; i++) {
buffer[i] = text.charAt(i);
}
return count;
}
public void SetString(String text) throws ECLErr {
if (delegate != null) {
delegate.setText(text);
}
}
public void SetText(String text) throws ECLErr {
SetString(text);
}
public void setText(String text) {
if (delegate != null) {
delegate.setText(text);
}
}
public boolean IsModified() {
return delegate != null && delegate.isModified();
}
public boolean isModified() { return IsModified(); }
public boolean IsProtected() {
return delegate != null && delegate.isProtected();
}
public boolean isProtected() { return IsProtected(); }
public boolean IsNumeric() {
return delegate != null && delegate.isNumeric();
}
public boolean isNumeric() { return IsNumeric(); }
public boolean IsHighIntensity() {
return delegate != null && delegate.isHighIntensity();
}
public boolean isHighIntensity() { return IsHighIntensity(); }
public boolean IsPenDetectable() {
return delegate != null && delegate.isPenDetectable();
}
public boolean isPenDetectable() { return IsPenDetectable(); }
public boolean IsDisplay() {
return delegate != null && delegate.isDisplay();
}
public boolean isDisplay() { return IsDisplay(); }
public short GetAttribute() {
return delegate != null ? (short) (delegate.getAttribute() & 0xFF) : 0;
}
public boolean IsDBCS() {
return delegate != null && delegate.isDBCS();
}
public boolean isDBCS() { return IsDBCS(); }
public boolean IsDBCSOnly() {
return delegate != null && delegate.isDBCSOnly();
}
public boolean isDBCSOnly() { return IsDBCSOnly(); }
public haus.nightmare.lib3270j.ecl.ECLField getDelegate() { return delegate; }
}
@@ -0,0 +1,113 @@
package com.ibm.eNetwork.ECL;
import java.util.Vector;
/**
* Collection of 3270 fields matching IBM Host On-Demand ECLFieldList.
*/
public class ECLFieldList extends Vector<ECLField> implements ECLConstants {
private static final long serialVersionUID = 1L;
private final haus.nightmare.lib3270j.ecl.ECLFieldList delegate;
private final ECLPS ps;
public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLFieldList delegate, ECLPS ps) {
this.delegate = delegate;
this.ps = ps;
refreshFields();
}
public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLFieldList delegate) {
this(delegate, null);
}
private void refreshFields() {
clear();
if (delegate != null) {
delegate.refresh();
for (haus.nightmare.lib3270j.ecl.ECLField f : delegate.getFields()) {
add(new ECLField(f));
}
}
}
public void Refresh() {
refreshFields();
}
public void Refresh(int planes) throws ECLErr {
refreshFields();
}
public void refresh() {
refreshFields();
}
public int GetFieldCount() {
return size();
}
public int getFieldCount() { return GetFieldCount(); }
public ECLField GetFirstField() {
return GetFirstField(0);
}
public ECLField GetFirstField(int filter) {
for (ECLField f : this) {
if (matchesFilter(f, filter)) return f;
}
return null;
}
public ECLField GetNextField(ECLField current) {
return GetNextField(current, 0);
}
public ECLField GetNextField(ECLField current, int filter) {
if (current == null) return GetFirstField(filter);
int idx = indexOf(current);
if (idx >= 0) {
for (int i = idx + 1; i < size(); i++) {
ECLField f = get(i);
if (matchesFilter(f, filter)) return f;
}
}
return null;
}
public ECLField FindField(int pos) throws ECLErr {
if (ps != null && (pos < 1 || pos > ps.GetSize())) {
throw new ECLErr("ECLFieldList:FindField", "ECL0010", "1", String.valueOf(pos));
}
for (ECLField f : this) {
int start = f.GetStart();
int end = f.GetEnd();
if (start <= end) {
if (pos >= start && pos <= end) return f;
} else {
// Wrapped field
if (pos >= start || pos <= end) return f;
}
}
return null;
}
public ECLField FindField(int row, int col) throws ECLErr {
if (ps == null) return null;
int pos = ps.ConvertRowColToPos(row, col);
return FindField(pos);
}
private boolean matchesFilter(ECLField f, int filter) {
if (filter == 0 || filter == ALL_FIELDS) return true;
if ((filter & MODIFIED_FIELD) != 0 && !f.IsModified()) return false;
if ((filter & PROTECTED_FIELD) != 0 && !f.IsProtected()) return false;
if ((filter & UNPROTECTED_FIELD) != 0 && f.IsProtected()) return false;
if ((filter & NUMERIC_FIELD) != 0 && !f.IsNumeric()) return false;
if ((filter & HILIGHT_FIELD) != 0 && !f.IsHighIntensity()) return false;
if ((filter & DISPLAYABLE_FIELD) != 0 && !f.IsDisplay()) return false;
return true;
}
}
@@ -0,0 +1,39 @@
package com.ibm.eNetwork.ECL;
import java.util.Properties;
/**
* Host Print Session facade matching IBM Host On-Demand ECLHostPrintSession.
*/
public class ECLHostPrintSession extends ECLSession {
public static final String SESSION_PRINT_SESSION_LABEL_DEFAULT = "";
public static final String SESSION_PRINT_SESSION_TYPE_DEFAULT = "5";
public static final String SESSION_PRINT_SESSION_HOST_DEFAULT = "";
public static final String SESSION_PRINT_SESSION_HOST_PORT_DEFAULT = "23";
public static final String SESSION_PRINT_SESSION_LU_NAME_DEFAULT = "";
public static final String SESSION_PRINT_SESSION_AUTOCONNECT_DEFAULT = "true";
public static final String SESSION_PRINT_SESSION_AUTORECONNECT_DEFAULT = "true";
public static final String SESSION_LU_TYPE = "printerLUType";
public ECLHostPrintSession(Properties properties) throws ECLErr {
super(properties);
}
public ECLHostPrintSession() throws ECLErr {
super();
}
public void cancelPrintJob() {
ECLPS ps = GetPS();
if (ps != null) {
try {
ps.SendKeys("[cancelpjob]");
} catch (Exception ignored) {}
}
}
public void CancelPrintJob() {
cancelPrintJob();
}
}
@@ -0,0 +1,130 @@
package com.ibm.eNetwork.ECL;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Operator Information Area facade matching IBM Host On-Demand ECLOIA.
*/
public class ECLOIA implements ECLConstants {
private final haus.nightmare.lib3270j.ecl.ECLOIA delegate;
private final List<ECLOIANotify> oiaNotifies = new CopyOnWriteArrayList<>();
public ECLOIA(haus.nightmare.lib3270j.ecl.ECLOIA delegate) {
this.delegate = delegate;
if (delegate != null) {
delegate.RegisterOIAEvent(new haus.nightmare.lib3270j.ecl.ECLOIAListener() {
@Override
public void OIANotifyEvent(haus.nightmare.lib3270j.ecl.ECLOIAEvent event) {
ECLOIAEvent facadeEvt = new ECLOIAEvent(event);
for (ECLOIANotify n : oiaNotifies) {
try {
n.OIANotifyEvent(facadeEvt);
} catch (Exception ignored) {}
}
}
});
}
}
public void RegisterOIAEvent(ECLOIANotify notify) {
if (notify != null && !oiaNotifies.contains(notify)) {
oiaNotifies.add(notify);
}
}
public void registerOIAEvent(ECLOIANotify notify) { RegisterOIAEvent(notify); }
public void RegisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAListener listener) {
if (delegate != null) {
delegate.RegisterOIAEvent(listener);
}
}
public void UnregisterOIAEvent(ECLOIANotify notify) {
if (notify != null) {
oiaNotifies.remove(notify);
}
}
public void unregisterOIAEvent(ECLOIANotify notify) { UnregisterOIAEvent(notify); }
public void UnregisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAListener listener) {
if (delegate != null) {
delegate.UnregisterOIAEvent(listener);
}
}
public boolean IsAlphanumeric() {
return delegate != null && delegate.IsAlphanumeric();
}
public boolean isAlphanumeric() { return IsAlphanumeric(); }
public boolean IsNumeric() {
return delegate != null && delegate.IsNumeric();
}
public boolean isNumeric() { return IsNumeric(); }
public boolean IsDBCS() {
return delegate != null && delegate.IsDBCS();
}
public boolean isDBCS() { return IsDBCS(); }
public boolean IsInsertMode() {
return delegate != null && delegate.IsInsertMode();
}
public boolean isInsertMode() { return IsInsertMode(); }
public boolean IsCommError() {
return delegate != null && delegate.IsCommError();
}
public boolean isCommError() { return IsCommError(); }
public boolean IsKeyLocked() {
return delegate != null && delegate.isKeyLocked();
}
public boolean isKeyLocked() { return IsKeyLocked(); }
public boolean IsMessageWaiting() {
return delegate != null && delegate.IsMessageWaiting();
}
public boolean isMessageWaiting() { return IsMessageWaiting(); }
public int GetInputInhibited() {
return delegate != null ? delegate.getInputInhibited() : 0;
}
public int getInputInhibited() { return GetInputInhibited(); }
public int GetAlphanumericType() {
return delegate != null ? delegate.getAlphanumericType() : 0;
}
public int getAlphanumericType() { return GetAlphanumericType(); }
public boolean WaitForInput(long timeout) {
return delegate != null && delegate.WaitForInput(timeout);
}
public boolean WaitForSystemAvailable(long timeout) {
return delegate != null && delegate.WaitForSystemAvailable(timeout);
}
public boolean WaitForAppAvailable(long timeout) {
return delegate != null && delegate.WaitForAppAvailable(timeout);
}
public boolean WaitForTransition(long timeout) {
return delegate != null && delegate.WaitForTransition(timeout);
}
public haus.nightmare.lib3270j.ecl.ECLOIA getDelegate() { return delegate; }
}
@@ -0,0 +1,80 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLEvent;
/**
* Event object dispatched on Operator Information Area (ECLOIA) status changes matching IBM HoD.
*/
public class ECLOIAEvent extends ECLEvent {
public String data;
public long newState;
public long changedMask;
private int inputInhibited = 0;
private int alphanumericType = 0;
private boolean insertMode = false;
public ECLOIAEvent(Object source, long newState, long changedMask, String data) {
super(source);
this.newState = newState;
this.changedMask = changedMask;
this.data = data != null ? data : "";
}
public ECLOIAEvent(Object source, int eventType, int inputInhibited, int alphanumericType,
boolean insertMode, String statusString) {
super(source);
this.inputInhibited = inputInhibited;
this.alphanumericType = alphanumericType;
this.insertMode = insertMode;
this.data = statusString != null ? statusString : "";
this.newState = inputInhibited;
this.changedMask = eventType;
}
public ECLOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAEvent delegate) {
super(delegate != null ? delegate.getSource() : null);
if (delegate != null) {
this.inputInhibited = delegate.getInputInhibited();
this.alphanumericType = delegate.getAlphanumericType();
this.insertMode = delegate.isInsertMode();
this.data = delegate.getStatusString();
this.newState = delegate.getInputInhibited();
this.changedMask = delegate.getEventType();
}
}
public ECLOIA GetOIA() {
Object src = GetSource();
if (src instanceof ECLOIA) {
return (ECLOIA) src;
} else if (src instanceof haus.nightmare.lib3270j.ecl.ECLOIA) {
return new ECLOIA((haus.nightmare.lib3270j.ecl.ECLOIA) src);
}
return null;
}
public ECLOIA getOIA() { return GetOIA(); }
public int GetInputInhibited() { return inputInhibited; }
public int getInputInhibited() { return inputInhibited; }
public int GetAlphanumericType() { return alphanumericType; }
public int getAlphanumericType() { return alphanumericType; }
public boolean IsInsertMode() { return insertMode; }
public boolean isInsertMode() { return insertMode; }
public boolean IsInputInhibited() { return inputInhibited != 0; }
public boolean isInputInhibited() { return IsInputInhibited(); }
public String GetStatusString() { return data; }
public String getStatusString() { return data; }
@Override
public String toString() {
return "ECLOIAEvent: changedMask=0x" + Long.toHexString(this.changedMask) +
", newState=0x" + Long.toHexString(this.newState) + ", data=" + this.data;
}
}
@@ -0,0 +1,17 @@
package com.ibm.eNetwork.ECL;
/**
* Operator Information Area notification interface matching IBM Host On-Demand ECLOIANotify.
*/
public interface ECLOIANotify {
void NotifyEvent(ECLOIA oia, int inhibitReason, int state, String text);
default void NotifyStop(ECLOIA oia, int reason) {}
default void NotifyError(ECLOIA oia, ECLErr err) {}
default void OIANotifyStop(ECLOIA oia, int reason) { NotifyStop(oia, reason); }
default void OIANotifyError(ECLOIA oia, ECLErr err) { NotifyError(oia, err); }
default void OIANotifyEvent(ECLOIAEvent event) {
if (event != null) {
NotifyEvent(event.GetOIA(), event.GetInputInhibited(), 0, "");
}
}
}
@@ -0,0 +1,571 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLPSListener;
import java.io.Serializable;
/**
* Presentation Space facade matching IBM Host On-Demand ECLPS.
* Provides 1-based indexing, method signature parity, and checked ECLErr exception semantics.
*/
public class ECLPS implements ECLConstants, Serializable, Cloneable {
private static final long serialVersionUID = 1L;
private final haus.nightmare.lib3270j.ecl.ECLPS delegate;
public ECLPS(haus.nightmare.lib3270j.ecl.ECLPS delegate) {
this.delegate = delegate;
}
public ECLPS(haus.nightmare.lib3270j.ecl.ECLSession session) {
this(session != null ? session.GetPS() : null);
}
public ECLPS(ECLSession session) {
this(session != null ? session.getDelegate() : null);
}
public haus.nightmare.lib3270j.ecl.ECLPS getDelegate() {
return delegate;
}
// ========== Dimensions & Position Conversion (1-based) ==========
public int GetSize() {
return delegate != null ? delegate.GetSize() : 0;
}
public int getSize() { return GetSize(); }
public int GetRows() {
return delegate != null ? delegate.GetRows() : 0;
}
public int getRows() { return GetRows(); }
public int GetCols() {
return delegate != null ? delegate.GetCols() : 0;
}
public int getCols() { return GetCols(); }
public int GetSizeRows() { return GetRows(); }
public int GetSizeCols() { return GetCols(); }
public int ConvertRowColToPos(int row, int col) throws ECLErr {
if (delegate != null) {
try {
return delegate.ConvertRowColToPos(row, col);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return 1;
}
public int convertRowColToPos(int row, int col) throws ECLErr {
return ConvertRowColToPos(row, col);
}
public int ConvertPosToRow(int pos) throws ECLErr {
if (delegate != null) {
try {
return delegate.ConvertPosToRow(pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return 1;
}
public int convertPosToRow(int pos) throws ECLErr {
return ConvertPosToRow(pos);
}
public int ConvertPosToCol(int pos) throws ECLErr {
if (delegate != null) {
try {
return delegate.ConvertPosToCol(pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return 1;
}
public int convertPosToCol(int pos) throws ECLErr {
return ConvertPosToCol(pos);
}
public void ConvertPosToRowCol(int pos, int[] rowCol) throws ECLErr {
if (delegate != null) {
try {
delegate.ConvertPosToRowCol(pos, rowCol);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
}
public void convertPosToRowCol(int pos, int[] rowCol) throws ECLErr {
ConvertPosToRowCol(pos, rowCol);
}
public int[] ConvertPosToRowCol(int pos) throws ECLErr {
if (delegate != null) {
try {
return delegate.ConvertPosToRowCol(pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return new int[]{1, 1};
}
public int[] convertPosToRowCol(int pos) throws ECLErr {
return ConvertPosToRowCol(pos);
}
// ========== Cursor Operations (1-based) ==========
public int GetCursorPos() {
return delegate != null ? delegate.GetCursorPos() : 1;
}
public int getCursorPos() { return GetCursorPos(); }
public int GetCursorRow() {
return delegate != null ? delegate.GetCursorRow() : 1;
}
public int getCursorRow() { return GetCursorRow(); }
public int GetCursorCol() {
return delegate != null ? delegate.GetCursorCol() : 1;
}
public int getCursorCol() { return GetCursorCol(); }
public void SetCursorPos(int pos) throws ECLErr {
if (delegate != null) {
try {
delegate.SetCursorPos(pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
}
public void setCursorPos(int pos) throws ECLErr {
SetCursorPos(pos);
}
public void SetCursorPos(int row, int col) throws ECLErr {
if (delegate != null) {
try {
delegate.SetCursorPos(row, col);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
}
public void setCursorPos(int row, int col) throws ECLErr {
SetCursorPos(row, col);
}
// ========== GetString & SetString Operations (1-based) ==========
public int GetString(char[] buffer, int len, int pos) throws ECLErr {
return GetString(buffer, len, pos, len);
}
public int GetString(char[] buffer, int len, int pos, int length) throws ECLErr {
if (delegate != null) {
try {
return delegate.GetString(buffer, len, pos, length);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return 0;
}
public int GetString(char[] buffer, int len, int row, int col, int length) throws ECLErr {
return GetString(buffer, len, ConvertRowColToPos(row, col), length);
}
public String GetString(int pos, int length) throws ECLErr {
if (delegate != null) {
try {
return delegate.GetString(pos, length);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return "";
}
public String getString(int pos, int length) throws ECLErr {
return GetString(pos, length);
}
public String GetString(int row, int col, int length) throws ECLErr {
return GetString(ConvertRowColToPos(row, col), length);
}
public String getString(int row, int col, int length) throws ECLErr {
return GetString(row, col, length);
}
public void SetString(String text, int pos) throws ECLErr {
if (delegate != null) {
try {
delegate.SetString(text, pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
}
public void setString(String text, int pos) throws ECLErr {
SetString(text, pos);
}
public void SetString(String text, int row, int col) throws ECLErr {
SetString(text, ConvertRowColToPos(row, col));
}
public void setString(String text, int row, int col) throws ECLErr {
SetString(text, row, col);
}
public void SetString(String text) throws ECLErr {
SetString(text, GetCursorPos());
}
public void setString(String text) throws ECLErr {
SetString(text);
}
public void SetText(String text, int pos) throws ECLErr {
SetString(text, pos);
}
public void SetText(String text, int row, int col) throws ECLErr {
SetString(text, row, col);
}
public void setText(String text, int pos) {
if (delegate != null) delegate.setText(text, pos);
}
public void setText(String text, int row, int col) {
if (delegate != null) delegate.setText(text, row, col);
}
// ========== Search Operations (1-based) ==========
public int SearchText(String text, int dir, int startPos) {
return delegate != null ? delegate.SearchText(text, dir, startPos) : 0;
}
public int SearchText(String text, int dir) {
return delegate != null ? delegate.SearchText(text, dir) : 0;
}
public int SearchText(String text) {
return delegate != null ? delegate.SearchText(text) : 0;
}
public int SearchText(String text, int row, int col, int dir) throws ECLErr {
return SearchText(text, dir, ConvertRowColToPos(row, col));
}
public int SearchString(String text, int dir, int startPos) {
return SearchText(text, dir, startPos);
}
public int SearchString(String text, int dir) {
return SearchText(text, dir);
}
public int SearchString(String text) {
return SearchText(text);
}
public int SearchString(String text, int row, int col, int dir) throws ECLErr {
return SearchText(text, row, col, dir);
}
public int SearchPS(String text) {
return delegate != null ? delegate.SearchPS(text) : 0;
}
public int SearchPS(String text, int startPos) {
return delegate != null ? delegate.SearchPS(text, startPos) : 0;
}
public int SearchPSExt(String text, int startPos, int endPos, int dir, boolean ignoreCase, boolean wrap) {
return delegate != null ? delegate.SearchPSExt(text, startPos, endPos, dir, ignoreCase, wrap) : 0;
}
// ========== Keystrokes & Mnemonics ==========
public void SendKeys(String keys) {
if (delegate != null) delegate.SendKeys(keys);
}
public void sendKeys(String keys) { SendKeys(keys); }
public void SendKeys(String keys, int pos) {
if (delegate != null) delegate.SendKeys(keys, pos);
}
public void sendKeys(String keys, int pos) { SendKeys(keys, pos); }
public void SendKeys(String keys, int row, int col) {
if (delegate != null) delegate.SendKeys(keys, row, col);
}
public void sendKeys(String keys, int row, int col) { SendKeys(keys, row, col); }
public void SendCharacters(String keys) {
if (delegate != null) delegate.SendCharacters(keys);
}
public void sendCharacters(String keys) { SendCharacters(keys); }
public void SendCharacters(String keys, int delayMs) {
if (delegate != null) delegate.SendCharacters(keys, delayMs);
}
public void sendCharacters(String keys, int delayMs) { SendCharacters(keys, delayMs); }
public void SendCharacters(String keys, int pos, int delayMs) {
if (delegate != null) delegate.SendCharacters(keys, pos, delayMs);
}
public void sendCharacters(String keys, int pos, int delayMs) { SendCharacters(keys, pos, delayMs); }
public void SendCharacters(String keys, int row, int col, int delayMs) {
if (delegate != null) delegate.SendCharacters(keys, row, col, delayMs);
}
public void sendCharacters(String keys, int row, int col, int delayMs) { SendCharacters(keys, row, col, delayMs); }
public String[] GetSendKeyMnemonics() {
return delegate != null ? delegate.GetSendKeyMnemonics() : new String[0];
}
public String[] getSendKeyMnemonics() { return GetSendKeyMnemonics(); }
// ========== Field Operations ==========
public ECLFieldList GetFieldList() {
haus.nightmare.lib3270j.ecl.ECLFieldList fl = delegate != null ? delegate.GetFieldList() : null;
return new ECLFieldList(fl, this);
}
public ECLFieldList getFieldList() { return GetFieldList(); }
public ECLField GetField(int pos) throws ECLErr {
return GetFieldList().FindField(pos);
}
public ECLField getField(int pos) throws ECLErr { return GetField(pos); }
public ECLField GetField(int row, int col) throws ECLErr {
return GetFieldList().FindField(row, col);
}
public ECLField getField(int row, int col) throws ECLErr { return GetField(row, col); }
public ECLField GetFirstField() {
return GetFieldList().GetFirstField();
}
public ECLField getFirstField() { return GetFirstField(); }
public ECLField GetNextField(ECLField prev) {
return GetFieldList().GetNextField(prev);
}
public ECLField getNextField(ECLField prev) { return GetNextField(prev); }
// ========== Listener Overloads for HoD Interfaces ==========
public void RegisterPSEvent(ECLPSNotify notify) {
if (delegate != null) delegate.RegisterPSEvent(notify);
}
public void registerPSEvent(ECLPSNotify notify) { RegisterPSEvent(notify); }
public void RegisterPSEvent(ECLPSNotify notify, int eventType) {
if (delegate != null) delegate.RegisterPSEvent(notify, eventType);
}
public void registerPSEvent(ECLPSNotify notify, int eventType) { RegisterPSEvent(notify, eventType); }
public void RegisterPSEvent(ECLPSNotify notify, ECLScreenDesc desc) {
if (delegate != null) delegate.RegisterPSEvent(notify, desc);
}
public void registerPSEvent(ECLPSNotify notify, ECLScreenDesc desc) { RegisterPSEvent(notify, desc); }
public void RegisterPSEvent(ECLPSNotify notify, ECLScreenDesc desc, int eventType) {
if (delegate != null) delegate.RegisterPSEvent(notify, desc, eventType);
}
public void registerPSEvent(ECLPSNotify notify, ECLScreenDesc desc, int eventType) { RegisterPSEvent(notify, desc, eventType); }
public void UnregisterPSEvent(ECLPSNotify notify) {
if (delegate != null) delegate.UnregisterPSEvent(notify);
}
public void unregisterPSEvent(ECLPSNotify notify) { UnregisterPSEvent(notify); }
public void RegisterPSEvent(ECLPSListener listener) {
if (delegate != null) delegate.RegisterPSEvent(listener);
}
public void registerPSEvent(ECLPSListener listener) { RegisterPSEvent(listener); }
public void UnregisterPSEvent(ECLPSListener listener) {
if (delegate != null) delegate.UnregisterPSEvent(listener);
}
public void unregisterPSEvent(ECLPSListener listener) { UnregisterPSEvent(listener); }
// ========== Synchronization & Waits ==========
public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) {
return delegate != null && delegate.WaitForScreen(desc, timeoutMs);
}
public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) { return WaitForScreen(desc, timeoutMs); }
public boolean WaitForScreen(ECLScreenDesc desc) {
return delegate != null && delegate.WaitForScreen(desc);
}
public boolean waitForScreen(ECLScreenDesc desc) { return WaitForScreen(desc); }
public boolean WaitWhileScreen(ECLScreenDesc desc, long timeoutMs) {
return delegate != null && delegate.WaitWhileScreen(desc, timeoutMs);
}
public boolean waitWhileScreen(ECLScreenDesc desc, long timeoutMs) { return WaitWhileScreen(desc, timeoutMs); }
public boolean WaitWhileScreen(ECLScreenDesc desc) {
return delegate != null && delegate.WaitWhileScreen(desc);
}
public boolean waitWhileScreen(ECLScreenDesc desc) { return WaitWhileScreen(desc); }
public boolean WaitForString(String text, long timeoutMs) {
return delegate != null && delegate.WaitForString(text, timeoutMs);
}
public boolean waitForString(String text, long timeoutMs) { return WaitForString(text, timeoutMs); }
public boolean WaitForString(String text) {
return delegate != null && delegate.WaitForString(text);
}
public boolean waitForString(String text) { return WaitForString(text); }
public boolean WaitForString(String text, int row, int col, long timeoutMs) {
return delegate != null && delegate.WaitForString(text, row, col, timeoutMs);
}
public boolean waitForString(String text, int row, int col, long timeoutMs) { return WaitForString(text, row, col, timeoutMs); }
public boolean WaitForCursor(int row, int col, long timeoutMs) {
return delegate != null && delegate.WaitForCursor(row, col, timeoutMs);
}
public boolean waitForCursor(int row, int col, long timeoutMs) { return WaitForCursor(row, col, timeoutMs); }
// ========== Screen Update Inquiries ==========
public ECLPSUpdate GetScreen(int startRow, int startCol, int length) throws ECLErr {
return GetScreen(startRow, startCol, length, false);
}
public ECLPSUpdate GetScreen(int startRow, int startCol, int length, boolean fillNulls) throws ECLErr {
int pos = ConvertRowColToPos(startRow, startCol);
char[] buf = new char[length];
GetString(buf, length, pos, length);
if (fillNulls) {
for (int i = 0; i < buf.length; i++) {
if (buf[i] == '\0') buf[i] = ' ';
}
}
int endPos = pos + length - 1;
int endRow = ConvertPosToRow(endPos);
int endCol = ConvertPosToCol(endPos);
return new ECLPSUpdate(this, startRow, startCol, endRow, endCol, new String(buf));
}
public int GetScreen(char[] buffer, int len, int row, int col, int length, int plane) {
return delegate.GetScreen(buffer, len, row, col, length, plane);
}
// ========== DBCS Character Status ==========
public boolean IsDBCSChar(int pos) {
return delegate != null && delegate.IsDBCSChar(pos);
}
public boolean isDBCSChar(int pos) { return IsDBCSChar(pos); }
public boolean IsDBCSChar(int row, int col) {
return delegate != null && delegate.IsDBCSChar(row, col);
}
public boolean isDBCSChar(int row, int col) { return IsDBCSChar(row, col); }
public boolean IsDBCS1stChar(int pos) {
return delegate != null && delegate.IsDBCS1stChar(pos);
}
public boolean isDBCS1stChar(int pos) { return IsDBCS1stChar(pos); }
public boolean IsDBCS1stChar(int row, int col) {
return delegate != null && delegate.IsDBCS1stChar(row, col);
}
public boolean isDBCS1stChar(int row, int col) { return IsDBCS1stChar(row, col); }
public boolean IsDBCS2ndChar(int pos) {
return delegate != null && delegate.IsDBCS2ndChar(pos);
}
public boolean isDBCS2ndChar(int pos) { return IsDBCS2ndChar(pos); }
public boolean IsDBCS2ndChar(int row, int col) {
return delegate != null && delegate.IsDBCS2ndChar(row, col);
}
public boolean isDBCS2ndChar(int row, int col) { return IsDBCS2ndChar(row, col); }
public boolean IsSOChar(int pos) {
return delegate != null && delegate.IsSOChar(pos);
}
public boolean isSOChar(int pos) { return IsSOChar(pos); }
public boolean IsSIChar(int pos) {
return delegate != null && delegate.IsSIChar(pos);
}
public boolean isSIChar(int pos) { return IsSIChar(pos); }
public boolean IsSOSIChar(int pos) {
return delegate != null && delegate.IsSOSIChar(pos);
}
public boolean isSOSIChar(int pos) { return IsSOSIChar(pos); }
}
@@ -0,0 +1,10 @@
package com.ibm.eNetwork.ECL;
/**
* Presentation space notification interface matching IBM Host On-Demand ECLPSNotify.
*/
public interface ECLPSNotify {
void NotifyEvent(ECLPS ps);
default void NotifyStop(ECLPS ps, int reason) {}
default void NotifyError(ECLPS ps, ECLErr err) {}
}
@@ -0,0 +1,90 @@
package com.ibm.eNetwork.ECL;
import java.io.Serializable;
/**
* Conforms to IBM Host On-Demand ECLPSUpdate.
*/
public class ECLPSUpdate implements Serializable {
private static final long serialVersionUID = 1L;
private final haus.nightmare.lib3270j.ecl.ECLPSUpdate delegate;
private final int startRow;
private final int startCol;
private final int endRow;
private final int endCol;
private final int start;
private final int end;
private final boolean fullUpdate;
private final String text;
private final transient ECLPS ps;
public ECLPSUpdate(ECLPS ps, int startRow, int startCol, int endRow, int endCol,
int start, int end, boolean fullUpdate, String text) {
this.ps = ps;
this.startRow = startRow;
this.startCol = startCol;
this.endRow = endRow;
this.endCol = endCol;
this.start = start;
this.end = end;
this.fullUpdate = fullUpdate;
this.text = text != null ? text : "";
this.delegate = null;
}
public ECLPSUpdate(ECLPS ps, int startRow, int startCol, int endRow, int endCol, String text) {
this(ps, startRow, startCol, endRow, endCol, 0, 0, false, text);
}
public ECLPSUpdate(int startRow, int startCol, int endRow, int endCol, String text) {
this(null, startRow, startCol, endRow, endCol, 0, 0, false, text);
}
public ECLPSUpdate(haus.nightmare.lib3270j.ecl.ECLPSUpdate delegate) {
this.delegate = delegate;
this.ps = (delegate != null && delegate.getPS() != null) ? new ECLPS(delegate.getPS()) : null;
this.startRow = delegate != null ? delegate.getStartRow() : 0;
this.startCol = delegate != null ? delegate.getStartCol() : 0;
this.endRow = delegate != null ? delegate.getEndRow() : 0;
this.endCol = delegate != null ? delegate.getEndCol() : 0;
this.start = delegate != null ? delegate.getStart() : 0;
this.end = delegate != null ? delegate.getEnd() : 0;
this.fullUpdate = delegate != null && delegate.isFullUpdate();
this.text = delegate != null ? delegate.getText() : "";
}
public ECLPS getPS() { return ps; }
public ECLPS GetPS() { return ps; }
public int getStartRow() { return startRow; }
public int GetStartRow() { return startRow + 1; }
public int getStartCol() { return startCol; }
public int GetStartCol() { return startCol + 1; }
public int getEndRow() { return endRow; }
public int GetEndRow() { return endRow + 1; }
public int getEndCol() { return endCol; }
public int GetEndCol() { return endCol + 1; }
public int getStart() { return start; }
public int GetStart() { return start + 1; }
public int getEnd() { return end; }
public int GetEnd() { return end + 1; }
public boolean isFullUpdate() { return fullUpdate; }
public boolean IsFullUpdate() { return fullUpdate; }
public String getText() { return text; }
public String GetText() { return text; }
@Override
public String toString() {
return String.format("ECLPSUpdate[start=(%d,%d), end=(%d,%d), full=%b]",
startRow, startCol, endRow, endCol, fullUpdate);
}
}
@@ -0,0 +1,21 @@
package com.ibm.eNetwork.ECL;
import java.io.Serializable;
/**
* Screen descriptor matching engine facade matching IBM Host On-Demand ECLScreenDesc.
*/
public class ECLScreenDesc extends haus.nightmare.lib3270j.ecl.ECLScreenDesc implements Serializable {
private static final long serialVersionUID = 1L;
public ECLScreenDesc() {
super();
}
public boolean Matches(ECLPS ps, ECLOIA oia) {
haus.nightmare.lib3270j.ecl.ECLPS p = (ps != null) ? ps.getDelegate() : null;
haus.nightmare.lib3270j.ecl.ECLOIA o = (oia != null) ? oia.getDelegate() : null;
return super.Matches(p, o);
}
}
@@ -0,0 +1,8 @@
package com.ibm.eNetwork.ECL;
/**
* Listener interface for ECLScreenReco matching notifications matching IBM HoD.
*/
public interface ECLScreenNotify {
void screenMatching(ECLScreenRecoEvent event);
}
@@ -0,0 +1,78 @@
package com.ibm.eNetwork.ECL;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Screen recognition facade matching IBM Host On-Demand ECLScreenReco.
*/
public class ECLScreenReco {
private final haus.nightmare.lib3270j.ecl.ECLScreenReco delegate;
private final ECLPS ps;
private final Map<ECLScreenDesc, ECLScreenNotify> notifyMap = new ConcurrentHashMap<>();
public ECLScreenReco() {
this((ECLPS) null);
}
public ECLScreenReco(ECLSession session) {
this(session != null ? session.GetPS() : null);
}
public ECLScreenReco(ECLPS ps) {
this(ps, (ECLOIA) null);
}
public ECLScreenReco(ECLPS ps, ECLOIA oia) {
this.ps = ps;
this.delegate = new haus.nightmare.lib3270j.ecl.ECLScreenReco(ps != null ? ps.getDelegate() : null,
oia != null ? oia.getDelegate() : null);
}
public ECLScreenReco(haus.nightmare.lib3270j.ecl.ECLScreenReco delegate) {
this.delegate = delegate;
this.ps = null;
}
public void RegisterScreen(ECLScreenDesc desc, ECLScreenNotify notify) {
if (desc != null && notify != null) {
notifyMap.put(desc, notify);
if (delegate != null) {
delegate.RegisterScreen(desc, event -> notify.screenMatching(new ECLScreenRecoEvent(this, desc, ps)));
}
}
}
public void registerScreen(ECLScreenDesc desc, ECLScreenNotify notify) {
RegisterScreen(desc, notify);
}
public void UnregisterScreen(ECLScreenDesc desc) {
if (desc != null) {
notifyMap.remove(desc);
if (delegate != null) {
delegate.UnregisterScreen(desc);
}
}
}
public void unregisterScreen(ECLScreenDesc desc) {
UnregisterScreen(desc);
}
public boolean IsMatch(ECLScreenDesc desc) {
if (delegate != null) {
return delegate.IsMatch(desc);
}
return false;
}
public static boolean IsMatch(ECLPS ps, ECLScreenDesc desc) {
if (ps == null) return false;
if (desc == null) return true;
return desc.Matches(ps, (ECLOIA) null);
}
public haus.nightmare.lib3270j.ecl.ECLScreenReco getDelegate() { return delegate; }
}
@@ -0,0 +1,39 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLEvent;
/**
* Event delivered when an ECLScreenDesc matches the Presentation Space.
*/
public class ECLScreenRecoEvent extends ECLEvent {
private final ECLScreenReco source;
private final ECLScreenDesc screenDesc;
private final ECLPS ps;
public ECLScreenRecoEvent(ECLScreenReco source, ECLScreenDesc screenDesc, ECLPS ps) {
super(source);
this.source = source;
this.screenDesc = screenDesc;
this.ps = ps;
}
public ECLScreenRecoEvent(Object source, ECLPS ps) {
super(source);
this.source = (source instanceof ECLScreenReco) ? (ECLScreenReco) source : null;
this.screenDesc = null;
this.ps = ps;
}
@Override
public Object GetSource() { return source != null ? source : super.GetSource(); }
public ECLScreenReco GetScreenReco() { return source; }
public ECLScreenReco getScreenReco() { return source; }
public ECLScreenDesc GetScreenDesc() { return screenDesc; }
public ECLScreenDesc getScreenDesc() { return screenDesc; }
public ECLPS GetPS() { return ps; }
public ECLPS getPS() { return ps; }
}
@@ -0,0 +1,190 @@
package com.ibm.eNetwork.ECL;
import java.io.IOException;
import java.util.Properties;
/**
* Top-level session facade matching IBM Host On-Demand ECLSession.
* Extends ECLConnection and provides access to Presentation Space (ECLPS),
* Operator Information Area (ECLOIA), File Transfer (ECLXfer), and Screen Recognition (ECLScreenReco).
*/
public class ECLSession extends ECLConnection implements ECLConstants {
public static final String SESSION_NAME = "SESSION_NAME";
public static final String SESSION_LABEL = "SESSION_LABEL";
public static final String SESSION_TYPE = "SESSION_TYPE";
public static final String SESSION_HOST = "SESSION_HOST";
public static final String SESSION_HOST_PORT = "SESSION_HOST_PORT";
public static final String SESSION_PORT = "SESSION_PORT";
public static final String SESSION_PS_SIZE = "SESSION_PS_SIZE";
public static final String SESSION_CODE_PAGE = "SESSION_CODE_PAGE";
public static final String SESSION_SSL = "SESSION_SSL";
public static final String SESSION_SECURITY_PROTOCOL = "SESSION_SECURITY_PROTOCOL";
public static final String SESSION_TLS_PROTOCOL_VERSION = "tlsProtocolVersion";
public static final String SESSION_LU_NAME = "SESSION_LU_NAME";
public static final String SESSION_TN3270E = "SESSION_TN3270E";
public static final String SESSION_AUTO_CONNECT = "SESSION_AUTO_CONNECT";
public static final String SESSION_AUTORECONNECT = "SESSION_AUTORECONNECT";
public static final String SESSION_RECONNECT_RETRIES = "SESSION_RECONNECT_RETRIES";
public static final String SESSION_KEEPALIVE = "SESSION_KEEPALIVE";
public static final String KEY_KEEPALIVE_TYPE = "keepAliveType";
public static final String KEY_KEEPALIVE_TIMEOUT = "keepAliveTimeout";
public static final String SESSION_AUTO_SYS_UNLOCK = "autoSysUnlock";
public static final String ENABLE_PASTE_FROM_EXCEL = "enablePasteFromExcel";
public static final String PASTE_TAB_OPTIONS = "pasteTabOptions";
public static final String PASTE_STOP_AT_PROTECTED_LINE = "pasteStopAtProtectedLine";
public static final String BLOCK_PASTE = "blockPaste";
public static final String PASTE_FIELD_WRAP = "pasteFieldWrap";
public static final String PASTE_LINE_WRAP = "pasteLineWrap";
public static final String PASTE_WORD_BREAK = "pasteWordBreak";
private final haus.nightmare.lib3270j.ecl.ECLSession delegate;
private final ECLPS ps;
private final ECLOIA oia;
private final ECLXfer xfer;
private final ECLScreenReco screenReco;
public ECLSession(Properties properties) throws ECLErr {
super(properties);
this.delegate = new haus.nightmare.lib3270j.ecl.ECLSession(properties);
this.ps = new ECLPS(delegate.getPS());
this.oia = new ECLOIA(delegate.getOIA());
this.xfer = new ECLXfer(delegate.getXfer());
this.screenReco = new ECLScreenReco(this.ps, this.oia);
}
public ECLSession() throws ECLErr {
this(new Properties());
}
public ECLSession(String host, int port) throws ECLErr {
super(host, port);
Properties props = new Properties();
if (host != null) props.setProperty(SESSION_HOST, host);
props.setProperty(SESSION_PORT, String.valueOf(port));
this.delegate = new haus.nightmare.lib3270j.ecl.ECLSession(props);
this.ps = new ECLPS(delegate.getPS());
this.oia = new ECLOIA(delegate.getOIA());
this.xfer = new ECLXfer(delegate.getXfer());
this.screenReco = new ECLScreenReco(this.ps, this.oia);
}
public ECLSession(haus.nightmare.lib3270j.ecl.ECLSession delegate) {
super(delegate);
this.delegate = delegate;
this.ps = new ECLPS(delegate != null ? delegate.getPS() : null);
this.oia = new ECLOIA(delegate != null ? delegate.getOIA() : null);
this.xfer = new ECLXfer(delegate != null ? delegate.getXfer() : null);
this.screenReco = new ECLScreenReco(this.ps, this.oia);
}
public ECLPS GetPS() {
return ps;
}
public ECLPS getPS() {
return ps;
}
public ECLOIA GetOIA() {
return oia;
}
public ECLOIA getOIA() {
return oia;
}
public ECLXfer GetXfer() {
return xfer;
}
public ECLXfer getXfer() {
return xfer;
}
public ECLScreenReco GetScreenReco() {
return screenReco;
}
public ECLScreenReco getScreenReco() {
return screenReco;
}
public ECLConnection GetConnection() {
return this;
}
public ECLConnection getConnection() {
return this;
}
public haus.nightmare.lib3270j.ecl.ECLSession getDelegate() {
return delegate;
}
@Override
public void StartCommunication() throws IOException {
if (delegate != null) {
delegate.StartCommunication();
} else {
super.StartCommunication();
}
}
@Override
public void StopCommunication() {
if (delegate != null) {
delegate.StopCommunication();
} else {
super.StopCommunication();
}
}
public boolean StartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
return StartCommunicationWithBlocking(timeout, (ECLScreenDesc) null);
}
public boolean startCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
return StartCommunicationWithBlocking(timeout);
}
public boolean StartCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws InterruptedException, ECLErr {
if (delegate != null) {
try {
return delegate.StartCommunicationWithBlocking(timeout, desc);
} catch (IOException e) {
throw new ECLErr("ECLSession.StartCommunicationWithBlocking", "ECL0001", e.getMessage());
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return false;
}
public boolean startCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws InterruptedException, ECLErr {
return StartCommunicationWithBlocking(timeout, desc);
}
public boolean RestartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
if (delegate != null) {
try {
return delegate.RestartCommunicationWithBlocking(timeout);
} catch (IOException e) {
throw new ECLErr("ECLSession.RestartCommunicationWithBlocking", "ECL0001", e.getMessage());
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return false;
}
public boolean restartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
return RestartCommunicationWithBlocking(timeout);
}
public void dispose() {
if (delegate != null) {
delegate.dispose();
}
}
}
@@ -0,0 +1,54 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLXferListener;
/**
* File transfer facade matching IBM Host On-Demand ECLXfer.
*/
public class ECLXfer {
private final haus.nightmare.lib3270j.ecl.ECLXfer delegate;
public ECLXfer(ECLSession session, Boolean bidi) {
this.delegate = (session != null && session.getDelegate() != null)
? session.getDelegate().GetXfer() : null;
}
public ECLXfer(haus.nightmare.lib3270j.ecl.ECLXfer delegate) {
this.delegate = delegate;
}
public void SendFile(String localFile, String hostFile, String options) throws ECLErr {
if (delegate != null) {
int rc = delegate.SendFile(localFile, hostFile, options);
if (rc != 0) {
throw new ECLErr("ECLXfer:SendFile", "ECL0150", String.valueOf(rc));
}
}
}
public void ReceiveFile(String localFile, String hostFile, String options) throws ECLErr {
if (delegate != null) {
int rc = delegate.ReceiveFile(localFile, hostFile, options);
if (rc != 0) {
throw new ECLErr("ECLXfer:ReceiveFile", "ECL0150", String.valueOf(rc));
}
}
}
public void Cancel() {
if (delegate != null) {
delegate.Cancel();
}
}
public void RegisterXferEvent(ECLXferListener listener) {
// Can bridge listener if needed
}
public void UnregisterXferEvent(ECLXferListener listener) {
// Can bridge listener if needed
}
public haus.nightmare.lib3270j.ecl.ECLXfer getDelegate() { return delegate; }
}
@@ -0,0 +1,25 @@
package com.ibm.eNetwork.ECL.event;
/**
* Base event class matching IBM Host On-Demand ECLEvent.
*/
public class ECLEvent {
private final Object originator;
public ECLEvent(Object object) {
this.originator = object;
}
public Object GetSource() {
return this.originator;
}
public Object getSource() {
return this.originator;
}
@Override
public String toString() {
return super.toString() + ": " + (this.originator != null ? this.originator.toString() : "null");
}
}
@@ -0,0 +1,126 @@
package com.ibm.eNetwork.ECL.event;
import com.ibm.eNetwork.ECL.ECLPS;
import com.ibm.eNetwork.ECL.ECLPSUpdate;
/**
* Presentation Space event class matching IBM Host On-Demand ECLPSEvent.
*/
public class ECLPSEvent extends ECLEvent {
public static final int USER_EVENTS = 1;
public static final int HOST_EVENTS = 2;
public static final int ALL_EVENTS = 3;
private int type = 0;
private int start = 1;
private int end = 1;
private int startRow = 1;
private int startCol = 1;
private int endRow = 1;
private int endCol = 1;
private int oldCursorPos = 1;
private int newCursorPos = 1;
private boolean fullUpdate = true;
private boolean cursorVisible = true;
private int ringCounter = 0;
private ECLPSUpdate psUpdate = null;
private haus.nightmare.lib3270j.ecl.ECLPSEvent delegate = null;
public ECLPSEvent(Object source) {
super(source);
if (source instanceof ECLPS) {
ECLPS ps = (ECLPS) source;
this.end = ps.GetSize();
this.endRow = ps.GetRows();
this.endCol = ps.GetCols();
this.newCursorPos = ps.GetCursorPos();
} else if (source instanceof haus.nightmare.lib3270j.ecl.ECLPS) {
haus.nightmare.lib3270j.ecl.ECLPS ps = (haus.nightmare.lib3270j.ecl.ECLPS) source;
this.end = ps.GetSize();
this.endRow = ps.GetRows();
this.endCol = ps.GetCols();
this.newCursorPos = ps.GetCursorPos();
}
}
public ECLPSEvent(Object source, int type) {
this(source);
this.type = type;
}
public ECLPSEvent(haus.nightmare.lib3270j.ecl.ECLPSEvent delegate) {
super(delegate != null ? delegate.getSource() : null);
this.delegate = delegate;
if (delegate != null) {
this.type = delegate.getType();
this.start = delegate.getStart();
this.end = delegate.getEnd();
this.startRow = delegate.getStartRow();
this.startCol = delegate.getStartCol();
this.endRow = delegate.getEndRow();
this.endCol = delegate.getEndCol();
this.oldCursorPos = delegate.getOldCursorAddress() + 1;
this.newCursorPos = delegate.getNewCursorAddress() + 1;
this.fullUpdate = delegate.isFullUpdate();
this.cursorVisible = delegate.isCursorVisible();
this.ringCounter = delegate.getRingCounter();
if (delegate.getPSUpdate() != null) {
this.psUpdate = new ECLPSUpdate(delegate.getPSUpdate());
}
}
}
public int GetType() { return type; }
public int getType() { return type; }
public ECLPS GetPS() {
Object src = GetSource();
if (src instanceof ECLPS) {
return (ECLPS) src;
} else if (src instanceof haus.nightmare.lib3270j.ecl.ECLPS) {
return new ECLPS((haus.nightmare.lib3270j.ecl.ECLPS) src);
}
return null;
}
public void SetStart(int start) { this.start = start; }
public int GetStart() { return start; }
public int getStart() { return start; }
public int GetStartRow() { return startRow; }
public int getStartRow() { return startRow; }
public int GetStartCol() { return startCol; }
public int getStartCol() { return startCol; }
public void SetEnd(int end) { this.end = end; }
public int GetEnd() { return end; }
public int getEnd() { return end; }
public int GetEndRow() { return endRow; }
public int getEndRow() { return endRow; }
public int GetEndCol() { return endCol; }
public int getEndCol() { return endCol; }
public int GetOldCursorPos() { return oldCursorPos; }
public int getOldCursorPos() { return oldCursorPos; }
public int GetNewCursorPos() { return newCursorPos; }
public int getNewCursorPos() { return newCursorPos; }
public boolean IsFullUpdate() { return fullUpdate; }
public boolean isFullUpdate() { return fullUpdate; }
public boolean IsCursorVisible() { return cursorVisible; }
public boolean isCursorVisible() { return cursorVisible; }
public int GetRingCounter() { return ringCounter; }
public int getRingCounter() { return ringCounter; }
public ECLPSUpdate GetPSUpdate() { return psUpdate; }
public ECLPSUpdate getPSUpdate() { return psUpdate; }
public haus.nightmare.lib3270j.ecl.ECLPSEvent getDelegate() { return delegate; }
}
@@ -0,0 +1,13 @@
package com.ibm.eNetwork.ECL.event;
import com.ibm.eNetwork.ECL.ECLErr;
import com.ibm.eNetwork.ECL.ECLPS;
/**
* Presentation Space listener interface matching IBM Host On-Demand ECLPSListener.
*/
public interface ECLPSListener {
void PSNotifyEvent(ECLPSEvent event);
default void PSNotifyStop(ECLPS ps, int reason) {}
default void PSNotifyError(ECLPS ps, ECLErr err) {}
}
@@ -0,0 +1,48 @@
package com.ibm.eNetwork.ECL.event;
import com.ibm.eNetwork.ECL.ECLXfer;
/**
* File transfer event matching IBM Host On-Demand ECLXferEvent.
*/
public class ECLXferEvent extends ECLEvent {
private final String sourceFile;
private final String targetFile;
private final boolean receive;
private final long unitsDone;
private final long unitsToDo;
public ECLXferEvent(Object source, long unitsDone, String sourceFile, String targetFile, boolean receive) {
this(source, unitsDone, -1L, sourceFile, targetFile, receive);
}
public ECLXferEvent(Object source, long unitsDone, long unitsToDo, String sourceFile, String targetFile, boolean receive) {
super(source);
this.unitsDone = unitsDone;
this.unitsToDo = unitsToDo;
this.sourceFile = sourceFile;
this.targetFile = targetFile;
this.receive = receive;
}
public ECLXfer GetECLXfer() {
Object src = GetSource();
return (src instanceof ECLXfer) ? (ECLXfer) src : null;
}
public String GetSourceFile() { return sourceFile; }
public String getSourceFile() { return sourceFile; }
public String GetTargetFile() { return targetFile; }
public String getTargetFile() { return targetFile; }
public long GetBytesSent() { return unitsDone; }
public long getBytesSent() { return unitsDone; }
public long GetBytesToSend() { return unitsToDo; }
public long getBytesToSend() { return unitsToDo; }
public boolean IsReceive() { return receive; }
public boolean isReceive() { return receive; }
}
@@ -0,0 +1,13 @@
package com.ibm.eNetwork.ECL.event;
import com.ibm.eNetwork.ECL.ECLErr;
import com.ibm.eNetwork.ECL.ECLXfer;
/**
* File transfer listener interface matching IBM Host On-Demand ECLXferListener.
*/
public interface ECLXferListener {
void XferNotifyEvent(ECLXferEvent event);
default void XferNotifyStop(ECLXfer xfer, int reason) {}
default void XferNotifyError(ECLXfer xfer, ECLErr err) {}
}
@@ -0,0 +1,14 @@
package com.ibm.eNetwork.ECL.macrovariable;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroExpressionParser.
*/
public class MacroExpressionParser extends haus.nightmare.lib3270j.macro.variable.MacroExpressionParser implements Serializable {
private static final long serialVersionUID = 1L;
public MacroExpressionParser() {
super();
}
}
@@ -0,0 +1,14 @@
package com.ibm.eNetwork.ECL.macrovariable;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroVariables.
*/
public class MacroVariables extends haus.nightmare.lib3270j.macro.variable.MacroVariables implements Serializable {
private static final long serialVersionUID = 1L;
public MacroVariables() {
super();
}
}
@@ -0,0 +1,20 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDAttrib drop-in facade.
*/
public class ECLSDAttrib extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDAttrib {
private static final long serialVersionUID = 1L;
public ECLSDAttrib() {
super();
}
public ECLSDAttrib(char attrib, int row, int col, int plane) {
super(attrib, row, col, plane);
}
public ECLSDAttrib(char attrib, int row, int col, int plane, boolean optional) {
super(attrib, row, col, plane, optional);
}
}
@@ -0,0 +1,24 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDBlock drop-in facade.
*/
public class ECLSDBlock extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDBlock {
private static final long serialVersionUID = 1L;
public ECLSDBlock() {
super();
}
public ECLSDBlock(int srow, int scol, boolean caseSense, boolean optional) {
super(srow, scol, caseSense, optional);
}
public ECLSDBlock(int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional) {
super(srow, scol, erow, ecol, caseSense, optional);
}
public ECLSDBlock(String[] textLines, int srow, int scol, boolean caseSense) {
super(textLines, srow, scol, caseSense);
}
}

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