9 Commits

Author SHA1 Message Date
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
rudi 46a022d86b Add bugfixes from discord
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m9s
Release j3270 / Build & Publish Release (push) Successful in 32s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 42s
2026-09-10 09:24:39 -04:00
69 changed files with 6616 additions and 610 deletions
+5 -5
View File
@@ -20,21 +20,21 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Java JDK (Temurin 17) - name: Set up Java JDK (Temurin 11)
uses: actions/setup-java@v3 uses: actions/setup-java@v3
continue-on-error: true continue-on-error: true
with: with:
java-version: '17' java-version: '11'
distribution: 'temurin' distribution: 'temurin'
- name: Ensure Java Available - name: Ensure Java Available
run: | run: |
if ! command -v javac >/dev/null 2>&1; then if ! command -v javac >/dev/null 2>&1; then
echo "Installing OpenJDK 17 via package manager..." echo "Installing OpenJDK 11 via package manager..."
if command -v apt-get >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then
apt-get update && apt-get install -y openjdk-17-jdk-headless || true apt-get update && apt-get install -y openjdk-11-jdk-headless || true
elif command -v apk >/dev/null 2>&1; then elif command -v apk >/dev/null 2>&1; then
apk add --no-cache openjdk17 || true apk add --no-cache openjdk11 || true
fi fi
fi fi
echo "Java compiler:" echo "Java compiler:"
+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
View File
@@ -3,6 +3,11 @@
; or: ./run.sh -c j3270.ini ; or: ./run.sh -c j3270.ini
[appearance] [appearance]
; UI font settings (menus, dialogs, status bar)
; uiFontFamily = SansSerif
; uiFontSize = 13
;
; Terminal font settings (3270 screen display)
; fontFamily = IBM 3270 ; fontFamily = IBM 3270
; fontSize = 18 ; fontSize = 18
@@ -10,6 +15,7 @@
; startupBehavior = AUTO_CONNECT | SHOW_CONNECT | DO_NOTHING ; startupBehavior = AUTO_CONNECT | SHOW_CONNECT | DO_NOTHING
; autoConnectHost = mainframe.example.com ; autoConnectHost = mainframe.example.com
; autoConnectPort = 23 ; autoConnectPort = 23
; blockPaste = true
[colors] [colors]
; Host colors 0-15 (hex RGB) ; Host colors 0-15 (hex RGB)
@@ -60,6 +60,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
super("j3270 — Java TN3270 Terminal Emulator"); super("j3270 — Java TN3270 Terminal Emulator");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBackground(Color.BLACK); setBackground(Color.BLACK);
AppIcon.applyTo(this);
buildUI(); buildUI();
buildMenuBar(); buildMenuBar();
@@ -712,6 +713,14 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
return terminalPanel; return terminalPanel;
} }
public Telnet3270Client getClient() {
return client;
}
public StatusBar getStatusBar() {
return statusBar;
}
void connect(ConnectionConfig config) { void connect(ConnectionConfig config) {
lastHost = config.getHost(); lastHost = config.getHost();
lastPort = config.getPort(); lastPort = config.getPort();
@@ -941,7 +950,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
JTextArea area = new JTextArea(text); JTextArea area = new JTextArea(text);
area.setEditable(false); area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); area.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextArea(area); ThemeManager.styleTextArea(area);
JScrollPane sp = new JScrollPane(area); JScrollPane sp = new JScrollPane(area);
ThemeManager.styleScrollPane(sp); ThemeManager.styleScrollPane(sp);
@@ -974,7 +983,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
JTextArea area = new JTextArea(text); JTextArea area = new JTextArea(text);
area.setEditable(false); area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); area.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextArea(area); ThemeManager.styleTextArea(area);
JScrollPane sp = new JScrollPane(area); JScrollPane sp = new JScrollPane(area);
ThemeManager.styleScrollPane(sp); ThemeManager.styleScrollPane(sp);
@@ -998,55 +1007,23 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
"About j3270", JOptionPane.INFORMATION_MESSAGE); "About j3270", JOptionPane.INFORMATION_MESSAGE);
} }
// ========== Main ========== /**
* Configure application logging. When debug is false, disables all logging
public static void main(String[] args) { * to console and file, preventing creation of j3270.log.
boolean debug = false; */
boolean cliTls = false; public static void configureLogging(boolean debug) {
boolean cliNoVerifyCert = false;
Boolean cliTn3270e = null;
Boolean cliAutoSysUnlock = null;
GraphicsMode cliGraphicsMode = null;
String configFile = null;
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if ("-d".equals(arg) || "--debug".equals(arg)) {
debug = true;
} else if ("-s".equals(arg) || "--tls".equals(arg) || "--ssl".equals(arg)) {
cliTls = true;
} else if ("--no-verify-cert".equals(arg) || "--insecure".equals(arg) || "-k".equals(arg)) {
cliNoVerifyCert = true;
} else if ("--no-tn3270e".equals(arg) || "--plain-tn3270".equals(arg) || "--plain".equals(arg) || "-P".equals(arg) || "-p".equals(arg) || "--non-e".equals(arg)) {
cliTn3270e = false;
} else if ("--tn3270e".equals(arg)) {
cliTn3270e = true;
} else if ("--auto-sys-unlock".equals(arg)) {
cliAutoSysUnlock = true;
} else if ("--no-auto-sys-unlock".equals(arg)) {
cliAutoSysUnlock = false;
} else if (arg.startsWith("--graphics=")) {
cliGraphicsMode = GraphicsMode.fromString(arg.substring(11));
} else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliGraphicsMode = GraphicsMode.fromString(args[++i]);
} else if ("--no-graphics".equals(arg)) {
cliGraphicsMode = GraphicsMode.NONE;
} else if (("-c".equals(arg) || "--config".equals(arg)) && i + 1 < args.length) {
configFile = args[++i];
} else if (arg.startsWith("-")) {
System.err.println("Unknown option: " + arg);
} else {
remainingArgs.add(arg);
}
}
Level logLevel = debug ? Level.FINE : Level.INFO;
Logger globalRoot = Logger.getLogger(""); Logger globalRoot = Logger.getLogger("");
for (java.util.logging.Handler h : globalRoot.getHandlers()) { for (java.util.logging.Handler h : globalRoot.getHandlers()) {
globalRoot.removeHandler(h); globalRoot.removeHandler(h);
try {
h.close();
} catch (Exception ignored) {
} }
}
if (debug) {
Level logLevel = Level.ALL;
globalRoot.setLevel(Level.ALL);
java.util.logging.Filter appFilter = record -> record.getLoggerName() != null && java.util.logging.Filter appFilter = record -> record.getLoggerName() != null &&
(record.getLoggerName().startsWith("haus.nightmare") || record.getLoggerName().startsWith("org.pubvm")); (record.getLoggerName().startsWith("haus.nightmare") || record.getLoggerName().startsWith("org.pubvm"));
@@ -1077,6 +1054,117 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} catch (Exception e) { } catch (Exception e) {
System.err.println("Could not create j3270.log: " + e.getMessage()); System.err.println("Could not create j3270.log: " + e.getMessage());
} }
} else {
globalRoot.setLevel(Level.OFF);
for (String pkg : new String[]{"haus.nightmare", "haus.nightmare.j3270", "haus.nightmare.lib3270j", "org.pubvm"}) {
Logger l = Logger.getLogger(pkg);
l.setLevel(Level.OFF);
for (java.util.logging.Handler h : l.getHandlers()) {
l.removeHandler(h);
try {
h.close();
} catch (Exception ignored) {
}
}
}
}
}
private static void applyProxyUrl(ConnectionConfig config, String proxyUrl) {
if (config == null || proxyUrl == null || proxyUrl.trim().isEmpty()) return;
try {
java.net.URI uri = new java.net.URI(proxyUrl.trim());
String scheme = uri.getScheme() != null ? uri.getScheme().toLowerCase() : "http";
ConnectionConfig.ProxyType pType;
int pPort;
if (scheme.equals("http") || scheme.equals("https")) {
pType = ConnectionConfig.ProxyType.HTTP;
pPort = uri.getPort() > 0 ? uri.getPort() : 8080;
} else if (scheme.equals("socks4") || scheme.equals("socks4a")) {
pType = ConnectionConfig.ProxyType.SOCKS4;
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
} else if (scheme.equals("socks5") || scheme.equals("socks")) {
pType = ConnectionConfig.ProxyType.SOCKS5;
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
} else {
return;
}
String pHost = uri.getHost();
String pUser = null;
String pPass = null;
String userInfo = uri.getUserInfo();
if (userInfo != null) {
int colon = userInfo.indexOf(':');
if (colon >= 0) {
pUser = userInfo.substring(0, colon);
pPass = userInfo.substring(colon + 1);
} else {
pUser = userInfo;
}
}
if (pHost != null && !pHost.isEmpty()) {
config.setProxy(pType, pHost, pPort, pUser, pPass);
}
} catch (Exception e) {
log.warning("Failed to parse CLI proxy URL: " + proxyUrl);
}
}
// ========== Main ==========
public static void main(String[] args) {
boolean debug = false;
boolean cliTls = false;
boolean cliNoVerifyCert = false;
Boolean cliTn3270e = null;
Boolean cliAutoSysUnlock = null;
GraphicsMode cliGraphicsMode = null;
String configFile = null;
String cliProxyUrl = null;
String cliProfileName = null;
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if ("-d".equals(arg) || "--debug".equals(arg)) {
debug = true;
} else if ("-s".equals(arg) || "--tls".equals(arg) || "--ssl".equals(arg)) {
cliTls = true;
} else if ("--no-verify-cert".equals(arg) || "--insecure".equals(arg) || "-k".equals(arg)) {
cliNoVerifyCert = true;
} else if ("--no-tn3270e".equals(arg) || "--plain-tn3270".equals(arg) || "--plain".equals(arg) || "-P".equals(arg) || "-p".equals(arg) || "--non-e".equals(arg)) {
cliTn3270e = false;
} else if ("--tn3270e".equals(arg)) {
cliTn3270e = true;
} else if ("--auto-sys-unlock".equals(arg)) {
cliAutoSysUnlock = true;
} else if ("--no-auto-sys-unlock".equals(arg)) {
cliAutoSysUnlock = false;
} else if (arg.startsWith("--graphics=")) {
cliGraphicsMode = GraphicsMode.fromString(arg.substring(11));
} else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliGraphicsMode = GraphicsMode.fromString(args[++i]);
} else if ("--no-graphics".equals(arg)) {
cliGraphicsMode = GraphicsMode.NONE;
} else if (("-c".equals(arg) || "--config".equals(arg)) && i + 1 < args.length) {
configFile = args[++i];
} else if (arg.startsWith("--proxy=") || arg.startsWith("-proxy=")) {
cliProxyUrl = arg.substring(arg.indexOf('=') + 1).trim();
} else if (("--proxy".equals(arg) || "-proxy".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliProxyUrl = args[++i].trim();
} else if (arg.startsWith("--profile=")) {
cliProfileName = arg.substring(arg.indexOf('=') + 1).trim();
} else if ("--profile".equals(arg) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliProfileName = args[++i].trim();
} else if (arg.startsWith("-")) {
System.err.println("Unknown option: " + arg);
} else {
remainingArgs.add(arg);
}
}
configureLogging(debug);
if (configFile != null) { if (configFile != null) {
try { try {
@@ -1104,11 +1192,33 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
final Boolean finalTn3270e = cliTn3270e; final Boolean finalTn3270e = cliTn3270e;
final Boolean finalAutoSysUnlock = cliAutoSysUnlock; final Boolean finalAutoSysUnlock = cliAutoSysUnlock;
final GraphicsMode finalGraphicsMode = cliGraphicsMode; final GraphicsMode finalGraphicsMode = cliGraphicsMode;
final String finalProxyUrl = cliProxyUrl;
final String finalProfileName = cliProfileName;
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
J3270App app = new J3270App(); J3270App app = new J3270App();
app.setVisible(true); app.setVisible(true);
if (finalProfileName != null) {
haus.nightmare.j3270.storage.SavedHost profile = haus.nightmare.j3270.storage.HostStorage.findByName(finalProfileName);
if (profile == null) {
profile = haus.nightmare.j3270.storage.HostStorage.findById(finalProfileName);
}
if (profile != null) {
ConnectionConfig config = profile.toConnectionConfig();
if (finalTls) config.setUseTls(true);
if (finalNoVerify) config.setTlsVerifyCert(false);
if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e);
if (finalAutoSysUnlock != null) config.setAutoSysUnlock(finalAutoSysUnlock);
if (finalGraphicsMode != null) config.setGraphicsMode(finalGraphicsMode);
if (finalProxyUrl != null) applyProxyUrl(config, finalProxyUrl);
app.connect(config);
return;
} else {
System.err.println("Profile not found: " + finalProfileName);
}
}
if (!remainingArgs.isEmpty()) { if (!remainingArgs.isEmpty()) {
String hostArg = remainingArgs.get(0); String hostArg = remainingArgs.get(0);
int port = finalTls ? 992 : 23; int port = finalTls ? 992 : 23;
@@ -1149,6 +1259,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} else { } else {
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode()); config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
} }
if (finalProxyUrl != null) {
applyProxyUrl(config, finalProxyUrl);
}
app.connect(config); app.connect(config);
} else { } else {
haus.nightmare.j3270.config.Settings.StartupBehavior behavior = haus.nightmare.j3270.config.Settings haus.nightmare.j3270.config.Settings.StartupBehavior behavior = haus.nightmare.j3270.config.Settings
@@ -1158,6 +1271,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
SwingUtilities.invokeLater(app::showConnectDialog); SwingUtilities.invokeLater(app::showConnectDialog);
break; break;
case AUTO_CONNECT: case AUTO_CONNECT:
haus.nightmare.j3270.storage.SavedHost autoHost = haus.nightmare.j3270.storage.HostStorage.getAutoConnectHost();
if (autoHost != null) {
ConnectionConfig config = autoHost.toConnectionConfig();
if (finalTls) config.setUseTls(true);
if (finalNoVerify) config.setTlsVerifyCert(false);
if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e);
if (finalAutoSysUnlock != null) config.setAutoSysUnlock(finalAutoSysUnlock);
if (finalGraphicsMode != null) config.setGraphicsMode(finalGraphicsMode);
if (finalProxyUrl != null) applyProxyUrl(config, finalProxyUrl);
app.connect(config);
} else {
String host = haus.nightmare.j3270.config.Settings.getAutoConnectHost(); String host = haus.nightmare.j3270.config.Settings.getAutoConnectHost();
int port = haus.nightmare.j3270.config.Settings.getAutoConnectPort(); int port = haus.nightmare.j3270.config.Settings.getAutoConnectPort();
boolean tls = haus.nightmare.j3270.config.Settings.getAutoConnectTls(); boolean tls = haus.nightmare.j3270.config.Settings.getAutoConnectTls();
@@ -1167,16 +1291,22 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls); ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
config.setTlsVerifyCert(verify); config.setTlsVerifyCert(verify);
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e); config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
config.setAutoReconnect(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
config.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : haus.nightmare.j3270.config.Settings.getAutoSysUnlock()); config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
if (finalGraphicsMode != null) { if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode); config.setGraphicsMode(finalGraphicsMode);
} else { } else {
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode()); config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
} }
if (finalProxyUrl != null) {
applyProxyUrl(config, finalProxyUrl);
}
app.connect(config); app.connect(config);
} else { } else {
SwingUtilities.invokeLater(app::showConnectDialog); SwingUtilities.invokeLater(app::showConnectDialog);
} }
}
break; break;
case DO_NOTHING: case DO_NOTHING:
default: default:
@@ -45,6 +45,24 @@ public class Settings {
flushPrefs(); 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() { private static void flushPrefs() {
try { try {
prefs.flush(); prefs.flush();
@@ -141,6 +159,14 @@ public class Settings {
flushPrefs(); flushPrefs();
} }
public static boolean getAutoReconnect() {
return getAutoConnectAutoReconnect();
}
public static void setAutoReconnect(boolean autoReconnect) {
setAutoConnectAutoReconnect(autoReconnect);
}
public static int getAutoConnectReconnectMaxRetries() { public static int getAutoConnectReconnectMaxRetries() {
return prefs.getInt("autoConnectReconnectMaxRetries", 5); return prefs.getInt("autoConnectReconnectMaxRetries", 5);
} }
@@ -150,6 +176,32 @@ public class Settings {
flushPrefs(); flushPrefs();
} }
public static boolean getInputMask() {
return prefs.getBoolean("inputMask", true);
}
public static void setInputMask(boolean mask) {
prefs.putBoolean("inputMask", mask);
flushPrefs();
}
public static boolean getInputMaskEnabled() {
return getInputMask();
}
public static void setInputMaskEnabled(boolean enabled) {
setInputMask(enabled);
}
public static String getInputMaskChar() {
return prefs.get("inputMaskChar", "*");
}
public static void setInputMaskChar(String ch) {
prefs.put("inputMaskChar", (ch != null && !ch.trim().isEmpty()) ? ch.trim().substring(0, 1) : "*");
flushPrefs();
}
public static haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() { public static haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() {
String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name()); String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name());
return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr); return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr);
@@ -356,6 +408,17 @@ public class Settings {
prefs.putBoolean("blockSelectMode", block); 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 ========== // ========== Clipboard & Tabular Paste ==========
public static boolean getEnablePasteFromExcel() { public static boolean getEnablePasteFromExcel() {
@@ -507,6 +570,16 @@ public class Settings {
switch (key) { switch (key) {
case "fontFamily": setFontFamily(value); break; case "fontFamily": setFontFamily(value); break;
case "fontSize": setFontSize(Integer.parseInt(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 "javaUiTheme":
case "theme": case "theme":
case "uiTheme": case "uiTheme":
@@ -574,10 +647,34 @@ public class Settings {
setDynamicCols(Integer.parseInt(value)); setDynamicCols(Integer.parseInt(value));
break; break;
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break; case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
case "blockPaste": setBlockPaste(Boolean.parseBoolean(value)); break;
case "autoReconnect":
case "auto_reconnect":
case "autoConnectAutoReconnect":
setAutoConnectAutoReconnect(Boolean.parseBoolean(value));
break;
case "reconnectMaxRetries":
case "autoConnectReconnectMaxRetries":
setAutoConnectReconnectMaxRetries(Integer.parseInt(value));
break;
case "inputMask":
case "input_mask":
case "inputMaskEnabled":
case "maskInput":
setInputMask(Boolean.parseBoolean(value));
break;
case "inputMaskChar":
case "input_mask_char":
case "maskChar":
setInputMaskChar(value);
break;
case "autoSysUnlock": case "autoSysUnlock":
case "auto_sys_unlock": case "auto_sys_unlock":
setAutoSysUnlock(Boolean.parseBoolean(value)); setAutoSysUnlock(Boolean.parseBoolean(value));
break; break;
case "blockPasteMode":
setBlockPaste(Boolean.parseBoolean(value));
break;
case "enablePasteFromExcel": case "enablePasteFromExcel":
case "pasteFromExcel": case "pasteFromExcel":
case "excelPaste": case "excelPaste":
@@ -595,6 +692,10 @@ public class Settings {
case "clipboard": case "clipboard":
switch (key) { switch (key) {
case "blockPaste":
case "blockPasteMode":
setBlockPaste(Boolean.parseBoolean(value));
break;
case "enablePasteFromExcel": case "enablePasteFromExcel":
case "pasteFromExcel": case "pasteFromExcel":
case "excelPaste": case "excelPaste":
@@ -707,6 +808,8 @@ public class Settings {
// [appearance] // [appearance]
w.println("[appearance]"); w.println("[appearance]");
w.println("javaUiTheme = " + getJavaUiTheme().name()); w.println("javaUiTheme = " + getJavaUiTheme().name());
w.println("uiFontFamily = " + getUiFontFamily());
w.println("uiFontSize = " + getUiFontSize());
w.println("fontFamily = " + getFontFamily()); w.println("fontFamily = " + getFontFamily());
w.println("fontSize = " + getFontSize()); w.println("fontSize = " + getFontSize());
w.println("crosshairRuler = " + getCrosshairRuler()); w.println("crosshairRuler = " + getCrosshairRuler());
@@ -729,6 +832,11 @@ public class Settings {
w.println("dynamicRows = " + getDynamicRows()); w.println("dynamicRows = " + getDynamicRows());
w.println("dynamicCols = " + getDynamicCols()); w.println("dynamicCols = " + getDynamicCols());
w.println("blockSelectMode = " + getBlockSelectMode()); w.println("blockSelectMode = " + getBlockSelectMode());
w.println("blockPaste = " + getBlockPaste());
w.println("autoReconnect = " + getAutoConnectAutoReconnect());
w.println("autoConnectReconnectMaxRetries = " + getAutoConnectReconnectMaxRetries());
w.println("inputMask = " + getInputMask());
w.println("inputMaskChar = " + getInputMaskChar());
w.println("autoSysUnlock = " + getAutoSysUnlock()); w.println("autoSysUnlock = " + getAutoSysUnlock());
w.println("enablePasteFromExcel = " + getEnablePasteFromExcel()); w.println("enablePasteFromExcel = " + getEnablePasteFromExcel());
w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine()); w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine());
@@ -1,5 +1,6 @@
package haus.nightmare.j3270.ft; package haus.nightmare.j3270.ft;
import haus.nightmare.j3270.ui.AppIcon;
import haus.nightmare.j3270.ui.HostDirectoryDialog; import haus.nightmare.j3270.ui.HostDirectoryDialog;
import haus.nightmare.j3270.ui.ThemeManager; import haus.nightmare.j3270.ui.ThemeManager;
import haus.nightmare.lib3270j.ft.FTConfig; import haus.nightmare.lib3270j.ft.FTConfig;
@@ -92,6 +93,7 @@ public class FileTransferDialog extends JDialog {
super(owner, "File Transfer (IND$FILE)", false); super(owner, "File Transfer (IND$FILE)", false);
this.owner = owner; this.owner = owner;
this.coordinator = coordinator; this.coordinator = coordinator;
AppIcon.applyTo(this);
buildUI(); buildUI();
pack(); pack();
@@ -0,0 +1,434 @@
package haus.nightmare.j3270.storage;
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) {
}
}
// ================= 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"));
if ("PROGRAMMED_SYMBOLS".equalsIgnoreCase(gm)) gm = "BOTH";
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,222 @@
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);
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;
}
}
@@ -1,16 +1,30 @@
package haus.nightmare.j3270.ui; 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.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel; import haus.nightmare.lib3270j.TerminalModel;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.TitledBorder;
import java.awt.*; 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 { 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 hostField;
private JTextField portField; private JTextField portField;
private JComboBox<TerminalModel> modelCombo; private JComboBox<TerminalModel> modelCombo;
@@ -26,72 +40,131 @@ public class ConnectDialog extends JDialog {
private JCheckBox tn3270eCheckBox; private JCheckBox tn3270eCheckBox;
private JCheckBox keepAliveCheckBox; private JCheckBox keepAliveCheckBox;
private JCheckBox autoReconnectCheckBox; 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 boolean confirmed;
private ConnectionConfig result; private ConnectionConfig result;
public ConnectDialog(Frame parent) { public ConnectDialog(Frame parent) {
super(parent, "Connect to Host", true); super(parent, "Connect to Host", true);
AppIcon.applyTo(this);
buildUI(); buildUI();
pack(); pack();
setLocationRelativeTo(parent); setLocationRelativeTo(parent);
setResizable(false); setResizable(true);
} }
private void buildUI() { private void buildUI() {
JPanel mainPanel = new JPanel(new GridBagLayout()); JPanel contentPanel = new JPanel(new GridBagLayout());
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16)); contentPanel.setBorder(BorderFactory.createEmptyBorder(12, 14, 12, 14));
GridBagConstraints gbc = new GridBagConstraints(); GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 6, 5, 6); gbc.insets = new Insets(4, 5, 4, 5);
gbc.fill = GridBagConstraints.HORIZONTAL; 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.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:"); JLabel hostLabel = new JLabel("Host:");
hostLabel.setFont(labelFont); hostLabel.setFont(labelFont);
mainPanel.add(hostLabel, gbc); contentPanel.add(hostLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
hostField = createField(20); hostField = createField(20);
mainPanel.add(hostField, gbc); contentPanel.add(hostField, gbc);
// Port gridy++;
// 2. Port
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 1; gbc.gridy = gridy;
gbc.weightx = 0; gbc.weightx = 0;
JLabel portLabel = new JLabel("Port:"); JLabel portLabel = new JLabel("Port:");
portLabel.setFont(labelFont); portLabel.setFont(labelFont);
mainPanel.add(portLabel, gbc); contentPanel.add(portLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
portField = createField(6); portField = createField(6);
portField.setText("23"); portField.setText("23");
mainPanel.add(portField, gbc); contentPanel.add(portField, gbc);
// Model gridy++;
// 3. Model
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 2; gbc.gridy = gridy;
gbc.weightx = 0; gbc.weightx = 0;
JLabel modelLabel = new JLabel("Model:"); JLabel modelLabel = new JLabel("Model:");
modelLabel.setFont(labelFont); modelLabel.setFont(labelFont);
mainPanel.add(modelLabel, gbc); contentPanel.add(modelLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
modelCombo = new JComboBox<>(TerminalModel.values()); modelCombo = new JComboBox<>(TerminalModel.values());
modelCombo.setSelectedItem(TerminalModel.IBM_3279_4); modelCombo.setSelectedItem(TerminalModel.IBM_3279_4);
modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); modelCombo.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleComboBox(modelCombo); 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.gridx = 0;
gbc.gridy = 3; gbc.gridy = gridy;
gbc.weightx = 0; gbc.weightx = 0;
dynamicDimLabel = new JLabel("Screen Size:"); dynamicDimLabel = new JLabel("Screen Size:");
dynamicDimLabel.setFont(labelFont); dynamicDimLabel.setFont(labelFont);
mainPanel.add(dynamicDimLabel, gbc); contentPanel.add(dynamicDimLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
@@ -112,7 +185,7 @@ public class ConnectDialog extends JDialog {
dynamicDimPanel.add(dynamicRowsSpinner); dynamicDimPanel.add(dynamicRowsSpinner);
dynamicDimPanel.add(colsLabel); dynamicDimPanel.add(colsLabel);
dynamicDimPanel.add(dynamicColsSpinner); dynamicDimPanel.add(dynamicColsSpinner);
mainPanel.add(dynamicDimPanel, gbc); contentPanel.add(dynamicDimPanel, gbc);
Runnable updateDynamicVisibility = () -> { Runnable updateDynamicVisibility = () -> {
TerminalModel m = (TerminalModel) modelCombo.getSelectedItem(); TerminalModel m = (TerminalModel) modelCombo.getSelectedItem();
@@ -124,40 +197,46 @@ public class ConnectDialog extends JDialog {
modelCombo.addActionListener(e -> updateDynamicVisibility.run()); modelCombo.addActionListener(e -> updateDynamicVisibility.run());
updateDynamicVisibility.run(); updateDynamicVisibility.run();
// LU Name gridy++;
// 5. LU Name
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 4; gbc.gridy = gridy;
gbc.weightx = 0; gbc.weightx = 0;
JLabel luLabel = new JLabel("LU Name:"); JLabel luLabel = new JLabel("LU Name:");
luLabel.setFont(labelFont); luLabel.setFont(labelFont);
mainPanel.add(luLabel, gbc); contentPanel.add(luLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
luField = createField(12); luField = createField(12);
mainPanel.add(luField, gbc); contentPanel.add(luField, gbc);
// Graphics Mode gridy++;
// 6. Graphics Mode
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 5; gbc.gridy = gridy;
gbc.weightx = 0; gbc.weightx = 0;
JLabel graphicsLabel = new JLabel("Graphics:"); JLabel graphicsLabel = new JLabel("Graphics:");
graphicsLabel.setFont(labelFont); graphicsLabel.setFont(labelFont);
mainPanel.add(graphicsLabel, gbc); contentPanel.add(graphicsLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values()); graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode()); 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); ThemeManager.styleComboBox(graphicsCombo);
mainPanel.add(graphicsCombo, gbc); contentPanel.add(graphicsCombo, gbc);
// Code Page gridy++;
// 7. Code Page
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 6; gbc.gridy = gridy;
gbc.weightx = 0; gbc.weightx = 0;
JLabel cpLabel = new JLabel("Code Page:"); JLabel cpLabel = new JLabel("Code Page:");
cpLabel.setFont(labelFont); cpLabel.setFont(labelFont);
mainPanel.add(cpLabel, gbc); contentPanel.add(cpLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
String[] commonCodePages = { String[] commonCodePages = {
@@ -178,43 +257,44 @@ public class ConnectDialog extends JDialog {
"1155 - Turkey (Turkish Latin-5 Euro \u20AC)", "1155 - Turkey (Turkish Latin-5 Euro \u20AC)",
"905 - Turkey (Turkish Latin-3)", "905 - Turkey (Turkish Latin-3)",
"420 - Arabic Bilingual", "420 - Arabic Bilingual",
"424 - Hebrew (with Lowercase)", "424 - Hebrew (with Latin)",
"803 - Hebrew Old / Standard", "803 - Hebrew Character Set",
"838 - Thai", "838 - Thai Extended",
"1160 - Thai (Euro \u20AC)", "1160 - Thai Euro \u20AC",
"1025 - Cyrillic Multilingual", "1025 - Cyrillic Multilingual",
"1123 - Cyrillic Ukraine", "1123 - Cyrillic Ukraine",
"1154 - Cyrillic Multilingual (Euro \u20AC)", "1154 - Cyrillic Euro \u20AC",
"880 - Cyrillic Russian", "880 - Cyrillic (Russian)",
"1140 - US / Canada (Euro \u20AC)", "1140 - US / Canada (Euro \u20AC)",
"1141 - Germany / Austria (Euro \u20AC)", "1141 - Germany / Austria (Euro \u20AC)",
"1142 - Denmark / Norway (Euro \u20AC)",
"1143 - Sweden / Finland (Euro \u20AC)",
"1144 - Italy (Euro \u20AC)",
"1145 - Spain / Latin America (Euro \u20AC)",
"1146 - United Kingdom (Euro \u20AC)",
"1147 - France (Euro \u20AC)",
"1148 - International (Euro \u20AC)", "1148 - International (Euro \u20AC)",
"1149 - Iceland (Euro \u20AC)",
"930 - Japanese Katakana Mixed DBCS", "930 - Japanese Katakana Mixed DBCS",
"939 - Japanese Latin Mixed DBCS", "939 - Japanese Latin Mixed DBCS",
"935 - Simplified Chinese Mixed DBCS", "935 - Simplified Chinese Mixed DBCS",
"1388 - Simplified Chinese Extended Mixed DBCS",
"937 - Traditional Chinese Mixed DBCS", "937 - Traditional Chinese Mixed DBCS",
"1371 - Traditional Chinese Extended Mixed DBCS", "1388 - Simplified Chinese Extended DBCS",
"1371 - Traditional Chinese Extended DBCS",
"933 - Korean Mixed DBCS" "933 - Korean Mixed DBCS"
}; };
codePageCombo = new JComboBox<>(commonCodePages); codePageCombo = new JComboBox<>(commonCodePages);
codePageCombo.setEditable(true); codePageCombo.setFont(ThemeManager.getMonospacedUiFont());
String currentCp = haus.nightmare.j3270.config.Settings.getCodePage();
for (String item : commonCodePages) {
if (item.startsWith(currentCp + " ") || item.equals(currentCp)) {
codePageCombo.setSelectedItem(item);
break;
}
}
codePageCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
ThemeManager.styleComboBox(codePageCombo); 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.gridx = 1;
gbc.gridy = 7; gbc.gridy = gridy;
gbc.weightx = 1.0; tlsCheckBox = new JCheckBox("Use TLS / SSL Encryption");
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
ThemeManager.styleCheckBox(tlsCheckBox); ThemeManager.styleCheckBox(tlsCheckBox);
tlsCheckBox.setFont(labelFont); tlsCheckBox.setFont(labelFont);
tlsCheckBox.addActionListener(e -> { tlsCheckBox.addActionListener(e -> {
@@ -227,46 +307,169 @@ public class ConnectDialog extends JDialog {
portField.setText("23"); portField.setText("23");
} }
}); });
mainPanel.add(tlsCheckBox, gbc); contentPanel.add(tlsCheckBox, gbc);
// Verify Certificate Checkbox gridy++;
// 9. Verify Cert Checkbox
gbc.gridx = 1; gbc.gridx = 1;
gbc.gridy = 8; gbc.gridy = gridy;
verifyCertCheckBox = new JCheckBox("Verify Server Certificate"); verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
ThemeManager.styleCheckBox(verifyCertCheckBox); ThemeManager.styleCheckBox(verifyCertCheckBox);
verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); verifyCertCheckBox.setFont(ThemeManager.getUiFont());
verifyCertCheckBox.setSelected(true); verifyCertCheckBox.setSelected(true);
verifyCertCheckBox.setEnabled(false); verifyCertCheckBox.setEnabled(false);
mainPanel.add(verifyCertCheckBox, gbc); contentPanel.add(verifyCertCheckBox, gbc);
// TN3270E Checkbox gridy++;
// 10. TN3270E Checkbox
gbc.gridx = 1; gbc.gridx = 1;
gbc.gridy = 9; gbc.gridy = gridy;
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)"); tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
ThemeManager.styleCheckBox(tn3270eCheckBox); 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()); 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.gridx = 1;
gbc.gridy = 10; gbc.gridy = gridy;
keepAliveCheckBox = new JCheckBox("Enable Keep-Alive Heartbeat (NOP)"); keepAliveCheckBox = new JCheckBox("Enable Keep-Alive Heartbeat (NOP)");
ThemeManager.styleCheckBox(keepAliveCheckBox); 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()); 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.gridx = 1;
gbc.gridy = 11; gbc.gridy = gridy;
autoReconnectCheckBox = new JCheckBox("Auto-Reconnect on Disconnect"); autoReconnectCheckBox = new JCheckBox("Auto-Reconnect on Disconnect");
ThemeManager.styleCheckBox(autoReconnectCheckBox); 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()); 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)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
JButton connectBtn = new JButton("Connect"); JButton connectBtn = new JButton("Connect");
@@ -284,13 +487,14 @@ public class ConnectDialog extends JDialog {
buttonPanel.add(cancelBtn); buttonPanel.add(cancelBtn);
buttonPanel.add(connectBtn); buttonPanel.add(connectBtn);
contentPanel.add(buttonPanel, gbc);
gbc.gridx = 0; JScrollPane scrollPane = new JScrollPane(contentPanel);
gbc.gridy = 12; scrollPane.setBorder(BorderFactory.createEmptyBorder());
gbc.gridwidth = 2; scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
mainPanel.add(buttonPanel, gbc); scrollPane.getVerticalScrollBar().setUnitIncrement(14);
setContentPane(scrollPane);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this); ThemeManager.applyThemeToWindow(this);
// Enter key triggers connect // Enter key triggers connect
@@ -299,11 +503,194 @@ public class ConnectDialog extends JDialog {
private JTextField createField(int cols) { private JTextField createField(int cols) {
JTextField field = new JTextField(cols); JTextField field = new JTextField(cols);
field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14)); field.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(field); ThemeManager.styleTextField(field);
return 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);
}
}
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() : "");
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();
boolean tls = tlsCheckBox.isSelected();
boolean verify = verifyCertCheckBox.isSelected();
boolean tn3270e = tn3270eCheckBox.isSelected();
String gfx = "BOTH";
if (graphicsCombo.getSelectedItem() != null) {
gfx = graphicsCombo.getSelectedItem().toString();
}
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,
"TSO", tls, verify, tn3270e, gfx, cp, ptStr, pHost, pPort, pUser, pPass);
}
// ================= Connect Action =================
private void onConnect() { private void onConnect() {
String host = hostField.getText().trim(); String host = hostField.getText().trim();
if (host.isEmpty()) { if (host.isEmpty()) {
@@ -320,19 +707,32 @@ public class ConnectDialog extends JDialog {
} }
TerminalModel selectedModel = (TerminalModel) modelCombo.getSelectedItem(); TerminalModel selectedModel = (TerminalModel) modelCombo.getSelectedItem();
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); result = new ConnectionConfig(host, port, selectedModel);
if (selectedModel != null && selectedModel.isDynamic()) { }
if (selectedModel.isDynamic()) {
int dRows = (Integer) dynamicRowsSpinner.getValue(); int dRows = (Integer) dynamicRowsSpinner.getValue();
int dCols = (Integer) dynamicColsSpinner.getValue(); int dCols = (Integer) dynamicColsSpinner.getValue();
result.setDynamicDimensions(dRows, dCols); result.setDynamicDimensions(dRows, dCols);
haus.nightmare.j3270.config.Settings.setDynamicRows(dRows); haus.nightmare.j3270.config.Settings.setDynamicRows(dRows);
haus.nightmare.j3270.config.Settings.setDynamicCols(dCols); haus.nightmare.j3270.config.Settings.setDynamicCols(dCols);
} }
String lu = luField.getText().trim(); String lu = luField.getText().trim();
if (!lu.isEmpty()) { if (!lu.isEmpty()) {
result.setLuName(lu); result.setLuName(lu);
} }
if (graphicsCombo.getSelectedItem() != null) {
result.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem()); result.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
}
Object cpSelection = codePageCombo.getSelectedItem(); Object cpSelection = codePageCombo.getSelectedItem();
if (cpSelection != null) { if (cpSelection != null) {
@@ -360,6 +760,22 @@ public class ConnectDialog extends JDialog {
result.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries()); result.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
haus.nightmare.j3270.config.Settings.setAutoConnectAutoReconnect(ar); 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);
}
confirmed = true; confirmed = true;
dispose(); dispose();
} }
@@ -406,4 +822,49 @@ public class ConnectDialog extends JDialog {
codePageCombo.setSelectedItem(cp); 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());
}
} }
@@ -32,6 +32,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
super(parent, "3270 Presentation Space Field Inspector", false); super(parent, "3270 Presentation Space Field Inspector", false);
this.client = client; this.client = client;
this.terminalPanel = terminalPanel; this.terminalPanel = terminalPanel;
AppIcon.applyTo(this);
buildUI(); buildUI();
setSize(850, 420); setSize(850, 420);
@@ -60,7 +61,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
JPanel topPanel = new JPanel(new BorderLayout()); JPanel topPanel = new JPanel(new BorderLayout());
topPanel.setOpaque(false); topPanel.setOpaque(false);
countLabel = new JLabel("0 fields detected on screen"); 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); topPanel.add(countLabel, BorderLayout.WEST);
JButton refreshBtn = new JButton("Refresh"); JButton refreshBtn = new JButton("Refresh");
@@ -83,12 +84,12 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
table = new JTable(tableModel); table = new JTable(tableModel);
ThemeManager.styleTable(table); ThemeManager.styleTable(table);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); table.setFont(ThemeManager.getMonospacedUiFont());
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20); table.setRowHeight(Math.max(22, ThemeManager.getMonospacedUiFont().getSize() + 8));
JTableHeader header = table.getTableHeader(); JTableHeader header = table.getTableHeader();
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12)); header.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
// Column widths // Column widths
int[] widths = {35, 45, 60, 60, 40, 45, 45, 45, 50, 55, 60, 260}; int[] widths = {35, 45, 60, 60, 40, 45, 45, 45, 50, 55, 60, 260};
@@ -32,6 +32,7 @@ public class FindDialog extends JDialog {
super(parent, "Find on Screen", false); super(parent, "Find on Screen", false);
this.client = client; this.client = client;
this.terminalPanel = terminalPanel; this.terminalPanel = terminalPanel;
AppIcon.applyTo(this);
buildUI(); buildUI();
pack(); pack();
@@ -50,7 +51,7 @@ public class FindDialog extends JDialog {
gbc.insets = new Insets(4, 4, 4, 4); gbc.insets = new Insets(4, 4, 4, 4);
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font labelFont = ThemeManager.getUiFont();
// Search text // Search text
gbc.gridx = 0; gbc.gridx = 0;
@@ -63,7 +64,7 @@ public class FindDialog extends JDialog {
gbc.weightx = 1.0; gbc.weightx = 1.0;
searchField = new JTextField(20); searchField = new JTextField(20);
searchField.setText(lastSearchText); searchField.setText(lastSearchText);
searchField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); searchField.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(searchField); ThemeManager.styleTextField(searchField);
formPanel.add(searchField, gbc); formPanel.add(searchField, gbc);
@@ -102,7 +103,7 @@ public class FindDialog extends JDialog {
gbc.gridwidth = 2; gbc.gridwidth = 2;
statusLabel = new JLabel(" "); statusLabel = new JLabel(" ");
statusLabel.setForeground(ThemeManager.getOiaFgAlert()); statusLabel.setForeground(ThemeManager.getOiaFgAlert());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(labelFont);
formPanel.add(statusLabel, gbc); formPanel.add(statusLabel, gbc);
mainPanel.add(formPanel, BorderLayout.CENTER); mainPanel.add(formPanel, BorderLayout.CENTER);
@@ -40,6 +40,7 @@ public class HostDirectoryDialog extends JDialog {
super(parent, "Host Catalog & Directory Browser", true); super(parent, "Host Catalog & Directory Browser", true);
this.client = client; this.client = client;
this.initialHostType = hostType; this.initialHostType = hostType;
AppIcon.applyTo(this);
buildUI(initialQuery); buildUI(initialQuery);
setSize(700, 420); setSize(700, 420);
@@ -50,7 +51,7 @@ public class HostDirectoryDialog extends JDialog {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 12, 12, 12)); 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 // Top Query bar
JPanel topPanel = new JPanel(new GridBagLayout()); JPanel topPanel = new JPanel(new GridBagLayout());
@@ -78,7 +79,7 @@ public class HostDirectoryDialog extends JDialog {
gbc.gridx = 3; gbc.gridx = 3;
gbc.weightx = 1.0; gbc.weightx = 1.0;
queryField = new JTextField(initialQuery != null ? initialQuery : "", 16); queryField = new JTextField(initialQuery != null ? initialQuery : "", 16);
queryField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); queryField.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(queryField); ThemeManager.styleTextField(queryField);
topPanel.add(queryField, gbc); topPanel.add(queryField, gbc);
@@ -96,12 +97,12 @@ public class HostDirectoryDialog extends JDialog {
tableModel = new DefaultTableModel(); tableModel = new DefaultTableModel();
table = new JTable(tableModel); table = new JTable(tableModel);
ThemeManager.styleTable(table); ThemeManager.styleTable(table);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); table.setFont(ThemeManager.getMonospacedUiFont());
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20); table.setRowHeight(Math.max(22, ThemeManager.getMonospacedUiFont().getSize() + 8));
JTableHeader header = table.getTableHeader(); 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() { table.addMouseListener(new MouseAdapter() {
@Override @Override
@@ -122,7 +123,7 @@ public class HostDirectoryDialog extends JDialog {
statusLabel = new JLabel("Enter a dataset pattern or parse active screen."); statusLabel = new JLabel("Enter a dataset pattern or parse active screen.");
statusLabel.setForeground(ThemeManager.getFgMuted()); statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(font);
bottomPanel.add(statusLabel, BorderLayout.WEST); bottomPanel.add(statusLabel, BorderLayout.WEST);
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
@@ -48,6 +48,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) { public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) {
super(parent, "IBM 3287 Printer Session Manager", false); super(parent, "IBM 3287 Printer Session Manager", false);
this.parent = parent; this.parent = parent;
AppIcon.applyTo(this);
config = new PrinterConfig(defaultHost, defaultPort); config = new PrinterConfig(defaultHost, defaultPort);
config.setUseTls(defaultTls); config.setUseTls(defaultTls);
@@ -71,7 +72,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
gbc.insets = new Insets(3, 4, 3, 4); gbc.insets = new Insets(3, 4, 3, 4);
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12); Font labelFont = ThemeManager.getUiFont();
// Host / Port // Host / Port
gbc.gridx = 0; gbc.gridy = 0; gbc.gridx = 0; gbc.gridy = 0;
@@ -205,7 +206,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Spool text area // Spool text area
spoolArea = new JTextArea(); spoolArea = new JTextArea();
spoolArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); spoolArea.setFont(ThemeManager.getMonospacedUiFont());
spoolArea.setEditable(false); spoolArea.setEditable(false);
ThemeManager.styleTextArea(spoolArea); ThemeManager.styleTextArea(spoolArea);
@@ -256,7 +257,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
private JTextField createField(String text, int cols) { private JTextField createField(String text, int cols) {
JTextField tf = new JTextField(text, cols); JTextField tf = new JTextField(text, cols);
tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); tf.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(tf); ThemeManager.styleTextField(tf);
return tf; return tf;
} }
@@ -25,6 +25,7 @@ public class ScriptDialog extends JDialog {
super(parent, "Run Keystrokes / ECL Script", false); super(parent, "Run Keystrokes / ECL Script", false);
this.client = client; this.client = client;
this.terminalPanel = terminalPanel; this.terminalPanel = terminalPanel;
AppIcon.applyTo(this);
buildUI(); buildUI();
setSize(600, 480); setSize(600, 480);
@@ -35,7 +36,7 @@ public class ScriptDialog extends JDialog {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14)); mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font labelFont = ThemeManager.getUiFont();
// Header // Header
JPanel topPanel = new JPanel(new BorderLayout(5, 5)); JPanel topPanel = new JPanel(new BorderLayout(5, 5));
@@ -48,7 +49,7 @@ public class ScriptDialog extends JDialog {
// Script area // Script area
scriptArea = new JTextArea(); scriptArea = new JTextArea();
scriptArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); scriptArea.setFont(ThemeManager.getMonospacedUiFont());
scriptArea.setLineWrap(true); scriptArea.setLineWrap(true);
scriptArea.setWrapStyleWord(false); scriptArea.setWrapStyleWord(false);
ThemeManager.styleTextArea(scriptArea); ThemeManager.styleTextArea(scriptArea);
@@ -74,7 +75,7 @@ public class ScriptDialog extends JDialog {
for (String token : tokens) { for (String token : tokens) {
JButton btn = new JButton(token); 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); ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.DEFAULT);
btn.setFocusable(false); btn.setFocusable(false);
btn.setMargin(new Insets(2, 4, 2, 4)); btn.setMargin(new Insets(2, 4, 2, 4));
@@ -94,7 +95,7 @@ public class ScriptDialog extends JDialog {
statusLabel = new JLabel("Ready"); statusLabel = new JLabel("Ready");
statusLabel.setForeground(ThemeManager.getFgMuted()); statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(labelFont);
bottomPanel.add(statusLabel, BorderLayout.WEST); bottomPanel.add(statusLabel, BorderLayout.WEST);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
File diff suppressed because it is too large Load Diff
@@ -34,9 +34,9 @@ public class StatusBar extends JPanel {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setBackground(ThemeManager.getStatusBarBg()); setBackground(ThemeManager.getStatusBarBg());
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder())); 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()); connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim());
tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal()); tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
@@ -143,9 +143,36 @@ public class StatusBar extends JPanel {
} }
setBackground(ThemeManager.getStatusBarBg(theme)); setBackground(ThemeManager.getStatusBarBg(theme));
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme))); setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme)));
Font oiaFont = ThemeManager.getMonospacedUiFont();
updateBarHeight(oiaFont);
applyFontRecursively(this, oiaFont);
revalidate();
repaint();
updateStatus(); 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() { public void updateStatus() {
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) { if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(this::updateStatus); SwingUtilities.invokeLater(this::updateStatus);
@@ -65,6 +65,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private static final Color CURSOR_COLOR = new Color(255, 255, 255, 180); private static final Color CURSOR_COLOR = new Color(255, 255, 255, 180);
private boolean crosshairRulerEnabled = false; private boolean crosshairRulerEnabled = false;
private static final Color CROSSHAIR_RULER_COLOR = new Color(0, 255, 0, 102); // 40% alpha (cRC) private static final Color CROSSHAIR_RULER_COLOR = new Color(0, 255, 0, 102); // 40% alpha (cRC)
private boolean inputMaskEnabled = haus.nightmare.j3270.config.Settings.getInputMask();
private String inputMaskChar = haus.nightmare.j3270.config.Settings.getInputMaskChar();
private boolean textBlinkVisible = true; private boolean textBlinkVisible = true;
private Image wallpaperImage = null; private Image wallpaperImage = null;
private haus.nightmare.lib3270j.graphics.HODWallpaper hodWallpaper = null; private haus.nightmare.lib3270j.graphics.HODWallpaper hodWallpaper = null;
@@ -496,11 +498,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
int curCol = cols > 0 ? curPos % cols : 0; int curCol = cols > 0 ? curPos % cols : 0;
boolean excelPaste = haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel(); boolean excelPaste = haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel();
boolean stopAtProtected = haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine(); boolean stopAtProtected = haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine();
boolean blockPaste = haus.nightmare.j3270.config.Settings.getBlockPaste();
if (client.getInputProcessor() != null && (excelPaste || stopAtProtected || text.contains("\t") || text.contains("\n") || text.contains("\r"))) { if (client.getInputProcessor() != null && (excelPaste || stopAtProtected || blockPaste || text.contains("\t") || text.contains("\n") || text.contains("\r"))) {
client.getInputProcessor().pasteText(text, excelPaste, stopAtProtected); client.getInputProcessor().pasteText(text, excelPaste, stopAtProtected, blockPaste);
} else if (client.getPS() != null) { } else if (client.getPS() != null) {
if (blockPaste) {
client.getPS().pasteRectangular(text, curRow, curCol);
} else {
client.getPS().pasteString(text, curRow, curCol); client.getPS().pasteString(text, curRow, curCol);
}
} else { } else {
for (char ch : text.toCharArray()) { for (char ch : text.toCharArray()) {
if (ch == '\n' || ch == '\r') { if (ch == '\n' || ch == '\r') {
@@ -575,6 +582,14 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
}); });
popup.add(blockModeItem); popup.add(blockModeItem);
JCheckBoxMenuItem blockPasteItem = new JCheckBoxMenuItem("Block Paste", haus.nightmare.j3270.config.Settings.getBlockPaste());
ThemeManager.styleMenuItem(blockPasteItem);
blockPasteItem.addActionListener(ev -> {
haus.nightmare.j3270.config.Settings.setBlockPaste(blockPasteItem.isSelected());
applyModeSettings();
});
popup.add(blockPasteItem);
popup.show(this, e.getX(), e.getY()); popup.show(this, e.getX(), e.getY());
} }
@@ -1179,10 +1194,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
ip.setInsertOffOnAid(haus.nightmare.j3270.config.Settings.getInsertOffOnAid()); ip.setInsertOffOnAid(haus.nightmare.j3270.config.Settings.getInsertOffOnAid());
ip.setNumericFieldLock(haus.nightmare.j3270.config.Settings.getNumericFieldLock()); ip.setNumericFieldLock(haus.nightmare.j3270.config.Settings.getNumericFieldLock());
ip.setAutoSkipEnabled(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled()); ip.setAutoSkipEnabled(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled());
ip.setBlockPaste(haus.nightmare.j3270.config.Settings.getBlockPaste());
} }
if (client.getPS() != null) { if (client.getPS() != null) {
client.getPS().setEnablePasteFromExcel(haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel()); client.getPS().setEnablePasteFromExcel(haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel());
client.getPS().setPasteStopAtProtectedLine(haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine()); client.getPS().setPasteStopAtProtectedLine(haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine());
client.getPS().setBlockPaste(haus.nightmare.j3270.config.Settings.getBlockPaste());
} }
} }
if (statusBar != null) { if (statusBar != null) {
@@ -1239,12 +1256,32 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
applyModeSettings(); applyModeSettings();
blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode(); blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler(); crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler();
inputMaskEnabled = haus.nightmare.j3270.config.Settings.getInputMask();
inputMaskChar = haus.nightmare.j3270.config.Settings.getInputMaskChar();
String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle(); String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK; cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
revalidate(); revalidate();
repaint(); repaint();
} }
public boolean isInputMaskEnabled() {
return inputMaskEnabled;
}
public void setInputMaskEnabled(boolean enabled) {
this.inputMaskEnabled = enabled;
repaint();
}
public String getInputMaskChar() {
return inputMaskChar;
}
public void setInputMaskChar(String maskChar) {
this.inputMaskChar = maskChar;
repaint();
}
public void setFontSize(int size) { public void setFontSize(int size) {
currentFontSize = size; currentFontSize = size;
haus.nightmare.j3270.config.Settings.setFontSize(size); haus.nightmare.j3270.config.Settings.setFontSize(size);
@@ -1527,12 +1564,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
// Password fields // Password fields
if (faIsZero(currentFA & 0xFF)) { if (faIsZero(currentFA & 0xFF)) {
if (inputMaskEnabled) {
char ch = ea.ucs4; char ch = ea.ucs4;
if (ch > 0x20 && ch != 0xFF) { if (ch > 0x20 && ch != 0xFF) {
Font f = bold ? boldTerminalFont : terminalFont; Font f = bold ? boldTerminalFont : terminalFont;
g2.setFont(f); g2.setFont(f);
g2.setColor(fgColor); g2.setColor(fgColor);
g2.drawString("*", x, y + fontAscent); String mask = (inputMaskChar != null && !inputMaskChar.isEmpty()) ? inputMaskChar : "*";
g2.drawString(mask, x, y + fontAscent);
}
} }
if (isCellSelected(row, col)) { if (isCellSelected(row, col)) {
@@ -20,6 +20,7 @@ import java.awt.event.MouseEvent;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import haus.nightmare.j3270.config.Settings;
/** /**
* Centralized theme manager for j3270 Java desktop UI. * Centralized theme manager for j3270 Java desktop UI.
@@ -33,6 +34,25 @@ public final class ThemeManager {
private 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() { public static UITheme getTheme() {
return currentTheme; return currentTheme;
} }
@@ -433,6 +453,47 @@ public final class ThemeManager {
UIManager.put("TitledBorder.titleColor", fgMain); UIManager.put("TitledBorder.titleColor", fgMain);
UIManager.put("OptionPane.background", bgPanel); UIManager.put("OptionPane.background", bgPanel);
UIManager.put("OptionPane.messageForeground", fgMain); 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) { public static JButton styleButton(JButton button, ButtonVariant variant) {
if (button == null) return null; if (button == null) return null;
button.setUI(new StyledButtonUI(variant)); button.setUI(new StyledButtonUI(variant));
button.setFont(getUiFont());
button.setFocusPainted(false); button.setFocusPainted(false);
button.setOpaque(false); button.setOpaque(false);
button.setContentAreaFilled(false); button.setContentAreaFilled(false);
@@ -465,6 +527,7 @@ public final class ThemeManager {
public static JTextField styleTextField(JTextField field) { public static JTextField styleTextField(JTextField field) {
if (field == null) return null; if (field == null) return null;
field.setFont(getUiFont());
field.setBackground(getBgComponent()); field.setBackground(getBgComponent());
field.setForeground(getFgMain()); field.setForeground(getFgMain());
field.setCaretColor(getFgMain()); field.setCaretColor(getFgMain());
@@ -478,6 +541,7 @@ public final class ThemeManager {
public static JTextArea styleTextArea(JTextArea area) { public static JTextArea styleTextArea(JTextArea area) {
if (area == null) return null; if (area == null) return null;
area.setFont(getMonospacedUiFont());
area.setBackground(getCodeAreaBg()); area.setBackground(getCodeAreaBg());
area.setForeground(getCodeAreaFg()); area.setForeground(getCodeAreaFg());
area.setCaretColor(getFgMain()); area.setCaretColor(getFgMain());
@@ -488,6 +552,7 @@ public final class ThemeManager {
public static JComboBox<?> styleComboBox(JComboBox<?> box) { public static JComboBox<?> styleComboBox(JComboBox<?> box) {
if (box == null) return null; if (box == null) return null;
box.setFont(getUiFont());
box.setBackground(getBgComponent()); box.setBackground(getBgComponent());
box.setForeground(getFgMain()); box.setForeground(getFgMain());
box.setRenderer(new DefaultListCellRenderer() { box.setRenderer(new DefaultListCellRenderer() {
@@ -510,12 +575,14 @@ public final class ThemeManager {
public static JTable styleTable(JTable table) { public static JTable styleTable(JTable table) {
if (table == null) return null; if (table == null) return null;
table.setFont(getUiFont());
table.setBackground(getBgComponent()); table.setBackground(getBgComponent());
table.setForeground(getFgMain()); table.setForeground(getFgMain());
table.setSelectionBackground(getSelectionBg()); table.setSelectionBackground(getSelectionBg());
table.setSelectionForeground(getSelectionFg()); table.setSelectionForeground(getSelectionFg());
table.setGridColor(getTableGrid()); table.setGridColor(getTableGrid());
table.setRowHeight(24); int rowHeight = Math.max(24, getUiFont().getSize() + 10);
table.setRowHeight(rowHeight);
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() { table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
@Override @Override
@@ -537,14 +604,14 @@ public final class ThemeManager {
if (header != null) { if (header != null) {
header.setBackground(getTableHeaderBg()); header.setBackground(getTableHeaderBg());
header.setForeground(getTableHeaderFg()); header.setForeground(getTableHeaderFg());
header.setFont(header.getFont().deriveFont(Font.BOLD)); header.setFont(getUiFont(Font.BOLD, 0));
header.setDefaultRenderer(new DefaultTableCellRenderer() { header.setDefaultRenderer(new DefaultTableCellRenderer() {
@Override @Override
public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) { public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) {
super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col); super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col);
setBackground(getTableHeaderBg()); setBackground(getTableHeaderBg());
setForeground(getTableHeaderFg()); setForeground(getTableHeaderFg());
setFont(getFont().deriveFont(Font.BOLD)); setFont(getUiFont(Font.BOLD, 0));
setBorder(BorderFactory.createCompoundBorder( setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 1, 1, getBorderColor()), BorderFactory.createMatteBorder(0, 0, 1, 1, getBorderColor()),
new EmptyBorder(4, 6, 4, 6))); new EmptyBorder(4, 6, 4, 6)));
@@ -558,6 +625,7 @@ public final class ThemeManager {
public static JTabbedPane styleTabbedPane(JTabbedPane tp) { public static JTabbedPane styleTabbedPane(JTabbedPane tp) {
if (tp == null) return null; if (tp == null) return null;
tp.setUI(new StyledTabbedPaneUI()); tp.setUI(new StyledTabbedPaneUI());
tp.setFont(getUiFont());
tp.setBackground(getBgMain()); tp.setBackground(getBgMain());
tp.setForeground(getFgMain()); tp.setForeground(getFgMain());
return tp; return tp;
@@ -566,6 +634,7 @@ public final class ThemeManager {
public static JMenuBar styleMenuBar(JMenuBar bar) { public static JMenuBar styleMenuBar(JMenuBar bar) {
if (bar == null) return null; if (bar == null) return null;
bar.setUI(new StyledMenuBarUI()); bar.setUI(new StyledMenuBarUI());
bar.setFont(getUiFont());
bar.setBackground(getMenuBarBg()); bar.setBackground(getMenuBarBg());
bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, getBorder())); bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, getBorder()));
return bar; return bar;
@@ -574,6 +643,7 @@ public final class ThemeManager {
public static JMenu styleMenu(JMenu menu) { public static JMenu styleMenu(JMenu menu) {
if (menu == null) return null; if (menu == null) return null;
menu.setUI(new StyledMenuUI()); menu.setUI(new StyledMenuUI());
menu.setFont(getUiFont());
menu.setForeground(getMenuBarFg()); menu.setForeground(getMenuBarFg());
menu.setBackground(getMenuBarBg()); menu.setBackground(getMenuBarBg());
menu.setOpaque(false); menu.setOpaque(false);
@@ -583,6 +653,7 @@ public final class ThemeManager {
public static JMenuItem styleMenuItem(JMenuItem item) { public static JMenuItem styleMenuItem(JMenuItem item) {
if (item == null) return null; if (item == null) return null;
item.setUI(new StyledMenuItemUI()); item.setUI(new StyledMenuItemUI());
item.setFont(getUiFont());
item.setBackground(getMenuPopupBg()); item.setBackground(getMenuPopupBg());
item.setForeground(getMenuItemFg()); item.setForeground(getMenuItemFg());
return item; return item;
@@ -591,6 +662,7 @@ public final class ThemeManager {
public static JPopupMenu stylePopupMenu(JPopupMenu popup) { public static JPopupMenu stylePopupMenu(JPopupMenu popup) {
if (popup == null) return null; if (popup == null) return null;
popup.setUI(new StyledPopupMenuUI()); popup.setUI(new StyledPopupMenuUI());
popup.setFont(getUiFont());
popup.setBackground(getMenuPopupBg()); popup.setBackground(getMenuPopupBg());
popup.setBorder(BorderFactory.createCompoundBorder( popup.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(getBorder(), 1), new LineBorder(getBorder(), 1),
@@ -610,12 +682,14 @@ public final class ThemeManager {
public static JSpinner styleSpinner(JSpinner sp) { public static JSpinner styleSpinner(JSpinner sp) {
if (sp == null) return null; if (sp == null) return null;
sp.setFont(getUiFont());
sp.setBackground(getBgComponent()); sp.setBackground(getBgComponent());
sp.setForeground(getFgMain()); sp.setForeground(getFgMain());
sp.setBorder(new LineBorder(getBorder(), 1)); sp.setBorder(new LineBorder(getBorder(), 1));
JComponent editor = sp.getEditor(); JComponent editor = sp.getEditor();
if (editor instanceof JSpinner.DefaultEditor) { if (editor instanceof JSpinner.DefaultEditor) {
JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField(); JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField();
tf.setFont(getUiFont());
tf.setBackground(getBgComponent()); tf.setBackground(getBgComponent());
tf.setForeground(getFgMain()); tf.setForeground(getFgMain());
tf.setCaretColor(getFgMain()); tf.setCaretColor(getFgMain());
@@ -626,6 +700,7 @@ public final class ThemeManager {
public static JCheckBox styleCheckBox(JCheckBox cb) { public static JCheckBox styleCheckBox(JCheckBox cb) {
if (cb == null) return null; if (cb == null) return null;
cb.setFont(getUiFont());
cb.setOpaque(false); cb.setOpaque(false);
cb.setForeground(getFgMain()); cb.setForeground(getFgMain());
cb.setFocusPainted(false); cb.setFocusPainted(false);
@@ -634,6 +709,7 @@ public final class ThemeManager {
public static JRadioButton styleRadioButton(JRadioButton rb) { public static JRadioButton styleRadioButton(JRadioButton rb) {
if (rb == null) return null; if (rb == null) return null;
rb.setFont(getUiFont());
rb.setOpaque(false); rb.setOpaque(false);
rb.setForeground(getFgMain()); rb.setForeground(getFgMain());
rb.setFocusPainted(false); rb.setFocusPainted(false);
@@ -646,7 +722,7 @@ public final class ThemeManager {
title, title,
TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_JUSTIFICATION,
TitledBorder.DEFAULT_POSITION, TitledBorder.DEFAULT_POSITION,
new Font(Font.SANS_SERIF, Font.BOLD, 12), getUiFont(Font.BOLD, 0),
getFgMain()); getFgMain());
} }
@@ -732,6 +808,7 @@ public final class ThemeManager {
if (comp instanceof JButton) { if (comp instanceof JButton) {
JButton btn = (JButton) comp; JButton btn = (JButton) comp;
btn.setFont(getUiFont());
if (!(btn.getUI() instanceof StyledButtonUI)) { if (!(btn.getUI() instanceof StyledButtonUI)) {
styleButton(btn, ButtonVariant.DEFAULT); styleButton(btn, ButtonVariant.DEFAULT);
} }
@@ -770,6 +847,21 @@ public final class ThemeManager {
if (comp instanceof JLabel) { if (comp instanceof JLabel) {
comp.setForeground(getFgMain(theme)); 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; return;
} }
@@ -783,6 +875,7 @@ public final class ThemeManager {
if (b instanceof TitledBorder) { if (b instanceof TitledBorder) {
TitledBorder tb = (TitledBorder) b; TitledBorder tb = (TitledBorder) b;
tb.setTitleColor(getFgMain(theme)); tb.setTitleColor(getFgMain(theme));
tb.setTitleFont(getUiFont(Font.BOLD, 0));
tb.setBorder(new LineBorder(getBorder(theme), 1)); tb.setBorder(new LineBorder(getBorder(theme), 1));
} }
} else if (comp instanceof Container) { } 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) { public UntrustedCertificateDialog(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) {
super(parent, "Untrusted SSL/TLS Certificate", true); super(parent, "Untrusted SSL/TLS Certificate", true);
AppIcon.applyTo(this);
buildUI(host, port, chain, exception); buildUI(host, port, chain, exception);
pack(); pack();
setLocationRelativeTo(parent); setLocationRelativeTo(parent);
@@ -31,18 +32,20 @@ public class UntrustedCertificateDialog extends JDialog {
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
headerPanel.setOpaque(false); headerPanel.setOpaque(false);
JLabel iconLabel = new JLabel("⚠️"); 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); headerPanel.add(iconLabel);
JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2)); JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2));
titleBox.setOpaque(false); titleBox.setOpaque(false);
JLabel titleLabel = new JLabel("Untrusted SSL Certificate"); JLabel titleLabel = new JLabel("Untrusted SSL Certificate");
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); titleLabel.putClientProperty("uiFontSizeDelta", 3);
titleLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 3));
titleLabel.setForeground(ThemeManager.getOiaFgWarn()); titleLabel.setForeground(ThemeManager.getOiaFgWarn());
titleBox.add(titleLabel); titleBox.add(titleLabel);
JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified."); JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified.");
subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); subtitleLabel.setFont(ThemeManager.getUiFont());
subtitleLabel.setForeground(ThemeManager.getFgMuted()); subtitleLabel.setForeground(ThemeManager.getFgMuted());
titleBox.add(subtitleLabel); titleBox.add(subtitleLabel);
headerPanel.add(titleBox); headerPanel.add(titleBox);
@@ -69,7 +72,7 @@ public class UntrustedCertificateDialog extends JDialog {
JTextArea detailsArea = new JTextArea(sb.toString()); JTextArea detailsArea = new JTextArea(sb.toString());
detailsArea.setEditable(false); detailsArea.setEditable(false);
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); detailsArea.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextArea(detailsArea); ThemeManager.styleTextArea(detailsArea);
detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
@@ -91,7 +94,7 @@ public class UntrustedCertificateDialog extends JDialog {
JButton trustBtn = new JButton("Connect Anyway"); JButton trustBtn = new JButton("Connect Anyway");
ThemeManager.styleButton(trustBtn, ThemeManager.ButtonVariant.DANGER); 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 -> { trustBtn.addActionListener(e -> {
accepted = true; accepted = true;
dispose(); 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,93 @@
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");
assertTrue(logFile.exists(), "j3270.log should be created when debug is enabled");
// Clean up
J3270App.configureLogging(false);
}
}
@@ -0,0 +1,307 @@
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();
}
}
}
@@ -0,0 +1,298 @@
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.lang.reflect.Field;
import static org.junit.jupiter.api.Assertions.*;
public class BehaviorSettingsTest {
private boolean origAutoReconnect;
private int origReconnectMaxRetries;
private boolean origInputMask;
private String origInputMaskChar;
private boolean origBlockPaste;
@BeforeEach
public void setUp() {
origAutoReconnect = Settings.getAutoConnectAutoReconnect();
origReconnectMaxRetries = Settings.getAutoConnectReconnectMaxRetries();
origInputMask = Settings.getInputMask();
origInputMaskChar = Settings.getInputMaskChar();
origBlockPaste = Settings.getBlockPaste();
}
@AfterEach
public void tearDown() {
Settings.setAutoConnectAutoReconnect(origAutoReconnect);
Settings.setAutoConnectReconnectMaxRetries(origReconnectMaxRetries);
Settings.setInputMask(origInputMask);
Settings.setInputMaskChar(origInputMaskChar);
Settings.setBlockPaste(origBlockPaste);
}
@Test
@DisplayName("Auto-reconnect settings get/set and alias consistency")
public void testAutoReconnectSettingsPersistence() {
Settings.setAutoConnectAutoReconnect(true);
assertTrue(Settings.getAutoConnectAutoReconnect());
assertTrue(Settings.getAutoReconnect());
Settings.setAutoReconnect(false);
assertFalse(Settings.getAutoConnectAutoReconnect());
assertFalse(Settings.getAutoReconnect());
Settings.setAutoConnectReconnectMaxRetries(8);
assertEquals(8, Settings.getAutoConnectReconnectMaxRetries());
}
@Test
@DisplayName("Input mask settings get/set and defaults")
public void testInputMaskSettingsPersistence() {
Settings.setInputMask(false);
assertFalse(Settings.getInputMask());
assertFalse(Settings.getInputMaskEnabled());
Settings.setInputMaskEnabled(true);
assertTrue(Settings.getInputMask());
assertTrue(Settings.getInputMaskEnabled());
Settings.setInputMaskChar("#");
assertEquals("#", Settings.getInputMaskChar());
Settings.setInputMaskChar("*");
assertEquals("*", Settings.getInputMaskChar());
// Empty string should fall back to '*'
Settings.setInputMaskChar("");
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 {
Settings.setAutoConnectAutoReconnect(true);
Settings.setAutoConnectReconnectMaxRetries(12);
Settings.setInputMask(false);
Settings.setInputMaskChar("@");
File tempFile = File.createTempFile("j3270_behavior_test", ".ini");
tempFile.deleteOnExit();
Settings.exportToIniFile(tempFile.getAbsolutePath());
// Reset to different values
Settings.setAutoConnectAutoReconnect(false);
Settings.setAutoConnectReconnectMaxRetries(3);
Settings.setInputMask(true);
Settings.setInputMaskChar("*");
// Load back from INI
Settings.loadFromIniFile(tempFile.getAbsolutePath());
assertTrue(Settings.getAutoConnectAutoReconnect());
assertEquals(12, Settings.getAutoConnectReconnectMaxRetries());
assertFalse(Settings.getInputMask());
assertEquals("@", Settings.getInputMaskChar());
tempFile.delete();
}
@Test
@DisplayName("TerminalPanel reloads input mask settings correctly")
public void testTerminalPanelInputMaskReload() {
try {
TerminalPanel panel = new TerminalPanel();
Settings.setInputMask(false);
Settings.setInputMaskChar("$");
panel.reloadSettings();
assertFalse(panel.isInputMaskEnabled());
assertEquals("$", panel.getInputMaskChar());
panel.setInputMaskEnabled(true);
assertTrue(panel.isInputMaskEnabled());
panel.setInputMaskChar("#");
assertEquals("#", panel.getInputMaskChar());
} catch (HeadlessException ignored) {
// Safe fallback for headless runner
}
}
@Test
@DisplayName("SettingsDialog contains Auto-Reconnect checkbox and Input Mask controls in Behavior panel")
public void testSettingsDialogBehaviorControls() throws Exception {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
// In automated/headless environments, JFrame cannot be initialized
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
// Access private fields in SettingsDialog to verify component bindings
Field autoRecField = SettingsDialog.class.getDeclaredField("autoReconnectCheck");
autoRecField.setAccessible(true);
JCheckBox autoReconnectCheck = (JCheckBox) autoRecField.get(dialog);
assertNotNull(autoReconnectCheck, "autoReconnectCheck must exist in SettingsDialog");
assertEquals("Auto-Reconnect on Disconnect", autoReconnectCheck.getText());
assertEquals(Settings.getAutoConnectAutoReconnect(), autoReconnectCheck.isSelected());
Field inputMaskCheckField = SettingsDialog.class.getDeclaredField("inputMaskCheck");
inputMaskCheckField.setAccessible(true);
JCheckBox inputMaskCheck = (JCheckBox) inputMaskCheckField.get(dialog);
assertNotNull(inputMaskCheck, "inputMaskCheck must exist in SettingsDialog");
assertEquals(Settings.getInputMask(), inputMaskCheck.isSelected());
Field inputMaskCharField = SettingsDialog.class.getDeclaredField("inputMaskCharField");
inputMaskCharField.setAccessible(true);
JTextField maskCharField = (JTextField) inputMaskCharField.get(dialog);
assertNotNull(maskCharField, "inputMaskCharField must exist in SettingsDialog");
assertEquals(Settings.getInputMaskChar(), maskCharField.getText());
assertEquals(inputMaskCheck.isSelected(), maskCharField.isEnabled());
// Test interaction: unchecking inputMask disables character field
inputMaskCheck.setSelected(false);
for (java.awt.event.ActionListener al : inputMaskCheck.getActionListeners()) {
al.actionPerformed(new java.awt.event.ActionEvent(inputMaskCheck, java.awt.event.ActionEvent.ACTION_PERFORMED, ""));
}
assertFalse(maskCharField.isEnabled());
dialog.dispose();
app.dispose();
} 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;
}
}
@@ -15,17 +15,23 @@ public class FontSettingsPersistenceTest {
private int originalFontSize; private int originalFontSize;
private String originalFontFamily; private String originalFontFamily;
private int originalUiFontSize;
private String originalUiFontFamily;
@BeforeEach @BeforeEach
public void setup() { public void setup() {
originalFontSize = Settings.getFontSize(); originalFontSize = Settings.getFontSize();
originalFontFamily = Settings.getFontFamily(); originalFontFamily = Settings.getFontFamily();
originalUiFontSize = Settings.getUiFontSize();
originalUiFontFamily = Settings.getUiFontFamily();
} }
@AfterEach @AfterEach
public void tearDown() { public void tearDown() {
Settings.setFontSize(originalFontSize); Settings.setFontSize(originalFontSize);
Settings.setFontFamily(originalFontFamily); Settings.setFontFamily(originalFontFamily);
Settings.setUiFontSize(originalUiFontSize);
Settings.setUiFontFamily(originalUiFontFamily);
} }
@Test @Test
@@ -39,6 +45,76 @@ public class FontSettingsPersistenceTest {
assertEquals(18, Settings.getFontSize()); 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 @Test
public void testTerminalPanelResizeDoesNotResetFontSize() { public void testTerminalPanelResizeDoesNotResetFontSize() {
try { try {
@@ -77,4 +153,57 @@ public class FontSettingsPersistenceTest {
// In headless environment without display, Settings persistence is verified above // 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,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) {
}
}
}
@@ -262,4 +262,81 @@ public class ThemeManagerTest {
// Handled in headless CI // 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);
}
}
} }
@@ -435,6 +435,14 @@ public class ConnectionConfig {
} }
} }
// Parse [lu@]host[:port] (e.g. "00C2@mvs.host.com:1023")
String parsedLu = null;
int atIdx = s.indexOf('@');
if (atIdx > 0 && atIdx < s.length() - 1) {
parsedLu = s.substring(0, atIdx).trim();
s = s.substring(atIdx + 1).trim();
}
String host = s; String host = s;
int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23); int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23);
@@ -458,6 +466,9 @@ public class ConnectionConfig {
} }
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4); ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
if (parsedLu != null && !parsedLu.isEmpty()) {
config.setLuName(parsedLu);
}
config.setUseTls(tls); config.setUseTls(tls);
config.setTn3270eEnabled(tn3270e); config.setTn3270eEnabled(tn3270e);
config.setKeepAliveEnabled(keepAlive); config.setKeepAliveEnabled(keepAlive);
@@ -4,10 +4,10 @@ import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
* EBCDIC Unicode character translator conforming to IBM Host On-Demand (HoD v14). * EBCDIC Unicode character translator conforming to Host On-Demand (HoD v14).
* Modular architecture delegating to pluggable CodePage implementations (SBCS and DBCS). * Modular architecture delegating to pluggable CodePage implementations (SBCS and DBCS).
* Supports custom per-instance character translation override tables and complete * Supports custom per-instance character translation override tables and complete
* IBM 3270 APL / Graphic Escape (GA23-0059) character mappings. * 3270 APL / Graphic Escape (GA23-0059) character mappings.
* Default: Code Page 037 (US/Canada EBCDIC). * Default: Code Page 037 (US/Canada EBCDIC).
*/ */
public class EbcdicTranslator { public class EbcdicTranslator {
@@ -210,10 +210,12 @@ public class EbcdicTranslator {
*/ */
public char ebcdicToUnicode(int ebc) { public char ebcdicToUnicode(int ebc) {
int b = ebc & 0xFF; int b = ebc & 0xFF;
if (!customEbcdicToUnicode.isEmpty()) {
Character custom = customEbcdicToUnicode.get(b); Character custom = customEbcdicToUnicode.get(b);
if (custom != null) { if (custom != null) {
return custom; return custom;
} }
}
return activeCodePage.ebcdicToUnicode(b); return activeCodePage.ebcdicToUnicode(b);
} }
@@ -236,10 +238,12 @@ public class EbcdicTranslator {
* Returns -1 if the character cannot be mapped. * Returns -1 if the character cannot be mapped.
*/ */
public int unicodeToEbcdic(char unicode) { public int unicodeToEbcdic(char unicode) {
if (!customUnicodeToEbcdic.isEmpty()) {
Integer custom = customUnicodeToEbcdic.get(unicode); Integer custom = customUnicodeToEbcdic.get(unicode);
if (custom != null) { if (custom != null) {
return custom; return custom;
} }
}
return activeCodePage.unicodeToEbcdic(unicode); return activeCodePage.unicodeToEbcdic(unicode);
} }
@@ -296,8 +300,8 @@ public class EbcdicTranslator {
} }
/** /**
* Map an IBM 3270 APL / Graphic Escape (GE) EBCDIC code point to its Unicode glyph. * Map a 3270 APL / Graphic Escape (GE) EBCDIC code point to its Unicode glyph.
* Conforms to IBM 3270 APL / Text character set and GA23-0059 specification. * Conforms to 3270 APL / Text character set and GA23-0059 specification.
*/ */
public char mapAPL(int ebcdicCodePoint) { public char mapAPL(int ebcdicCodePoint) {
switch (ebcdicCodePoint & 0xFF) { switch (ebcdicCodePoint & 0xFF) {
@@ -331,7 +335,7 @@ public class EbcdicTranslator {
case 0xBF: return '\u00B5'; // Micro 'µ' case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬' case 0x5F: return '\u00AC'; // Not sign '¬'
// IBM 3270 APL Operational & Structural Glyphs // 3270 APL Operational & Structural Glyphs
case 0x80: return '\u22C4'; // Diamond '⋄' case 0x80: return '\u22C4'; // Diamond '⋄'
case 0x81: return '\u237A'; // APL Alpha '' case 0x81: return '\u237A'; // APL Alpha ''
case 0x82: return '\u22A5'; // Up Tack / Decode '⊥' case 0x82: return '\u22A5'; // Up Tack / Decode '⊥'
@@ -383,7 +387,7 @@ public class EbcdicTranslator {
} }
/** /**
* Translate an IBM 3270 Graphic Escape (GE) / APL character code to Unicode. * Translate a 3270 Graphic Escape (GE) / APL character code to Unicode.
*/ */
public char getAplGraphic(int ec) { public char getAplGraphic(int ec) {
return mapAPL(ec); return mapAPL(ec);
@@ -54,6 +54,12 @@ public class DataStreamProcessor {
private boolean unlockSysPending = false; private boolean unlockSysPending = false;
private boolean rcvdRead = false; private boolean rcvdRead = false;
// Modal SA (set attribute) character attributes
private byte currentFg = 0;
private byte currentBg = 0;
private byte currentGr = 0;
private byte currentCs = 0;
/** Functional interface for sending output back through the telnet stack. */ /** Functional interface for sending output back through the telnet stack. */
@FunctionalInterface @FunctionalInterface
public interface OutputSender { public interface OutputSender {
@@ -240,6 +246,10 @@ public class DataStreamProcessor {
programSymbolManager.commitStagedSymbols(); programSymbolManager.commitStagedSymbols();
log.info(">>> EAU: erasing all unprotected fields"); log.info(">>> EAU: erasing all unprotected fields");
screen.eraseAllUnprotected(); screen.eraseAllUnprotected();
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
break; break;
case CMD_WSF: case CMD_WSF:
case SNA_CMD_WSF: case SNA_CMD_WSF:
@@ -378,11 +388,19 @@ public class DataStreamProcessor {
if (wccReset(wcc)) { if (wccReset(wcc)) {
// Reset all character attributes to defaults // Reset all character attributes to defaults
log.fine("WCC reset: clearing default attributes"); log.fine("WCC reset: clearing default attributes");
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
} }
if (eraseFirst) { if (eraseFirst) {
screen.clear(); screen.clear();
log.fine("Cleared screen for Erase/Write"); log.fine("Cleared screen for Erase/Write");
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
} }
// Process orders and data starting at byte 2 // Process orders and data starting at byte 2
@@ -390,9 +408,6 @@ public class DataStreamProcessor {
int end = offset + length; int end = offset + length;
int baddr = screen.getBufferAddress(); int baddr = screen.getBufferAddress();
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
// Current SA (set attribute) values for character-mode
byte currentFg = 0, currentBg = 0, currentGr = 0, currentCs = 0;
boolean lastWasOrder = false; boolean lastWasOrder = false;
while (pos < end) { while (pos < end) {
@@ -427,10 +442,6 @@ public class DataStreamProcessor {
// FA position is a display position that shows as blank // FA position is a display position that shows as blank
ea.ec = 0; ea.ec = 0;
ea.ucs4 = ' '; ea.ucs4 = ' ';
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
screen.setFormatted(true); screen.setFormatted(true);
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr); screen.setBufferAddress(baddr);
@@ -455,10 +466,6 @@ public class DataStreamProcessor {
ea.clear(); ea.clear();
ea.ec = 0; ea.ec = 0;
ea.ucs4 = ' '; ea.ucs4 = ' ';
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
for (int i = 0; i < nPairs; i++) { for (int i = 0; i < nPairs; i++) {
int attrType = data[pos + 2 + i * 2] & 0xFF; int attrType = data[pos + 2 + i * 2] & 0xFF;
@@ -485,6 +492,12 @@ public class DataStreamProcessor {
int attrType = data[pos + 1] & 0xFF; int attrType = data[pos + 1] & 0xFF;
int attrValue = data[pos + 2] & 0xFF; int attrValue = data[pos + 2] & 0xFF;
switch (attrType) { switch (attrType) {
case XA_ALL:
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
break;
case XA_FOREGROUND: case XA_FOREGROUND:
currentFg = (byte) attrValue; currentFg = (byte) attrValue;
break; break;
@@ -535,6 +548,8 @@ public class DataStreamProcessor {
applyExtendedAttribute(ea, attrType, attrValue); applyExtendedAttribute(ea, attrType, attrValue);
} }
} }
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
pos += 2 + nPairs * 2; pos += 2 + nPairs * 2;
lastWasOrder = true; lastWasOrder = true;
break; break;
@@ -984,7 +999,13 @@ public class DataStreamProcessor {
break; break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
if (fieldLen > 3) { if (fieldLen > 3) {
graphicsPlane.setCharDimensions(qrBuilder.getCharWidth(), qrBuilder.getCharHeight());
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows()); graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
int targetW = screen.getCols() * qrBuilder.getCharWidth();
int targetH = screen.getRows() * qrBuilder.getCharHeight();
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
graphicsPlane.resize(targetW, targetH);
}
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3); gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
notifyScreenUpdated(); notifyScreenUpdated();
} }
@@ -1298,9 +1319,10 @@ public class DataStreamProcessor {
int orderOffset = (fieldLen >= 7) ? (offset + 7) : (offset + 4); int orderOffset = (fieldLen >= 7) ? (offset + 7) : (offset + 4);
int orderLen = Math.max(0, fieldLen - (orderOffset - offset)); int orderLen = Math.max(0, fieldLen - (orderOffset - offset));
graphicsPlane.setCharDimensions(qrBuilder.getCharWidth(), qrBuilder.getCharHeight());
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows()); graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
int targetW = screen.getCols() * 9; int targetW = screen.getCols() * qrBuilder.getCharWidth();
int targetH = screen.getRows() * 16; int targetH = screen.getRows() * qrBuilder.getCharHeight();
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) { if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
graphicsPlane.resize(targetW, targetH); graphicsPlane.resize(targetW, targetH);
} }
@@ -0,0 +1,86 @@
package haus.nightmare.lib3270j.datastream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* Reusable, resizable byte buffer that provides zero-copy access to its internal
* array and supports ByteBuffer slicing to minimize heap allocations during
* network stream processing.
*/
public class FastByteBuffer extends OutputStream {
private byte[] buf;
private int count;
public FastByteBuffer() {
this(32768);
}
public FastByteBuffer(int initialCapacity) {
this.buf = new byte[Math.max(32, initialCapacity)];
this.count = 0;
}
private void ensureCapacity(int minCapacity) {
if (minCapacity > buf.length) {
int newCap = Math.max(buf.length << 1, minCapacity);
buf = Arrays.copyOf(buf, newCap);
}
}
@Override
public synchronized void write(int b) {
ensureCapacity(count + 1);
buf[count++] = (byte) b;
}
@Override
public synchronized void write(byte[] b, int off, int len) {
if (b == null || len <= 0) return;
ensureCapacity(count + len);
System.arraycopy(b, off, buf, count, len);
count += len;
}
/**
* Direct reference to internal buffer array.
* Use {@link #size()} to determine active length.
*/
public synchronized byte[] buffer() {
return buf;
}
public synchronized int size() {
return count;
}
public synchronized void reset() {
count = 0;
}
/**
* Creates a read-only ByteBuffer view wrapping active bytes without copying.
*/
public synchronized ByteBuffer asByteBuffer() {
return ByteBuffer.wrap(buf, 0, count).asReadOnlyBuffer();
}
/**
* Creates a read-only ByteBuffer slice for a sub-range without copying.
*/
public synchronized ByteBuffer slice(int offset, int length) {
if (offset < 0 || length < 0 || offset + length > count) {
throw new IndexOutOfBoundsException("offset=" + offset + " length=" + length + " size=" + count);
}
return ByteBuffer.wrap(buf, offset, length).slice().asReadOnlyBuffer();
}
/**
* Produces a copied byte array if an isolated copy is explicitly required.
*/
public synchronized byte[] toByteArray() {
return Arrays.copyOf(buf, count);
}
}
@@ -9,7 +9,7 @@ import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/** /**
* Builds Query Reply structured fields in response to host Read Partition queries. * Builds Query Reply structured fields in response to host Read Partition queries.
* Matches IBM 3270 Architecture (GA23-0059) and x3270 sf.c exact binary layout. * Matches 3270 Architecture (GA23-0059) and x3270 sf.c exact binary layout.
*/ */
public class QueryReplyBuilder { public class QueryReplyBuilder {
@@ -18,10 +18,102 @@ public class QueryReplyBuilder {
private static final int SW_3279_2 = 0x09; private static final int SW_3279_2 = 0x09;
private static final int SH_3279_2 = 0x0c; private static final int SH_3279_2 = 0x0c;
// Usable Area physical dimensions matching IBM Host On-Demand DS3270.java (Inches, 96 dpi: 0x00010060) // Usable Area physical dimensions matching HOD DS3270.java (Inches, 96 dpi: 0x00010060)
private static final int Xr_HOD = 0x00010060; private static final int Xr_HOD = 0x00010060;
private static final int Yr_HOD = 0x00010060; private static final int Yr_HOD = 0x00010060;
// Pre-computed static query reply payloads to eliminate allocation churn
private static final byte[] STATIC_QR_COLOR = new byte[] {
0x00, 0x08, 0x00, (byte) 0xF4,
(byte) 0xF1, (byte) 0xF1, // Blue
(byte) 0xF2, (byte) 0xF2, // Red
(byte) 0xF3, (byte) 0xF3, // Pink
(byte) 0xF4, (byte) 0xF4, // Green
(byte) 0xF5, (byte) 0xF5, // Turquoise
(byte) 0xF6, (byte) 0xF6, // Yellow
(byte) 0xF7, (byte) 0xF7 // Neutral/White
};
private static final byte[] STATIC_QR_HIGHLIGHTING = new byte[] {
0x04, 0x00, (byte) 0xF0,
(byte) 0xF1, (byte) 0xF1,
(byte) 0xF2, (byte) 0xF2,
(byte) 0xF4, (byte) 0xF4
};
private static final byte[] STATIC_QR_REPLY_MODES = new byte[] {
SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR
};
private static final byte[] STATIC_QR_OUTLINING = new byte[] {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
private static final byte[] STATIC_QR_DBCS_ASIA = new byte[] {
0x00, 0x03, 0x01, (byte) 0x80, 0x03, 0x02, 0x01
};
private static final byte[] STATIC_QR_AUXDA = new byte[] {
0x00, 0x00
};
private static final byte[] STATIC_QR_TRANSPARENCY = new byte[] {
0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF
};
private static final byte[] STATIC_QR_SEGMENT = new byte[] {
(byte) 0x80, 0x02, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00
};
private static final byte[] STATIC_QR_PROCEDURE = new byte[] {
0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00,
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
};
private static final byte[] STATIC_QR_LINETYPE = new byte[] {
0x00, 0x09, 0x00, 0x07,
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
};
private static final byte[] STATIC_PORT_BLOCKS;
static {
ByteArrayOutputStream pOut = new ByteArrayOutputStream(64);
byte[][] data = {
{ 0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF },
{ 0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02 },
{ 0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF },
{ 0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C }
};
for (byte[] d : data) {
int len = 4 + d.length;
pOut.write((len >> 8) & 0xFF);
pOut.write(len & 0xFF);
pOut.write(SFID_QREPLY);
pOut.write(QR_PORT);
pOut.write(d, 0, d.length);
}
STATIC_PORT_BLOCKS = pOut.toByteArray();
}
private static final byte[] STATIC_QR_GRCOLOR;
static {
ByteArrayOutputStream gOut = new ByteArrayOutputStream(110);
gOut.write(0x00); gOut.write(0x04); gOut.write(0x00); gOut.write(0xFF); gOut.write(0xFF);
gOut.write(0x00); gOut.write(0x10); gOut.write(0x00); gOut.write(0x10);
for (int i = 0; i < 16; i++) {
gOut.write(0x00);
gOut.write(i);
int argb = haus.nightmare.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
gOut.write((argb >> 16) & 0xFF);
gOut.write((argb >> 8) & 0xFF);
gOut.write(argb & 0xFF);
gOut.write(0x00);
}
STATIC_QR_GRCOLOR = gOut.toByteArray();
}
private final ScreenBuffer screen; private final ScreenBuffer screen;
private GraphicsMode graphicsMode = GraphicsMode.BOTH; private GraphicsMode graphicsMode = GraphicsMode.BOTH;
@@ -101,27 +193,27 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_CHARSETS, buildCharsets()); appendQueryReply(out, QR_CHARSETS, buildCharsets());
// Color (0x86) // Color (0x86)
appendQueryReply(out, QR_COLOR, buildColor()); appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR);
// Highlighting (0x87) // Highlighting (0x87)
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING);
// Reply Modes (0x88) // Reply Modes (0x88)
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); appendQueryReply(out, QR_REPLY_MODES, STATIC_QR_REPLY_MODES);
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()); boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
if (isDbcs) { if (isDbcs) {
// Outlining (0x8C) // Outlining (0x8C)
appendQueryReply(out, QR_OUTLINING, buildOutlining()); appendQueryReply(out, QR_OUTLINING, STATIC_QR_OUTLINING);
// DBCS Asia (0x91) // DBCS Asia (0x91)
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia()); appendQueryReply(out, QR_DBCS_ASIA, STATIC_QR_DBCS_ASIA);
} }
// Distributed Data Management (0x95) // Distributed Data Management (0x95)
appendQueryReply(out, QR_DDM, buildDdm(4096)); appendQueryReply(out, QR_DDM, buildDdm(4096));
// Auxiliary Devices (0x99) // Auxiliary Devices (0x99)
appendQueryReply(out, QR_AUXDA, buildAuxDa()); appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
// Implicit Partition (0xA6) // Implicit Partition (0xA6)
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
@@ -141,26 +233,26 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize)); appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows)); appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
appendQueryReply(out, QR_CHARSETS, buildCharsets()); appendQueryReply(out, QR_CHARSETS, buildCharsets());
appendQueryReply(out, QR_COLOR, buildColor()); appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR);
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING);
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); appendQueryReply(out, QR_REPLY_MODES, STATIC_QR_REPLY_MODES);
appendQueryReply(out, QR_OUTLINING, buildOutlining()); appendQueryReply(out, QR_OUTLINING, STATIC_QR_OUTLINING);
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()); boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
if (isDbcs) { if (isDbcs) {
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia()); appendQueryReply(out, QR_DBCS_ASIA, STATIC_QR_DBCS_ASIA);
} }
appendQueryReply(out, QR_DDM, buildDdm(4096)); appendQueryReply(out, QR_DDM, buildDdm(4096));
appendQueryReply(out, QR_AUXDA, buildAuxDa()); appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); // 0xA8 appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); // 0xA8
appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows)); // 0xB0 appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT); // 0xB0
appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows)); // 0xB1 appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE); // 0xB1
appendQueryReply(out, QR_LINETYPE, buildLineType()); // 0xB2 appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE); // 0xB2
appendPort(out); // 0xB3 appendPort(out); // 0xB3
appendQueryReply(out, QR_GRCOLOR, buildGrColor()); // 0xB4 appendQueryReply(out, QR_GRCOLOR, STATIC_QR_GRCOLOR); // 0xB4
appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet()); // 0xB6 appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet()); // 0xB6
} }
@@ -197,20 +289,20 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_CHARSETS, buildCharsets()); appendQueryReply(out, QR_CHARSETS, buildCharsets());
break; break;
case QR_COLOR: case QR_COLOR:
appendQueryReply(out, QR_COLOR, buildColor()); appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR);
break; break;
case QR_HIGHLIGHTING: case QR_HIGHLIGHTING:
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING);
break; break;
case QR_REPLY_MODES: case QR_REPLY_MODES:
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); appendQueryReply(out, QR_REPLY_MODES, STATIC_QR_REPLY_MODES);
break; break;
case QR_OUTLINING: // 0x8C case QR_OUTLINING: // 0x8C
appendQueryReply(out, QR_OUTLINING, buildOutlining()); appendQueryReply(out, QR_OUTLINING, STATIC_QR_OUTLINING);
break; break;
case QR_DBCS_ASIA: // 0x91 case QR_DBCS_ASIA: // 0x91
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) { if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia()); appendQueryReply(out, QR_DBCS_ASIA, STATIC_QR_DBCS_ASIA);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
@@ -219,35 +311,35 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_DDM, buildDdm(4096)); appendQueryReply(out, QR_DDM, buildDdm(4096));
break; break;
case QR_AUXDA: // 0x99 case QR_AUXDA: // 0x99
appendQueryReply(out, QR_AUXDA, buildAuxDa()); appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
break; break;
case QR_IMP_PART: // 0xA6 case QR_IMP_PART: // 0xA6
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
break; break;
case QR_TRANSPARENCY: // 0xA8 case QR_TRANSPARENCY: // 0xA8
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
break; break;
case QR_SEGMENT: // 0xB0 case QR_SEGMENT: // 0xB0
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows)); appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
break; break;
case QR_PROCEDURE: // 0xB1 case QR_PROCEDURE: // 0xB1
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows)); appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
break; break;
case QR_LINETYPE: // 0xB2 case QR_LINETYPE: // 0xB2
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_LINETYPE, buildLineType()); appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
@@ -261,7 +353,7 @@ public class QueryReplyBuilder {
break; break;
case QR_GRCOLOR: // 0xB4 case QR_GRCOLOR: // 0xB4
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_GRCOLOR, buildGrColor()); appendQueryReply(out, QR_GRCOLOR, STATIC_QR_GRCOLOR);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
@@ -321,19 +413,19 @@ public class QueryReplyBuilder {
out.write(maxCols & 0xFF); // usable width low out.write(maxCols & 0xFF); // usable width low
out.write((maxRows >> 8) & 0xFF); // usable height high out.write((maxRows >> 8) & 0xFF); // usable height high
out.write(maxRows & 0xFF); // usable height low out.write(maxRows & 0xFF); // usable height low
out.write(0x00); // units (0x00 = inches, matching IBM Host On-Demand QR_USEAREA_STRING) out.write(0x00); // units (0x00 = inches, matching HOD QR_USEAREA_STRING)
// Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI) // Xr (4 bytes) - matching HOD QR_USEAREA_STRING (96 DPI)
out.write((Xr_HOD >> 24) & 0xFF); out.write((Xr_HOD >> 24) & 0xFF);
out.write((Xr_HOD >> 16) & 0xFF); out.write((Xr_HOD >> 16) & 0xFF);
out.write((Xr_HOD >> 8) & 0xFF); out.write((Xr_HOD >> 8) & 0xFF);
out.write(Xr_HOD & 0xFF); out.write(Xr_HOD & 0xFF);
// Yr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI) // Yr (4 bytes) - matching HOD QR_USEAREA_STRING (96 DPI)
out.write((Yr_HOD >> 24) & 0xFF); out.write((Yr_HOD >> 24) & 0xFF);
out.write((Yr_HOD >> 16) & 0xFF); out.write((Yr_HOD >> 16) & 0xFF);
out.write((Yr_HOD >> 8) & 0xFF); out.write((Yr_HOD >> 8) & 0xFF);
out.write(Yr_HOD & 0xFF); out.write(Yr_HOD & 0xFF);
int charW = getCharWidth(); int charW = getCharWidth(maxRows);
int charH = getCharHeight(); int charH = getCharHeight(maxRows);
out.write(charW); // AW out.write(charW); // AW
out.write(charH); // AH out.write(charH); // AH
int buf = maxCols * maxRows; int buf = maxCols * maxRows;
@@ -343,6 +435,11 @@ public class QueryReplyBuilder {
} }
public int getCharWidth() { public int getCharWidth() {
int rows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return getCharWidth(rows);
}
public int getCharWidth(int rows) {
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) { if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
return 12; return 12;
} }
@@ -350,18 +447,11 @@ public class QueryReplyBuilder {
} }
public int getCharHeight() { public int getCharHeight() {
// ARCHITECTURAL NOTE ON 3179G GOCA VERTICAL ALIGNMENT & QUERY REPLIES: int rows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
// Why hardcoding SH = 12 (0x0C) in Character Sets & Usable Area failed in past iterations: return getCharHeight(rows);
// When SDH/AH is declared as 12 (0x0C) in Query Reply, the mainframe host GDDM engine computes }
// total presentation space as rows * 12 (e.g. 43 * 12 = 516 units, yMax = 257).
// GDDM then places the top menu bar at Row 1 (gy = 187..200). public int getCharHeight(int rows) {
// Meanwhile, the client emulator rendered into a 16-pitch grid (43 * 16 = 688 units, yMax = 343).
// On a 688-unit canvas, gy = 200 mapped to Row 9.2 (middle of the screen), leaving a massive void above.
// When the user clicked on the visual menu drawn at Row 9, the client emitted gy = 189 with cursor at Row 9,
// which GDDM rejected as outside its menu hit box (causing terminal alarm beeps).
//
// Solution: Declare SDH = 16 (0x10) when Vector Graphics is enabled (3179G standard), ensuring host GDDM
// and client GraphicsPlane share the exact same 16-pitch presentation space (720x688, yMax = 343).
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2; return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
} }
@@ -442,31 +532,15 @@ public class QueryReplyBuilder {
} }
public byte[] buildColor() { public byte[] buildColor() {
// Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total) return STATIC_QR_COLOR.clone();
return new byte[] {
0x00, 0x08, 0x00, (byte) 0xF4,
(byte) 0xF1, (byte) 0xF1, // Blue
(byte) 0xF2, (byte) 0xF2, // Red
(byte) 0xF3, (byte) 0xF3, // Pink
(byte) 0xF4, (byte) 0xF4, // Green
(byte) 0xF5, (byte) 0xF5, // Turquoise
(byte) 0xF6, (byte) 0xF6, // Yellow
(byte) 0xF7, (byte) 0xF7 // Neutral/White
};
} }
public byte[] buildHighlighting() { public byte[] buildHighlighting() {
// Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total) return STATIC_QR_HIGHLIGHTING.clone();
return new byte[] {
0x04, 0x00, (byte) 0xF0,
(byte) 0xF1, (byte) 0xF1,
(byte) 0xF2, (byte) 0xF2,
(byte) 0xF4, (byte) 0xF4
};
} }
public byte[] buildReplyModes() { public byte[] buildReplyModes() {
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR }; return STATIC_QR_REPLY_MODES.clone();
} }
public byte[] buildDdm() { public byte[] buildDdm() {
@@ -517,39 +591,27 @@ public class QueryReplyBuilder {
} }
public byte[] buildOutlining() { public byte[] buildOutlining() {
// HOD QueryReply3270Constants.java QR_OUTLINING_STRING ("\u0000\n\u0081\u008c\u0000\u0000\u0000\u0000\u0000\u0000") return STATIC_QR_OUTLINING.clone();
return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
} }
public byte[] buildDbcsAsia() { public byte[] buildDbcsAsia() {
// HOD QueryReply3270Constants.java QR_DBCS_ASIA_STRING ("\u0000\u000b\u0081\u0091\u0000\u0003\u0001\u0080\u0003\u0002\u0001") return STATIC_QR_DBCS_ASIA.clone();
return new byte[]{ 0x00, 0x03, 0x01, (byte) 0x80, 0x03, 0x02, 0x01 };
} }
public byte[] buildAuxDa() { public byte[] buildAuxDa() {
// HOD QueryReply3270Constants.java QR_AUXDA_STRING ("\u0000\u0006\u0081\u0099\u0000\u0000") return STATIC_QR_AUXDA.clone();
return new byte[]{ 0x00, 0x00 };
} }
public byte[] buildTransparency() { public byte[] buildTransparency() {
// HOD QueryReply3270Constants.java QR_TRANSPARENCY_STRING ("\u0000\t\u0081\u00a8\u0002\u0000\u00f0\u00ff\u00ff") return STATIC_QR_TRANSPARENCY.clone();
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
} }
public byte[] buildSegment() { public byte[] buildSegment() {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS; return STATIC_QR_SEGMENT.clone();
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildSegment(maxCols, maxRows);
} }
public byte[] buildSegment(int maxCols, int maxRows) { public byte[] buildSegment(int maxCols, int maxRows) {
// HOD QueryReply3270Constants.java QR_SEGMENT_STRING ("\u0000\u000b\u0081\u00b0\u0080\u0002\u0000\u0000\u0000\u00fc\u0000") return STATIC_QR_SEGMENT.clone();
return new byte[]{
(byte) 0x80, 0x02,
0x00, 0x00,
0x00, (byte) 0xFC,
0x00
};
} }
public byte[] buildGraphics() { public byte[] buildGraphics() {
@@ -561,21 +623,11 @@ public class QueryReplyBuilder {
} }
public byte[] buildProcedure() { public byte[] buildProcedure() {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS; return STATIC_QR_PROCEDURE.clone();
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildProcedure(maxCols, maxRows);
} }
public byte[] buildProcedure(int maxCols, int maxRows) { public byte[] buildProcedure(int maxCols, int maxRows) {
// HOD QueryReply3270Constants.java QR_PROCEDURE_STRING ("\u0000\u0015\u0081\u00b1\u0000\u0001\u0000\u0000\u0000\u00fc\u0000\u0006@\u0006@\u0006\u0001\u00ff\u00ff\u00ff\u00f0") return STATIC_QR_PROCEDURE.clone();
return new byte[]{
0x00, 0x01,
0x00, 0x00,
0x00, (byte) 0xFC,
0x00,
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
};
} }
public byte[] buildGImage() { public byte[] buildGImage() {
@@ -587,12 +639,7 @@ public class QueryReplyBuilder {
} }
public byte[] buildLineType() { public byte[] buildLineType() {
// HOD QueryReply3270Constants.java QR_LINETYPE_STRING ("\u0000\u0018\u0081\u00b2\u0000\t\u0000\u0007\u0001\u0001\u0002\u0002\u0003\u0003\u0004\u0004\u0005\u0005\u0006\u0006\u0007\u0007\b\b") return STATIC_QR_LINETYPE.clone();
return new byte[]{
0x00, 0x09, 0x00, 0x07,
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
};
} }
public byte[] buildAuxDev() { public byte[] buildAuxDev() {
@@ -604,19 +651,7 @@ public class QueryReplyBuilder {
} }
public void appendPort(ByteArrayOutputStream out) { public void appendPort(ByteArrayOutputStream out) {
// HOD QueryReply3270Constants.java QR_PORT_STRING (4 OEM format sub-fields, 64 bytes total) out.write(STATIC_PORT_BLOCKS, 0, STATIC_PORT_BLOCKS.length);
appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
});
appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02
});
appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
});
appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C
});
} }
public void appendOemFmt(ByteArrayOutputStream out) { public void appendOemFmt(ByteArrayOutputStream out) {
@@ -624,9 +659,7 @@ public class QueryReplyBuilder {
} }
public byte[] buildPort() { public byte[] buildPort() {
ByteArrayOutputStream out = new ByteArrayOutputStream(70); return STATIC_PORT_BLOCKS.clone();
appendPort(out);
return out.toByteArray();
} }
public byte[] buildOemFormat() { public byte[] buildOemFormat() {
@@ -634,24 +667,7 @@ public class QueryReplyBuilder {
} }
public byte[] buildGrColor() { public byte[] buildGrColor() {
// HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total) return STATIC_QR_GRCOLOR.clone();
ByteArrayOutputStream out = new ByteArrayOutputStream(110);
out.write(0x00); out.write(0x04); out.write(0x00); out.write(0xFF); out.write(0xFF);
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x10);
for (int i = 0; i < 16; i++) {
out.write(0x00);
out.write(i);
int argb = haus.nightmare.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
int r = (argb >> 16) & 0xFF;
int g = (argb >> 8) & 0xFF;
int b = argb & 0xFF;
out.write(r);
out.write(g);
out.write(b);
out.write(0x00); // 6th byte in HOD color table
}
return out.toByteArray();
} }
public byte[] buildGraphicColor() { public byte[] buildGraphicColor() {
@@ -0,0 +1,72 @@
package haus.nightmare.lib3270j.datastream;
import java.nio.ByteBuffer;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* Thread-safe memory pool providing reusable byte buffers for high-throughput
* 3270 stream operations, minimizing garbage collector pressure.
*/
public final class ReusableByteBufferPool {
public static final int SIZE_SMALL = 512;
public static final int SIZE_MEDIUM = 4096;
public static final int SIZE_LARGE = 32768;
private static final int MAX_POOLED_PER_TIER = 32;
private static final Queue<byte[]> smallPool = new ConcurrentLinkedQueue<>();
private static final Queue<byte[]> mediumPool = new ConcurrentLinkedQueue<>();
private static final Queue<byte[]> largePool = new ConcurrentLinkedQueue<>();
private ReusableByteBufferPool() {}
/**
* Acquires a pooled byte array with at least the specified capacity.
*/
public static byte[] acquire(int minCapacity) {
if (minCapacity <= SIZE_SMALL) {
byte[] b = smallPool.poll();
return (b != null) ? b : new byte[SIZE_SMALL];
} else if (minCapacity <= SIZE_MEDIUM) {
byte[] b = mediumPool.poll();
return (b != null) ? b : new byte[SIZE_MEDIUM];
} else if (minCapacity <= SIZE_LARGE) {
byte[] b = largePool.poll();
return (b != null) ? b : new byte[SIZE_LARGE];
}
return new byte[minCapacity];
}
/**
* Acquires a ByteBuffer wrapping a pooled array up to minCapacity.
*/
public static ByteBuffer acquireByteBuffer(int minCapacity) {
byte[] b = acquire(minCapacity);
return ByteBuffer.wrap(b);
}
/**
* Returns a buffer to the pool for reuse if it matches a standard tier.
*/
public static void release(byte[] buffer) {
if (buffer == null) return;
if (buffer.length == SIZE_SMALL && smallPool.size() < MAX_POOLED_PER_TIER) {
smallPool.offer(buffer);
} else if (buffer.length == SIZE_MEDIUM && mediumPool.size() < MAX_POOLED_PER_TIER) {
mediumPool.offer(buffer);
} else if (buffer.length == SIZE_LARGE && largePool.size() < MAX_POOLED_PER_TIER) {
largePool.offer(buffer);
}
}
/**
* Clears all pools to release held memory.
*/
public static void clear() {
smallPool.clear();
mediumPool.clear();
largePool.clear();
}
}
@@ -546,6 +546,29 @@ public class ECLPS implements ECLConstants {
private boolean enablePasteFromExcel = true; private boolean enablePasteFromExcel = true;
private boolean pasteStopAtProtectedLine = false; private boolean pasteStopAtProtectedLine = false;
private boolean blockPaste = true;
public boolean isBlockPaste() {
if (session != null && session.getProperties() != null) {
String p = session.getProperties().getProperty(ECLSession.BLOCK_PASTE);
if (p != null) return Boolean.parseBoolean(p);
}
return blockPaste;
}
public boolean IsBlockPaste() { return isBlockPaste(); }
public void setBlockPaste(boolean val) {
this.blockPaste = val;
if (session != null && session.getProperties() != null) {
session.getProperties().setProperty(ECLSession.BLOCK_PASTE, String.valueOf(val));
}
if (inputProcessor != null) {
inputProcessor.setBlockPaste(val);
}
}
public void SetBlockPaste(boolean val) { setBlockPaste(val); }
public boolean isEnablePasteFromExcel() { public boolean isEnablePasteFromExcel() {
if (session != null && session.getProperties() != null) { if (session != null && session.getProperties() != null) {
@@ -591,7 +614,7 @@ public class ECLPS implements ECLConstants {
setCursorPos(row, col); setCursorPos(row, col);
} }
if (inputProcessor != null) { if (inputProcessor != null) {
return inputProcessor.pasteText(text, true, isPasteStopAtProtectedLine()); return inputProcessor.pasteText(text, true, isPasteStopAtProtectedLine(), isBlockPaste());
} }
return 0; return 0;
} }
@@ -605,9 +628,9 @@ public class ECLPS implements ECLConstants {
*/ */
public synchronized int pasteString(String text, int row, int col) { public synchronized int pasteString(String text, int row, int col) {
if (text == null || text.isEmpty() || screen == null) return 0; if (text == null || text.isEmpty() || screen == null) return 0;
if (inputProcessor != null && (text.contains("\t") || isEnablePasteFromExcel() || isPasteStopAtProtectedLine())) { if (inputProcessor != null && (text.contains("\t") || isEnablePasteFromExcel() || isPasteStopAtProtectedLine() || isBlockPaste())) {
setCursorPos(row, col); setCursorPos(row, col);
return inputProcessor.pasteText(text, isEnablePasteFromExcel(), isPasteStopAtProtectedLine()); return inputProcessor.pasteText(text, isEnablePasteFromExcel(), isPasteStopAtProtectedLine(), isBlockPaste());
} }
int rows = screen.getRows(); int rows = screen.getRows();
int cols = screen.getCols(); int cols = screen.getCols();
@@ -38,6 +38,7 @@ public class ECLSession {
public static final String ENABLE_PASTE_FROM_EXCEL = "enablePasteFromExcel"; public static final String ENABLE_PASTE_FROM_EXCEL = "enablePasteFromExcel";
public static final String PASTE_TAB_OPTIONS = "pasteTabOptions"; public static final String PASTE_TAB_OPTIONS = "pasteTabOptions";
public static final String PASTE_STOP_AT_PROTECTED_LINE = "pasteStopAtProtectedLine"; 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_FIELD_WRAP = "pasteFieldWrap";
public static final String PASTE_LINE_WRAP = "pasteLineWrap"; public static final String PASTE_LINE_WRAP = "pasteLineWrap";
public static final String ENTRYASSIST_DOCMODE = "EntryAssist_DOCmode"; public static final String ENTRYASSIST_DOCMODE = "EntryAssist_DOCmode";
@@ -0,0 +1,77 @@
package haus.nightmare.lib3270j.epi;
import java.io.Serializable;
import java.util.Objects;
/**
* Attention Identifier (AID) representation for 3270 presentation space.
*/
public final class AID implements Serializable {
private static final long serialVersionUID = 1L;
private final byte code;
private final String name;
public static final AID clear = new AID((byte) 0x6D, "CLEAR");
public static final AID enter = new AID((byte) 0x7D, "ENTER");
public static final AID PA1 = new AID((byte) 0x6C, "PA1");
public static final AID PA2 = new AID((byte) 0x6E, "PA2");
public static final AID PA3 = new AID((byte) 0x6B, "PA3");
public static final AID PF1 = new AID((byte) 0xF1, "PF1");
public static final AID PF2 = new AID((byte) 0xF2, "PF2");
public static final AID PF3 = new AID((byte) 0xF3, "PF3");
public static final AID PF4 = new AID((byte) 0xF4, "PF4");
public static final AID PF5 = new AID((byte) 0xF5, "PF5");
public static final AID PF6 = new AID((byte) 0xF6, "PF6");
public static final AID PF7 = new AID((byte) 0xF7, "PF7");
public static final AID PF8 = new AID((byte) 0xF8, "PF8");
public static final AID PF9 = new AID((byte) 0xF9, "PF9");
public static final AID PF10 = new AID((byte) 0x7A, "PF10");
public static final AID PF11 = new AID((byte) 0x7B, "PF11");
public static final AID PF12 = new AID((byte) 0x7C, "PF12");
public static final AID PF13 = new AID((byte) 0xC1, "PF13");
public static final AID PF14 = new AID((byte) 0xC2, "PF14");
public static final AID PF15 = new AID((byte) 0xC3, "PF15");
public static final AID PF16 = new AID((byte) 0xC4, "PF16");
public static final AID PF17 = new AID((byte) 0xC5, "PF17");
public static final AID PF18 = new AID((byte) 0xC6, "PF18");
public static final AID PF19 = new AID((byte) 0xC7, "PF19");
public static final AID PF20 = new AID((byte) 0xC8, "PF20");
public static final AID PF21 = new AID((byte) 0xC9, "PF21");
public static final AID PF22 = new AID((byte) 0x4A, "PF22");
public static final AID PF23 = new AID((byte) 0x4B, "PF23");
public static final AID PF24 = new AID((byte) 0x4C, "PF24");
public AID(byte code, String name) {
this.code = code;
this.name = name;
}
public byte translate() {
return code;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AID aid = (AID) o;
return code == aid.code;
}
@Override
public int hashCode() {
return Objects.hash(code);
}
@Override
public String toString() {
return name != null ? name : String.format("AID(0x%02X)", code & 0xFF);
}
}
@@ -0,0 +1,34 @@
package haus.nightmare.lib3270j.epi;
/**
* Standard External Presentation Interface (EPI) DataStream processor interface.
*/
public interface DataStream {
/**
* Analyzes an inbound 3270 data stream buffer and updates the screen model.
*
* @param buffer Byte array containing the inbound 3270 record
* @param length Length of active bytes in the buffer
* @throws EPIException If a data stream format or command error is encountered
*/
void analyze(byte[] buffer, int length) throws EPIException;
/**
* Formats modified screen fields into an outbound 3270 data stream.
*
* @param buffer Target byte array to receive formatted outbound record
* @return Number of bytes written into the buffer
* @throws EPIException If encoding or formatting fails
*/
int format(byte[] buffer) throws EPIException;
/**
* Serializes the entire screen buffer into an outbound 3270 data stream.
*
* @param buffer Target byte array to receive the full screen dump
* @return Number of bytes written into the buffer
* @throws EPIException If encoding fails
*/
int readBuffer(byte[] buffer) throws EPIException;
}
@@ -0,0 +1,499 @@
package haus.nightmare.lib3270j.epi;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* 3270 stream processor implementing the External Presentation Interface (EPI).
* Provides high-level stream analysis, buffer formatting, 12/14-bit buffer address
* encoding/decoding, and stream-level character translation conforming to GA23-0059.
*/
public class DataStream3270 implements DataStream, Serializable {
private static final long serialVersionUID = 1L;
private static final Logger log = Logger.getLogger(DataStream3270.class.getName());
public static final byte[] ENCODE_TABLE = new byte[]{
32, 65, 66, 67, 68, 69, 70, 71, 72, 73, 91, 46, 60, 40, 43, 33,
38, 74, 75, 76, 77, 78, 79, 80, 81, 82, 93, 36, 42, 41, 59, 94,
45, 47, 83, 84, 85, 86, 87, 88, 89, 90, 124, 44, 37, 95, 62, 63,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 35, 64, 39, 61, 34
};
public static final int[] DECODE_TABLE = new int[]{
0, 15, 63, 59, 27, 44, 16, 61, 13, 29, 28, 14, 43, 32, 11, 33,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 30, 12, 62, 46, 47,
60, 1, 2, 3, 4, 5, 6, 7, 8, 9, 17, 18, 19, 20, 21, 22,
23, 24, 25, 34, 35, 36, 37, 38, 39, 40, 41, 10, -1, 26, 31, 45,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 42, -1, -1, -1
};
public static final char[] EBCDIC_TABLE = new char[]{
'@', 'O', '\u007f', '{', '[', 'l', 'P', '}', 'M', ']', '\\', 'N', 'k', '`', 'K', 'a',
'\u00f0', '\u00f1', '\u00f2', '\u00f3', '\u00f4', '\u00f5', '\u00f6', '\u00f7', '\u00f8', '\u00f9', 'z', '^', 'L', '~', 'n', 'o',
'|', '\u00c1', '\u00c2', '\u00c3', '\u00c4', '\u00c5', '\u00c6', '\u00c7', '\u00c8', '\u00c9', '\u00d1', '\u00d2', '\u00d3', '\u00d4', '\u00d5', '\u00d6',
'\u00d7', '\u00d8', '\u00d9', '\u00e2', '\u00e3', '\u00e4', '\u00e5', '\u00e6', '\u00e7', '\u00e8', '\u00e9', 'J', '\u0000', 'Z', '_', 'm',
'\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000',
'\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', 'j', '\u0000', '\u0000', '\u0000'
};
private static final char[] blankChars = new char[]{'\u0000', '\n', '\f', '\r', '\u000e', '\u000f', '\u0019'};
private static final String blanks = new String(blankChars);
private Screen screen;
private boolean formatted = false;
public DataStream3270(Screen screen) {
this.screen = screen;
}
public Screen getScreen() {
return screen;
}
public void setScreen(Screen screen) {
this.screen = screen;
}
public boolean isFormatted() {
return formatted;
}
public void setFormatted(boolean formatted) {
this.formatted = formatted;
}
@Override
public void analyze(byte[] buffer, int length) throws EPIException {
if (buffer == null || length == 0) {
return;
}
int index = 0;
int fieldCount = 0;
int bufCount = 0;
Field currentField = null;
int curPos = 0;
int cursorTarget = 0;
int spanTarget = 0;
byte[] lineBuffer = new byte[screen.getWidth()];
boolean isNewField = true;
int wcc = 3;
int screenWidth = screen.getWidth();
if (length > 2) {
wcc = toEbcdic(buffer[1]);
}
switch (buffer[index]) {
case 49: // Write (0x31)
case (byte) 0xF1:
curPos = cursorTarget = (screen.getCursorRow() - 1) * screenWidth + (screen.getCursorColumn() - 1);
if ((wcc & 1) != 0) {
int totalFields = screen.fieldCount();
for (int i = 1; i <= totalFields; i++) {
Field f = screen.field(i);
if (f != null && f.dataTag() == 1) {
f.resetDataTag();
}
}
}
break;
case 50: // Read Buffer (0x32)
case (byte) 0xF2:
screen.readMode = true;
return;
case 53: // Erase / Write (0x35)
case (byte) 0xF5:
screen.reset();
curPos = 0;
cursorTarget = 0;
this.formatted = false;
break;
default:
throw new EPI3270Exception(96, buffer[index], 4608);
}
try {
index = 2;
while (index < length) {
int op = buffer[index] & 0xFF;
switch (op) {
case 9: { // PT (Program Tab)
log.finer("PT");
curPos++;
break;
}
case 16: { // SFE (Start Field Extended)
log.finer("SFE");
this.formatted = true;
if (currentField != null) {
if (bufCount > 0) {
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
}
bufCount = 0;
if (isNewField) {
screen.insertField(currentField);
}
}
currentField = screen.getField(curPos);
if (currentField == null) {
currentField = new Field(screen, curPos);
isNewField = true;
} else {
currentField.setAttribute(true);
currentField.setBaseAttribute('\u0000');
currentField.setExtAttribute('A', '\u0000');
currentField.setExtAttribute('B', '\u0000');
currentField.setExtAttribute('E', '\u0000');
currentField.setExtAttribute('F', '\u0000');
isNewField = false;
}
int numPairs = buffer[++index] & 0xFF;
for (int p = 1; p <= numPairs; p++) {
char attrType = (char) buffer[index + 1];
char attrVal = toEbcdic(buffer[index + 2]);
currentField.setExtAttribute(attrType, attrVal);
index += 2;
}
curPos++;
break;
}
case 17: { // SBA (Set Buffer Address)
log.finer("SBA");
if (currentField != null) {
if (bufCount > 0) {
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
}
bufCount = 0;
if (isNewField) {
screen.insertField(currentField);
}
currentField = null;
}
curPos = decodeAddress(buffer[index + 1], buffer[index + 2]);
index += 2;
break;
}
case 18: { // EUA (Erase Unprotected to Address)
log.finer("EUA");
spanTarget = decodeAddress(buffer[index + 1], buffer[index + 2]);
screen.resetFields(curPos, spanTarget);
index += 2;
curPos = spanTarget;
break;
}
case 19: { // IC (Insert Cursor)
log.finer("IC");
cursorTarget = curPos;
break;
}
case 20: { // RA (Repeat to Address)
log.finer("RA");
spanTarget = decodeAddress(buffer[index + 1], buffer[index + 2]);
index += 3;
byte repeatByte = buffer[index];
if (currentField == null) {
currentField = new Field(screen, curPos);
currentField.setAttribute(false);
isNewField = true;
}
if (spanTarget <= curPos) {
int totalSize = screenWidth * screen.getDepth();
while (curPos < totalSize) {
if (bufCount >= screenWidth) {
currentField.setBytes(curPos - bufCount, lineBuffer, screenWidth);
bufCount = 0;
}
lineBuffer[bufCount++] = repeatByte;
curPos++;
}
if (bufCount > 0) {
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
}
bufCount = 0;
if (isNewField) {
screen.insertField(currentField);
}
currentField = null;
curPos = 0;
if (spanTarget > 0) {
currentField = new Field(screen, curPos);
currentField.setAttribute(false);
isNewField = true;
}
}
while (curPos < spanTarget) {
if (bufCount >= screenWidth) {
currentField.setBytes(curPos - bufCount, lineBuffer, screenWidth);
bufCount = 0;
}
lineBuffer[bufCount++] = repeatByte;
curPos++;
}
break;
}
case 29: { // SF (Start Field)
log.finer("SF");
this.formatted = true;
if (currentField != null) {
if (bufCount > 0) {
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
}
bufCount = 0;
if (isNewField) {
screen.insertField(currentField);
}
}
currentField = screen.getField(curPos);
if (currentField == null) {
currentField = new Field(screen, curPos);
isNewField = true;
} else {
currentField.setAttribute(true);
currentField.setExtAttribute('A', '\u0000');
currentField.setExtAttribute('B', '\u0000');
currentField.setExtAttribute('E', '\u0000');
currentField.setExtAttribute('F', '\u0000');
isNewField = false;
}
currentField.setBaseAttribute(toEbcdic(buffer[++index]));
curPos++;
break;
}
case 26:
case 30: { // MF (Modify Field)
log.finer("MF");
if (currentField != null) {
if (bufCount > 0) {
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
}
bufCount = 0;
if (isNewField) {
screen.insertField(currentField);
}
currentField = null;
}
int numPairs = buffer[++index] & 0xFF;
currentField = screen.getField(curPos);
if (currentField != null) {
currentField.setAttribute(true);
for (int p = 1; p <= numPairs; p++) {
currentField.setExtAttribute((char) buffer[index + 1], toEbcdic(buffer[index + 2]));
index += 2;
}
isNewField = false;
curPos++;
break;
}
for (int p = 1; p <= numPairs; p++) {
index += 2;
}
break;
}
case 31:
case 40: { // SA (Set Attribute)
log.finer("SA");
index += 2;
break;
}
default: {
if (currentField == null) {
currentField = screen.getField(curPos - 1);
if (currentField == null) {
currentField = new Field(screen, curPos);
isNewField = true;
currentField.setAttribute(false);
} else {
isNewField = false;
}
}
if (bufCount >= screenWidth) {
currentField.setBytes(curPos - bufCount, lineBuffer, screenWidth);
bufCount = 0;
}
byte b = buffer[index];
lineBuffer[bufCount++] = (blanks.indexOf(b) != -1) ? (byte) 32 : b;
curPos++;
break;
}
}
index++;
}
if (currentField != null) {
if (bufCount > 0) {
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
}
if (isNewField) {
screen.insertField(currentField);
}
}
} catch (Exception e) {
log.log(Level.WARNING, "Error during analyze", e);
throw new EPI3270Exception(90, e, 4609);
}
int maxCell = screenWidth * screen.getDepth();
if (cursorTarget >= 0 && cursorTarget < maxCell) {
screen.setCursor(cursorTarget / screenWidth + 1, cursorTarget % screenWidth + 1);
} else {
log.fine("Cursor address out of range: " + cursorTarget);
}
}
@Override
public int format(byte[] buffer) throws EPIException {
if (buffer == null || buffer.length == 0) {
return 0;
}
int pos = 0;
AID aid = screen.getAID();
buffer[pos++] = aid.translate();
if (aid.equals(AID.clear)) {
screen.initList();
screen.setCursor(1, 1);
this.formatted = false;
return pos;
}
if (aid.equals(AID.PA1) || aid.equals(AID.PA2) || aid.equals(AID.PA3)) {
return pos;
}
int cursorAddr = (screen.getCursorRow() - 1) * screen.getWidth() + (screen.getCursorColumn() - 1);
encodeAddress(buffer, pos, cursorAddr);
pos += 2;
int totalFields = screen.fieldCount();
try {
if (this.formatted) {
for (int i = 1; i <= totalFields; i++) {
Field field = screen.field(i);
if (field != null && field.dataTag() == 1) {
buffer[pos++] = 17; // SBA order
encodeAddress(buffer, pos, field.getPosition() + 1);
pos += 2;
byte[] bytes = field.getBytes();
if (bytes != null && bytes.length > 0) {
System.arraycopy(bytes, 0, buffer, pos, bytes.length);
pos += bytes.length;
}
}
}
} else if (totalFields > 0) {
Field f1 = screen.field(1);
byte[] bytes = (f1 != null) ? f1.getBytes() : null;
if (bytes != null && bytes.length > 0) {
System.arraycopy(bytes, 0, buffer, pos, bytes.length);
pos += bytes.length;
}
}
} catch (UnsupportedEncodingException uee) {
log.log(Level.WARNING, "Unsupported encoding during format", uee);
throw new EPI3270Exception(90, uee, 4609);
}
return pos;
}
@Override
public int readBuffer(byte[] buffer) throws EPIException {
if (buffer == null || buffer.length == 0) {
return 0;
}
int pos = 0;
buffer[pos++] = screen.getAID().translate();
int cursorAddr = (screen.getCursorRow() - 1) * screen.getWidth() + (screen.getCursorColumn() - 1);
encodeAddress(buffer, pos, cursorAddr);
pos += 2;
int totalFields = screen.fieldCount();
try {
for (int i = 1; i <= totalFields; i++) {
Field field = screen.field(i);
if (field == null) continue;
buffer[pos++] = 17; // SBA
encodeAddress(buffer, pos, field.getPosition());
pos += 2;
if (field.hasAttribute()) {
buffer[pos++] = 29; // SF
buffer[pos++] = toAscii(field.baseAttribute());
}
byte[] bytes = field.getBytes();
if (bytes != null && bytes.length > 0) {
System.arraycopy(bytes, 0, buffer, pos, bytes.length);
pos += bytes.length;
}
}
} catch (UnsupportedEncodingException uee) {
log.log(Level.WARNING, "Unsupported encoding during readBuffer", uee);
throw new EPI3270Exception(90, uee, 4609);
}
return pos;
}
/**
* Encodes 12-bit / 14-bit presentation space address into 2 bytes.
*/
public void encodeAddress(byte[] target, int offset, int address) {
if (address < 0 || address > 4096) {
target[offset] = 32;
target[offset + 1] = 32;
return;
}
int hi = address / 64;
int lo = address % 64;
target[offset] = ENCODE_TABLE[hi];
target[offset + 1] = ENCODE_TABLE[lo];
}
/**
* Decodes 2-byte presentation space address into linear buffer position.
*/
public int decodeAddress(int b1, int b2) {
int v1 = b1 & 0xFF;
int v2 = b2 & 0xFF;
if (v1 < 32 || v1 > 127 || v2 < 32 || v2 > 127) {
return -1;
}
int d1 = DECODE_TABLE[v1 - 32];
int d2 = DECODE_TABLE[v2 - 32];
if (d1 < 0 || d2 < 0) {
return -1;
}
return d1 * 64 + d2;
}
/**
* Translates single byte to EBCDIC presentation character.
*/
public char toEbcdic(int b) {
int v = b & 0xFF;
if (v < 32 || v > 127) {
return (char) v;
}
return EBCDIC_TABLE[v - 32];
}
/**
* Translates character back to ASCII byte representation.
*/
public byte toAscii(char c) {
if (c < '@' || c > '\u00f9') {
return (byte) c;
}
for (int i = 0; i < EBCDIC_TABLE.length; i++) {
if (EBCDIC_TABLE[i] == c) {
return (byte) (i + 32);
}
}
return 0;
}
}
@@ -0,0 +1,28 @@
package haus.nightmare.lib3270j.epi;
/**
* 3270 protocol specific External Presentation Interface (EPI) exception.
*/
public class EPI3270Exception extends EPIException {
private static final long serialVersionUID = 1L;
private int commandOrOrder = 0;
public EPI3270Exception(int errorCode, int commandOrOrder, int reasonCode) {
super(errorCode, reasonCode);
this.commandOrOrder = commandOrOrder;
}
public EPI3270Exception(int errorCode, Throwable cause, int reasonCode) {
super(errorCode, cause, reasonCode);
}
public EPI3270Exception(String message) {
super(message);
}
public int getCommandOrOrder() {
return commandOrOrder;
}
}
@@ -0,0 +1,50 @@
package haus.nightmare.lib3270j.epi;
/**
* Base exception for External Presentation Interface (EPI) stream operations.
*/
public class EPIException extends Exception {
private static final long serialVersionUID = 1L;
private int errorCode = 0;
private int reasonCode = 0;
public EPIException() {
super();
}
public EPIException(String message) {
super(message);
}
public EPIException(int errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public EPIException(int errorCode, Throwable cause) {
super(cause);
this.errorCode = errorCode;
}
public EPIException(int errorCode, Throwable cause, int reasonCode) {
super(cause);
this.errorCode = errorCode;
this.reasonCode = reasonCode;
}
public EPIException(int errorCode, int reasonCode) {
super("EPI Exception error=" + errorCode + " reason=" + reasonCode);
this.errorCode = errorCode;
this.reasonCode = reasonCode;
}
public int getErrorCode() {
return errorCode;
}
public int getReasonCode() {
return reasonCode;
}
}
@@ -0,0 +1,105 @@
package haus.nightmare.lib3270j.epi;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import java.io.UnsupportedEncodingException;
/**
* Bidirectional bridge between EPI Screen model and lib3270j ScreenBuffer.
*/
public class EpiScreenBufferBridge {
/**
* Copies contents from an EPI Screen into a ScreenBuffer presentation space.
*/
public static void copyToScreenBuffer(Screen epiScreen, ScreenBuffer target) {
if (epiScreen == null || target == null) return;
synchronized (target.getRenderLock()) {
int w = epiScreen.getWidth();
int h = epiScreen.getDepth();
if (target.getCols() != w || target.getRows() != h) {
target.setDimensions(h, w);
}
target.clear();
int count = epiScreen.fieldCount();
for (int i = 1; i <= count; i++) {
Field f = epiScreen.field(i);
if (f == null) continue;
int pos = f.getPosition();
if (pos >= 0 && pos < target.getSize()) {
if (f.hasAttribute()) {
byte fa = (byte) (f.baseAttribute() & 0xFF);
target.setFieldAttribute(pos, fa);
}
try {
byte[] bytes = f.getBytes();
if (bytes != null) {
int textPos = f.hasAttribute() ? pos + 1 : pos;
for (int bIdx = 0; bIdx < bytes.length && (textPos + bIdx) < target.getSize(); bIdx++) {
ExtendedAttribute ea = target.getCell(textPos + bIdx);
ea.ec = bytes[bIdx];
}
}
} catch (UnsupportedEncodingException ignored) {}
}
}
int r = Math.max(1, Math.min(target.getRows(), epiScreen.getCursorRow()));
int c = Math.max(1, Math.min(target.getCols(), epiScreen.getCursorColumn()));
target.setCursorAddress((r - 1) * target.getCols() + (c - 1));
target.translateToUnicode();
target.markAllChanged();
}
}
/**
* Extracts fields from a ScreenBuffer presentation space into an EPI Screen.
*/
public static void copyFromScreenBuffer(ScreenBuffer source, Screen epiScreen) {
if (source == null || epiScreen == null) return;
synchronized (source.getRenderLock()) {
epiScreen.setWidth(source.getCols());
epiScreen.setDepth(source.getRows());
epiScreen.initList();
int size = source.getSize();
Field currentField = null;
byte[] buf = new byte[source.getCols()];
int bufLen = 0;
for (int i = 0; i < size; i++) {
byte fa = source.getFieldAttributeAt(i);
if (fa != 0) {
if (currentField != null && bufLen > 0) {
currentField.setBytes(0, buf, bufLen);
epiScreen.insertField(currentField);
bufLen = 0;
}
currentField = new Field(epiScreen, i);
currentField.setAttribute(true);
currentField.setBaseAttribute((char) (fa & 0xFF));
} else if (currentField != null) {
ExtendedAttribute ea = source.getCell(i);
if (bufLen >= buf.length) {
currentField.setBytes(0, buf, bufLen);
bufLen = 0;
}
buf[bufLen++] = ea.ec;
}
}
if (currentField != null && bufLen > 0) {
currentField.setBytes(0, buf, bufLen);
epiScreen.insertField(currentField);
}
int cursorAddr = source.getCursorAddress();
int cols = source.getCols();
epiScreen.setCursor((cursorAddr / cols) + 1, (cursorAddr % cols) + 1);
}
}
}
@@ -0,0 +1,127 @@
package haus.nightmare.lib3270j.epi;
import java.io.ByteArrayOutputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
/**
* Field representation within an EPI 3270 Screen.
*/
public class Field implements Serializable {
private static final long serialVersionUID = 1L;
private Screen screen;
private int position;
private int length;
private boolean hasAttribute;
private char baseAttribute;
private final Map<Character, Character> extAttributes = new HashMap<>();
private int dataTag; // 1 = modified, 0 = unmodified
private final ByteArrayOutputStream content = new ByteArrayOutputStream();
public Field(Screen screen, int position) {
this.screen = screen;
this.position = position;
this.hasAttribute = false;
this.baseAttribute = '\0';
this.dataTag = 0;
}
public Screen getScreen() {
return screen;
}
public int getPosition() {
return position;
}
public void setPosition(int position) {
this.position = position;
}
public int getLength() {
return length > 0 ? length : content.size();
}
public void setLength(int length) {
this.length = length;
}
public boolean hasAttribute() {
return hasAttribute;
}
public void setAttribute(boolean hasAttribute) {
this.hasAttribute = hasAttribute;
}
public char baseAttribute() {
return baseAttribute;
}
public void setBaseAttribute(char baseAttribute) {
this.hasAttribute = true;
this.baseAttribute = baseAttribute;
// Bit 0x01 in 3270 attribute indicates Modified Data Tag (MDT)
if ((baseAttribute & 0x01) != 0) {
this.dataTag = 1;
}
}
public void setExtAttribute(char type, char value) {
this.hasAttribute = true;
extAttributes.put(type, value);
}
public char getExtAttribute(char type) {
Character val = extAttributes.get(type);
return val != null ? val : '\0';
}
public int dataTag() {
return dataTag;
}
public void resetDataTag() {
this.dataTag = 0;
}
public void setDataTag(int dataTag) {
this.dataTag = dataTag;
}
public void setBytes(int offset, byte[] data, int length) {
if (data != null && length > 0) {
content.write(data, 0, length);
this.length = content.size();
}
}
public byte[] getBytes() throws UnsupportedEncodingException {
return content.toByteArray();
}
public String getText() {
byte[] bytes = content.toByteArray();
return new String(bytes, java.nio.charset.StandardCharsets.ISO_8859_1);
}
public void setText(String text) {
content.reset();
if (text != null) {
byte[] b = text.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
content.write(b, 0, b.length);
this.length = b.length;
this.dataTag = 1;
}
}
public void clear() {
content.reset();
this.length = 0;
this.dataTag = 0;
}
}
@@ -0,0 +1,142 @@
package haus.nightmare.lib3270j.epi;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Screen representation within an EPI 3270 session.
*/
public class Screen implements Serializable {
private static final long serialVersionUID = 1L;
private int width = 80;
private int depth = 24;
private int cursorRow = 1;
private int cursorColumn = 1;
private AID aid = AID.enter;
public boolean readMode = false;
private final List<Field> fields = new ArrayList<>();
public Screen() {
this(80, 24);
}
public Screen(int width, int depth) {
this.width = width;
this.depth = depth;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getDepth() {
return depth;
}
public void setDepth(int depth) {
this.depth = depth;
}
public int getCursorRow() {
return cursorRow;
}
public int getCursorColumn() {
return cursorColumn;
}
public void setCursor(int row, int col) {
this.cursorRow = row;
this.cursorColumn = col;
}
public AID getAID() {
return aid;
}
public void setAID(AID aid) {
this.aid = aid != null ? aid : AID.enter;
}
public int fieldCount() {
return fields.size();
}
/**
* Retrieves field by 1-based index matching EPI convention.
*/
public Field field(int oneBasedIndex) {
if (oneBasedIndex >= 1 && oneBasedIndex <= fields.size()) {
return fields.get(oneBasedIndex - 1);
}
return null;
}
/**
* Finds the field starting at or covering the given linear buffer position.
*/
public Field getField(int position) {
for (Field f : fields) {
if (f.getPosition() == position) {
return f;
}
}
return null;
}
public void insertField(Field field) {
if (field == null) return;
// Keep fields ordered by position
for (int i = 0; i < fields.size(); i++) {
if (fields.get(i).getPosition() == field.getPosition()) {
fields.set(i, field);
return;
} else if (fields.get(i).getPosition() > field.getPosition()) {
fields.add(i, field);
return;
}
}
fields.add(field);
}
public void initList() {
fields.clear();
}
public void reset() {
fields.clear();
cursorRow = 1;
cursorColumn = 1;
readMode = false;
}
/**
* Resets fields within the range [start, end) by erasing unprotected content.
*/
public void resetFields(int start, int end) {
int maxPos = width * depth;
for (Field f : fields) {
int pos = f.getPosition();
boolean inRange;
if (start <= end) {
inRange = (pos >= start && pos < end);
} else {
inRange = (pos >= start || pos < end);
}
if (inRange) {
// If unprotected, clear content
if (f.hasAttribute() && (f.baseAttribute() & 0x20) == 0) {
f.clear();
}
}
}
}
}
@@ -11,18 +11,18 @@ import java.util.regex.Pattern;
import java.util.logging.Logger; import java.util.logging.Logger;
/** /**
* VM/CMS Spool and Print File Transfer facility matching IBM Host On-Demand * VM/CMS Spool and Print File Transfer facility matching Host On-Demand specifications.
* (com.ibm.eNetwork.ECL.xfer3270.CMSPrintXfer).
* *
* Provides: * Provides:
* 1. VM/CMS Virtual Reader and Printer spool file catalog parsing (CP QUERY RDR / PRT). * 1. VM/CMS Virtual Reader and Printer spool file catalog parsing (CP QUERY RDR / PRT).
* 2. ANSI / ASA carriage control conversion (Fortran print formatting: ' ', '0', '-', '1', '+'). * 2. ANSI / ASA carriage control conversion (Fortran print formatting: ' ', '0', '-', '1', '+').
* 3. IBM 1403/3211 Machine carriage control channel command byte translation. * 3. Machine carriage control channel command byte translation.
* 4. High-level print spool stream extraction and transfer helpers. * 4. High-level print spool stream extraction and transfer helpers.
*/ */
public class CMSPrintXfer { public class CMSPrintXfer {
private static final Logger log = Logger.getLogger(CMSPrintXfer.class.getName()); private static final Logger log = Logger.getLogger(CMSPrintXfer.class.getName());
private static final Pattern HEADER_PATTERN = Pattern.compile("(?i)ORIGINID|FILE\\s+CLASS|RECORDS|HOLD\\s+DATE");
private final ECLXfer xfer; private final ECLXfer xfer;
private final EbcdicTranslator translator; private final EbcdicTranslator translator;
@@ -126,13 +126,12 @@ public class CMSPrintXfer {
if (text == null || text.trim().isEmpty()) return entries; if (text == null || text.trim().isEmpty()) return entries;
String[] lines = text.split("\r?\n"); String[] lines = text.split("\r?\n");
Pattern headerPattern = Pattern.compile("(?i)ORIGINID|FILE\\s+CLASS|RECORDS|HOLD\\s+DATE");
for (String line : lines) { for (String line : lines) {
String trimmed = line.trim(); String trimmed = line.trim();
if (trimmed.isEmpty()) continue; if (trimmed.isEmpty()) continue;
if (trimmed.startsWith("--") || trimmed.startsWith("==")) continue; if (trimmed.startsWith("--") || trimmed.startsWith("==")) continue;
if (headerPattern.matcher(trimmed).find()) continue; if (HEADER_PATTERN.matcher(trimmed).find()) continue;
SpoolFileEntry entry = parseSpoolLine(trimmed, defaultDevice); SpoolFileEntry entry = parseSpoolLine(trimmed, defaultDevice);
if (entry != null) { if (entry != null) {
@@ -303,11 +302,11 @@ public class CMSPrintXfer {
} }
// ========================================================================= // =========================================================================
// IBM 1403/3211 Machine Carriage Control Translation // Machine Carriage Control Translation
// ========================================================================= // =========================================================================
/** /**
* Translates IBM Machine Carriage Control Channel Command bytes into formatted text bytes. * Translates Machine Carriage Control Channel Command bytes into formatted text bytes.
* *
* Command codes: * Command codes:
* 0x01: Write without line advance * 0x01: Write without line advance
@@ -1,11 +1,22 @@
package haus.nightmare.lib3270j.ft; package haus.nightmare.lib3270j.ft;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* Configuration for an IND$FILE file transfer session. * Configuration for an IND$FILE file transfer session.
* Ported from x3270's ft_conf_t (ft_private.h). * Ported from x3270's ft_conf_t (ft_private.h).
*/ */
public class FTConfig { public class FTConfig {
private static final Pattern RECFM_PATTERN = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?");
private static final Pattern LRECL_PATTERN = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?");
private static final Pattern BLK_PATTERN = Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?");
private static final Pattern SPACE_PATTERN = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?");
private static final Pattern AVB_PATTERN = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?");
private static final Pattern CP_PATTERN = Pattern.compile("(?i)CODEPAGE[\\s\\(]+([A-Za-z0-9_-]+)\\)?");
private static final Pattern MTU_PATTERN = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?");
/** Host operating system type */ /** Host operating system type */
public enum HostType { public enum HostType {
TSO, CMS, CICS TSO, CMS, CICS
@@ -210,26 +221,22 @@ public class FTConfig {
String trimmed = opts.trim(); String trimmed = opts.trim();
// Extract and process parenthesized or space-separated tokens // Extract and process parenthesized or space-separated tokens
java.util.regex.Pattern recfmPattern = java.util.regex.Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?"); Matcher recfmMatcher = RECFM_PATTERN.matcher(trimmed);
java.util.regex.Matcher recfmMatcher = recfmPattern.matcher(trimmed);
if (recfmMatcher.find()) { if (recfmMatcher.find()) {
setRecfm(recfmMatcher.group(1)); setRecfm(recfmMatcher.group(1));
} }
java.util.regex.Pattern lreclPattern = java.util.regex.Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?"); Matcher lreclMatcher = LRECL_PATTERN.matcher(trimmed);
java.util.regex.Matcher lreclMatcher = lreclPattern.matcher(trimmed);
if (lreclMatcher.find()) { if (lreclMatcher.find()) {
setLrecl(lreclMatcher.group(1)); setLrecl(lreclMatcher.group(1));
} }
java.util.regex.Pattern blkPattern = java.util.regex.Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?"); Matcher blkMatcher = BLK_PATTERN.matcher(trimmed);
java.util.regex.Matcher blkMatcher = blkPattern.matcher(trimmed);
if (blkMatcher.find()) { if (blkMatcher.find()) {
setBlksize(blkMatcher.group(1)); setBlksize(blkMatcher.group(1));
} }
java.util.regex.Pattern spacePattern = java.util.regex.Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?"); Matcher spaceMatcher = SPACE_PATTERN.matcher(trimmed);
java.util.regex.Matcher spaceMatcher = spacePattern.matcher(trimmed);
if (spaceMatcher.find()) { if (spaceMatcher.find()) {
try { try {
this.primarySpace = Integer.parseInt(spaceMatcher.group(1)); this.primarySpace = Integer.parseInt(spaceMatcher.group(1));
@@ -239,8 +246,7 @@ public class FTConfig {
} catch (NumberFormatException ignored) {} } catch (NumberFormatException ignored) {}
} }
java.util.regex.Pattern avbPattern = java.util.regex.Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?"); Matcher avbMatcher = AVB_PATTERN.matcher(trimmed);
java.util.regex.Matcher avbMatcher = avbPattern.matcher(trimmed);
if (avbMatcher.find()) { if (avbMatcher.find()) {
try { try {
this.avblock = Integer.parseInt(avbMatcher.group(1)); this.avblock = Integer.parseInt(avbMatcher.group(1));
@@ -248,14 +254,12 @@ public class FTConfig {
} catch (NumberFormatException ignored) {} } catch (NumberFormatException ignored) {}
} }
java.util.regex.Pattern cpPattern = java.util.regex.Pattern.compile("(?i)CODEPAGE[\\s\\(]+([A-Za-z0-9_-]+)\\)?"); Matcher cpMatcher = CP_PATTERN.matcher(trimmed);
java.util.regex.Matcher cpMatcher = cpPattern.matcher(trimmed);
if (cpMatcher.find()) { if (cpMatcher.find()) {
this.codePage = cpMatcher.group(1); this.codePage = cpMatcher.group(1);
} }
java.util.regex.Pattern mtuPattern = java.util.regex.Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?"); Matcher mtuMatcher = MTU_PATTERN.matcher(trimmed);
java.util.regex.Matcher mtuMatcher = mtuPattern.matcher(trimmed);
if (mtuMatcher.find()) { if (mtuMatcher.find()) {
try { try {
setDftBufferSize(Integer.parseInt(mtuMatcher.group(1))); setDftBufferSize(Integer.parseInt(mtuMatcher.group(1)));
@@ -395,13 +395,16 @@ public class GocaDecoder {
if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 || order == GocaConstants.G_COMT) { if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 || order == GocaConstants.G_COMT) {
return 1; return 1;
} }
// Orders with optional 0x00 trailing length/qualifier byte (e.g. 3E 00, 71 00, 60 00, 7E 00, 3F 00, 93 00, 91 00) // Orders with optional 0x00 trailing length/qualifier byte (e.g. 3E 00, 71 00, 60 00, 7E 00, 3F 00)
if (order == GocaConstants.G_ENDPROLOGUE || order == GocaConstants.G_ENDSEGM || if (order == GocaConstants.G_ENDPROLOGUE || order == GocaConstants.G_ENDSEGM ||
order == GocaConstants.G_GEAR || order == GocaConstants.G_GERASE || order == GocaConstants.G_GEAR || order == GocaConstants.G_GERASE ||
order == GocaConstants.G_GPOP || order == GocaConstants.G_GEIMG || order == GocaConstants.G_GPOP) {
(inImage && order == GocaConstants.G_GEIMG_ALT)) {
return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1; return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1;
} }
// G_GEIMG (0x93 End Image): self-defining draw order (e.g. 93 02 00 00 or 93 00 or 93)
if (order == GocaConstants.G_GEIMG) {
return (idx + 1 < end) ? ((data[idx + 1] & 0xFF) + 2) : 1;
}
if (idx + 1 >= end) { if (idx + 1 >= end) {
return -1; return -1;
} }
@@ -552,18 +555,33 @@ public class GocaDecoder {
idx += orderLen; idx += orderLen;
break; break;
} }
case GocaConstants.G_GEIMG: // 0x93 case GocaConstants.G_GEIMG: { // 0x93 End Image
case GocaConstants.G_GEIMG_ALT: { // 0x91
if (inImage || order == GocaConstants.G_GEIMG) {
endImage(); endImage();
idx += orderLen; idx += orderLen;
} else { break;
// 0x91: Begin Image at Current Position (G_GBIMGC) }
if (payloadLen >= 4 && idx + 2 + payloadLen <= end) { case GocaConstants.G_GBIMGC: { // 0x91: Begin Image at Current Position (G_GBIMGC)
int w = readCoord(inputData, idx + 2); if (inImage) {
int h = readCoord(inputData, idx + 4); endImage();
}
int w = 0, h = 0;
int bitDepth = GocaConstants.BPP_1; int bitDepth = GocaConstants.BPP_1;
int compression = GocaConstants.IMG_UNCOMPRESSED; int compression = GocaConstants.IMG_UNCOMPRESSED;
if (payloadLen >= 6 && idx + 2 + payloadLen <= end) {
w = readCoord(inputData, idx + 4);
h = readCoord(inputData, idx + 6);
if (payloadLen >= 7) {
int fmt = inputData[idx + 8] & 0xFF;
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
}
if (payloadLen >= 8) {
compression = inputData[idx + 9] & 0xFF;
}
} else if (payloadLen >= 4 && idx + 2 + payloadLen <= end) {
w = readCoord(inputData, idx + 2);
h = readCoord(inputData, idx + 4);
if (payloadLen >= 5) { if (payloadLen >= 5) {
int fmt = inputData[idx + 6] & 0xFF; int fmt = inputData[idx + 6] & 0xFF;
if (fmt == 2) bitDepth = GocaConstants.BPP_2; if (fmt == 2) bitDepth = GocaConstants.BPP_2;
@@ -573,10 +591,9 @@ public class GocaDecoder {
if (payloadLen >= 6) { if (payloadLen >= 6) {
compression = inputData[idx + 7] & 0xFF; compression = inputData[idx + 7] & 0xFF;
} }
}
beginImage(curX, curY, w, h, bitDepth, compression); beginImage(curX, curY, w, h, bitDepth, compression);
}
idx += orderLen; idx += orderLen;
}
break; break;
} }
case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70) case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70)
@@ -986,13 +1003,30 @@ public class GocaDecoder {
break; break;
} }
case GocaConstants.G_GBIMG: { // Begin Image (0xD1) case GocaConstants.G_GBIMG: { // Begin Image (0xD1)
if (inImage) {
endImage();
}
if (payloadLen >= 8 && idx + 2 + payloadLen <= end) { if (payloadLen >= 8 && idx + 2 + payloadLen <= end) {
int x = readCoord(inputData, idx + 2); int x = readCoord(inputData, idx + 2);
int y = readCoord(inputData, idx + 4); int y = readCoord(inputData, idx + 4);
int w = readCoord(inputData, idx + 6); int w, h;
int h = readCoord(inputData, idx + 8);
int bitDepth = GocaConstants.BPP_1; int bitDepth = GocaConstants.BPP_1;
int compression = GocaConstants.IMG_UNCOMPRESSED; int compression = GocaConstants.IMG_UNCOMPRESSED;
if (payloadLen >= 10) {
w = readCoord(inputData, idx + 8);
h = readCoord(inputData, idx + 10);
if (payloadLen >= 11) {
int fmt = inputData[idx + 12] & 0xFF;
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
}
if (payloadLen >= 12) {
compression = inputData[idx + 13] & 0xFF;
}
} else {
w = readCoord(inputData, idx + 6);
h = readCoord(inputData, idx + 8);
if (payloadLen >= 9) { if (payloadLen >= 9) {
int fmt = inputData[idx + 10] & 0xFF; int fmt = inputData[idx + 10] & 0xFF;
if (fmt == 2) bitDepth = GocaConstants.BPP_2; if (fmt == 2) bitDepth = GocaConstants.BPP_2;
@@ -1002,6 +1036,7 @@ public class GocaDecoder {
if (payloadLen >= 10) { if (payloadLen >= 10) {
compression = inputData[idx + 11] & 0xFF; compression = inputData[idx + 11] & 0xFF;
} }
}
beginImage(x, y, w, h, bitDepth, compression); beginImage(x, y, w, h, bitDepth, compression);
} }
idx += orderLen; idx += orderLen;
@@ -418,9 +418,27 @@ public class GraphicsPlane implements PixelBuffer {
return transform; return transform;
} }
// Character cell dimensions matching Query Reply presentation space
private int charWidth = 9;
private int charHeight = 16;
public synchronized void setCharDimensions(int width, int height) {
if (width > 0) this.charWidth = width;
if (height > 0) this.charHeight = height;
this.transform.setDefaultCharMetrics(this.charWidth, this.charHeight);
}
public int getCharWidth() {
return charWidth;
}
public int getCharHeight() {
return charHeight;
}
public int getTotalWidth() { public int getTotalWidth() {
int cols = screenCols > 0 ? screenCols : 80; int cols = screenCols > 0 ? screenCols : 80;
return cols * 9; return cols * charWidth;
} }
/** /**
@@ -457,7 +475,7 @@ public class GraphicsPlane implements PixelBuffer {
*/ */
public int getTotalHeight() { public int getTotalHeight() {
int rows = screenRows > 0 ? screenRows : 24; int rows = screenRows > 0 ? screenRows : 24;
return rows * 16; return rows * charHeight;
} }
public int getXMax() { public int getXMax() {
@@ -137,6 +137,10 @@ public class InputProcessor {
private boolean aplKeyboardMode = false; private boolean aplKeyboardMode = false;
private boolean numericFieldLock = true; private boolean numericFieldLock = true;
private boolean autoSkipEnabled = true; private boolean autoSkipEnabled = true;
private boolean blockPaste = true;
public boolean isBlockPaste() { return blockPaste; }
public void setBlockPaste(boolean val) { this.blockPaste = val; }
public void setBellListener(BellListener listener) { this.bellListener = listener; } public void setBellListener(BellListener listener) { this.bellListener = listener; }
public BellListener getBellListener() { return bellListener; } public BellListener getBellListener() { return bellListener; }
@@ -446,6 +450,19 @@ public class InputProcessor {
* @return number of characters pasted * @return number of characters pasted
*/ */
public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine) { public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine) {
return pasteText(text, enablePasteFromExcel, pasteStopAtProtectedLine, this.blockPaste);
}
/**
* Pastes text onto the screen with tabular, boundary, and block alignment controls.
*
* @param text text to paste
* @param enablePasteFromExcel whether to parse tabs as field advances and newlines as row advances
* @param pasteStopAtProtectedLine whether to halt paste when encountering protected boundaries
* @param blockPaste whether newlines align each line with the initial starting cursor column
* @return number of characters pasted
*/
public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine, boolean blockPaste) {
if (text == null || text.isEmpty() || screen == null || keyboardLocked) { if (text == null || text.isEmpty() || screen == null || keyboardLocked) {
return 0; return 0;
} }
@@ -463,6 +480,7 @@ public class InputProcessor {
int count = 0; int count = 0;
int len = text.length(); int len = text.length();
int i = 0; int i = 0;
int startCol = screen.getCursorCol();
while (i < len && !keyboardLocked) { while (i < len && !keyboardLocked) {
char ch = text.charAt(i); char ch = text.charAt(i);
@@ -472,7 +490,34 @@ public class InputProcessor {
if (ch == '\r' && (i + 1) < len && text.charAt(i + 1) == '\n') { if (ch == '\r' && (i + 1) < len && text.charAt(i + 1) == '\n') {
i++; // skip \n of \r\n i++; // skip \n of \r\n
} }
if (enablePasteFromExcel) { if (blockPaste) {
int curRow = screen.getCursorRow();
int nextRow = (curRow + 1) % screen.getRows();
if (pasteStopAtProtectedLine && isLineProtected(nextRow)) {
break; // Stop paste when next line is protected
}
int cols = screen.getCols();
int rows = screen.getRows();
int targetAddr = nextRow * cols + startCol;
if (screen.isFormatted()) {
int size = rows * cols;
ExtendedAttribute ea = screen.getCell(targetAddr);
if (ea.isFieldAttribute()) {
targetAddr = (targetAddr + 1) % size;
}
byte faVal = screen.getFieldAttributeAt(targetAddr);
if (faIsProtected(faVal & 0xFF)) {
if (pasteStopAtProtectedLine) {
break;
} else {
targetAddr = screen.findNextUnprotected(targetAddr == 0 ? (size - 1) : (targetAddr - 1));
}
}
}
screen.setCursorAddress(targetAddr);
screen.markAllChanged();
screen.updateDisplaySnapshot();
} else if (enablePasteFromExcel) {
int curRow = screen.getCursorRow(); int curRow = screen.getCursorRow();
int nextRow = (curRow + 1) % screen.getRows(); int nextRow = (curRow + 1) % screen.getRows();
if (pasteStopAtProtectedLine && isLineProtected(nextRow)) { if (pasteStopAtProtectedLine && isLineProtected(nextRow)) {
@@ -47,7 +47,7 @@ public class TelnetFSM {
private final boolean[] hisOpts = new boolean[256]; // options the host has enabled private final boolean[] hisOpts = new boolean[256]; // options the host has enabled
// 3270 input buffer (accumulated between telnet framing) // 3270 input buffer (accumulated between telnet framing)
private final ByteArrayOutputStream ibuf = new ByteArrayOutputStream(32768); private final haus.nightmare.lib3270j.datastream.FastByteBuffer ibuf = new haus.nightmare.lib3270j.datastream.FastByteBuffer(32768);
// Sub-negotiation buffer // Sub-negotiation buffer
private final ByteArrayOutputStream sbbuf = new ByteArrayOutputStream(4096); private final ByteArrayOutputStream sbbuf = new ByteArrayOutputStream(4096);
@@ -70,8 +70,22 @@ public class TelnetFSM {
private List<String> getCandidateTerminalTypes() { private List<String> getCandidateTerminalTypes() {
List<String> list = new ArrayList<>(); List<String> list = new ArrayList<>();
String currentLu = null;
List<String> lus = config.getLuNames();
if (lus != null && !lus.isEmpty() && luIndex < lus.size()) {
currentLu = lus.get(luIndex);
} else if (config.getLuName() != null && !config.getLuName().trim().isEmpty()) {
currentLu = config.getLuName().trim();
}
if (config.getTerminalName() != null && !config.getTerminalName().trim().isEmpty()) { if (config.getTerminalName() != null && !config.getTerminalName().trim().isEmpty()) {
list.add(config.getTerminalName().trim()); String tName = config.getTerminalName().trim();
if (currentLu != null && !currentLu.isEmpty() && !tName.contains("@")) {
list.add(tName + "@" + currentLu);
list.add(tName);
} else {
list.add(tName);
}
return list; return list;
} }
if (config.isDynamicModel()) { if (config.isDynamicModel()) {
@@ -82,8 +96,7 @@ public class TelnetFSM {
list.add("IBM-3279-2"); list.add("IBM-3279-2");
list.add("IBM-3278-2"); list.add("IBM-3278-2");
list.add("UNKNOWN"); list.add("UNKNOWN");
return list; } else {
}
TerminalModel model = config.getModel(); TerminalModel model = config.getModel();
list.add(model.getTerminalType()); list.add(model.getTerminalType());
list.add(model.getBaseTerminalType()); list.add(model.getBaseTerminalType());
@@ -100,6 +113,24 @@ public class TelnetFSM {
list.add("IBM-3278-2"); list.add("IBM-3278-2");
} }
list.add("UNKNOWN"); list.add("UNKNOWN");
}
if (currentLu != null && !currentLu.isEmpty()) {
List<String> luCandidates = new ArrayList<>();
for (String item : list) {
if (!"UNKNOWN".equalsIgnoreCase(item) && !item.contains("@")) {
luCandidates.add(item + "@" + currentLu);
}
}
for (String item : list) {
if (!"UNKNOWN".equalsIgnoreCase(item)) {
luCandidates.add(item);
}
}
luCandidates.add("UNKNOWN");
return luCandidates;
}
return list; return list;
} }
@@ -589,6 +620,9 @@ public class TelnetFSM {
out.write(IAC); out.write(IAC);
out.write(SE); out.write(SE);
sendBytes(out.toByteArray()); sendBytes(out.toByteArray());
if (termType.contains("@")) {
connectedLu = termType.substring(termType.indexOf('@') + 1).trim();
}
log.warning(">>> SENT SB TTYPE IS " + termType + " SE"); log.warning(">>> SENT SB TTYPE IS " + termType + " SE");
} }
} }
@@ -990,9 +1024,9 @@ public class TelnetFSM {
// ========== End of Record processing ========== // ========== End of Record processing ==========
private void processEndOfRecord() { private void processEndOfRecord() {
byte[] data = ibuf.toByteArray(); int dataLen = ibuf.size();
ibuf.reset(); if (dataLen == 0) return;
if (data.length == 0) return; byte[] data = ibuf.buffer();
if ((connectionState == ConnectionState.TELNET_PENDING || if ((connectionState == ConnectionState.TELNET_PENDING ||
connectionState == ConnectionState.CONNECTED_NVT || connectionState == ConnectionState.CONNECTED_NVT ||
@@ -1003,14 +1037,15 @@ public class TelnetFSM {
if (tn3270eNegotiated) { if (tn3270eNegotiated) {
// TN3270E mode: data starts with 5-byte header // TN3270E mode: data starts with 5-byte header
processTN3270ERecord(data); processTN3270ERecord(data, 0, dataLen);
} else { } else {
// Plain TN3270 mode: data is raw 3270 data stream // Plain TN3270 mode: data is raw 3270 data stream
if (dsProcessor != null) { if (dsProcessor != null) {
dsProcessor.processRecord(data, 0, data.length, false); dsProcessor.processRecord(data, 0, dataLen, false);
} }
notifyScreenUpdate(); notifyScreenUpdate();
} }
ibuf.reset();
// Phase 10: Contention Resolution & AUTO_SYS_UNLOCK handling on EOR // Phase 10: Contention Resolution & AUTO_SYS_UNLOCK handling on EOR
if (dsProcessor != null) { if (dsProcessor != null) {
@@ -1062,19 +1097,23 @@ public class TelnetFSM {
} }
public void processTn3270eHeader(byte[] data) { public void processTn3270eHeader(byte[] data) {
processTN3270ERecord(data); processTN3270ERecord(data, 0, data != null ? data.length : 0);
} }
private void processTN3270ERecord(byte[] data) { private void processTN3270ERecord(byte[] data) {
if (data.length < EH_SIZE) { processTN3270ERecord(data, 0, data != null ? data.length : 0);
log.warning("TN3270E record too short: " + data.length); }
private void processTN3270ERecord(byte[] data, int offset, int length) {
if (data == null || length < EH_SIZE) {
log.warning("TN3270E record too short: " + length);
return; return;
} }
int dataType = data[0] & 0xFF; int dataType = data[offset] & 0xFF;
int requestFlag = data[1] & 0xFF; int requestFlag = data[offset + 1] & 0xFF;
int responseFlag = data[2] & 0xFF; int responseFlag = data[offset + 2] & 0xFF;
int seqNumber = ((data[3] & 0xFF) << 8) | (data[4] & 0xFF); int seqNumber = ((data[offset + 3] & 0xFF) << 8) | (data[offset + 4] & 0xFF);
this.sdi_flag = (requestFlag & 0x01) != 0; this.sdi_flag = (requestFlag & 0x01) != 0;
this.kri_flag = (requestFlag & 0x02) != 0; this.kri_flag = (requestFlag & 0x02) != 0;
@@ -1089,7 +1128,7 @@ public class TelnetFSM {
switch (dataType) { switch (dataType) {
case DT_3270_DATA: case DT_3270_DATA:
if (data.length > EH_SIZE) { if (length > EH_SIZE) {
// Transition to 3270 mode from any non-3270 state (E_NVT, UNBOUND, SSCP) // Transition to 3270 mode from any non-3270 state (E_NVT, UNBOUND, SSCP)
if (connectionState != ConnectionState.CONNECTED_TN3270E) { if (connectionState != ConnectionState.CONNECTED_TN3270E) {
// Clear screen on transition to 3270 mode from unbound/SSCP/NVT // Clear screen on transition to 3270 mode from unbound/SSCP/NVT
@@ -1099,7 +1138,7 @@ public class TelnetFSM {
tn3270eSubmode = TN3270ESubmode.E_3270; tn3270eSubmode = TN3270ESubmode.E_3270;
} }
try { try {
dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, false); dsProcessor.processRecord(data, offset + EH_SIZE, length - EH_SIZE, false);
notifyScreenUpdate(); notifyScreenUpdate();
// Send positive response if required // Send positive response if required
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
@@ -1119,9 +1158,9 @@ public class TelnetFSM {
break; break;
case DT_SCS_DATA: case DT_SCS_DATA:
if (data.length > EH_SIZE) { if (length > EH_SIZE) {
try { try {
processSCSInbound(data, EH_SIZE, data.length - EH_SIZE); processSCSInbound(data, offset + EH_SIZE, length - EH_SIZE);
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber); sendTN3270EPositiveResponse(seqNumber);
} }
@@ -1148,9 +1187,9 @@ public class TelnetFSM {
changeState(ConnectionState.CONNECTED_SSCP); changeState(ConnectionState.CONNECTED_SSCP);
tn3270eSubmode = TN3270ESubmode.E_SSCP; tn3270eSubmode = TN3270ESubmode.E_SSCP;
} }
if (data.length > EH_SIZE) { if (length > EH_SIZE) {
try { try {
dsProcessor.processSscpLuData(data, EH_SIZE, data.length - EH_SIZE); dsProcessor.processSscpLuData(data, offset + EH_SIZE, length - EH_SIZE);
notifyScreenUpdate(); notifyScreenUpdate();
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber); sendTN3270EPositiveResponse(seqNumber);
@@ -1169,11 +1208,15 @@ public class TelnetFSM {
break; break;
case DT_BIND_IMAGE: case DT_BIND_IMAGE:
process_bind(data, responseFlag, seqNumber); {
byte[] bindData = new byte[length];
System.arraycopy(data, offset, bindData, 0, length);
process_bind(bindData, responseFlag, seqNumber);
}
break; break;
case DT_UNBIND: case DT_UNBIND:
int unbindReason = (data.length > EH_SIZE) ? (data[EH_SIZE] & 0xFF) : UNBIND_NORMAL; int unbindReason = (length > EH_SIZE) ? (data[offset + EH_SIZE] & 0xFF) : UNBIND_NORMAL;
process_unbind(unbindReason, responseFlag, seqNumber); process_unbind(unbindReason, responseFlag, seqNumber);
break; break;
@@ -1186,9 +1229,9 @@ public class TelnetFSM {
if (dsProcessor != null && dsProcessor.getInputProcessor() != null) { if (dsProcessor != null && dsProcessor.getInputProcessor() != null) {
dsProcessor.getInputProcessor().setKeyboardLocked(false); dsProcessor.getInputProcessor().setKeyboardLocked(false);
} }
if (data.length > EH_SIZE) { if (length > EH_SIZE) {
try { try {
processNVTData(data, EH_SIZE, data.length - EH_SIZE); processNVTData(data, offset + EH_SIZE, length - EH_SIZE);
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber); sendTN3270EPositiveResponse(seqNumber);
} }
@@ -1243,12 +1286,12 @@ public class TelnetFSM {
// This happens when the server ignores or drops TN3270E framing and speaks plain 3270 data stream. // This happens when the server ignores or drops TN3270E framing and speaks plain 3270 data stream.
if (dataType == 0xF5 || dataType == 0x7E || dataType == 0xF1 || dataType == 0x6F || if (dataType == 0xF5 || dataType == 0x7E || dataType == 0xF1 || dataType == 0x6F ||
dataType == 0x6E || dataType == 0xF2 || dataType == 0xF6 || dataType == 0x6E || dataType == 0xF2 || dataType == 0xF6 ||
dataType == 0x0D || (data.length >= 2 && (data[0] & 0xFF) == 0x11)) { dataType == 0x0D || (length >= 2 && (data[offset] & 0xFF) == 0x11)) {
log.warning("Received plain 3270 command (0x" + Integer.toHexString(dataType) + log.warning("Received plain 3270 command (0x" + Integer.toHexString(dataType) +
") in TN3270E mode — automatically switching to plain TN3270 mode"); ") in TN3270E mode — automatically switching to plain TN3270 mode");
tn3270eNegotiated = false; tn3270eNegotiated = false;
changeState(ConnectionState.CONNECTED_3270); changeState(ConnectionState.CONNECTED_3270);
dsProcessor.processRecord(data, 0, data.length, true); dsProcessor.processRecord(data, offset, length, true);
notifyScreenUpdate(); notifyScreenUpdate();
} else { } else {
log.info("Unhandled TN3270E data type: " + dataType); log.info("Unhandled TN3270E data type: " + dataType);
@@ -1257,6 +1300,7 @@ public class TelnetFSM {
} }
} }
public void processSCSInbound(byte[] data) { public void processSCSInbound(byte[] data) {
if (data == null) return; if (data == null) return;
processSCSInbound(data, 0, data.length); processSCSInbound(data, 0, data.length);
@@ -1289,7 +1333,7 @@ public class TelnetFSM {
} }
/** /**
* HoD 5-byte send_response compatible signature (com.ibm.eNetwork.ECL.tn3270.Telnet3270E). * HoD 5-byte send_response compatible signature.
*/ */
public void send_response(short s, short s2, int n) { public void send_response(short s, short s2, int n) {
byte[] byArray = new byte[5]; byte[] byArray = new byte[5];
@@ -26,8 +26,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
* Core 3270 File Transfer Controller conforming to IBM Host On-Demand * Core 3270 File Transfer Controller conforming to Host On-Demand specifications.
* (com.ibm.eNetwork.ECL.xfer3270.Xfer3270).
* *
* Implements FileTransferInterface and handles TSO/CMS/CICS IND$FILE options, * Implements FileTransferInterface and handles TSO/CMS/CICS IND$FILE options,
* dynamic MTU buffering, host/local dataset name mappings, directory queries, * dynamic MTU buffering, host/local dataset name mappings, directory queries,
@@ -37,6 +36,13 @@ public class Xfer3270 implements FileTransferInterface {
private static final Logger log = Logger.getLogger(Xfer3270.class.getName()); private static final Logger log = Logger.getLogger(Xfer3270.class.getName());
private static final Pattern RECFM_PATTERN = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?");
private static final Pattern LRECL_PATTERN = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?");
private static final Pattern BLK_PATTERN = Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?");
private static final Pattern SPACE_PATTERN = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?");
private static final Pattern AVB_PATTERN = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?");
private static final Pattern MTU_PATTERN = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?");
public static final String UNICODE_UCS2_STR = "UCS2"; public static final String UNICODE_UCS2_STR = "UCS2";
public static final String UNICODE_UTF8_STR = "UTF8"; public static final String UNICODE_UTF8_STR = "UTF8";
public static final String UNICODE_UTF_8_STR = "UTF-8"; public static final String UNICODE_UTF_8_STR = "UTF-8";
@@ -175,20 +181,20 @@ public class Xfer3270 implements FileTransferInterface {
} }
// Mainframe dataset parameters // Mainframe dataset parameters
Matcher recfmMatcher = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?").matcher(options); Matcher recfmMatcher = RECFM_PATTERN.matcher(options);
if (recfmMatcher.find()) this.recfm = recfmMatcher.group(1).toUpperCase(); if (recfmMatcher.find()) this.recfm = recfmMatcher.group(1).toUpperCase();
Matcher lreclMatcher = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?").matcher(options); Matcher lreclMatcher = LRECL_PATTERN.matcher(options);
if (lreclMatcher.find()) { if (lreclMatcher.find()) {
try { this.lrecl = Integer.parseInt(lreclMatcher.group(1)); } catch (NumberFormatException ignored) {} try { this.lrecl = Integer.parseInt(lreclMatcher.group(1)); } catch (NumberFormatException ignored) {}
} }
Matcher blkMatcher = Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?").matcher(options); Matcher blkMatcher = BLK_PATTERN.matcher(options);
if (blkMatcher.find()) { if (blkMatcher.find()) {
try { this.blksize = Integer.parseInt(blkMatcher.group(1)); } catch (NumberFormatException ignored) {} try { this.blksize = Integer.parseInt(blkMatcher.group(1)); } catch (NumberFormatException ignored) {}
} }
Matcher spaceMatcher = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?").matcher(options); Matcher spaceMatcher = SPACE_PATTERN.matcher(options);
if (spaceMatcher.find()) { if (spaceMatcher.find()) {
try { try {
this.primarySpace = Integer.parseInt(spaceMatcher.group(1)); this.primarySpace = Integer.parseInt(spaceMatcher.group(1));
@@ -198,7 +204,7 @@ public class Xfer3270 implements FileTransferInterface {
} catch (NumberFormatException ignored) {} } catch (NumberFormatException ignored) {}
} }
Matcher avbMatcher = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?").matcher(options); Matcher avbMatcher = AVB_PATTERN.matcher(options);
if (avbMatcher.find()) { if (avbMatcher.find()) {
try { try {
this.avblock = Integer.parseInt(avbMatcher.group(1)); this.avblock = Integer.parseInt(avbMatcher.group(1));
@@ -212,7 +218,7 @@ public class Xfer3270 implements FileTransferInterface {
this.spaceUnits = "CYLINDERS"; this.spaceUnits = "CYLINDERS";
} }
Matcher mtuMatcher = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?").matcher(options); Matcher mtuMatcher = MTU_PATTERN.matcher(options);
if (mtuMatcher.find()) { if (mtuMatcher.find()) {
try { try {
SetMTUSize(Integer.parseInt(mtuMatcher.group(1))); SetMTUSize(Integer.parseInt(mtuMatcher.group(1)));
@@ -82,4 +82,193 @@ public class DataStreamProcessorTest {
assertNotNull(sentData.get()); assertNotNull(sentData.get());
assertEquals((byte) AID_ENTER, sentData.get()[0], "ReadBuffer must transmit operator's stored AID"); assertEquals((byte) AID_ENTER, sentData.get()[0], "ReadBuffer must transmit operator's stored AID");
} }
@Test
public void testCharacterAttributesPersistAcrossStartField() throws java.io.IOException {
// User's exact ISPF Option 0 sequence:
// SBA(7, 1) -> SF(prot,skip) -> SA(yellow) -> ' 4 ' -> SBA(7, 7) -> SF(prot,skip) -> 'DISPLAY'
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
stream.write(CMD_EW);
stream.write(0xC3); // WCC
// SBA(7, 1) -> address 7*80 + 1 = 561
byte[] addr1 = encodeAddress(561, 24, 80);
stream.write(ORDER_SBA);
stream.write(addr1);
// SF(prot, skip)
stream.write(ORDER_SF);
stream.write(FA_PRINTABLE | FA_PROTECT | FA_NUMERIC);
// SA(yellow) -> XA_FOREGROUND, COLOR_YELLOW (0xF6)
stream.write(ORDER_SA);
stream.write(XA_FOREGROUND);
stream.write(0xF6);
// Data: ' 4 '
stream.write(translator.stringToEbcdic(" 4 "));
// SBA(7, 7) -> address 7*80 + 7 = 567
byte[] addr2 = encodeAddress(567, 24, 80);
stream.write(ORDER_SBA);
stream.write(addr2);
// SF(prot, skip)
stream.write(ORDER_SF);
stream.write(FA_PRINTABLE | FA_PROTECT | FA_NUMERIC);
// Data: 'DISPLAY'
stream.write(translator.stringToEbcdic("DISPLAY"));
byte[] record = stream.toByteArray();
processor.processRecord(record, 0, record.length, true);
// Positions 562..566 (' 4 ') must be yellow (0xF6)
for (int i = 562; i <= 566; i++) {
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " should have yellow foreground (0xF6)");
}
// Positions 568..574 ('DISPLAY') across the second SF must also retain yellow (0xF6)
for (int i = 568; i <= 574; i++) {
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " ('DISPLAY') should retain yellow foreground (0xF6)");
}
// Now test that SA with XA_ALL resets character attributes to default (0)
java.io.ByteArrayOutputStream resetStream = new java.io.ByteArrayOutputStream();
resetStream.write(CMD_W);
resetStream.write(0xC3);
resetStream.write(ORDER_SA);
resetStream.write(XA_ALL);
resetStream.write(0x00);
resetStream.write(translator.stringToEbcdic("TEST"));
byte[] resetRecord = resetStream.toByteArray();
processor.processRecord(resetRecord, 0, resetRecord.length, true);
for (int i = 575; i < 579; i++) {
assertEquals((byte) 0, screen.getCell(i).fg, "Cell at " + i + " should have default foreground (0) after SA(XA_ALL)");
}
}
@Test
public void testCharacterAttributesPersistAcrossStartFieldExtended() throws java.io.IOException {
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
stream.write(CMD_EW);
stream.write(0xC3);
// SA(yellow)
stream.write(ORDER_SA);
stream.write(XA_FOREGROUND);
stream.write(0xF6);
// SFE with 1 pair (3270 FA)
stream.write(ORDER_SFE);
stream.write(0x01); // 1 pair
stream.write(XA_3270);
stream.write(FA_PRINTABLE);
// Data 'HELLO'
stream.write(translator.stringToEbcdic("HELLO"));
byte[] record = stream.toByteArray();
processor.processRecord(record, 0, record.length, true);
// Check cells of HELLO (positions 1..5) have fg == 0xF6
for (int i = 1; i <= 5; i++) {
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " should retain yellow foreground across SFE");
}
}
@Test
public void testCharacterAttributesResetOnErase() throws java.io.IOException {
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
stream.write(CMD_EW);
stream.write(0xC3);
stream.write(ORDER_SA);
stream.write(XA_FOREGROUND);
stream.write(0xF6);
stream.write(translator.stringToEbcdic("A"));
processor.processRecord(stream.toByteArray(), 0, stream.size(), true);
assertEquals((byte) 0xF6, screen.getCell(0).fg);
// New EW command resets attributes
java.io.ByteArrayOutputStream ewStream = new java.io.ByteArrayOutputStream();
ewStream.write(CMD_EW);
ewStream.write(0xC3);
ewStream.write(translator.stringToEbcdic("B"));
processor.processRecord(ewStream.toByteArray(), 0, ewStream.size(), true);
assertEquals((byte) 0, screen.getCell(0).fg);
}
@Test
public void testModifyFieldAdvancesBufferAddressAndPreservesFieldAttributes() throws java.io.IOException {
// Step 1: Initialize screen with an Erase/Write defining two fields
java.io.ByteArrayOutputStream initStream = new java.io.ByteArrayOutputStream();
initStream.write(CMD_EW);
initStream.write(0xC3);
// Field 1 at pos 0: SFE with FA_PRINTABLE, text "ABC"
initStream.write(ORDER_SBA);
initStream.write(0x40); initStream.write(0x40); // pos 0
initStream.write(ORDER_SFE);
initStream.write(0x01); // 1 pair
initStream.write(XA_3270);
initStream.write(FA_PRINTABLE);
initStream.write(translator.stringToEbcdic("ABC"));
// Field 2 at pos 10: SFE with FA_PRINTABLE, text "XYZ"
initStream.write(ORDER_SBA);
byte[] addr10 = encodeAddress(10, screen.getRows(), screen.getCols());
initStream.write(addr10);
initStream.write(ORDER_SFE);
initStream.write(0x01);
initStream.write(XA_3270);
initStream.write(FA_PRINTABLE);
initStream.write(translator.stringToEbcdic("XYZ"));
processor.processRecord(initStream.toByteArray(), 0, initStream.size(), true);
assertTrue(screen.getCell(0).isFieldAttribute());
assertEquals('A', screen.getCell(1).ucs4);
assertTrue(screen.getCell(10).isFieldAttribute());
assertEquals('X', screen.getCell(11).ucs4);
// Step 2: Issue a Write command that uses ORDER_MF on Field 1 and Field 2
java.io.ByteArrayOutputStream updateStream = new java.io.ByteArrayOutputStream();
updateStream.write(CMD_W);
updateStream.write(0xC1);
// SBA to pos 0
updateStream.write(ORDER_SBA);
updateStream.write(0x40); updateStream.write(0x40);
// MF on pos 0: set foreground to green (0xF4)
updateStream.write(ORDER_MF);
updateStream.write(0x01); // 1 pair
updateStream.write(XA_FOREGROUND);
updateStream.write(0xF4);
// Data "123" following MF directly
updateStream.write(translator.stringToEbcdic("123"));
// MF on pos 10
updateStream.write(ORDER_SBA);
updateStream.write(addr10);
updateStream.write(ORDER_MF);
updateStream.write(0x01);
updateStream.write(XA_FOREGROUND);
updateStream.write(0xF5); // turquoise
updateStream.write(translator.stringToEbcdic("789"));
processor.processRecord(updateStream.toByteArray(), 0, updateStream.size(), false);
// Verify Field 1: pos 0 MUST still be a field attribute, NOT overwritten by '1'
assertTrue(screen.getCell(0).isFieldAttribute(), "Position 0 must remain a field attribute");
assertEquals((byte) 0xF4, screen.getCell(0).fg, "Position 0 field attribute must have updated fg");
assertEquals('1', screen.getCell(1).ucs4, "Data must start at pos 1, not overwrite pos 0");
assertEquals('2', screen.getCell(2).ucs4);
assertEquals('3', screen.getCell(3).ucs4);
// Verify Field 2: pos 10 MUST still be a field attribute, NOT overwritten by '7'
assertTrue(screen.getCell(10).isFieldAttribute(), "Position 10 must remain a field attribute");
assertEquals((byte) 0xF5, screen.getCell(10).fg, "Position 10 field attribute must have updated fg");
assertEquals('7', screen.getCell(11).ucs4, "Data must start at pos 11, not overwrite pos 10");
assertEquals('8', screen.getCell(12).ucs4);
assertEquals('9', screen.getCell(13).ucs4);
}
} }
@@ -0,0 +1,154 @@
package haus.nightmare.lib3270j.datastream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for high-throughput zero-allocation buffer primitives.
*/
public class FastByteBufferTest {
@BeforeEach
public void setUp() {
ReusableByteBufferPool.clear();
}
@Test
@DisplayName("FastByteBuffer operations: write, grow, reset, slice, and direct array access")
public void testFastByteBufferBasicOperations() {
FastByteBuffer buf = new FastByteBuffer(16);
assertEquals(0, buf.size());
assertTrue(buf.buffer().length >= 16);
buf.write(0x11);
buf.write(0x22);
assertEquals(2, buf.size());
assertEquals((byte) 0x11, buf.buffer()[0]);
assertEquals((byte) 0x22, buf.buffer()[1]);
byte[] payload = new byte[]{0x01, 0x02, 0x03, 0x04, 0x05};
buf.write(payload, 0, payload.length);
assertEquals(7, buf.size());
buf.write(payload, 1, 3); // write 0x02, 0x03, 0x04
assertEquals(10, buf.size());
// Test auto-growth
byte[] largeData = new byte[100];
for (int i = 0; i < largeData.length; i++) {
largeData[i] = (byte) (i & 0xFF);
}
buf.write(largeData, 0, largeData.length);
assertEquals(110, buf.size());
assertTrue(buf.buffer().length >= 110);
// Verify direct array access
byte[] raw = buf.buffer();
assertNotNull(raw);
assertEquals((byte) 0x11, raw[0]);
assertEquals((byte) 0x22, raw[1]);
// Verify ByteBuffer view
ByteBuffer readOnly = buf.asByteBuffer();
assertEquals(110, readOnly.remaining());
assertEquals((byte) 0x11, readOnly.get());
// Verify ByteBuffer slice
ByteBuffer slice = buf.slice(2, 5);
assertEquals(5, slice.remaining());
assertEquals((byte) 0x01, slice.get(0));
assertEquals((byte) 0x05, slice.get(4));
// Verify copied array
byte[] copied = buf.toByteArray();
assertEquals(110, copied.length);
assertEquals((byte) 0x11, copied[0]);
// Test reset
buf.reset();
assertEquals(0, buf.size());
// Buffer retained for zero-allocation reuse
assertTrue(buf.buffer().length >= 110);
}
@Test
@DisplayName("ReusableByteBufferPool acquires, releases, and recycles tiered buffers")
public void testByteBufferPoolRecycling() {
byte[] small1 = ReusableByteBufferPool.acquire(256);
assertEquals(ReusableByteBufferPool.SIZE_SMALL, small1.length);
byte[] medium1 = ReusableByteBufferPool.acquire(ReusableByteBufferPool.SIZE_MEDIUM);
assertEquals(ReusableByteBufferPool.SIZE_MEDIUM, medium1.length);
byte[] large1 = ReusableByteBufferPool.acquire(ReusableByteBufferPool.SIZE_LARGE);
assertEquals(ReusableByteBufferPool.SIZE_LARGE, large1.length);
// Non-standard oversized buffer
byte[] huge = ReusableByteBufferPool.acquire(65536);
assertEquals(65536, huge.length);
// Release back to pool
ReusableByteBufferPool.release(small1);
ReusableByteBufferPool.release(medium1);
ReusableByteBufferPool.release(large1);
ReusableByteBufferPool.release(huge);
// Next acquire should reuse the released instances
byte[] small2 = ReusableByteBufferPool.acquire(128);
assertSame(small1, small2, "Small buffer should be recycled from pool");
byte[] medium2 = ReusableByteBufferPool.acquire(2048);
assertSame(medium1, medium2, "Medium buffer should be recycled from pool");
byte[] large2 = ReusableByteBufferPool.acquire(30000);
assertSame(large1, large2, "Large buffer should be recycled from pool");
ByteBuffer bb = ReusableByteBufferPool.acquireByteBuffer(ReusableByteBufferPool.SIZE_SMALL);
assertNotNull(bb);
assertEquals(ReusableByteBufferPool.SIZE_SMALL, bb.capacity());
}
@Test
@DisplayName("ReusableByteBufferPool is safe under concurrent acquisition and release")
public void testConcurrentPoolAccess() throws InterruptedException {
int threads = 8;
int iterations = 1000;
ExecutorService executor = Executors.newFixedThreadPool(threads);
CountDownLatch latch = new CountDownLatch(threads);
AtomicInteger failures = new AtomicInteger(0);
for (int t = 0; t < threads; t++) {
executor.submit(() -> {
try {
for (int i = 0; i < iterations; i++) {
byte[] buf = ReusableByteBufferPool.acquire(ReusableByteBufferPool.SIZE_SMALL);
if (buf == null || buf.length != ReusableByteBufferPool.SIZE_SMALL) {
failures.incrementAndGet();
}
buf[0] = (byte) 0xAA;
buf[1] = (byte) 0xBB;
ReusableByteBufferPool.release(buf);
}
} catch (Exception e) {
failures.incrementAndGet();
} finally {
latch.countDown();
}
});
}
assertTrue(latch.await(5, TimeUnit.SECONDS));
executor.shutdown();
assertEquals(0, failures.get(), "No failures occurred during concurrent pool operations");
}
}
@@ -0,0 +1,141 @@
package haus.nightmare.lib3270j.epi;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class EpiDataStream3270Test {
private Screen screen;
private DataStream3270 dataStream;
@BeforeEach
public void setUp() {
screen = new Screen(80, 24);
dataStream = new DataStream3270(screen);
}
@Test
public void testEncodeAndDecodeAddress() {
int[] testAddresses = {0, 1, 79, 80, 1919, 2000, 4095};
byte[] target = new byte[2];
for (int addr : testAddresses) {
dataStream.encodeAddress(target, 0, addr);
int decoded = dataStream.decodeAddress(target[0], target[1]);
assertEquals(addr, decoded, "Address round-trip mismatch for " + addr);
}
// Test invalid decode
assertEquals(-1, dataStream.decodeAddress((byte) 0x00, (byte) 0x00));
assertEquals(-1, dataStream.decodeAddress((byte) 0xFF, (byte) 0xFF));
}
@Test
public void testCharacterTranslations() {
char ebcdicChar = dataStream.toEbcdic('A'); // ASCII 65 -> EBCDIC 0xC1
assertEquals('\u00c1', ebcdicChar);
byte asciiByte = dataStream.toAscii(ebcdicChar); // EBCDIC 0xC1 -> ASCII 65
assertEquals('A', (char) asciiByte);
// Boundary cases
char low = dataStream.toEbcdic(0x10);
assertEquals((char) 0x10, low);
}
@Test
public void testAnalyzeWriteAndFormat() throws Exception {
// Build 3270 Write buffer:
// CMD_W (49 / 0x31), WCC (0xC3), SBA (17), Addr(0, 0), SF (29), Attr (0xC1 = MDT set), Text "TEST"
byte[] stream = new byte[11];
stream[0] = 49; // Write
stream[1] = (byte) 0xC3; // WCC
stream[2] = 17; // SBA
dataStream.encodeAddress(stream, 3, 0);
stream[5] = 29; // SF
stream[6] = (byte) 0xC1; // Attribute (MDT=1)
stream[7] = (byte) 'T';
stream[8] = (byte) 'E';
stream[9] = (byte) 'S';
stream[10] = (byte) 'T';
dataStream.analyze(stream, stream.length);
assertEquals(1, screen.fieldCount());
Field f = screen.field(1);
assertNotNull(f);
assertTrue(f.hasAttribute());
assertEquals(1, f.dataTag());
// Now format outbound buffer
screen.setAID(AID.enter);
screen.setCursor(1, 1);
byte[] outBuf = new byte[100];
int outLen = dataStream.format(outBuf);
assertTrue(outLen > 0);
assertEquals(AID.enter.translate(), outBuf[0]);
// Cursor addr at [1, 2]
// SBA (17) at [3]
assertEquals(17, outBuf[3]);
// Data text starting at [6]
assertEquals((byte) 'T', outBuf[6]);
assertEquals((byte) 'E', outBuf[7]);
assertEquals((byte) 'S', outBuf[8]);
assertEquals((byte) 'T', outBuf[9]);
}
@Test
public void testReadBufferSerialization() throws Exception {
// Erase/Write (53), WCC (0), SBA (17), Addr(0), SF (29), Attr(0xC0), Text "OK"
byte[] stream = new byte[9];
stream[0] = 53; // Erase / Write
stream[1] = 0; // WCC
stream[2] = 17; // SBA
dataStream.encodeAddress(stream, 3, 0);
stream[5] = 29; // SF
stream[6] = (byte) 0xC0; // Unmodified attribute
stream[7] = (byte) 'O';
stream[8] = (byte) 'K';
dataStream.analyze(stream, stream.length);
assertEquals(1, screen.fieldCount());
byte[] outBuf = new byte[100];
int len = dataStream.readBuffer(outBuf);
assertTrue(len >= 8);
assertEquals(screen.getAID().translate(), outBuf[0]);
assertEquals(17, outBuf[3]); // SBA
assertEquals(29, outBuf[6]); // SF
}
@Test
public void testEpiScreenBufferBridge() {
screen.setWidth(80);
screen.setDepth(24);
screen.setCursor(2, 5);
Field f = new Field(screen, 80);
f.setAttribute(true);
f.setBaseAttribute((char) 0xC8);
f.setBytes(0, new byte[]{(byte) 0xC1, (byte) 0xC2}, 2); // 'A', 'B'
screen.insertField(f);
ScreenBuffer sb = new ScreenBuffer();
EpiScreenBufferBridge.copyToScreenBuffer(screen, sb);
assertEquals(80, sb.getCols());
assertEquals(24, sb.getRows());
assertEquals(84, sb.getCursorAddress()); // row 2 (index 1) * 80 + col 5 (index 4) = 84
// Reverse copy
Screen backScreen = new Screen();
EpiScreenBufferBridge.copyFromScreenBuffer(sb, backScreen);
assertEquals(80, backScreen.getWidth());
assertEquals(24, backScreen.getDepth());
assertEquals(2, backScreen.getCursorRow());
assertEquals(5, backScreen.getCursorColumn());
}
}
@@ -285,6 +285,79 @@ public class GocaDecoderTest {
assertTrue(plane.hasContent(), "Expected plane to have content after image decoding"); assertTrue(plane.hasContent(), "Expected plane to have content after image decoding");
} }
@Test
public void testImonGddmImageRendering() throws Exception {
GraphicsPlane plane = new GraphicsPlane(1040, 1118);
plane.setCharDimensions(13, 26);
plane.setScreenDimensions(80, 43);
GocaDecoder decoder = new GocaDecoder(plane);
assertEquals(1040, plane.getTotalWidth());
assertEquals(1118, plane.getTotalHeight());
assertEquals(520, plane.getXMax());
assertEquals(558, plane.getYMax());
ByteArrayOutputStream out = new ByteArrayOutputStream();
// GSCP: Set Current Position (377, 494)
out.write(GocaConstants.G_GSCP);
out.write(0x04);
out.write(0x01); out.write(0x79); // X = 377
out.write(0x01); out.write(0xEE); // Y = 494
// GSCOL: Color 1 = Blue
out.write(GocaConstants.G_GSCOL);
out.write(0x01);
// G_GBIMGC (0x91): Begin Image Current Position: len=6, flags=0, w=80, h=80
out.write(GocaConstants.G_GBIMGC);
out.write(0x06);
out.write(0x00); out.write(0x00); // flags/format
out.write(0x00); out.write(0x50); // w = 80
out.write(0x00); out.write(0x50); // h = 80
// G_GIMD (0x92): 10 bytes scanline
out.write(GocaConstants.G_GIMD);
out.write(0x0A);
out.write(new byte[] { (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF });
// G_GEIMG (0x93): End Image with 2-byte operand (02 00 00 as sent by IMON)
out.write(GocaConstants.G_GEIMG);
out.write(0x02);
out.write(0x00); out.write(0x00);
// Subsequent order to verify stream synchronization: GSCOL Color 2 = Red
out.write(GocaConstants.G_GSCOL);
out.write(0x02);
// G_GBIMGC (0x91): Second image tile
out.write(GocaConstants.G_GBIMGC);
out.write(0x06);
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x50);
out.write(0x00); out.write(0x50);
out.write(GocaConstants.G_GIMD);
out.write(0x0A);
out.write(new byte[] { (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA });
out.write(GocaConstants.G_GEIMG);
out.write(0x02);
out.write(0x00); out.write(0x00);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent(), "Expected plane to have content after IMON GOCA decoding");
int mappedX = plane.mapX(377);
int mappedY = plane.mapY(494);
assertEquals(897, mappedX);
assertEquals(64, mappedY);
int[] rgb = plane.getRgbBuffer();
int pixelAtImg = rgb[mappedY * 1040 + mappedX];
assertNotEquals(0, pixelAtImg, "Pixel at image location should be rendered");
}
@Test @Test
public void testDirectObjectControlSf24() { public void testDirectObjectControlSf24() {
haus.nightmare.lib3270j.screen.ScreenBuffer sb = new haus.nightmare.lib3270j.screen.ScreenBuffer( haus.nightmare.lib3270j.screen.ScreenBuffer sb = new haus.nightmare.lib3270j.screen.ScreenBuffer(
@@ -0,0 +1,144 @@
package haus.nightmare.lib3270j.input;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLPS;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for the "Block paste" feature.
* Ensures that multi-line pastes maintain column alignment with the initial start column
* when blockPaste is active (the default), and revert to standard 3270 Newline field advance
* when blockPaste is disabled.
*/
public class BlockPasteTest {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
private ECLPS ps;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
ps = new ECLPS(screen, input, translator);
}
@Test
public void testBlockPasteDefaultsToTrue() {
assertTrue(input.isBlockPaste(), "InputProcessor blockPaste should default to true");
assertTrue(ps.isBlockPaste(), "ECLPS blockPaste should default to true");
}
@Test
public void testBlockPasteAlignsStartColumnInUnformattedScreen() {
screen.erase(false);
// Start cursor at row 0, column 15
int startPos = screen.rowColToAddress(0, 15);
screen.setCursorAddress(startPos);
String multilineText = "FIRST\nSECOND\nTHIRD";
int pasted = input.pasteText(multilineText, true, false, true);
assertEquals(16, pasted);
// Verify row 0, col 15-19 has "FIRST"
assertEquals("FIRST", ps.getString(startPos, 5));
// Verify row 1, col 15-20 has "SECOND"
int row1Col15 = screen.rowColToAddress(1, 15);
assertEquals("SECOND", ps.getString(row1Col15, 6));
// Verify row 2, col 15-19 has "THIRD"
int row2Col15 = screen.rowColToAddress(2, 15);
assertEquals("THIRD", ps.getString(row2Col15, 5));
// Verify start of row 1 (cols 0-5) did NOT receive the paste
int row1Col0 = screen.rowColToAddress(1, 0);
assertNotEquals("SECOND", ps.getString(row1Col0, 6));
}
@Test
public void testBlockPasteAlignsStartColumnInFormattedScreen() {
screen.erase(false);
// Set up 3 rows each with an unprotected field starting at col 0 (FA at 0, 80, 160)
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(80, (byte) FA_PRINTABLE);
screen.setFieldAttribute(160, (byte) FA_PRINTABLE);
// Start pasting at Row 0, Col 25
int startPos = screen.rowColToAddress(0, 25);
screen.setCursorAddress(startPos);
String multilineText = "ALPHA\r\nBETA\r\nGAMMA";
int pasted = input.pasteText(multilineText, true, false, true);
assertEquals(14, pasted);
assertEquals("ALPHA", ps.getString(screen.rowColToAddress(0, 25), 5));
assertEquals("BETA", ps.getString(screen.rowColToAddress(1, 25), 4));
assertEquals("GAMMA", ps.getString(screen.rowColToAddress(2, 25), 5));
}
@Test
public void testDisabledBlockPasteRevertsToFirstUnprotectedField() {
screen.erase(false);
// Row 0: FA at 0 (unprotected col 1-79)
// Row 1: FA at 80 (unprotected col 1-79)
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(80, (byte) FA_PRINTABLE);
// Start pasting at Row 0, Col 25
int startPos = screen.rowColToAddress(0, 25);
screen.setCursorAddress(startPos);
// blockPaste = false: newline should advance to first unprotected field on next row (pos 81 / col 1)
String multilineText = "LINE1\nLINE2";
input.pasteText(multilineText, true, false, false);
assertEquals("LINE1", ps.getString(screen.rowColToAddress(0, 25), 5));
// LINE2 must be at row 1, col 1 (pos 81) rather than col 25
assertEquals("LINE2", ps.getString(81, 5));
}
@Test
public void testBlockPasteStopAtProtectedBoundary() {
screen.erase(false);
// Row 0: unprotected field at pos 0
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
// Row 1: entirely protected row
screen.setFieldAttribute(80, (byte) (FA_PRINTABLE | FA_PROTECT));
int startPos = screen.rowColToAddress(0, 10);
screen.setCursorAddress(startPos);
// pasteStopAtProtectedLine = true
String multilineText = "ROW0\nROW1";
int pasted = input.pasteText(multilineText, true, true, true);
// Should paste "ROW0" (4 chars) and halt when row 1 is protected
assertEquals(4, pasted);
assertEquals("ROW0", ps.getString(startPos, 4));
}
@Test
public void testEclPsPasteStringHonorsBlockPaste() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(80, (byte) FA_PRINTABLE);
ps.setBlockPaste(true);
assertTrue(ps.isBlockPaste());
int pasted = ps.pasteString("DATA1\nDATA2", 0, 12);
assertEquals(10, pasted);
assertEquals("DATA1", ps.getString(screen.rowColToAddress(0, 12), 5));
assertEquals("DATA2", ps.getString(screen.rowColToAddress(1, 12), 5));
}
}
@@ -84,6 +84,41 @@ public class TelnetFSMTest {
ConnectionConfig c4 = ConnectionConfig.parseHostString("non-e:vm.ibm.com", 23, TerminalModel.IBM_3279_4); ConnectionConfig c4 = ConnectionConfig.parseHostString("non-e:vm.ibm.com", 23, TerminalModel.IBM_3279_4);
assertFalse(c4.isTn3270eEnabled()); assertFalse(c4.isTn3270eEnabled());
assertEquals("vm.ibm.com", c4.getHost()); assertEquals("vm.ibm.com", c4.getHost());
ConnectionConfig c5 = ConnectionConfig.parseHostString("00C2@mvs.hugfreevikings.wtf:1023", 23, TerminalModel.IBM_3279_4);
assertEquals("mvs.hugfreevikings.wtf", c5.getHost());
assertEquals(1023, c5.getPort());
assertEquals("00C2", c5.getLuName());
ConnectionConfig c6 = ConnectionConfig.parseHostString("L:TSO01@secure.mvs.com:992", 23, TerminalModel.IBM_3279_4);
assertTrue(c6.isUseTls());
assertEquals("secure.mvs.com", c6.getHost());
assertEquals(992, c6.getPort());
assertEquals("TSO01", c6.getLuName());
}
@Test
public void testPlainTn3270TTypeWithLuName() {
config.setTn3270eEnabled(false);
config.setLuName("00C2");
fsm.onConnected();
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TTYPE);
// Host sends SB TTYPE SEND
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
TelnetConstants.TELQUAL_SEND,
TelnetConstants.IAC, TelnetConstants.SE);
boolean foundTtypeLu = false;
for (byte[] pkt : connection.sentData) {
String s = new String(pkt, java.nio.charset.StandardCharsets.ISO_8859_1);
if (s.contains("IBM-3279-4-E@00C2")) {
foundTtypeLu = true;
break;
}
}
assertTrue(foundTtypeLu, "Plain TN3270 TTYPE IS must append @<luName> when configured");
assertEquals("00C2", fsm.getConnectedLu());
} }
@Test @Test