From 64d713a283b7a355ebdc96ceb9ee054eb3bd291d Mon Sep 17 00:00:00 2001 From: Rudi Date: Tue, 14 Apr 2026 09:18:03 -0400 Subject: [PATCH] Initial Commit --- .gitignore | 2 + .gradle/8.5/checksums/checksums.lock | Bin 0 -> 17 bytes .gradle/8.5/checksums/md5-checksums.bin | Bin 0 -> 19397 bytes .gradle/8.5/checksums/sha1-checksums.bin | Bin 0 -> 23033 bytes .../dependencies-accessors.lock | Bin 0 -> 17 bytes .../8.5/dependencies-accessors/gc.properties | 0 .gradle/8.5/fileChanges/last-build.bin | Bin 0 -> 1 bytes .gradle/8.5/fileHashes/fileHashes.lock | Bin 0 -> 17 bytes .gradle/8.5/gc.properties | 0 .gradle/9.2.0/checksums/checksums.lock | Bin 0 -> 17 bytes .gradle/9.2.0/checksums/md5-checksums.bin | Bin 0 -> 19397 bytes .gradle/9.2.0/checksums/sha1-checksums.bin | Bin 0 -> 21089 bytes .gradle/9.2.0/fileChanges/last-build.bin | Bin 0 -> 1 bytes .gradle/9.2.0/fileHashes/fileHashes.bin | Bin 0 -> 18547 bytes .gradle/9.2.0/fileHashes/fileHashes.lock | Bin 0 -> 17 bytes .gradle/9.2.0/gc.properties | 0 .../buildOutputCleanup.lock | Bin 0 -> 17 bytes .gradle/buildOutputCleanup/cache.properties | 2 + .gradle/vcs-1/gc.properties | 0 LICENSE | 24 + README.md | 33 + build.gradle | 29 + build/MANIFEST.MF | 2 + build/j3270/org/pubvm/j3270/J3270App$1.class | Bin 0 -> 960 bytes build/j3270/org/pubvm/j3270/J3270App$2.class | Bin 0 -> 878 bytes build/j3270/org/pubvm/j3270/J3270App.class | Bin 0 -> 15945 bytes .../config/Settings$StartupBehavior.class | Bin 0 -> 1286 bytes .../org/pubvm/j3270/config/Settings.class | Bin 0 -> 7046 bytes .../org/pubvm/j3270/ui/ConnectDialog.class | Bin 0 -> 5795 bytes .../org/pubvm/j3270/ui/SettingsDialog$1.class | Bin 0 -> 1599 bytes .../org/pubvm/j3270/ui/SettingsDialog$2.class | Bin 0 -> 1802 bytes .../org/pubvm/j3270/ui/SettingsDialog$3.class | Bin 0 -> 665 bytes .../org/pubvm/j3270/ui/SettingsDialog$4.class | Bin 0 -> 1355 bytes .../org/pubvm/j3270/ui/SettingsDialog.class | Bin 0 -> 16457 bytes .../org/pubvm/j3270/ui/StatusBar$1.class | Bin 0 -> 1152 bytes .../j3270/org/pubvm/j3270/ui/StatusBar.class | Bin 0 -> 4176 bytes .../org/pubvm/j3270/ui/TerminalPanel$1.class | Bin 0 -> 1362 bytes .../org/pubvm/j3270/ui/TerminalPanel$2.class | Bin 0 -> 781 bytes .../org/pubvm/j3270/ui/TerminalPanel.class | Bin 0 -> 16529 bytes .../org/lib3270j/ConnectionConfig.class | Bin 0 -> 2493 bytes .../org/lib3270j/ConnectionState.class | Bin 0 -> 2509 bytes .../org/lib3270j/DiagnosticClient$1.class | Bin 0 -> 1559 bytes .../org/lib3270j/DiagnosticClient$2.class | Bin 0 -> 1612 bytes .../org/lib3270j/DiagnosticClient.class | Bin 0 -> 5432 bytes .../org/lib3270j/Telnet3270Client.class | Bin 0 -> 6272 bytes .../lib3270j/org/lib3270j/TerminalModel.class | Bin 0 -> 3598 bytes .../lib3270j/charset/EbcdicTranslator.class | Bin 0 -> 3139 bytes .../DataStreamProcessor$OutputSender.class | Bin 0 -> 375 bytes .../datastream/DataStreamProcessor.class | Bin 0 -> 13935 bytes .../datastream/QueryReplyBuilder.class | Bin 0 -> 3772 bytes .../org/lib3270j/input/InputProcessor.class | Bin 0 -> 5610 bytes .../listener/ConnectionListener.class | Bin 0 -> 420 bytes .../listener/ScreenUpdateListener.class | Bin 0 -> 378 bytes .../lib3270j/protocol/DS3270Constants.class | Bin 0 -> 9902 bytes .../lib3270j/protocol/TN3270EConstants.class | Bin 0 -> 4119 bytes .../lib3270j/protocol/TelnetConstants.class | Bin 0 -> 3231 bytes .../lib3270j/screen/ExtendedAttribute.class | Bin 0 -> 1298 bytes .../org/lib3270j/screen/ScreenBuffer.class | Bin 0 -> 6327 bytes .../lib3270j/telnet/TelnetConnection.class | Bin 0 -> 5634 bytes .../telnet/TelnetFSM$TN3270ESubmode.class | Bin 0 -> 1287 bytes .../org/lib3270j/telnet/TelnetFSM.class | Bin 0 -> 17208 bytes gradle/wrapper/gradle-wrapper.properties | 7 + j3270.ini.sample | 64 ++ j3270/build.gradle | 11 + .../main/java/org/pubvm/j3270/J3270App.java | 442 ++++++++ .../java/org/pubvm/j3270/config/Settings.java | 220 ++++ .../org/pubvm/j3270/ui/ConnectDialog.java | 181 ++++ .../org/pubvm/j3270/ui/SettingsDialog.java | 490 +++++++++ .../java/org/pubvm/j3270/ui/StatusBar.java | 140 +++ .../org/pubvm/j3270/ui/TerminalPanel.java | 581 +++++++++++ lib3270j/build.gradle | 13 + .../java/org/lib3270j/ConnectionConfig.java | 67 ++ .../java/org/lib3270j/ConnectionState.java | 62 ++ .../java/org/lib3270j/DiagnosticClient.java | 174 ++++ .../java/org/lib3270j/Telnet3270Client.java | 192 ++++ .../main/java/org/lib3270j/TerminalModel.java | 75 ++ .../lib3270j/charset/EbcdicTranslator.java | 128 +++ .../datastream/DataStreamProcessor.java | 813 +++++++++++++++ .../datastream/QueryReplyBuilder.java | 215 ++++ .../org/lib3270j/input/InputProcessor.java | 322 ++++++ .../lib3270j/listener/ConnectionListener.java | 17 + .../listener/ScreenUpdateListener.java | 18 + .../lib3270j/protocol/DS3270Constants.java | 376 +++++++ .../lib3270j/protocol/TN3270EConstants.java | 142 +++ .../lib3270j/protocol/TelnetConstants.java | 117 +++ .../lib3270j/screen/ExtendedAttribute.java | 77 ++ .../org/lib3270j/screen/ScreenBuffer.java | 329 ++++++ .../org/lib3270j/telnet/TelnetConnection.java | 151 +++ .../java/org/lib3270j/telnet/TelnetFSM.java | 954 ++++++++++++++++++ run.sh | 44 + settings.gradle | 4 + 91 files changed, 6518 insertions(+) create mode 100644 .gitignore create mode 100644 .gradle/8.5/checksums/checksums.lock create mode 100644 .gradle/8.5/checksums/md5-checksums.bin create mode 100644 .gradle/8.5/checksums/sha1-checksums.bin create mode 100644 .gradle/8.5/dependencies-accessors/dependencies-accessors.lock create mode 100644 .gradle/8.5/dependencies-accessors/gc.properties create mode 100644 .gradle/8.5/fileChanges/last-build.bin create mode 100644 .gradle/8.5/fileHashes/fileHashes.lock create mode 100644 .gradle/8.5/gc.properties create mode 100644 .gradle/9.2.0/checksums/checksums.lock create mode 100644 .gradle/9.2.0/checksums/md5-checksums.bin create mode 100644 .gradle/9.2.0/checksums/sha1-checksums.bin create mode 100644 .gradle/9.2.0/fileChanges/last-build.bin create mode 100644 .gradle/9.2.0/fileHashes/fileHashes.bin create mode 100644 .gradle/9.2.0/fileHashes/fileHashes.lock create mode 100644 .gradle/9.2.0/gc.properties create mode 100644 .gradle/buildOutputCleanup/buildOutputCleanup.lock create mode 100644 .gradle/buildOutputCleanup/cache.properties create mode 100644 .gradle/vcs-1/gc.properties create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build.gradle create mode 100644 build/MANIFEST.MF create mode 100644 build/j3270/org/pubvm/j3270/J3270App$1.class create mode 100644 build/j3270/org/pubvm/j3270/J3270App$2.class create mode 100644 build/j3270/org/pubvm/j3270/J3270App.class create mode 100644 build/j3270/org/pubvm/j3270/config/Settings$StartupBehavior.class create mode 100644 build/j3270/org/pubvm/j3270/config/Settings.class create mode 100644 build/j3270/org/pubvm/j3270/ui/ConnectDialog.class create mode 100644 build/j3270/org/pubvm/j3270/ui/SettingsDialog$1.class create mode 100644 build/j3270/org/pubvm/j3270/ui/SettingsDialog$2.class create mode 100644 build/j3270/org/pubvm/j3270/ui/SettingsDialog$3.class create mode 100644 build/j3270/org/pubvm/j3270/ui/SettingsDialog$4.class create mode 100644 build/j3270/org/pubvm/j3270/ui/SettingsDialog.class create mode 100644 build/j3270/org/pubvm/j3270/ui/StatusBar$1.class create mode 100644 build/j3270/org/pubvm/j3270/ui/StatusBar.class create mode 100644 build/j3270/org/pubvm/j3270/ui/TerminalPanel$1.class create mode 100644 build/j3270/org/pubvm/j3270/ui/TerminalPanel$2.class create mode 100644 build/j3270/org/pubvm/j3270/ui/TerminalPanel.class create mode 100644 build/lib3270j/org/lib3270j/ConnectionConfig.class create mode 100644 build/lib3270j/org/lib3270j/ConnectionState.class create mode 100644 build/lib3270j/org/lib3270j/DiagnosticClient$1.class create mode 100644 build/lib3270j/org/lib3270j/DiagnosticClient$2.class create mode 100644 build/lib3270j/org/lib3270j/DiagnosticClient.class create mode 100644 build/lib3270j/org/lib3270j/Telnet3270Client.class create mode 100644 build/lib3270j/org/lib3270j/TerminalModel.class create mode 100644 build/lib3270j/org/lib3270j/charset/EbcdicTranslator.class create mode 100644 build/lib3270j/org/lib3270j/datastream/DataStreamProcessor$OutputSender.class create mode 100644 build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class create mode 100644 build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class create mode 100644 build/lib3270j/org/lib3270j/input/InputProcessor.class create mode 100644 build/lib3270j/org/lib3270j/listener/ConnectionListener.class create mode 100644 build/lib3270j/org/lib3270j/listener/ScreenUpdateListener.class create mode 100644 build/lib3270j/org/lib3270j/protocol/DS3270Constants.class create mode 100644 build/lib3270j/org/lib3270j/protocol/TN3270EConstants.class create mode 100644 build/lib3270j/org/lib3270j/protocol/TelnetConstants.class create mode 100644 build/lib3270j/org/lib3270j/screen/ExtendedAttribute.class create mode 100644 build/lib3270j/org/lib3270j/screen/ScreenBuffer.class create mode 100644 build/lib3270j/org/lib3270j/telnet/TelnetConnection.class create mode 100644 build/lib3270j/org/lib3270j/telnet/TelnetFSM$TN3270ESubmode.class create mode 100644 build/lib3270j/org/lib3270j/telnet/TelnetFSM.class create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 j3270.ini.sample create mode 100644 j3270/build.gradle create mode 100644 j3270/src/main/java/org/pubvm/j3270/J3270App.java create mode 100644 j3270/src/main/java/org/pubvm/j3270/config/Settings.java create mode 100644 j3270/src/main/java/org/pubvm/j3270/ui/ConnectDialog.java create mode 100644 j3270/src/main/java/org/pubvm/j3270/ui/SettingsDialog.java create mode 100644 j3270/src/main/java/org/pubvm/j3270/ui/StatusBar.java create mode 100644 j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java create mode 100644 lib3270j/build.gradle create mode 100644 lib3270j/src/main/java/org/lib3270j/ConnectionConfig.java create mode 100644 lib3270j/src/main/java/org/lib3270j/ConnectionState.java create mode 100644 lib3270j/src/main/java/org/lib3270j/DiagnosticClient.java create mode 100644 lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java create mode 100644 lib3270j/src/main/java/org/lib3270j/TerminalModel.java create mode 100644 lib3270j/src/main/java/org/lib3270j/charset/EbcdicTranslator.java create mode 100644 lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java create mode 100644 lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java create mode 100644 lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java create mode 100644 lib3270j/src/main/java/org/lib3270j/listener/ConnectionListener.java create mode 100644 lib3270j/src/main/java/org/lib3270j/listener/ScreenUpdateListener.java create mode 100644 lib3270j/src/main/java/org/lib3270j/protocol/DS3270Constants.java create mode 100644 lib3270j/src/main/java/org/lib3270j/protocol/TN3270EConstants.java create mode 100644 lib3270j/src/main/java/org/lib3270j/protocol/TelnetConstants.java create mode 100644 lib3270j/src/main/java/org/lib3270j/screen/ExtendedAttribute.java create mode 100644 lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java create mode 100644 lib3270j/src/main/java/org/lib3270j/telnet/TelnetConnection.java create mode 100644 lib3270j/src/main/java/org/lib3270j/telnet/TelnetFSM.java create mode 100755 run.sh create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ea0a7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +*.jar diff --git a/.gradle/8.5/checksums/checksums.lock b/.gradle/8.5/checksums/checksums.lock new file mode 100644 index 0000000000000000000000000000000000000000..a20b0af561aa33341131d807c8a23a8581de8339 GIT binary patch literal 17 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 k8U_YOo^O#Iw z9ozZR&XwB~bt;{et&un}UpBkUsU*+i^ZBgl=`Y~@+3tCsz3=mWo_k);^V>a76vuhF zbUpIWum>eNcUj_G9oA6z$deSNVb{w&FrPAtE9CI0S`^g~G`S7}&Y zZ1ArfdEld!?x!EiOB9a|)Hr4~lRUbC<%Tc1RgK5j_|pA{v%Kv2aKp9Khc$FRAuKPC zi));-H$qG2Z(zBlb*H1kI&Ln_pRv6D;k%r4&aicV#WVTz;Cezxs_SY{X zPoAa?`hJ0Qv$#=aPdm5FcR!bx?7XP7_KA!%bc4w;oK50}bIn4#SJR?eeM_k&rT?Xi zv@xADWGZn(zH(UNsfke;l3%&;1AY};rj5m<;Uc^((ZfoXXq%CopEhvDwJ)CAD>}Ss zLpWo8xC`$g^k~#dr<Vck51dA*y=yNh>TLP6p%vzHTyOVo=b(&%0b@NT+|c+$9Q>L3jy4{U KhV&-C4*U%!jo}gi literal 0 HcmV?d00001 diff --git a/.gradle/9.2.0/checksums/sha1-checksums.bin b/.gradle/9.2.0/checksums/sha1-checksums.bin new file mode 100644 index 0000000000000000000000000000000000000000..77e51ad39949c19caff06ae48665bd3095a8cf06 GIT binary patch literal 21089 zcmeI4do)!09>BL(9FM%ipoj7}s3wn6>bePe3}bS1D`k4C$g?DrBh)k}PH`lU6Y7MD zM0u1)LQc`4BBN6}Lpe$+QE~Ke_nzPH=iED^wff`!b=PmNWoFiA_Gf?h{_L5lwajKm z5SmJ3a3TI))c<~nAK?PH04{(F-~zY+E`ST*0=NJ!fD7OPxBxDI3*Z8{04{(F-~zb7 z|4RW+@<3$4!DyO_Qx6pP5CmPGd_a%(95s60b2bP(?Usywe{f)CW!U@8FCzhW;UONI z*b$mzTJHk5dj#Tf8{V!pYFtwRxN8;ST;_Kpsa}q$aQokm^FHTt%MT%^0cY++{HLJQ zt>-M1I{|mxgLry{Tb8q!)L6&qJvR5^F^E#Qtz5w9vt&{$_u zPzyNA1My#NWSj0DW*q?BDGu>lmpLl?`+vI(_zDxmA2bc7wX$>z;QDV6=j$uHpu6AI z2HbTB@h*vY+Pt6kT>+eRc$_yo)t0l3UI5P6i}=%2#%N_%i6Pv+7UI3V%y$X2&_KYQ zlMsJ)sZ+#VT_g=)`j*1=ZSzfUNZik8Z=NsHhMP|(30e96#T-w7uZmzMxJir+} z<2)%sQoYoO0oOl-xLom=($XyrjetAfLR{fo1WPM{u@G>VY{b<{&AKH*ut(AJx2j|YeIbfk|%?!SD3Qlb{-&ZC25p? zWdBJ;z#R(^x306{dGCKz3b?x>;&vgeH*DI50{~~~BJSq3c6W`X!wkUP*ob>-&6@nf zefevEI~yXtvLsE9D`pG#hxHC|KhNH&Ya(@{!TO9s#DgtvXKx7JS_il*3-R5L$D~il znWzHJx`sHqCkV-7;5mGY3*Z8{04{(F-~zY+E`ST*0=NJ!fD7OPxBxDI3*Z8{04{(F z-~zY+E`ST*0=NJ!fD7OPxBxDI3*Z8{04{(F-~zY+E`ST*0$)?WoLp%(`J+$$sGZ)C zq+)HLaU1Hy^uB7BH@r$GV4x*%$n_c*PU-%@;WdETGZJk7d3D4 zM#yST-qze;Hhe3yc&BRp{hP@e7MZ@J74FJ$JRy(>srO%Pk!Sg0?w75QpINE9<;mkJvILg0hUgx~=*S)HtZlYh zsN)sgx79@cQ;lgewBT9RBws7SXZeF?dv|45$PoKptX^-5=faIjp0A;GP%ulviu^-*A>J5+EV~xpK|M5Dc9T%PT1&f+h2KI0wYhH@F^E7$R?+*3h8K**(^Y`E2rNA2Z z1eVyz5z$$e$INaT|7?>NZnm!UWvey&kpzwFDva#zfT8j7{b`r)6Bp^lY0X zZTe2tu`!eZtV znWEOUFZ20*uVJRti1<*G54q$`98Uf4`~GFP^*x zMPN~%G6W%EzFgFL`Nw@-ucB;D_mbOANy?hq^rCbQu;7(Kl6{7v7K@&8{f>Rz&&8v* zXP7Yx%T^`rB(E=_W|hQJqX@Bi+eEE2zKUe9F^BW8!D{5V{7&hbT}ANKTWT$^h$+-n zLP8DQ_P*L6JGPpa&els@@whdytnJ6UU{)!-hDw9_BoML;xNB@=l9mqaX497~;FcuDc2S)XPRbDQbHOX|YsKpNpPu=5?Z?@^A+)$HYVSF>!g4{OM@QOep2(5lm zOLw#(B6@mfcmq9WRbuAVzh3y5Z-&+;pvjZ}S_s$JV{RXMSG=Bc;J91b%Jrs!b7H6Z zL5o_OT-4&p8=;2n`{q|Co_#dy5W8aHhQ?b2o)Xk}3jK}_`j4IWdydu@@Vj(5QA z+x=0RNA-$&;iu@8DqyJ=koQ8f?pt5xH%HB&IUSM;v-sx*m(hVg=73r70yK%~)L%^$ zvfhk*PN`b;%yav6wzSHww$-)uBfmjwonV$k1$EwpEG1R%Ep+ja*$Q)$XB>UC{1L&m zeFdzF-2zK0D^Jwg$PCKkZHODpkDtld=38}M?jDy9ttNpbPB2BSS(7IjM%=0HS4()Z zHm&_%rQRC@GJsV{-jmBW4e6p5$4@JY-SWI|;BIHPY0wQ BJ1zhK literal 0 HcmV?d00001 diff --git a/.gradle/9.2.0/fileChanges/last-build.bin b/.gradle/9.2.0/fileChanges/last-build.bin new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/.gradle/9.2.0/fileHashes/fileHashes.bin b/.gradle/9.2.0/fileHashes/fileHashes.bin new file mode 100644 index 0000000000000000000000000000000000000000..7412165b8db753a863f0b4cb7b4af878ff863b75 GIT binary patch literal 18547 zcmeI(A&Npl7y#gT27?G%gjbD%h#(scE(R~Kw=f6>i8^Z(40|NrXD3`0ok*?olPXgpIH0RjXF5FkK+009C72oNAZfB*pk1PBly z@Gb&hv5`vAOxiBxNo^KFw;Ge=XIuT7Z>Hnocr diff --git a/README.md b/README.md new file mode 100644 index 0000000..dfd18c8 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ + +# j3270 + +An x3270 based emulator ported to Java using AI + + +## Acknowledgements + + - [x3270](https://awesomeopensource.com/project/elangosundar/awesome-README-templates) + - [RFC ](https://github.com/matiassingers/awesome-readme) + - [Claude](https://bulldogjob.com/news/449-how-to-write-a-good-readme-for-your-github-project) + - [Antigravity]() + - [Gemini ]() + +## Deployment + +To build your own jar you can read or use run.sh + +```bash + ./run.sh +``` +Or from the jar [with optional config] +```bash +java -jar j3270.jar [-c config.ini] +``` + +## Contributing + +Contributions are welcome! + +I have contributed no code to this project, it was entirely written by LLMs with my guidance only. + + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..df820f0 --- /dev/null +++ b/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'java' +} + +allprojects { + group = 'org.j3270' + version = '0.1.0' + + repositories { + mavenCentral() + } +} + +subprojects { + apply plugin: 'java' + + java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + test { + useJUnitPlatform() + } + + dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + } +} diff --git a/build/MANIFEST.MF b/build/MANIFEST.MF new file mode 100644 index 0000000..3cf97e5 --- /dev/null +++ b/build/MANIFEST.MF @@ -0,0 +1,2 @@ +Main-Class: org.pubvm.j3270.J3270App + diff --git a/build/j3270/org/pubvm/j3270/J3270App$1.class b/build/j3270/org/pubvm/j3270/J3270App$1.class new file mode 100644 index 0000000000000000000000000000000000000000..e8a51ce4acce9fd0a35b30945d7b43e4e9a36fa3 GIT binary patch literal 960 zcmZ`%T~8B16g|@xmM&W;f`}9ZD_~ocEub+GMH@a8QwRx!CcaI#BVBBF*PUtWqkqW+ zG4a7iAN*0qJKKVWhKIfP?#Vq3j-KrnD)a{J7`tgP5W$p z?OD-&r)?<+7FHPsRo#^fMS4}f>Dh*fAtVi$7KULl%$)Hyw|Pg|qAfgSS6r{=cS&I?W zgO8Ur`Da^pT<_G5k|ggArVQM*Fs(Vp`d%<3r1Ap|J7ZxM_ZU(nwds1iamYQmI7Z2UAbss2@e@2Lh-33q}uhJmfZ8Yvx{c+ zo?$lEC*yKg8~GDzW6Q4zhS9R?iGx;ft6&T;G4Rdj*LZZ30yISm_& zU?)ik@Dc_#=~Q1KgV$I@2`iM1gJlTTz9Px>NUSTQg~~sLW+Y&j`gwx{iV?*s*2pL5 R0+U#eG9!3G-s~Y!_!k+D(USlG literal 0 HcmV?d00001 diff --git a/build/j3270/org/pubvm/j3270/J3270App$2.class b/build/j3270/org/pubvm/j3270/J3270App$2.class new file mode 100644 index 0000000000000000000000000000000000000000..c31a9edac41c030e5d1bf65c035f2c181fb3c2e6 GIT binary patch literal 878 zcmb7C+iuf95IyUpaS~kWmO|5VX(@&zTxtlQN`Me0O`FihesG`yCLWBOxv;xRMaXP4mDoJI$7{E#n%4dXmrV4!ILC$}nO zwCo#(Y+dv?!_2Phb0a?N@}OgPJ@QOb)0}toZ2xf45^>=0EjJ~PpLi`PLE#PG@kB_? zZu4j$dNL{urPKOCG7KI@O%akJrRDoPsC#x8@=(TIhULk)iE*NG69c8L&_rCN*<@-d znTcdao@K0%y-w%@GPDEMzk?n9EPjRjCHbenVK!@iLUt#G3k0W5;1a=H3O`eL0rObE zMU-#@x5$=J!(*}%Rw;2VNzXnK2h1Y3Clk~DzaR^Z2njbS6oksOR#Q(Ix5>^CC*cn6 Ly;vmO^r`tjgQ3uP literal 0 HcmV?d00001 diff --git a/build/j3270/org/pubvm/j3270/J3270App.class b/build/j3270/org/pubvm/j3270/J3270App.class new file mode 100644 index 0000000000000000000000000000000000000000..daefe95ec216b46164261a87c6887d92477a52cf GIT binary patch literal 15945 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 literal 0 HcmV?d00001 diff --git a/build/j3270/org/pubvm/j3270/config/Settings$StartupBehavior.class b/build/j3270/org/pubvm/j3270/config/Settings$StartupBehavior.class new file mode 100644 index 0000000000000000000000000000000000000000..cd3bfd229d4263cdc952d6156b84edebbe8b69c3 GIT binary patch literal 1286 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 literal 0 HcmV?d00001 diff --git a/build/j3270/org/pubvm/j3270/ui/ConnectDialog.class b/build/j3270/org/pubvm/j3270/ui/ConnectDialog.class new file mode 100644 index 0000000000000000000000000000000000000000..e8ba19b84576350a7c485f813e8b1bd7f28870f4 GIT binary patch literal 5795 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 B=$!xn literal 0 HcmV?d00001 diff --git a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$1.class b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$1.class new file mode 100644 index 0000000000000000000000000000000000000000..5609b4ab4c58dcb381b293e4de7fd32106ab05fa GIT binary patch literal 1599 zcma)6U31e`5Ir|BQ4*BINgxntAStGGj6or^Nn2AWaS~!`12bmm15elTomjD?>(RY- z;H^KSf1@)=+u@;)edu#%`Z4_}4AZ?=rsI&bpc!eU)#~gyyQ}^6^~;|DY@#-XAq+dn zxHyFohAT4J^*Y@~FY@+ot*)aL#e~T%7l=opQX5lto4zWE66#x>f$4O%rJkXs5YeBl2REZnQkDZ``ud-Tro zd24DaCpni1F;kXJ!7x<~V)3vWHAM1+H$w7cYeKhV^D$3&B&f|)S~oInFNcBO7DVyV z0L6i^*fJARGT0M!O^Rx~V<8=^QM>*fBwKi@cwKfAUu*{^=j_qn;f4t&Zdla)a|$iaZk2Ie`?eD-sZlR$qR<>`c)YWOcU+!xETtSt4i7K zcK&maj(_;%MMs+>EaK_o18;7=bUBtJFTccu!KK}_g`4U z-}rvu?lm44;$DQ1lo0(s~wfCym5CqIMVe>1l?rO%=?sg9oT${vX%3 Brkwx) literal 0 HcmV?d00001 diff --git a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$2.class b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$2.class new file mode 100644 index 0000000000000000000000000000000000000000..d4c041dc34ef5a6754d958b1d259f99948e9867c GIT binary patch literal 1802 zcma)6U31$+6g}%WQQ|0xla!RSq_os6j_Xz-1zK<^)Jj5H;xyopJn+KW-mw+ST934L z$QwU{zrjobv=0nVJn(;*;d{8dazXhhMKjWs?jGHH?%lKZr@x+l4`2nmV@P4hLfXbK zMi{Qju<3MrwSLFhyS;p8$?17cRVd{J&1l8rzHH`}8Akiu&vm#LFr3@i<9+^x6YUek zd9=j?;g@vKQ5$2(FpL;%n_uPKEqIcac71)F>m7n zE;5|&@V0PeQ0HpbQ!RRn6c1Fya6NyT|Ke%a7#48J!mBne;|jx-7ZPWfRHDRrbQVSo3uJ+2d#WmhKYq zSPkmFjOb*>>20iHjbXYT3a;o>Xmt)+IG2yLb)s|{4VxlT>oTG}%$B88kqUV?4i}B$ z5do^gCv9G4QzO{Opn^>ckM-%>V#pa?nhD_rec2X+K@lIwHC|UTJYe`LDUJa!PO;8| zhA*PrhLr7I_qkF`W{7{>KIn=w;GZS-@(j%la=YC3tDX`i+NFmZu{~JnUE3S)1ONyo)kj`fH*apbbur-#3sj)Oc36Y|E4)2mJ(c3cFVX_wr z#Urr7%_CTa`ScOS3pb81S@;$+U(+hXJ$jCi1%AeR^qfuuUuI2Mol4&f_whbGb=U)> zF+|oa98%OnSWodXKrfSrctw9d!D}h}fc$qTREj@hA+c0^f~6tsl4rd54c^rGQzrkM zVfqVl_!W8lMm7A7GXBH|gct?;17(Z+!+2o+D8nV2rs2DS4~=I5>oF1^5zUAAn7(Ex Ow(f|&KEXD2F#k6$8|A40 literal 0 HcmV?d00001 diff --git a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$3.class b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$3.class new file mode 100644 index 0000000000000000000000000000000000000000..f85b833fa85f79be66c3875b23171992ed54c470 GIT binary patch literal 665 zcma)3O-~y!5PeQU62bz3Kq$1R1#(CLu?P-GP1Q?6)wF>kQE`BilMKO;vm33weBAg! zTsWXgJ@g0kM^zm!LTV2cwPe4sXXefL&Cg%oe*kD=c^oOEbI5oYLsnodjQaj4?pzN2 z!S2r9wjV41n5Z>#@N{9nnL2GX;)yZ+G^EK1q#Pu9Chh~(o9wQ zTc$17G#A(px;ixc*GJwQ76ewF70tsUmIP+H5lKs*>1HTLM>25Al&iHwkS7U0CGT~C z{2>|pF*H`dYXt#C4K0l!gEu_lJWpE`i*CqlrFc(Zl5vWy3ew0hdg~hx5bK-E-;lfK zJ9p1#mJ#ALifm7&fm>%hE^ZLB>~`_Sc>M;aeVao7 literal 0 HcmV?d00001 diff --git a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$4.class b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$4.class new file mode 100644 index 0000000000000000000000000000000000000000..748008323a7086064212b2647af71964dee9a927 GIT binary patch literal 1355 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 VIPRG4&Qf<--;N_41&%`U|6f|>e5?Qf literal 0 HcmV?d00001 diff --git a/build/j3270/org/pubvm/j3270/ui/StatusBar$1.class b/build/j3270/org/pubvm/j3270/ui/StatusBar$1.class new file mode 100644 index 0000000000000000000000000000000000000000..c4929f9d94af6c3bf58f197e58a8cf20f6a41ad0 GIT binary patch literal 1152 zcmaJ=ZBNrs6n;9!x{W&+2!eoYQ;m>C+!Sza0a};$adIG>M2ZG$SUU&-L~s z+bT~_%}-P%*>xP#2rSnr26|u$h#l#6&GZEfj*jhao|n6jKpY7XNro0M0sTDruvR`g zkgELUQq7Y1OKbj$?qwzgjAV+>tiagc(hoC~oB7>D=AR}aySW5)-OA90w1AYF*Gh6; zRTWuNvUJkV0MYd-mZRGOnnuTRA}$K(t?Ea*Wb4kJq`JkLvA=4Wc17_# z*W-uwFkIq?wrFypR8Z7xPF>^i%M5)y-l{2^s-iVY{R~&@(rJ$+J^)@d$Z(BI?V+Tp zTvWK4VYvQRVX-I|xHQ5r%B56TsP1T7xxsLgD;<9*C3$`M5tnZ>jB~jyl$Fv+=>)?h z*BZLnR#)=dYF5OwfOI&Se7Q=t5D;H9Y|F8NrTPGO1Vm-GLN;mJv>a2d9h6N^)5|tR zQuM8jr}S-4c(Ul$Ji}bIxY&AXgmHcb)hmu+yT0Y@ZJEKoTM=>p#G3ze5cQ&(ez5NP z)E3K;Z9Uo6ec$xc17yT0GP{uO;a223ip*e^aE{0tnrH{S{|e#wd-5~HPy9)JL3=d+ z0nv>*?IzkB(q5vmkX|7whID{vJfuTJ(+%0foHwM8ao&)Bi}OTSFL9o%^N&a(flhRz z3q2UZFyRPYZFoJoyeyl!( u4cwvq3_2f+q->qBs62Mof++4%Z3OqQKp4d$txNUDA|4PX>w~9FI{piHVDh;D literal 0 HcmV?d00001 diff --git a/build/j3270/org/pubvm/j3270/ui/StatusBar.class b/build/j3270/org/pubvm/j3270/ui/StatusBar.class new file mode 100644 index 0000000000000000000000000000000000000000..e1574137ed7a7bda89d1f43513b1b519911a6749 GIT binary patch literal 4176 zcmaJ^Yjjjs75+|MGYpq7nes|_1qhN1B@>{9LMTcC2?I$8%n%r$z|G7}I$?5eyfc#k zrL}4)eP~;$Rn+NMA{9tfrMR65zB+X7RPM#>Q`Ftr(Ifq~l{RAn*A+-_wIsJZ z>s*1#f^(4UK@H~1LoT4E`e0BUsTPY|Sgc_Q>IAAMI>(L~cAFkeS@to3s;F(~DWg+Q zrHmF!z7kj?r38y@ZBT?96upW!P_R!O=3N?=q26&*PTT_LAVoW!x9Uv(EX)d<&GA9Q zOvWwKg9cnL&$U9JoEAAq1vE5aC0$8xb=yXtVcSM56hCeV%r0Pjt&QNj zaif5z(=x5(kRCN+9<0Jzsq3b3F$tX!7fRU-yk@E0jrA1MWuAN+&2b%<@I?nCTdu7zNXYDH#Jnm#^R`d8Wi}(beZ&QMrrPfxBxw3_B zh&r~=Urvw;%$RJC8w@|lok0!T@3l6$)NW)?10@Ei3>QUykWJ? zusG;3rjfrODq5`^-~ZWs3;H$Oia5Pkthhaqrp#IOlkRlkNO7rBSa~pjK^a#jk1`4T zGNuPGq~X0_-nkglG7KoHz-Sd14r59~I#(*%EZcBOh4k-;hNECQS40zW`axh}hpc0X zc(1Hs1A(xSFpZR)w<-oY>NcEk;dTvo;C%w~-;tXJ^&6>d*k?nH&Zwm`ChRNXV5Ghu zA8_G=8t%eLfd%hWTI-n9%j!ZS@l^EIPJO73ymdMA>xu*mlUhaIc2@aDTRL z=QhX+FH>~fRu}74+|-$dyfe6W03X)yfW$?5J!~9hO?Zg;o!e;kH#Ro9ahfg-cS*mw z@loY50;5peIV4MjrXEv#|p@Kp`ZNO4*` z88nlIo$6%ryK#XI3wDKAFr@L%X}E|C59WkVU%a3Ci;d2?+%h>M3k@E_^Dexg;Sydf zj?H3jnnmGKIk8<(EIVx2gA#W2o`NsqWf#7t;p-BXnhBnmz{*-gsX0>k*pMNQ^NNOV z;#nw6R`ed2Ql~*Mp%fSP@g0x!gDqNDda&29!+LLm6xn<^^-ExJ{lwaHO@(Xdp_|e}F?Kh(t8p49 z(Ic!bS$XaaESF9v=?PhBJ8HDWrJhP>0~@|Z=_X#}T}Cq1X-HF&lp17lv0D>*GHE2S zh~>A0y{iX4c_(w`lXsmOd9A9Ex276-O>!)ewVGp%Y!B7Q8(EF)B-F^emt#5D`JjT| zzXrq+YLI&V?H`5kUxF**FCE2{3^e}*_%fK~FDorAy@Z;Gw_bimF>eeDGgwOAhDcRu zt#{TamS@m-39BN$8%n*}DAsiN{l0a}FQ7StjTh0vRU21Z)oOco)umQDvnz))%+;Pe zXT3bckaRoQv zDsIGIum*p}TKo$)(a~(+9LKZ0DDA#^ zQFdOA865ApfIA80-HlIC82=a_+$UoYG#=>6J~58R`8>quY{B!QM9H@f8>m*%zA%n-ypp=X4L_?m0#Ls8&KM>0o)@V!M)-!+$S#Me*A&f)}Ye; zHrib{HB!0A1&R6^xb;5)W!ha&>D+1{rq973~(w8UYw2y`4oC2-VNZz2_h6P_6b zd3V8fEpI&z_B=_!j!GQEabhtYo45tqR3uqO?vTS!nK*ab(i2=rwrgEGG z+X_p}F(`3~2{_0zcE2KVmCaIYHmU5DQpnA^E^z}l1)9mOVERhAW6|<5Pqu4!8-n<= z{=XHq;x>jf+>sb&ws_*({occY(kG%hZ#`hoyAt=btm z`P4kBC7a*eu?dL>yqcP?Du&Iy>dqd#*S$|EM9#Bl#LZK-t7f8dQTg*m(IF%~t-_LL z%^JQ@R>;DoN&PFa4Zj5Pg6Sdh`wS{zW>?&z`gz`Zr>dWS+E6l8mJK z`oBW-eS`LyZUPVKtS1S)z#}^ANPU+kU<%W8vg>cuQb^!K-xr7~nig1o!I7VMyQNie zqKdYUDD_v-xrt@1O?kl?CQ#oAUUo*dJ+IwPw0*)rTWg|#5P#^Bz~k6` Y7EeefXz`Pni;+VpkW5k0+$Z||1DwxCng9R* literal 0 HcmV?d00001 diff --git a/build/j3270/org/pubvm/j3270/ui/TerminalPanel$2.class b/build/j3270/org/pubvm/j3270/ui/TerminalPanel$2.class new file mode 100644 index 0000000000000000000000000000000000000000..2f3341d3f6b823148822e823e03dbb7361b81878 GIT binary patch literal 781 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 literal 0 HcmV?d00001 diff --git a/build/lib3270j/org/lib3270j/ConnectionConfig.class b/build/lib3270j/org/lib3270j/ConnectionConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..c724b820cca4b0461e36dc6792dabb73a37e6897 GIT binary patch literal 2493 zcmbtUT~ixX7=D(7e7QjQNT`8^(n3j=mOx5BL9~3e(S%l$;tV&P1(vv&Y?yp7dgrbG zK!1TZUR1~Ej2FH1!XM@MoUk6-}4Ap9x@ z5l|34)z9@zMYqbC7l)_DQBy%+-L%Z+h5}zIy&pyh;UFR^-h-;(daYj0RLsMB*@w%g znXQ^-(Z#G;+?|+Z1^%;IofJwgjA4uf5mhmYF$IZBekG$`H7&hTs2v*>1(Dol;WZf_ zzg|@^oqsoUO{ker5fhYyRpBj8CRJR+`xIVj?derR!C1cgs$#QlT4m`urD7V_d2M{v zG^}Iecw2Akyf^eJB_9Z?85K7KB+zOYrAk8_-BfW)JUx13zqVvnjasW&keBl+Zi~w? zt9F*NnnwLxuM`a;kL7hrMOr9Rc_Uve^a;{M6?epi|BG6q8DXxLRAjKMV5Dp`JLxHX zea6R3q|*J*5iVI3_k_zhT{d;s<3oPrGgoNwT+(b>g3h&tGpvOm~aIorej?@vhuA(A;gk>sndXaofzhI6npsjHwO za+t(ap{Ov*EG!|>E z`jN3~ie`y9qAx~#NlX@I*}?<-o`kuwkGUHV-v?(*T;CmRn#)c7l>X7$nyme5N(?p8Zb&{J&o(XyC5NwaBh6Sd`;gt(hFMd-G765{S8 zq>0_Zt|OsWZNi;|MS`B;xdZLymPtssY3~q~^Q29>X_pB7*qb)#rp*xhiD$`X-Lxx& z=DkZc>!!^TTkxcvbJMO8y5~(h=cc_+>fEj!hMLJ=O z9n-=)s$&di6+}-iPiL0WmV*9U2VaPWb2uNxSsfQ}Q9-y+EP0NCuEbzw(|%}=7Hw~1 zG`qIR>rZO9jE|!DSVs~QEaUEiw70#*T_4)TZ6|wI!G%OqbgF1qD#<2;RV};TMzVPa zAt3c9bzH%eg2;sHxz$Mphnl7Q$hpB~jiut(gAsF{keuuiIosnAulspwGAt>UnXqbmAT99MBt#c35oDn`WG z+d96)9Z_L<*ouPViNXJ=G52dM4y8)#EN@%J^&E41Yt1R=?6o2-YO)&3>>Ib@_ph_gu3!fj?$6r~{C(Tp%B)?y+qA-P zE7!J*#c!HSESFU&JfON;$$6r3RFBu!Sx%+G!N9hjC=~rW8*933akCQ#A7nDAK(k8Q z<$`m~6~*YC_^uC={1zM*B2NYrtEiM5E}tO8QM<_(iI&V?tOtze&^3O_YrZi08=IM zf1MD3!$o&7%jYWjMOs9{okPztT|*q=)43K7xG>!9FSI?g;KlDt>8al1KLDL}uZ9N1UdkL```UzSM1B4dd z5J6{g2=N+*t+zWu#g=@LSNY0!`+5BRC$s3t1YcnO8G@3SAxWZ=WW$nZk}O0diA(Z% zR1!myMOBhcNi3tj?vP|TqrR|5vL&OwvRATYBPQeeCHunABpZ+foxj5IrNv#<<8GL{Xv!NdQ?8uLOe_f>;&7A}M_EX$WH&*vwG76RP+Ve?uSC z;=vE_qbz%pts;Rfvb8gto<7}ux_kQXzq3C8*0GtyHFP=Xc5xj&4E=}vgnK?$HE(=&x%yiwi<@ve)XS%#5p~ki4FZjnnuetIJ_S;lvIzvQ=aRUE zJ_r3S1~Ax(Hl$W#a89`25ZhlF9%ou6+G5FN+eBoN5V=93G3;UlqYPtpek2N7Rk`^r z%>jLdhAR^?ylOWVAurpCF`vY3j61mF;w~l_hGJMz(8rdeuZVVxpX}}+81CB`Coj;7 zB8l-kNM#vz@Q`6r2Q|-^drMDWE*yI6lGl_DjjR@YDU?YsI!H5QI+T1#rBEX9CzxU;jpL*fK-40o&~O2u-@oF0oeETwpzw!R*`Ng|I` z2XAc$tuds}uc%Zf`bc=KHlm_X<8Sb)(ZMOhxAr_nimm82x!U(dm@a93)Hu#vX4fX^ z09Gin81?r2w1!5)1(bg<4ab%|44s17#hB3-RY^Qy%Q zDLkM{#PEnFL^r)O-{{{Iechv-S;@>Vuvaq=`3MmdZ zi(bqTt2uIT4NovndpA5R;#pJT;W_QYWNU@&zG$wmn(H#&(TN=~4`{c)0*ZK#@&DNr Bk8=P3 literal 0 HcmV?d00001 diff --git a/build/lib3270j/org/lib3270j/DiagnosticClient$2.class b/build/lib3270j/org/lib3270j/DiagnosticClient$2.class new file mode 100644 index 0000000000000000000000000000000000000000..8ba4e60eeaa2863c559b5ac13f65c40e2ba3e27e GIT binary patch literal 1612 zcmaJ>e^VMq7=AXQ2lNsXjHEU(l8On~G?!|Av>~A-h?V3tMv%;;|KvD!@buVa?v_mL zC+XLyov|JN=m+SB>hxWXMnI-8>@DxT@AE$U?6dp#KWBdd*uc9aE@8?*%)~Ti7_J^V zA05kgMB94PJmjuon0f9APZb%aa`{FAaU={RO?(EEVNnKc%lDe=Yfm2@S{t6z7BWpN%K2-M|y7 zrULGC3MnFf-NZc73^zK?5#b8gQSUr;K(kOeLWNYDtLCHf{$i-s6YWA0pJTzm7bb4v z7Q@^Gd-bsUeZlg|A#$vX`39UiJJIwfTMmr|-w1x`0Y2;}2+L zH%j$NW|_tDisAaun_BgK@sA}Pq8OH$|G(?{*O+|rsFCZ z95ehnZ0|v0#AM46EuV*3Tgszew{USKLy?if`f-;}NFUZBx>pLrr*i;Nc6`6)DPExM zstVfnvhRc;4-M4F<`>p9iK>BphTH$2O2;!=7czKE*B`@Ex>92Fqbrd9eMhrJdNLhZ z{R8YbnlL=0QNIQ(8VjFN?LGvMJ%JJX@H>nXT&C$6=73Y&_!EmK$ebcOb&Azt5h8RZQc1%DjXhP@-pyuwJ6lkHoR5G3ecE!u_#7R`Hf%DT+O&r~d4rjs_O~ E1yN9uMF0Q* literal 0 HcmV?d00001 diff --git a/build/lib3270j/org/lib3270j/DiagnosticClient.class b/build/lib3270j/org/lib3270j/DiagnosticClient.class new file mode 100644 index 0000000000000000000000000000000000000000..f4c643d0b5ba6917bbb25136ae7fc9ad61ef32ea GIT binary patch literal 5432 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

f$~Ut^`A~EyA6gFOJH(+Z-${#P%|SYW^;rp? zWd9U@?L?`=_qTzQ{1T| z2T;eUhW6RsT-`3r8Ngi5%^!mC)7)J+ZUBuO)yZG&yRqo>J^bGa3}WRDiXrec`xz)_ z-+&N~;%E+E)G^G&(P+e6Dm@Q6FOhSxfM^yj6k95=g~|xhwGSaq*2*B(x}?n|V|U|| zdvK=QsgqLmNU0PyO0;1BeR9OsH;9cc$;z2QoSlmpw|p|G6sd&EraZ`j>v~ttUjBL!-(fF>%ZEmSP?I3mWgfJKRB|noLJi`IAbKOe zzJ%|3=zPYFL8wj8%lKXas1k&pMGz}H4>~o^mW>k!aZS!3u6COV{64xp_?d*4QskMm z-3_Z6_AV-35ULJMz8&Kmszb*vkoIY;E+LyOA+i0V*K>hV#Yh9|?s2#XHKa2b7cxjj zq3A#d`5+|Cb>F_cStUc;$4!9qj-;Ga}@8BWTV(mVn*=+Nq6L2 z_FCjT_8pPU?AJx{B+2S1o+3FZil<2yNAV2F2~j*tGB1jKB(tM_xYe=HR806c`b!V|m6Vhm7?yJUAL#-?6M9a<*{+jt zyz3;Mo34{&{!L_?$~H5SauhHf1$swN;0PW(;0s|A?%*|YC+hexn1j1`0^g0}aE~Kg z7$uW(Qv7s;@ZwoIEWGY#cs)SL`2t}tle>mghLgu$P`B36GZc3l+T;O==*}`M~LIAr!e|kLG+`<@l8}1{ZJ777;$_p z6-EyP(U0@|{n-$@maF8ST2(fP-MjJFPQ-&NrZM4vPD~MgfnSnVarAr6y~4141%Dxj X-{PO%{J(e)zExU|xA89C$Kn44KDAbV literal 0 HcmV?d00001 diff --git a/build/lib3270j/org/lib3270j/TerminalModel.class b/build/lib3270j/org/lib3270j/TerminalModel.class new file mode 100644 index 0000000000000000000000000000000000000000..bf03ca1df09b83d28453e487e1bcae71b61f5a53 GIT binary patch literal 3598 zcmbVOYgZfB72Ov-W+ab6CKfheY>dH?gl!BC2^eDYFmBXB>_(VWb{jf~F_sz45NTw` zZQ7)*o3u^e&!kT``H(ODkdhL!k|xbs{np>mAJVnDR@{BBBp6|Zu2xH$quF!Lx##SA z?m6?1Pu~9szy-Xep-G_6Ev^jM`K2>MPn^CvuvT|7cvr1o#@{VcGxyzOvK>%R`8gzkZdTMTw)+38U0*7Wl-Dpf&MGfq!w>l%?y#|`=;m0H# zGqA579+vPU2KM{m(ZwMNw;4E44=dm820H3t<@=CC#WD11IBK8|#|1(;+jT5~{fXr4P4kvHV4Kd$z@?>|R?ZuXB7vlal!1Pn5NO_3 zm~qMlu6fI}%hsjq0>cTP^|Wo4N@G4C;}!GH%9wuz=|K5EWnciO1;XQbC-0pTX!E6q>YY?jLJK48Khbnd5Q-2NTnNlD3|ZsY|%5;q%h^8G(J<)@jGH zR-|oPBJH=W;9U5zJ%wvfGU%cz>!39*V@$ZWq}@FZnkZ$nD%6GrChKaZ{~%ZwdUnrrMc-Grec?-i@u6pa{9zAl4`raa|VOia@+U#>oX4d)~lJtO~TOSYEA_%->mi z=%j2Bph+u7`gPe>8u(48ucrA)F6h{!qgls39s6}0(9xlzOIp8R;Ev==>#Y$MoMxdj zh}pbjRmy$dT(XIrZm>Xdw`0$ppR>Fh?($_8Tft(raX%uN{0eKlT%^ZHq81+unn|m^ zSvz8ZL^92|@+H>1JY2lzhD)^z+vlndkcWe7Qe)D8l6F89t-Tq~%&pFuYbrSn-{bLU zPZptiK^&6tIepR#r-Av-Y(4c#6t?>2noLy}?OlU#il&{!jgBstB(Q{**B^kF| z%vl%na!*=oVX0tF$uefTQ?BckyrQ{Qp`RV&mptcJoK?rY?R2X~cD7q|=OnRDik2{5 zkKzsdTElN-f&W&Zcl$i#om=jzHBgDaQu7ztIo#qMf!F;NQB|xg`IPA_+g7P})^%6Q zYYqCcFY7u(S?>+h8*GjLm{na&o3@?Fdlp$T(vD*l)l{%Js{g>L{!mVhLe=m`fy2A2 zvb$9Bni%8>O0Y4sOti~E4^VP+E6G`{B!{z-oW@FW{3^+Lt0V_3J+#Uxqa-JalAIw* za%L#W8KER60ubwv6PWich{%Qqfv@nchf;o`>p1)#dOpUncX%i8RbIpDS%muico9ui z{U(J*TLY<0Jo+&L59@!8wjsKG88l+2Vajr;=O`nTsZ{@=`#72E+Q48c9^AliDjwRv zXeu7wz<4UIZQy(=u5V!K&(yAj(xXy7#E`m3*I~ZD9N}9@H}8(}O`!)#Ugfe!$xMt= z{yydNb>dBUiH={!H+Yqybx}$lLaD!C@K5~XC!mja-^90g-KjdfQ}sBN?l$x~x>NO0 zs(!nn>iAC81XW*YsCs^+QqKOoUM6Z#OJ$p*;8@Eg!$ zqi-V=8`(rUlMPCCHt}?36B)VdO};7_#CM4Fqv-)(B8A4S4r-X!@Lhhz1G-jNXd9D+ zzt&*+?Ekg=$p5~41mDLGs@cDYAWI9F*u=HD6Ca%mJRXb(yWT|aiFk16Y^XiN#5?Yx zMgAG?yoYdm=$*Sk@%Dp%et^YnN9X%6f%~xjw#7;t`2^0R4HG=?lVs*R2KjC#S!zb$ z6GSx}6QcWHiuWIG5wTr~@8QPIqm_jmlhxMIn#t__-<)%(8@Ro?&p0e!dHflwfJFm_q~ z?xXN7LJD3BhAVJ)6^U{_;9^*>z@7VWS9xhscu5Nd>zubL*we!mxaK^r z;BqXiRq&ekg9^VD)AS0y<-T(hx8KF#?KW~;HJ~V4#~z%5ff?*&FKmX#uJ{_8##?MG nf2B?kl`ioU{FEd#;b-_crA}&o#osURJKjb4@AtTeH_`qdzxW5M literal 0 HcmV?d00001 diff --git a/build/lib3270j/org/lib3270j/charset/EbcdicTranslator.class b/build/lib3270j/org/lib3270j/charset/EbcdicTranslator.class new file mode 100644 index 0000000000000000000000000000000000000000..2469a256fa6115f71134d38f9fe9ad404c3659aa GIT binary patch literal 3139 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 literal 0 HcmV?d00001 diff --git a/build/lib3270j/org/lib3270j/input/InputProcessor.class b/build/lib3270j/org/lib3270j/input/InputProcessor.class new file mode 100644 index 0000000000000000000000000000000000000000..68143e5407a01ee2fc095cf2f2c56bdd0f2ba39a GIT binary patch literal 5610 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 RMgP<)w<T6T literal 0 HcmV?d00001 diff --git a/build/lib3270j/org/lib3270j/listener/ConnectionListener.class b/build/lib3270j/org/lib3270j/listener/ConnectionListener.class new file mode 100644 index 0000000000000000000000000000000000000000..e55630749e869e625aeff7841c3711ebba25deae GIT binary patch literal 420 zcmaJ-yH3ME5S&ed4dIa>gn%!=G#Eiak%Gd6$})u``g5`}XPGOJZq$R^laFrnKg#zUR#rYK`{n|aigA8 z+A340w^bto!l5;txb(_%b}u|H2vVIixhTEUrk(`t zo;9-~oG~r9wqA=9@o>CCy?TC-*##3uxi(xZA1Zb;RkeZ)OS^P)zR}@x&>iue%xSO_ t5eR(=7U)Y*Ylsj6i?J>UVFN>{o6;WzMZlgvi7ly-P{3H~w!A&;d;lS(e3t=YBb3VYL&XN93PN#oYm-Pw(h?M`Pi&j*Rkg=_P|$bGX)#Xah@ zA~Rr+ZA^Y$X}3~1ZgUsKn46+;E~;75UgwYvz36e8?% n9uQCXysqB2LXR_~1oS!kTin__`^Pq-ZSa5E5p6^6cQE<^(vM6S literal 0 HcmV?d00001 diff --git a/build/lib3270j/org/lib3270j/protocol/DS3270Constants.class b/build/lib3270j/org/lib3270j/protocol/DS3270Constants.class new file mode 100644 index 0000000000000000000000000000000000000000..1fdbea2390673139afb6d865ff6fff3ac318e8c4 GIT binary patch literal 9902 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 UMZ<%L8+K^RWuV(2tCF|>4(sGP{{R30 literal 0 HcmV?d00001 diff --git a/build/lib3270j/org/lib3270j/protocol/TN3270EConstants.class b/build/lib3270j/org/lib3270j/protocol/TN3270EConstants.class new file mode 100644 index 0000000000000000000000000000000000000000..0de4f6972d84c5e0f0cf70b09944fe2f5b2b5e98 GIT binary patch literal 4119 zcmZ{n`F9&v702&*(^!!g#<7zaccDOVoTzRSpp>Ld$I{52M4D0Njg%^ZPGv`R z(Ztz&FOcfA>&?}pI*n@7cl%ur_rA65f?V=jdQhjlJ-(Rf6b)mI=UVcZq%Bqo%_QH+dAEd ztPb7C3OA+b-2~nIq1B6&b-D$yGOpvKi>Bv0mZ#HUjYhIo z+0IyL+ew#{rPHS&vYm3;RLafRrVr6)5t2o`U^;yc^=69gxavB69zy;V>ZC2tbK(6M zT$P-Yj(ghaiqJY^B}bKtMc4DKY}&I<>2#My$pzcVrtN~c2xklNil&v1Rw<-2eqN`n zM#G+^imsz9rBe>RRYvhxI^$+pw1m}A6&1QXfjVNUzU6SoAdGVJCH|>X9*&e_rj-Rx zIype@&@}guPDRX>{YK=0x6 zEx0Q+y2b$aV{c&VbO^!%_aej{+7)*li7w@7VG+1K0k3`#_9;AslJM+_IyFEy#s?K_ z;$GFr2=&laJdWi+w_@QVVc4%4O^4L$ST>J4w(Af*%hlxKq_Td%JsWUrxB{~G9(9AG zD=;Q=9tKA}9ND3VW4k0q;`QVG2bbvKAYY1yIcC_y0eTo40f-*vr5)y4p2~3nx!1Bm zCU2RZg$+BxV(x(0N2qqvsw}vsm*pw?u7~L)Lp6KRF;TrSPvru~-KrnT$C7Z-gm8j| z(Y6rMNtPB}W&3v7+AepjMRS{cg!M&XM`7FTE2NFsFv6q?1rw9-EN*v=_OTGV+49QK z`GS#CB|Dt*1dq8z%R?w!RhcC#a}vuNyTLYxMUb^syeYX#32P}(_8Alcr)9WSj_Vb; z7JFh2g*lvD(3N|4e^}o2yo91#n8J8$AFlBtvf_nI_Uz#| zYnixi%eVo;Ro;cI6dR!@a;BXxc~*dl7T7d(%~9Vf6nPYv`Dkp*z2THX=~yp~1v4A< zHfw*N(Lknku7+O7*PAtG^ZatH?N^r@kR;o+>PGjb98sO>$^(V!dOVmqSM5~&hu3S- zMLN~4ZLH&OM{TKA#oeB3R9B;c5qaU_56RT**)!L;MsdO)TWQ39c#^8M*YT;o z_CGZ@^ZyCD6_d`&Rt^L4?T&o=~fK936Ke7-4|<3A>t<3BE# z<3Ayo<9|yq$N#oqj{l@!j{lTkj`tnG9Phh=Io|gKbG+{h=6F94%<-NU%<+CGnCJT= z!Ce1m1V0P@v0%>cCxSV@p9<#uekPdn`MF@W|Ak<-e@-yl|57mPKQEZ|eg)5zO`ctzfR_1;Jd;*9E@;{+;08ga07- zkKjKE{xkSZ!Eb@z7JQMW`4tCyd758E!YAH4!YAG_@JwKrwKs>fi+(mm79)5P@Kl(_OL?ZRXF@C6z)M&86wpD^ywWk?Wh zx0#?cJmCdm2Tx;>r?DM8J+$4^J3D$ZB2UH+o*vom>2EuFnv6V6?%?Us?VjGH_u|<- zhBENoaOZopw{n@L^Zj?s%)#5Cnen05X#d>Ik>_ao;3fL#iBZ z@kF0?;eY?a;DKH_uSXFHhl-AaWD1AJ3=WJr91=&sw`22jnMY~hzm!bSUhM|`KUiL! z{=T)u|E2+G5M3PC-=}n?-CHRY@4Psfupgt}5e0#!-AHj62jt zN{lBWi{orzvPFh1EVfu=3p=zp3Ei>rWTabQU6*xFu`Wy#hGN}PsCzo}Uct9+oW=LB r@kLl0Ly>X7##J_6fpoIhkVZI(d+9K4t9xh-XFv<(ejn57qbvUh;Nd9? literal 0 HcmV?d00001 diff --git a/build/lib3270j/org/lib3270j/protocol/TelnetConstants.class b/build/lib3270j/org/lib3270j/protocol/TelnetConstants.class new file mode 100644 index 0000000000000000000000000000000000000000..f95779f11dd8039f003a8cbcbf08063090a7e49f GIT binary patch literal 3231 zcmb`J`EwLS6vy8rK!zj`Y%YUxNq~TaKtMo(=w^397G`Ibok>Cj43o`}Y_i!2yAw`9 z5Cl)W74Q4L#-FU>2mb)2w6wIew6w6aw6vf=P&^Ru+q<)Fqv%g2^Z;W{dhEC>(U2vHRKi^hKJ; zaBag)CsJMYGF9XF;Q*q}#PLJAX38{6qM{IPI~$hPtYr?Lg;iCiI;i;ZwH6h;zNl~7os zsZP49Hks()8SAO-Pdly?capWHolM!Ta48AC!}8o>B?s(cB;y=cXSq7V)gN4)=IVE@ ze&gy_u72U_6jwiU^%D-(5K{sxIGn_ZLJ>YY!PSpk9p~y8QeiY3Q9eJ4`DR6L z)Q@Fm&x+D6&l`Ak@|`HR{0C8{+)^oqZ_P%USO}jhM(#JM5G`Ak+%{RF3jYiF^5kZe zWZs;wQ*NFj5%axKu>Nvdv z{q=&I-=YM|c^l0w{q_aiR}22R-d->EJwHUyN+ykjYu6+1;Z({_2a{GNV`oa}RdkfP8R75Ixroa*KEP*xfY=L#~9D$?Y zI)PV$=L;MIFA&%OFA~@UFBZ5NyhPv@@KS+W!K(yr179ofYHHQ+{p*Mdg~d>y!5 z;B{b2;0|zH;7+hD@Op5Uz}?`4z&+qzfs^1qfm2{d;C}E1fz#lOz%F<|;Emu-0&fOy z5qK+jo50tDw+p-je1pI{!Mg;$5xiUAJ>Z)J-V5F*@Xg>`1ilq~o4~x@ZWoy6^A3S| zKKBdE^LeMh-2X0tx&Pe)bN_n;=J~%@VD5jP!1se65cokV#;d?TX?P(A@IuC8lrRpS z`4&lUVj|H)(0rL_D1INNX_&)R>cPmZXU4q@jX&yHjfRi$UC4XB?E`=0>;IR0gjk>a zOT5ou{}%E+SO=3Xz9RMcEWJ;GGe=%pvrc_bEajl13ksPM=bgKBtxRCB^6~GUyvJ z={stsAElJ#9-k{1q0l{cz2LJ#7 literal 0 HcmV?d00001 diff --git a/build/lib3270j/org/lib3270j/screen/ExtendedAttribute.class b/build/lib3270j/org/lib3270j/screen/ExtendedAttribute.class new file mode 100644 index 0000000000000000000000000000000000000000..63a37cfb2d827c1e686ba3cdc8b71f7bfeaa4060 GIT binary patch literal 1298 zcma)5+fEZv6kVqmrj!B7#cCA4ro7XI$U#I$Iez=OTlNE3hJJm?$FUGM}0X%XqS%e8qEvs)p5zAS&v@UamAx~M|Q93 zxaLu_BD7z}b&uMn&_NwHJbIWFI;`U+Mic^7vogc(>ml4$SlC!Qu*{{A^wjBK&ZuL) zhS@siof`&UmZPAZ7{#i!lT(Psw??=B$;gC} zLR!P5jwww4HSS#wtCZ)sb7kkq;N0iqP3LR7nS-XK+WTahyVSFI=k@X-$KJVZm#in% zqpamTHL^vn1kIvlP-w_3AHUpm%0~)``1XIjVrF!Yg6+ztZ55l}mbQ4N(7x*$=8MP1 zalKKc@ofr`T#2(Q=jEW3mOq3A&L=162^bm(GkTk~`&m)2b{?{@X#3E%ZQPeUe ztYDSD2Uw$+d1llkrHL|%_5_u@g%)^+fd4%gd^n%p28+qKeQm3fy~vui({JkIEp*?C zXy4%5_p^-FgG3sxc~qF literal 0 HcmV?d00001 diff --git a/build/lib3270j/org/lib3270j/screen/ScreenBuffer.class b/build/lib3270j/org/lib3270j/screen/ScreenBuffer.class new file mode 100644 index 0000000000000000000000000000000000000000..f531834bab071dfb28fefc9e4451429e0d17e717 GIT binary patch literal 6327 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 GKKLK29R~IQ literal 0 HcmV?d00001 diff --git a/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class b/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class new file mode 100644 index 0000000000000000000000000000000000000000..3671d4353d966dba207f995d3471a5695b6e17c8 GIT binary patch literal 5634 zcmcIod0-UP8UMZPCbL;42P**riG)kCIT4gAsE|umbI>G)22>ofv&oXpPP#iAz@wtY zdbZxJP^B$u)5Ep~$tDd-tF78vd)e03-Y0GCeYX^)zc;hV?k17`)jxLL%zNMWz3=?J z@9j%(j-LQfFD`Z?0}cgF6`9Bq$lt9EYSkev+*94UYq#EQ3S?~zhJ)tC0*+E|XEt2O zR^V1K3#z~}BhphH3hr9HYVFG1)utW_>t=O_HPjj5F!_T(}~pn%AwF+k$2<-=d}#HlqS7N~g>8&A87pELU)ziWMjonEzJh2{?n{fFV#Y zgJ5r`f--?Ti&;jg-RSPqO*g!7NUv9_s6sVE6}9FCX4}doNolVa=c`zS)dFf%H!s)q zfz6@dpw5j;w|hI~`h_aiOsu!I*80MsU|5$6>r||l3pr%$=pJY>>h+MePeBa>HmQ@3 zyV-he_WL7xH0s6%TqIMI={I)sr5*=~HnTE*Hq=qq zif;HxLUr2oe#6w$LeIRI&}w>++z6mYL9dD+cBi41U7HN`pcabhtpS1aQuD?JwVD~K z%maattXYCi8cU+uXJC40NL z@0}`c#C}FkDym0pg#|n*^!6MxB7&P$yi3+{HfyoM)j>z14RHD9bI>8LdB~^P|#YZIM zI|B(6{iuqM;o}0XV6-k|M0J)A&l@V_#S@MQ%rtN4m^OnQruR-o)Y zB;19QntI^G5h>v7D!zeN7^Z-(`)l`^)(L5){Y&_kif>EF`K*c7K#OkfF(Q5SnyD%H z&cxxK1iZ-*xbZ4pQ}I37x44Oz%{(2o9^J!g56OD?p^6{LVo;4f7CQYMySgsrG5_|-%mrF1y8v0%u8m6Q05gq2s^IEmk?_#J-F zMnM}Gpb4^6>ZYe}f|V%sN2c%cm8q1(TCxUDzTeN-x+A^W2!X>f^ADpwBQ-P|tRwlik z>}HlJ)Yh=MerN3!jg1X$JKKHR33b9p1Ym(!J!Ypo0tGftIcAb43*ThdeU?*5w%FcI zhOo}?Q&e6Pfv6?czl&#t4V3Q545fuueUUn+-PF4Knzexmq9j_-VX_vR3}ZkMa|JSX z)N*QLH~U2ElHqPQVv%mWF(^^EVCq$(N@fiWuQd!aYDTnyX5H*H{85)EVoFUIMtCxf z60w0UW>c7(m}Z5`*?eT>Ws1eb4zWZPbCBf{%V=18LrX`ADA~ngjczC*=XsoLtLv;! zn`Fu4+g(bDg_wSew2T%fD~(fmS)#%%Dn*qdstNmIC2vZ}d=G{PjXu5F&KfGmn?a-2 zZ5k0C=ntjlK!VV=af=rAhxF(&daf@vP?M7CiR@3ANn`BTH=xfz{y&N(A)-zTh1z)t z*U+R@Zn0Wipoj}qu|}*FSTfC|Car&$U!%TSV8WnT{i$Cs|NqT9tLLh7qe||VL9ilu$GlO%1^{`YjRWhrwnij83k~&A zwach}laT*oP)S!+iWWkgLk>G*h7%^VmpPtqD?%({t2DKRT5cNAGM`>GW)$Hm1(M=(#alozj_jWh3z<7)>T zV(854wwP}Yow<7mTblXBeG4g#MtpZ1-?vkLVyhJif<@MLKk)pRRAW*C$DfYlXOjBo z!%V?U?8Ei!JLUMuWE^%AhJb#~8ib<(6cJiVc>gP94OFwHR`H6v273rPdl{)~8J+9c z@?A^n8@O^KW!*uk`}uVf4&Y|o&&%W?&OO5VIm|2MlVivXMvN>t0{2K z#{MH%UEn+ncV&J_9ItPy^!&m33^KYLWeW?O*I14n!=Kv6@b@_Wz3qUL4t~CfY>$z{ zb6U<-#_{HplR0rN6u#fhe?;%0Gw!7$?qfaOLtK7$ zn@AcavCJtz^|<0fjEStLFgu9_cE-(tL(C^lhA0%${VSn}avHqQdKQcGxxPr0@F!=N YTF>RybA|Qvigo02b8Wq-5f`EGzxq4C9RL6T literal 0 HcmV?d00001 diff --git a/build/lib3270j/org/lib3270j/telnet/TelnetFSM$TN3270ESubmode.class b/build/lib3270j/org/lib3270j/telnet/TelnetFSM$TN3270ESubmode.class new file mode 100644 index 0000000000000000000000000000000000000000..89f0eb331a4ea9216ffd9b6b050297f782e3ac3c GIT binary patch literal 1287 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 { + if (client != null) { + statusBar.updateStatus(); + // Keep focus on terminal panel when window is active + if (isActive() && !terminalPanel.hasFocus()) { + terminalPanel.requestFocusInWindow(); + } + } + }); + refreshTimer.start(); + + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + disconnect(); + if (refreshTimer != null) + refreshTimer.stop(); + terminalPanel.dispose(); + } + + @Override + public void windowActivated(WindowEvent e) { + // When window gains focus, push to terminal panel + terminalPanel.requestFocusInWindow(); + } + }); + } + + private void buildUI() { + terminalPanel = new TerminalPanel(); + + // Status bar + statusBar = new StatusBar(); + + // Layout + getContentPane().setLayout(new BorderLayout()); + getContentPane().setBackground(new Color(10, 10, 10)); + getContentPane().add(terminalPanel, BorderLayout.CENTER); + getContentPane().add(statusBar, BorderLayout.SOUTH); + } + + private void buildMenuBar() { + JMenuBar menuBar = new JMenuBar(); + menuBar.setBackground(new Color(30, 30, 30)); + menuBar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(50, 50, 50))); + + // File menu + JMenu fileMenu = createMenu("File"); + fileMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog)); + fileMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect)); + fileMenu.addSeparator(); + fileMenu.add(createMenuItem("Settings...", -1, this::showSettingsDialog)); + fileMenu.addSeparator(); + fileMenu.add(createMenuItem("Quit", KeyEvent.VK_Q, () -> { + disconnect(); + System.exit(0); + })); + menuBar.add(fileMenu); + + // View menu + JMenu viewMenu = createMenu("View"); + viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2))); + viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2))); + viewMenu.add(createMenuItem("Reset Font", KeyEvent.VK_0, () -> { + terminalPanel.setFontSize(16); + pack(); + })); + menuBar.add(viewMenu); + + // Actions menu + JMenu actionsMenu = createMenu("Actions"); + actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_L, () -> { + if (client != null) + client.sendClear(); + terminalPanel.repaint(); + })); + actionsMenu.add(createMenuItem("Reset", KeyEvent.VK_R, () -> { + if (client != null) + client.reset(); + terminalPanel.repaint(); + })); + menuBar.add(actionsMenu); + + // Help menu + JMenu helpMenu = createMenu("Help"); + helpMenu.add(createMenuItem("Key Mappings", -1, this::showKeyMappings)); + helpMenu.add(createMenuItem("About", -1, this::showAbout)); + menuBar.add(helpMenu); + + setJMenuBar(menuBar); + } + + private JMenu createMenu(String name) { + JMenu menu = new JMenu(name); + menu.setForeground(new Color(200, 200, 200)); + return menu; + } + + private JMenuItem createMenuItem(String text, int acceleratorKey, Runnable action) { + JMenuItem item = new JMenuItem(text); + item.setBackground(new Color(40, 40, 40)); + item.setForeground(new Color(200, 200, 200)); + if (acceleratorKey > 0) { + item.setAccelerator(KeyStroke.getKeyStroke(acceleratorKey, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); + } + item.addActionListener(e -> action.run()); + return item; + } + + // ========== Connection management ========== + + private void showConnectDialog() { + ConnectDialog dialog = new ConnectDialog(this); + dialog.setInitialHost(lastHost); + dialog.setInitialPort(lastPort); + dialog.setVisible(true); + + if (dialog.isConfirmed()) { + ConnectionConfig config = dialog.getConnectionConfig(); + lastHost = config.getHost(); + lastPort = config.getPort(); + connect(config); + } + } + + private void showSettingsDialog() { + org.pubvm.j3270.ui.SettingsDialog dialog = new org.pubvm.j3270.ui.SettingsDialog(this); + dialog.setVisible(true); + } + + public TerminalPanel getTerminalPanel() { + return terminalPanel; + } + + void connect(ConnectionConfig config) { + lastHost = config.getHost(); + lastPort = config.getPort(); + + // Save for auto-connect + org.pubvm.j3270.config.Settings.setAutoConnectHost(config.getHost()); + org.pubvm.j3270.config.Settings.setAutoConnectPort(config.getPort()); + + // Disconnect existing connection + disconnect(); + + client = new Telnet3270Client(config); + client.addConnectionListener(this); + client.addScreenUpdateListener(this); + + terminalPanel.setClient(client); + statusBar.setClient(client); + + setTitle("j3270 — " + config.getHost() + ":" + config.getPort()); + + // Resize window to match model + terminalPanel.revalidate(); + pack(); + + new Thread(() -> { + try { + client.connect(); + } catch (IOException e) { + log.log(Level.WARNING, "Connection failed", e); + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(this, + "Connection failed: " + e.getMessage(), + "Connection Error", JOptionPane.ERROR_MESSAGE); + }); + } + }, "Connect-Thread").start(); + + // Focus the terminal panel + terminalPanel.requestFocusInWindow(); + } + + private void disconnect() { + if (client != null) { + client.disconnect(); + client = null; + terminalPanel.setClient(null); + statusBar.setClient(null); + terminalPanel.repaint(); + setTitle("j3270 — Java TN3270 Terminal Emulator"); + } + } + + private void adjustFontSize(int delta) { + int current = terminalPanel.getFontSize(); + int newSize = Math.max(8, Math.min(36, current + delta)); + terminalPanel.setFontSize(newSize); + pack(); + } + + // ========== ConnectionListener ========== + + @Override + public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) { + log.info("Connection state: " + oldState + " -> " + newState); + SwingUtilities.invokeLater(() -> { + statusBar.updateStatus(); + terminalPanel.repaint(); + + // Auto-resize and focus on first full session + if (newState.isFullSession() && !oldState.isFullSession()) { + terminalPanel.revalidate(); + pack(); + terminalPanel.requestFocusInWindow(); + } + }); + } + + @Override + public void onConnectionError(String message) { + log.warning("Connection error: " + message); + SwingUtilities.invokeLater(statusBar::updateStatus); + } + + @Override + public void onTN3270ENegotiated(String deviceType, String deviceName) { + log.info("TN3270E negotiated: type=" + deviceType + " name=" + deviceName); + SwingUtilities.invokeLater(() -> { + if (deviceName != null) { + setTitle("j3270 — " + lastHost + ":" + lastPort + " [" + deviceName + "]"); + } + }); + } + + // ========== ScreenUpdateListener ========== + + @Override + public void onScreenUpdated() { + SwingUtilities.invokeLater(() -> { + if (client != null) { + client.getInputProcessor().setKeyboardLocked(false); + } + terminalPanel.repaint(); + statusBar.updateStatus(); + }); + } + + @Override + public void onSoundAlarm() { + Toolkit.getDefaultToolkit().beep(); + } + + @Override + public void onScreenSizeChanged(int rows, int cols) { + SwingUtilities.invokeLater(() -> { + terminalPanel.revalidate(); + pack(); + }); + } + + // ========== Help dialogs ========== + + private void showKeyMappings() { + String text = "Key Mappings:\n\n" + + "Enter — Send Enter (AID)\n" + + "F1-F12 — PF1-PF12\n" + + "Shift+F1-F12 — PF13-PF24\n" + + "Alt+1/2/3 — PA1/PA2/PA3\n" + + "Tab — Tab to next field\n" + + "Shift+Tab — Back-tab\n" + + "Arrow keys — Cursor movement\n" + + "Home — Cursor to home\n" + + "End — Erase to end of field\n" + + "Delete — Delete character\n" + + "Backspace — Backspace\n" + + "Insert — Toggle insert mode\n" + + "Escape — Reset\n" + + "PageUp/Down — PF7/PF8\n" + + "Alt+C — Clear\n" + + "Alt+1/2/3 — PA1/PA2/PA3\n" + + "Cmd+D — Disconnect\n" + + "Cmd+Q — Quit\n" + + "Cmd+=/-/0 — Font size +/-/reset"; + + JTextArea area = new JTextArea(text); + area.setEditable(false); + area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); + area.setBackground(new Color(30, 30, 30)); + area.setForeground(new Color(200, 200, 200)); + JScrollPane sp = new JScrollPane(area); + sp.setPreferredSize(new Dimension(360, 400)); + JOptionPane.showMessageDialog(this, sp, "Key Mappings", JOptionPane.INFORMATION_MESSAGE); + } + + private void showAbout() { + JOptionPane.showMessageDialog(this, + "j3270 — Java TN3270 Terminal Emulator\n\n" + + "A Java reimplementation of x3270.\n" + + "lib3270j v0.1.0\n\n" + + "Supports: TN3270, TN3270E (RFC 2355)\n" + + "Models: IBM 3278/3279 Models 2-5\n" + + "Colors, Extended Attributes, Query Replies", + "About j3270", JOptionPane.INFORMATION_MESSAGE); + } + + // ========== Main ========== + + public static void main(String[] args) { + boolean debug = false; + String configFile = null; + java.util.List remainingArgs = new java.util.ArrayList<>(); + for (int i = 0; i < args.length; i++) { + if ("--debug".equals(args[i]) || "-d".equals(args[i])) { + debug = true; + } else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) { + configFile = args[++i]; + } else { + remainingArgs.add(args[i]); + } + } + + // Configure logging over the absolute root logger + Logger globalRoot = Logger.getLogger(""); + for (java.util.logging.Handler h : globalRoot.getHandlers()) { + globalRoot.removeHandler(h); + } + globalRoot.setLevel(debug ? Level.ALL : Level.WARNING); + ConsoleHandler handler = new ConsoleHandler(); + handler.setLevel(Level.ALL); + handler.setFormatter(new SimpleFormatter()); + globalRoot.addHandler(handler); + + // Load INI config file if specified + if (configFile != null) { + try { + org.pubvm.j3270.config.Settings.loadFromIniFile(configFile); + } catch (Exception e) { + System.err.println("Failed to load config file: " + configFile); + System.err.println(e.getMessage()); + System.exit(1); + } + } + + // Dark look and feel + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { + log.fine("Could not set system look and feel"); + } + + // Use native macOS menu bar if applicable + System.setProperty("apple.laf.useScreenMenuBar", "true"); + System.setProperty("apple.awt.application.name", "j3270"); + + SwingUtilities.invokeLater(() -> { + J3270App app = new J3270App(); + app.setVisible(true); + + // If host:port given on command line, connect directly + if (!remainingArgs.isEmpty()) { + String host = remainingArgs.get(0); + int port = 23; + if (remainingArgs.size() >= 2) { + try { + port = Integer.parseInt(remainingArgs.get(1)); + } catch (NumberFormatException e) { + } + } + TerminalModel model = TerminalModel.IBM_3279_4; + if (remainingArgs.size() >= 3) { + try { + int modelNum = Integer.parseInt(remainingArgs.get(2)); + model = TerminalModel.forModel(modelNum, true); + } catch (Exception e) { + } + } + ConnectionConfig config = new ConnectionConfig(host, port, model); + app.connect(config); + } else { + org.pubvm.j3270.config.Settings.StartupBehavior behavior = org.pubvm.j3270.config.Settings + .getStartupBehavior(); + switch (behavior) { + case SHOW_CONNECT: + SwingUtilities.invokeLater(app::showConnectDialog); + break; + case AUTO_CONNECT: + String host = org.pubvm.j3270.config.Settings.getAutoConnectHost(); + int port = org.pubvm.j3270.config.Settings.getAutoConnectPort(); + if (host != null && !host.isEmpty()) { + app.connect(new ConnectionConfig(host, port, TerminalModel.IBM_3279_4)); + } else { + SwingUtilities.invokeLater(app::showConnectDialog); + } + break; + case DO_NOTHING: + default: + break; + } + } + }); + } +} diff --git a/j3270/src/main/java/org/pubvm/j3270/config/Settings.java b/j3270/src/main/java/org/pubvm/j3270/config/Settings.java new file mode 100644 index 0000000..4bf43bd --- /dev/null +++ b/j3270/src/main/java/org/pubvm/j3270/config/Settings.java @@ -0,0 +1,220 @@ +package org.pubvm.j3270.config; + +import java.util.prefs.Preferences; +import java.awt.Color; +import java.io.*; +import java.util.logging.Logger; +import java.util.logging.Level; + +public class Settings { + private static final Preferences prefs = Preferences.userNodeForPackage(Settings.class); + private static final Logger log = Logger.getLogger(Settings.class.getName()); + + public enum StartupBehavior { + SHOW_CONNECT, + DO_NOTHING, + AUTO_CONNECT + } + + public static String getFontFamily() { + return prefs.get("fontFamily", "Monospaced"); + } + + public static void setFontFamily(String family) { + prefs.put("fontFamily", family); + } + + public static int getFontSize() { + return prefs.getInt("fontSize", 16); + } + + public static void setFontSize(int size) { + prefs.putInt("fontSize", size); + } + + public static StartupBehavior getStartupBehavior() { + String behaviorStr = prefs.get("startupBehavior", StartupBehavior.SHOW_CONNECT.name()); + try { + return StartupBehavior.valueOf(behaviorStr); + } catch (IllegalArgumentException e) { + return StartupBehavior.SHOW_CONNECT; + } + } + + public static void setStartupBehavior(StartupBehavior behavior) { + prefs.put("startupBehavior", behavior.name()); + } + + public static String getAutoConnectHost() { + return prefs.get("autoConnectHost", ""); + } + + public static void setAutoConnectHost(String host) { + prefs.put("autoConnectHost", host); + } + + public static int getAutoConnectPort() { + return prefs.getInt("autoConnectPort", 23); + } + + public static void setAutoConnectPort(int port) { + prefs.putInt("autoConnectPort", port); + } + + public static Color getColorOverride(int index, Color defaultColor) { + String hex = prefs.get("color_" + index, null); + try { + return hex != null ? Color.decode(hex) : defaultColor; + } catch (NumberFormatException e) { + return defaultColor; + } + } + + public static void setColorOverride(int index, Color color) { + prefs.put("color_" + index, String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue())); + } + + public static Color getMonoColorOverride(String key, Color defaultColor) { + String hex = prefs.get("mono_" + key, null); + try { + return hex != null ? Color.decode(hex) : defaultColor; + } catch (NumberFormatException e) { + return defaultColor; + } + } + + public static void setMonoColorOverride(String key, Color color) { + prefs.put("mono_" + key, String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue())); + } + + public static String getKeyBinding(String action, String defaultBinding) { + return prefs.get("key_" + action, defaultBinding); + } + + public static void setKeyBinding(String action, String binding) { + prefs.put("key_" + action, binding); + } + + /** + * Load settings from an INI-style configuration file. + * Sections: [appearance], [behavior], [colors], [keybindings] + * + * Example file: + *

+     * [appearance]
+     * fontFamily = IBM 3270
+     * fontSize = 18
+     *
+     * [behavior]
+     * startupBehavior = AUTO_CONNECT
+     * autoConnectHost = mainframe.example.com
+     * autoConnectPort = 23
+     *
+     * [colors]
+     * ; Host colors 0-15
+     * color_0 = #000000
+     * color_4 = #00ff00
+     * ; Monochrome / background
+     * mono_NORMAL = #32cd32
+     * mono_BACKGROUND = #0a0a0a
+     *
+     * [keybindings]
+     * ; Comma-separated for multiple bindings
+     * PF12 = F12, shift F12
+     * PF24 = UNBOUND
+     * CLEAR = alt C
+     * 
+ */ + public static void loadFromIniFile(String path) throws IOException { + File file = new File(path); + if (!file.exists()) { + throw new FileNotFoundException("Config file not found: " + path); + } + + log.info("Loading configuration from: " + path); + String currentSection = ""; + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + int lineNum = 0; + while ((line = reader.readLine()) != null) { + lineNum++; + line = line.trim(); + + // Skip empty lines and comments + if (line.isEmpty() || line.startsWith(";") || line.startsWith("#")) { + continue; + } + + // Section header + if (line.startsWith("[") && line.endsWith("]")) { + currentSection = line.substring(1, line.length() - 1).toLowerCase().trim(); + continue; + } + + // Key = Value + int eq = line.indexOf('='); + if (eq < 0) { + log.warning("Config line " + lineNum + ": no '=' found, skipping: " + line); + continue; + } + String key = line.substring(0, eq).trim(); + String value = line.substring(eq + 1).trim(); + + try { + applyConfigEntry(currentSection, key, value); + } catch (Exception e) { + log.log(Level.WARNING, "Config line " + lineNum + ": error applying " + key + "=" + value, e); + } + } + } + log.info("Configuration loaded successfully."); + } + + private static void applyConfigEntry(String section, String key, String value) { + switch (section) { + case "appearance": + switch (key) { + case "fontFamily": setFontFamily(value); break; + case "fontSize": setFontSize(Integer.parseInt(value)); break; + default: + log.warning("Unknown appearance key: " + key); + } + break; + + case "behavior": + switch (key) { + case "startupBehavior": + setStartupBehavior(StartupBehavior.valueOf(value.toUpperCase())); + break; + case "autoConnectHost": setAutoConnectHost(value); break; + case "autoConnectPort": setAutoConnectPort(Integer.parseInt(value)); break; + default: + log.warning("Unknown behavior key: " + key); + } + break; + + case "colors": + if (key.startsWith("color_")) { + int index = Integer.parseInt(key.substring(6)); + setColorOverride(index, Color.decode(value)); + } else if (key.startsWith("mono_")) { + String monoKey = key.substring(5); + setMonoColorOverride(monoKey, Color.decode(value)); + } else { + log.warning("Unknown colors key: " + key); + } + break; + + case "keybindings": + setKeyBinding(key, value); + break; + + default: + // Allow bare keys outside any section — treat as raw prefs + log.fine("Setting raw preference: " + key + " = " + value); + prefs.put(key, value); + break; + } + } +} diff --git a/j3270/src/main/java/org/pubvm/j3270/ui/ConnectDialog.java b/j3270/src/main/java/org/pubvm/j3270/ui/ConnectDialog.java new file mode 100644 index 0000000..30ac8b8 --- /dev/null +++ b/j3270/src/main/java/org/pubvm/j3270/ui/ConnectDialog.java @@ -0,0 +1,181 @@ +package org.pubvm.j3270.ui; + +import org.lib3270j.ConnectionConfig; +import org.lib3270j.TerminalModel; + +import javax.swing.*; +import java.awt.*; + +/** + * Connection dialog for entering host, port, model, and LU name. + */ +public class ConnectDialog extends JDialog { + + private JTextField hostField; + private JTextField portField; + private JComboBox modelCombo; + private JTextField luField; + private boolean confirmed; + private ConnectionConfig result; + + public ConnectDialog(Frame parent) { + super(parent, "Connect to Host", true); + buildUI(); + pack(); + setLocationRelativeTo(parent); + setResizable(false); + } + + private void buildUI() { + JPanel mainPanel = new JPanel(new GridBagLayout()); + mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16)); + mainPanel.setBackground(new Color(30, 30, 30)); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(4, 4, 4, 4); + gbc.fill = GridBagConstraints.HORIZONTAL; + + Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 14); + Color fg = new Color(200, 200, 200); + + // Host + gbc.gridx = 0; + gbc.gridy = 0; + JLabel hostLabel = new JLabel("Host:"); + hostLabel.setForeground(fg); + hostLabel.setFont(labelFont); + mainPanel.add(hostLabel, gbc); + gbc.gridx = 1; + gbc.weightx = 1.0; + hostField = createDarkField(20); + mainPanel.add(hostField, gbc); + + // Port + gbc.gridx = 0; + gbc.gridy = 1; + gbc.weightx = 0; + JLabel portLabel = new JLabel("Port:"); + portLabel.setForeground(fg); + portLabel.setFont(labelFont); + mainPanel.add(portLabel, gbc); + gbc.gridx = 1; + gbc.weightx = 1.0; + portField = createDarkField(6); + portField.setText("23"); + mainPanel.add(portField, gbc); + + // Model + gbc.gridx = 0; + gbc.gridy = 2; + gbc.weightx = 0; + JLabel modelLabel = new JLabel("Model:"); + modelLabel.setForeground(fg); + modelLabel.setFont(labelFont); + mainPanel.add(modelLabel, gbc); + gbc.gridx = 1; + gbc.weightx = 1.0; + modelCombo = new JComboBox<>(TerminalModel.values()); + modelCombo.setSelectedItem(TerminalModel.IBM_3279_4); + modelCombo.setBackground(new Color(45, 45, 45)); + modelCombo.setForeground(fg); + modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); + mainPanel.add(modelCombo, gbc); + + // LU Name + gbc.gridx = 0; + gbc.gridy = 3; + gbc.weightx = 0; + JLabel luLabel = new JLabel("LU Name:"); + luLabel.setForeground(fg); + luLabel.setFont(labelFont); + mainPanel.add(luLabel, gbc); + gbc.gridx = 1; + gbc.weightx = 1.0; + luField = createDarkField(12); + mainPanel.add(luField, gbc); + + // Buttons + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + buttonPanel.setBackground(new Color(30, 30, 30)); + + JButton connectBtn = new JButton("Connect"); + connectBtn.setBackground(new Color(50, 120, 50)); + connectBtn.setForeground(Color.WHITE); + connectBtn.setFont(labelFont); + connectBtn.addActionListener(e -> onConnect()); + + JButton cancelBtn = new JButton("Cancel"); + cancelBtn.setBackground(new Color(60, 60, 60)); + cancelBtn.setForeground(fg); + cancelBtn.setFont(labelFont); + cancelBtn.addActionListener(e -> { + confirmed = false; + dispose(); + }); + + buttonPanel.add(cancelBtn); + buttonPanel.add(connectBtn); + + gbc.gridx = 0; + gbc.gridy = 4; + gbc.gridwidth = 2; + mainPanel.add(buttonPanel, gbc); + + setContentPane(mainPanel); + + // Enter key triggers connect + getRootPane().setDefaultButton(connectBtn); + } + + private JTextField createDarkField(int cols) { + JTextField field = new JTextField(cols); + field.setBackground(new Color(45, 45, 45)); + field.setForeground(new Color(200, 200, 200)); + field.setCaretColor(new Color(200, 200, 200)); + field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14)); + field.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(60, 60, 60)), + BorderFactory.createEmptyBorder(4, 6, 4, 6))); + return field; + } + + private void onConnect() { + String host = hostField.getText().trim(); + if (host.isEmpty()) { + hostField.requestFocus(); + return; + } + + int port; + try { + port = Integer.parseInt(portField.getText().trim()); + } catch (NumberFormatException e) { + portField.requestFocus(); + return; + } + + result = new ConnectionConfig(host, port, (TerminalModel) modelCombo.getSelectedItem()); + String lu = luField.getText().trim(); + if (!lu.isEmpty()) { + result.setLuName(lu); + } + confirmed = true; + dispose(); + } + + public boolean isConfirmed() { + return confirmed; + } + + public ConnectionConfig getConnectionConfig() { + return result; + } + + /** Set initial values. */ + public void setInitialHost(String host) { + hostField.setText(host); + } + + public void setInitialPort(int port) { + portField.setText(String.valueOf(port)); + } +} diff --git a/j3270/src/main/java/org/pubvm/j3270/ui/SettingsDialog.java b/j3270/src/main/java/org/pubvm/j3270/ui/SettingsDialog.java new file mode 100644 index 0000000..00be7ae --- /dev/null +++ b/j3270/src/main/java/org/pubvm/j3270/ui/SettingsDialog.java @@ -0,0 +1,490 @@ +package org.pubvm.j3270.ui; + +import javax.swing.*; + +import org.pubvm.j3270.J3270App; +import org.pubvm.j3270.config.Settings; + +import java.awt.*; +import java.awt.event.*; +import java.util.HashMap; +import java.util.Map; +import javax.swing.table.DefaultTableModel; +import javax.swing.border.LineBorder; + +public class SettingsDialog extends JDialog { + + private final J3270App parentApp; + + // Appearance tab + private JComboBox fontBox; + private JSpinner fontSizeSpinner; + + // Behavior tab + private JComboBox startupBehaviorBox; + private JPanel autoConnectPanel; + private JTextField hostField; + private JTextField portField; + + // Advanced tab state tracking + private final Color[] tempHostColors = new Color[16]; + private final Map tempMonoColors = new HashMap<>(); + private final Map tempKeyBindings = new HashMap<>(); + private DefaultTableModel keymapModel; + + public SettingsDialog(J3270App parent) { + super(parent, "Settings", true); + this.parentApp = parent; + + initComponents(); + setSize(550, 450); + setLocationRelativeTo(parent); + } + + private void initComponents() { + JTabbedPane tabbedPane = new JTabbedPane(); + + tabbedPane.addTab("Appearance", createAppearancePanel()); + tabbedPane.addTab("Behavior", createBehaviorPanel()); + tabbedPane.addTab("Advanced", createAdvancedPanel()); + + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton btnOk = new JButton("OK"); + JButton btnApply = new JButton("Apply"); + JButton btnCancel = new JButton("Cancel"); + + btnOk.addActionListener((ActionEvent e) -> { + boolean success = applySettings(); + if (success) { + dispose(); + } + }); + + btnApply.addActionListener((ActionEvent e) -> { + applySettings(); + }); + + btnCancel.addActionListener((ActionEvent e) -> { + dispose(); + }); + + buttonPanel.add(btnApply); + buttonPanel.add(btnCancel); + buttonPanel.add(btnOk); + + getContentPane().add(tabbedPane, BorderLayout.CENTER); + getContentPane().add(buttonPanel, BorderLayout.SOUTH); + } + + private JPanel createAppearancePanel() { + JPanel panel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(10, 10, 10, 10); + gbc.anchor = GridBagConstraints.WEST; + + // Font family + gbc.gridx = 0; + gbc.gridy = 0; + JLabel fontLabel = new JLabel("Terminal Font:"); + panel.add(fontLabel, gbc); + + String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); + fontBox = new JComboBox<>(fonts); + // Find default + String currentFont = Settings.getFontFamily(); + for (int i = 0; i < fonts.length; i++) { + if (fonts[i].equalsIgnoreCase(currentFont)) { + fontBox.setSelectedIndex(i); + break; + } + } + gbc.gridx = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + panel.add(fontBox, gbc); + + // Font size + gbc.gridx = 0; + gbc.gridy = 1; + gbc.fill = GridBagConstraints.NONE; + JLabel sizeLabel = new JLabel("Font Size:"); + panel.add(sizeLabel, gbc); + + SpinnerNumberModel sizeModel = new SpinnerNumberModel(Settings.getFontSize(), 8, 72, 1); + fontSizeSpinner = new JSpinner(sizeModel); + gbc.gridx = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + panel.add(fontSizeSpinner, gbc); + + // Fill remaining space + gbc.gridy = 2; + gbc.weighty = 1.0; + panel.add(Box.createGlue(), gbc); + + return panel; + } + + private JPanel createBehaviorPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(10, 10, 10, 10); + gbc.anchor = GridBagConstraints.WEST; + + gbc.gridx = 0; + gbc.gridy = 0; + JLabel actionLabel = new JLabel("Startup Action:"); + panel.add(actionLabel, gbc); + + startupBehaviorBox = new JComboBox<>(Settings.StartupBehavior.values()); + startupBehaviorBox.setSelectedItem(Settings.getStartupBehavior()); + gbc.gridx = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + panel.add(startupBehaviorBox, gbc); + + // Auto-connect panel + autoConnectPanel = new JPanel(new GridBagLayout()); + GridBagConstraints acGbc = new GridBagConstraints(); + acGbc.insets = new Insets(5, 5, 5, 5); + acGbc.anchor = GridBagConstraints.WEST; + + JLabel hostLabel = new JLabel("Host:"); + acGbc.gridx = 0; + acGbc.gridy = 0; + autoConnectPanel.add(hostLabel, acGbc); + + hostField = new JTextField(Settings.getAutoConnectHost(), 15); + acGbc.gridx = 1; + autoConnectPanel.add(hostField, acGbc); + + JLabel portLabel = new JLabel("Port:"); + acGbc.gridx = 2; + autoConnectPanel.add(portLabel, acGbc); + + portField = new JTextField(String.valueOf(Settings.getAutoConnectPort()), 4); + acGbc.gridx = 3; + autoConnectPanel.add(portField, acGbc); + + gbc.gridx = 0; + gbc.gridy = 1; + gbc.gridwidth = 2; + panel.add(autoConnectPanel, gbc); + + // Toggle visibility + startupBehaviorBox.addActionListener(e -> { + autoConnectPanel.setVisible(startupBehaviorBox.getSelectedItem() == Settings.StartupBehavior.AUTO_CONNECT); + }); + autoConnectPanel.setVisible(Settings.getStartupBehavior() == Settings.StartupBehavior.AUTO_CONNECT); + + // Placeholder for potentially more behavior options below + gbc.gridy = 2; + gbc.weighty = 1.0; + panel.add(Box.createGlue(), gbc); + + return panel; + } + + private JPanel createAdvancedPanel() { + JPanel panel = new JPanel(new BorderLayout()); + + JTabbedPane advancedTabs = new JTabbedPane(); + advancedTabs.addTab("Colors", createColorsPanel()); + advancedTabs.addTab("Keymap", createKeymapPanel()); + + panel.add(advancedTabs, BorderLayout.CENTER); + return panel; + } + + private JPanel createColorsPanel() { + JPanel main = new JPanel(new BorderLayout()); + + // Host colors grid (16 colors) + JPanel hostPanel = new JPanel(new GridLayout(2, 8, 5, 5)); + hostPanel.setBorder(BorderFactory.createTitledBorder("Host Colors")); + + for (int i = 0; i < 16; i++) { + Color init = org.pubvm.j3270.config.Settings.getColorOverride(i, TerminalPanel.DEFAULT_HOST_COLORS[i]); + tempHostColors[i] = init; + JPanel cb = new JPanel(); + cb.setBackground(init); + cb.setPreferredSize(new Dimension(30, 30)); + cb.setBorder(new LineBorder(Color.DARK_GRAY)); + + final int index = i; + cb.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + Color c = JColorChooser.showDialog(main, "Select Color " + index, tempHostColors[index]); + if (c != null) { + tempHostColors[index] = c; + cb.setBackground(c); + } + } + }); + hostPanel.add(cb); + } + + // Mono colors + JPanel monoPanel = new JPanel(new GridLayout(2, 3, 5, 5)); + monoPanel.setBorder(BorderFactory.createTitledBorder("Monochrome / BG Colors")); + + String[] monoKeys = {"NORMAL", "INTENSIFY", "PROTECTED", "PROTECTED_HIGH", "BACKGROUND"}; + Color[] monoDefs = {TerminalPanel.DEFAULT_MONO_NORMAL, TerminalPanel.DEFAULT_MONO_INTENSIFY, + TerminalPanel.DEFAULT_MONO_PROTECTED, TerminalPanel.DEFAULT_MONO_PROTECTED_HIGH, TerminalPanel.DEFAULT_BG_COLOR}; + + for (int i = 0; i < monoKeys.length; i++) { + String mk = monoKeys[i]; + Color init = org.pubvm.j3270.config.Settings.getMonoColorOverride(mk, monoDefs[i]); + tempMonoColors.put(mk, init); + + JPanel p = new JPanel(new BorderLayout()); + p.add(new JLabel(mk, SwingConstants.CENTER), BorderLayout.NORTH); + JPanel cb = new JPanel(); + cb.setBackground(init); + cb.setPreferredSize(new Dimension(40, 40)); + cb.setBorder(new LineBorder(Color.DARK_GRAY)); + + cb.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + Color c = JColorChooser.showDialog(main, "Select " + mk, tempMonoColors.get(mk)); + if (c != null) { + tempMonoColors.put(mk, c); + cb.setBackground(c); + } + } + }); + p.add(cb, BorderLayout.CENTER); + monoPanel.add(p); + } + + JPanel resetPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton btnResetColors = new JButton("Reset to Defaults"); + btnResetColors.addActionListener(e -> { + for (int i=0; i<16; i++) { + tempHostColors[i] = TerminalPanel.DEFAULT_HOST_COLORS[i]; + } + for (int i=0; i> captureKey = (title, callback) -> { + JDialog captureDialog = new JDialog(this, title, true); + captureDialog.setSize(320, 100); + captureDialog.setLocationRelativeTo(this); + JLabel lbl = new JLabel("Press any key combination now...", SwingConstants.CENTER); + captureDialog.add(lbl); + captureDialog.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + int code = e.getKeyCode(); + if (code == KeyEvent.VK_SHIFT || code == KeyEvent.VK_CONTROL || + code == KeyEvent.VK_ALT || code == KeyEvent.VK_META) return; + KeyStroke ks = KeyStroke.getKeyStrokeForEvent(e); + if (ks != null) { + String s = ks.toString().replace("pressed ", ""); + callback.accept(s); + captureDialog.dispose(); + } + } + }); + captureDialog.setVisible(true); + }; + + // Replace Binding — sets the action to exactly one new key + JButton btnReplace = new JButton("Replace Binding"); + btnReplace.addActionListener(e -> { + int row = table.getSelectedRow(); + if (row < 0) return; + String action = (String) keymapModel.getValueAt(row, 0); + captureKey.accept("Press key for " + action, s -> { + tempKeyBindings.put(action, s); + keymapModel.setValueAt(s, row, 1); + }); + }); + btnPanel.add(btnReplace); + + // Add Binding — appends an additional key to the existing binding(s) + JButton btnAdd = new JButton("Add Binding"); + btnAdd.addActionListener(e -> { + int row = table.getSelectedRow(); + if (row < 0) return; + String action = (String) keymapModel.getValueAt(row, 0); + captureKey.accept("Press additional key for " + action, s -> { + String existing = tempKeyBindings.get(action); + String updated; + if (existing == null || existing.isEmpty() || "UNBOUND".equals(existing)) { + updated = s; + } else { + updated = existing + ", " + s; + } + tempKeyBindings.put(action, updated); + keymapModel.setValueAt(updated, row, 1); + }); + }); + btnPanel.add(btnAdd); + + // Remove Last — removes the last comma-separated binding entry + JButton btnRemoveLast = new JButton("Remove Last"); + btnRemoveLast.addActionListener(e -> { + int row = table.getSelectedRow(); + if (row < 0) return; + String action = (String) keymapModel.getValueAt(row, 0); + String existing = tempKeyBindings.get(action); + if (existing == null || "UNBOUND".equals(existing)) return; + int lastComma = existing.lastIndexOf(','); + String updated; + if (lastComma > 0) { + updated = existing.substring(0, lastComma).trim(); + } else { + updated = "UNBOUND"; + } + tempKeyBindings.put(action, updated); + keymapModel.setValueAt(updated, row, 1); + }); + btnPanel.add(btnRemoveLast); + + // Unbind All — clears all bindings for the action + JButton btnUnbind = new JButton("Unbind All"); + btnUnbind.addActionListener(e -> { + int row = table.getSelectedRow(); + if (row < 0) return; + String action = (String) keymapModel.getValueAt(row, 0); + tempKeyBindings.put(action, "UNBOUND"); + keymapModel.setValueAt("UNBOUND", row, 1); + }); + btnPanel.add(btnUnbind); + + // Reset Keymaps — restore all defaults + JButton btnReset = new JButton("Reset All"); + btnReset.addActionListener(e -> { + for (int row = 0; row < keymapModel.getRowCount(); row++) { + String action = (String) keymapModel.getValueAt(row, 0); + String def = getDefaultBinding(action); + tempKeyBindings.put(action, def); + keymapModel.setValueAt(def, row, 1); + } + }); + btnPanel.add(btnReset); + + main.add(new JScrollPane(table), BorderLayout.CENTER); + main.add(btnPanel, BorderLayout.SOUTH); + + return main; + } + + /** Returns the factory-default binding for a given action name. */ + private String getDefaultBinding(String action) { + if (action.startsWith("PF")) { + int n = Integer.parseInt(action.substring(2)); + return n <= 12 ? "F" + n : "shift F" + (n - 12); + } + if (action.startsWith("PA")) return "alt " + action.substring(2); + if (action.equals("CLEAR")) return "alt C"; + return action; // nav keys default to their own name + } + + private boolean applySettings() { + try { + // Apply Appearance + String fontFam = (String) fontBox.getSelectedItem(); + if (fontFam != null) { + Settings.setFontFamily(fontFam); + } + Settings.setFontSize((Integer) fontSizeSpinner.getValue()); + + // Apply Behavior + Settings.StartupBehavior behavior = (Settings.StartupBehavior) startupBehaviorBox.getSelectedItem(); + if (behavior != null) { + Settings.setStartupBehavior(behavior); + } + if (behavior == Settings.StartupBehavior.AUTO_CONNECT) { + Settings.setAutoConnectHost(hostField.getText().trim()); + try { + Settings.setAutoConnectPort(Integer.parseInt(portField.getText().trim())); + } catch (NumberFormatException ignored) { + } + } + + // Propagate visual changes to the app + // Save Colors + for (int i=0; i<16; i++) { + org.pubvm.j3270.config.Settings.setColorOverride(i, tempHostColors[i]); + } + for (Map.Entry entry : tempMonoColors.entrySet()) { + org.pubvm.j3270.config.Settings.setMonoColorOverride(entry.getKey(), entry.getValue()); + } + + // Save Keybindings + for (Map.Entry entry : tempKeyBindings.entrySet()) { + org.pubvm.j3270.config.Settings.setKeyBinding(entry.getKey(), entry.getValue()); + } + + parentApp.getTerminalPanel().reloadSettings(); + + return true; + } catch (Exception e) { + JOptionPane.showMessageDialog(this, "Failed to completely apply settings: " + e.getMessage(), "Error", + JOptionPane.ERROR_MESSAGE); + return false; + } + } +} diff --git a/j3270/src/main/java/org/pubvm/j3270/ui/StatusBar.java b/j3270/src/main/java/org/pubvm/j3270/ui/StatusBar.java new file mode 100644 index 0000000..fa6a038 --- /dev/null +++ b/j3270/src/main/java/org/pubvm/j3270/ui/StatusBar.java @@ -0,0 +1,140 @@ +package org.pubvm.j3270.ui; + +import org.lib3270j.ConnectionState; +import org.lib3270j.Telnet3270Client; +import org.lib3270j.screen.ScreenBuffer; + +import javax.swing.*; +import java.awt.*; + +/** + * Status bar displaying connection state, cursor position, timing, and lock + * status. + * Equivalent to the OIA (Operator Information Area) on a real 3270 terminal. + */ +public class StatusBar extends JPanel { + + private final JLabel connectionStatus; + private final JLabel cursorPosition; + private final JLabel luName; + private final JLabel lockStatus; + private final JLabel modelInfo; + + private Telnet3270Client client; + + // OIA colors + private static final Color OIA_BG = new Color(20, 20, 20); + private static final Color OIA_FG = new Color(50, 205, 50); + private static final Color OIA_DIM = new Color(80, 80, 80); + private static final Color OIA_ALERT = new Color(255, 80, 80); + + public StatusBar() { + setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + setBackground(OIA_BG); + setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(40, 40, 40))); + setPreferredSize(new Dimension(800, 22)); + + Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12); + + connectionStatus = createLabel("Not Connected", oiaFont, OIA_DIM); + luName = createLabel("", oiaFont, OIA_FG); + lockStatus = createLabel("", oiaFont, OIA_ALERT); + modelInfo = createLabel("", oiaFont, OIA_DIM); + cursorPosition = createLabel("001/001", oiaFont, OIA_FG); + + add(Box.createHorizontalStrut(6)); + add(connectionStatus); + add(Box.createHorizontalStrut(12)); + add(luName); + add(Box.createHorizontalStrut(12)); + add(lockStatus); + add(Box.createHorizontalGlue()); + add(modelInfo); + add(Box.createHorizontalStrut(12)); + add(cursorPosition); + add(Box.createHorizontalStrut(6)); + } + + private JLabel createLabel(String text, Font font, Color fg) { + JLabel label = new JLabel(text); + label.setFont(font); + label.setForeground(fg); + return label; + } + + public void setClient(Telnet3270Client client) { + this.client = client; + } + + public void updateStatus() { + if (client == null) + return; + + // Connection state + ConnectionState state = client.getConnectionState(); + switch (state) { + case NOT_CONNECTED: + connectionStatus.setText("Not Connected"); + connectionStatus.setForeground(OIA_DIM); + break; + case TCP_PENDING: + case TELNET_PENDING: + connectionStatus.setText("Connecting..."); + connectionStatus.setForeground(OIA_ALERT); + break; + case CONNECTED_3270: + connectionStatus.setText("TN3270"); + connectionStatus.setForeground(OIA_FG); + break; + case CONNECTED_TN3270E: + connectionStatus.setText("TN3270E"); + connectionStatus.setForeground(OIA_FG); + break; + case CONNECTED_SSCP: + connectionStatus.setText("SSCP-LU"); + connectionStatus.setForeground(OIA_FG); + break; + case CONNECTED_NVT: + case CONNECTED_NVT_CHAR: + case CONNECTED_E_NVT: + connectionStatus.setText("NVT"); + connectionStatus.setForeground(OIA_FG); + break; + case CONNECTED_UNBOUND: + connectionStatus.setText("Unbound"); + connectionStatus.setForeground(new Color(255, 255, 80)); + break; + default: + connectionStatus.setText(state.name()); + connectionStatus.setForeground(OIA_DIM); + break; + } + + // LU name + String lu = ""; + if (client.getConnectionState().isTn3270e()) { + // lu would come from FSM + } + luName.setText(lu); + + // Lock status + if (client.getInputProcessor().isKeyboardLocked()) { + lockStatus.setText("X SYSTEM"); + lockStatus.setForeground(OIA_ALERT); + } else if (client.getInputProcessor().isInsertMode()) { + lockStatus.setText("INSERT"); + lockStatus.setForeground(OIA_FG); + } else { + lockStatus.setText(""); + } + + // Model info + modelInfo.setText(client.getConfig().getModel().getTerminalType()); + + // Cursor position + ScreenBuffer sb = client.getScreenBuffer(); + int row = sb.getCursorRow() + 1; + int col = sb.getCursorCol() + 1; + cursorPosition.setText(String.format("%03d/%03d", row, col)); + } +} diff --git a/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java b/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java new file mode 100644 index 0000000..3163aab --- /dev/null +++ b/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java @@ -0,0 +1,581 @@ +package org.pubvm.j3270.ui; + +import org.lib3270j.Telnet3270Client; +import org.lib3270j.screen.ExtendedAttribute; +import org.lib3270j.screen.ScreenBuffer; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; + +import static org.lib3270j.protocol.DS3270Constants.*; + +/** + * Custom JPanel that renders the 3270 screen buffer. + * Supports colors, bold, underline, reverse, blink, and all 3278/3279 + * attributes. + */ +public class TerminalPanel extends JPanel { + + private Telnet3270Client client; + + // Font and cell dimensions + private Font terminalFont; + private int cellWidth; + private int cellHeight; + private int fontAscent; + private int fontDescent; + private int padding = 4; + private int currentFontSize = 16; + + // Cursor state + private boolean cursorVisible = true; + private Timer blinkTimer; + + // Host color mapping + private final Color[] hostColors = new Color[16]; + + // 3278 monochrome colors (based on field attributes) + private Color monoNormal; + private Color monoIntensify; + private Color monoProtected; + private Color monoProtectedHigh; + + // Background + private Color bgColor; + + // Default Host color mapping + public static final Color[] DEFAULT_HOST_COLORS = { + new Color(0, 0, 0), // 0: Neutral Black + new Color(80, 120, 255), // 1: Blue + new Color(255, 50, 50), // 2: Red + new Color(255, 130, 180), // 3: Pink + new Color(50, 205, 50), // 4: Green + new Color(64, 224, 208), // 5: Turquoise + new Color(255, 255, 80), // 6: Yellow + new Color(255, 255, 255), // 7: Neutral White + new Color(0, 0, 0), // 8: Black + new Color(30, 60, 180), // 9: Deep Blue + new Color(255, 165, 0), // 10: Orange + new Color(180, 130, 255), // 11: Purple + new Color(144, 238, 144), // 12: Pale Green + new Color(175, 238, 238), // 13: Pale Turquoise + new Color(170, 170, 170), // 14: Grey + new Color(255, 255, 255), // 15: White + }; + + // Default 3278 monochrome colors + public static final Color DEFAULT_MONO_NORMAL = new Color(50, 205, 50); + public static final Color DEFAULT_MONO_INTENSIFY = new Color(255, 255, 255); + public static final Color DEFAULT_MONO_PROTECTED = new Color(80, 120, 255); + public static final Color DEFAULT_MONO_PROTECTED_HIGH = new Color(255, 255, 255); + + // Default Background + public static final Color DEFAULT_BG_COLOR = new Color(10, 10, 10); + + public TerminalPanel() { + setupColors(); + setBackground(bgColor); + setFocusable(true); + setDoubleBuffered(true); + setFocusTraversalKeysEnabled(false); + + // Use key bindings instead of KeyListener for reliable key handling + // This avoids focus/event issues with JScrollPane + setOpaque(true); + setupKeyBindings(); + setupFont(); + setupCursorBlink(); + + // Handle mouse clicks to position cursor and grab focus + addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + requestFocusInWindow(); + if (client != null && client.getConnectionState().isFullSession()) { + int col = (e.getX() - padding) / cellWidth; + int row = (e.getY() - padding) / cellHeight; + ScreenBuffer sb = client.getScreenBuffer(); + if (row >= 0 && row < sb.getRows() && col >= 0 && col < sb.getCols()) { + sb.setCursorAddress(row * sb.getCols() + col); + repaint(); + } + } + } + }); + } + + /** + * Use InputMap/ActionMap (key bindings) instead of KeyListener. + * This works reliably even inside a JScrollPane — the WHEN_FOCUSED + * condition ensures our panel receives all key events when focused. + */ + private void bindKeyToMap(InputMap im, String action, String bindingStr) { + if ("UNBOUND".equals(bindingStr) || bindingStr == null || bindingStr.isEmpty()) { + return; + } + // Support multiple bindings separated by commas + String[] bindings = bindingStr.split(","); + for (String binding : bindings) { + binding = binding.trim(); + if (!binding.isEmpty() && !"UNBOUND".equals(binding)) { + KeyStroke ks = KeyStroke.getKeyStroke(binding); + if (ks != null) { + im.put(ks, "j3270-" + action); + } + } + } + } + + private void setupKeyBindings() { + InputMap im = getInputMap(JComponent.WHEN_FOCUSED); + ActionMap am = getActionMap(); + + im.clear(); + am.clear(); + + // Block the scroll pane from handling Tab, arrows, Page keys + String[] navKeys = { "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT", + "PAGE_UP", "PAGE_DOWN", "HOME", "END", "ENTER", + "ESCAPE", "INSERT", "DELETE", "BACK_SPACE" }; + for (String key : navKeys) { + String binding = org.pubvm.j3270.config.Settings.getKeyBinding(key, key); + bindKeyToMap(im, key, binding); + } + + // PF keys 1-24 + // PF1-12 default to F1-F12, PF13-24 default to shift F1-shift F12 + for (int i = 1; i <= 24; i++) { + String defaultBinding; + if (i <= 12) { + defaultBinding = "F" + i; + } else { + defaultBinding = "shift F" + (i - 12); + } + String binding = org.pubvm.j3270.config.Settings.getKeyBinding("PF" + i, defaultBinding); + bindKeyToMap(im, "PF" + i, binding); + } + + // PA keys: Alt+1, Alt+2, Alt+3 + String pa1Def = "alt 1"; + String pa2Def = "alt 2"; + String pa3Def = "alt 3"; + bindKeyToMap(im, "PA1", org.pubvm.j3270.config.Settings.getKeyBinding("PA1", pa1Def)); + bindKeyToMap(im, "PA2", org.pubvm.j3270.config.Settings.getKeyBinding("PA2", pa2Def)); + bindKeyToMap(im, "PA3", org.pubvm.j3270.config.Settings.getKeyBinding("PA3", pa3Def)); + + // Clear + bindKeyToMap(im, "CLEAR", org.pubvm.j3270.config.Settings.getKeyBinding("CLEAR", "alt C")); + + // Create actions for all bound keys + am.put("j3270-ENTER", createAction(this::handleEnter)); + am.put("j3270-ESCAPE", createAction(this::handleReset)); + am.put("j3270-TAB", createAction(() -> handleTab(false))); + am.put("j3270-shift TAB", createAction(() -> handleTab(true))); + am.put("j3270-UP", createAction(() -> handleCursor("up"))); + am.put("j3270-DOWN", createAction(() -> handleCursor("down"))); + am.put("j3270-LEFT", createAction(() -> handleCursor("left"))); + am.put("j3270-RIGHT", createAction(() -> handleCursor("right"))); + am.put("j3270-HOME", createAction(() -> handleCursor("home"))); + am.put("j3270-PAGE_UP", createAction(() -> handlePF(7))); + am.put("j3270-PAGE_DOWN", createAction(() -> handlePF(8))); + am.put("j3270-END", createAction(this::handleEraseEOF)); + am.put("j3270-DELETE", createAction(this::handleDelete)); + am.put("j3270-BACK_SPACE", createAction(this::handleBackspace)); + am.put("j3270-INSERT", createAction(this::handleInsert)); + am.put("j3270-CLEAR", createAction(this::handleClear)); + + for (int i = 1; i <= 24; i++) { + final int pf = i; + am.put("j3270-PF" + i, createAction(() -> handlePF(pf))); + } + + am.put("j3270-PA1", createAction(() -> handlePA(1))); + am.put("j3270-PA2", createAction(() -> handlePA(2))); + am.put("j3270-PA3", createAction(() -> handlePA(3))); + + // For printable character input, we override processKeyEvent + enableEvents(AWTEvent.KEY_EVENT_MASK); + } + + @Override + protected void processKeyEvent(KeyEvent e) { + // Handle character typing via processKeyEvent to capture ALL typed chars + if (e.getID() == KeyEvent.KEY_TYPED) { + char ch = e.getKeyChar(); + if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED + && !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) { + if (client != null && client.getConnectionState().isFullSession()) { + client.typeCharacter(ch); + refreshScreen(); + e.consume(); + return; + } + } + } + super.processKeyEvent(e); + } + + private AbstractAction createAction(Runnable r) { + return new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + r.run(); + } + }; + } + + // ========== Key action handlers ========== + + private void handleEnter() { + if (client != null && client.getConnectionState().isFullSession()) { + client.sendEnter(); + refreshScreen(); + } + } + + private void handleReset() { + if (client != null) { + client.reset(); + refreshScreen(); + } + } + + private void handleTab(boolean shift) { + if (client != null && client.getConnectionState().isFullSession()) { + if (shift) + client.backTab(); + else + client.tab(); + refreshScreen(); + } + } + + private void handleCursor(String dir) { + if (client != null && client.getConnectionState().isFullSession()) { + switch (dir) { + case "up": + client.cursorUp(); + break; + case "down": + client.cursorDown(); + break; + case "left": + client.cursorLeft(); + break; + case "right": + client.cursorRight(); + break; + case "home": + client.cursorHome(); + break; + } + refreshScreen(); + } + } + + private void handlePF(int n) { + if (client != null && client.getConnectionState().isFullSession()) { + client.sendPF(n); + refreshScreen(); + } + } + + private void handlePA(int n) { + if (client != null && client.getConnectionState().isFullSession()) { + client.sendPA(n); + refreshScreen(); + } + } + + private void handleEraseEOF() { + if (client != null && client.getConnectionState().isFullSession()) { + client.getInputProcessor().eraseEof(); + refreshScreen(); + } + } + + private void handleDelete() { + if (client != null && client.getConnectionState().isFullSession()) { + client.getInputProcessor().deleteChar(); + refreshScreen(); + } + } + + private void handleBackspace() { + if (client != null && client.getConnectionState().isFullSession()) { + client.getInputProcessor().backspace(); + refreshScreen(); + } + } + + private void handleInsert() { + if (client != null) { + var ip = client.getInputProcessor(); + ip.setInsertMode(!ip.isInsertMode()); + refreshScreen(); + } + } + + private void handleClear() { + if (client != null && client.getConnectionState().isFullSession()) { + client.sendClear(); + refreshScreen(); + } + } + + private void refreshScreen() { + repaint(); + // Notify parent to update status bar too + Container parent = getParent(); + while (parent != null) { + if (parent instanceof JFrame) { + parent.repaint(); + break; + } + parent = parent.getParent(); + } + } + + private void setupFont() { + currentFontSize = org.pubvm.j3270.config.Settings.getFontSize(); + String fontFamily = org.pubvm.j3270.config.Settings.getFontFamily(); + + terminalFont = new Font(fontFamily, Font.PLAIN, currentFontSize); + if (terminalFont.getFamily().equals("Dialog") && !fontFamily.equals("Dialog")) { + // Fallback + terminalFont = new Font(Font.MONOSPACED, Font.PLAIN, currentFontSize); + } + updateCellSize(); + } + + private void setupColors() { + for (int i = 0; i < 16; i++) { + hostColors[i] = org.pubvm.j3270.config.Settings.getColorOverride(i, DEFAULT_HOST_COLORS[i]); + } + monoNormal = org.pubvm.j3270.config.Settings.getMonoColorOverride("NORMAL", DEFAULT_MONO_NORMAL); + monoIntensify = org.pubvm.j3270.config.Settings.getMonoColorOverride("INTENSIFY", DEFAULT_MONO_INTENSIFY); + monoProtected = org.pubvm.j3270.config.Settings.getMonoColorOverride("PROTECTED", DEFAULT_MONO_PROTECTED); + monoProtectedHigh = org.pubvm.j3270.config.Settings.getMonoColorOverride("PROTECTED_HIGH", DEFAULT_MONO_PROTECTED_HIGH); + bgColor = org.pubvm.j3270.config.Settings.getMonoColorOverride("BACKGROUND", DEFAULT_BG_COLOR); + setBackground(bgColor); + } + + public void reloadSettings() { + setupColors(); + setupKeyBindings(); + setupFont(); + revalidate(); + repaint(); + } + + public void setFontSize(int size) { + currentFontSize = size; + org.pubvm.j3270.config.Settings.setFontSize(size); + terminalFont = terminalFont.deriveFont((float) size); + updateCellSize(); + revalidate(); + repaint(); + } + + public int getFontSize() { + return currentFontSize; + } + + private void updateCellSize() { + FontMetrics fm = getFontMetrics(terminalFont); + cellWidth = fm.charWidth('M'); + cellHeight = fm.getHeight(); + fontAscent = fm.getAscent(); + fontDescent = fm.getDescent(); + } + + private void setupCursorBlink() { + blinkTimer = new Timer(530, e -> { + cursorVisible = !cursorVisible; + repaint(); + }); + blinkTimer.start(); + } + + public void setClient(Telnet3270Client client) { + this.client = client; + } + + @Override + public Dimension getPreferredSize() { + if (client != null) { + ScreenBuffer sb = client.getScreenBuffer(); + // Always size to the MAXIMUM (alternate) screen dimensions, + // matching how x3270 works — the window shows the full model size + int displayCols = sb.getMaxCols(); + int displayRows = sb.getMaxRows(); + return new Dimension(displayCols * cellWidth + padding * 2, + displayRows * cellHeight + padding * 2); + } + return new Dimension(80 * cellWidth + padding * 2, 24 * cellHeight + padding * 2); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + if (client == null) + return; + + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); + + // Manually clear the entire graphics bounds to ensure no ghosting on + // resize/clip + g2.setColor(bgColor); + g2.fillRect(0, 0, getWidth(), getHeight()); + + ScreenBuffer sb = client.getScreenBuffer(); + int rows = sb.getRows(); + int cols = sb.getCols(); + boolean isColorModel = client.getConfig().getModel().isColor(); + + // Track current field attribute for monochrome color decisions + byte currentFA = 0; + ExtendedAttribute currentFieldEa = null; + + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + int baddr = row * cols + col; + ExtendedAttribute ea = sb.getCell(baddr); + + int x = padding + col * cellWidth; + int y = padding + row * cellHeight; + + // Determine colors and attributes + Color fgColor; + Color bgColor; + boolean bold = false; + boolean underline = false; + boolean reverse = false; + + if (ea.isFieldAttribute()) { + currentFA = ea.fa; + currentFieldEa = ea; + // Field attributes display as blanks + g2.setColor(this.bgColor); + g2.fillRect(x, y, cellWidth, cellHeight); + continue; + } + + // Determine foreground color + if (isColorModel) { + fgColor = getColorForAttribute(ea, currentFieldEa, currentFA); + bgColor = getBackgroundForAttribute(ea, currentFieldEa); + } else { + fgColor = getMonoColor(currentFA); + bgColor = this.bgColor; + } + + // Graphics rendition + byte gr = ea.gr != 0 ? ea.gr : (currentFieldEa != null ? currentFieldEa.gr : 0); + if (gr != 0) { + if ((gr & GR_INTENSIFY) != 0) + bold = true; + if ((gr & GR_UNDERLINE) != 0) + underline = true; + if ((gr & GR_REVERSE) != 0) + reverse = true; + } + + // Field attribute implicit intensify + if (faIsHigh(currentFA & 0xFF)) + bold = true; + + // Handle invisible fields (zero intensity) + if (faIsZero(currentFA & 0xFF)) { + g2.setColor(this.bgColor); + g2.fillRect(x, y, cellWidth, cellHeight); + continue; + } + + // Apply reverse video + if (reverse) { + Color tmp = fgColor; + fgColor = bgColor; + bgColor = tmp; + } + + // Draw background + g2.setColor(bgColor); + g2.fillRect(x, y, cellWidth, cellHeight); + + // Draw character + char ch = ea.ucs4; + if (ch > 0x20 && ch != 0xFF) { + Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont; + g2.setFont(f); + g2.setColor(fgColor); + g2.drawString(String.valueOf(ch), x, y + fontAscent); + } + + // Draw underline + if (underline) { + g2.setColor(fgColor); + g2.drawLine(x, y + cellHeight - fontDescent, + x + cellWidth - 1, y + cellHeight - fontDescent); + } + } + } + + // Draw cursor + if (cursorVisible && client.getConnectionState().isFullSession()) { + int curAddr = sb.getCursorAddress(); + int curRow = curAddr / cols; + int curCol = curAddr % cols; + int cx = padding + curCol * cellWidth; + int cy = padding + curRow * cellHeight; + + g2.setColor(new Color(255, 255, 255, 180)); + g2.setXORMode(bgColor); + g2.fillRect(cx, cy, cellWidth, cellHeight); + g2.setPaintMode(); + } + } + + private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) { + int fg = ea.fg != 0 ? (ea.fg & 0xFF) + : (currentFieldEa != null && currentFieldEa.fg != 0 ? (currentFieldEa.fg & 0xFF) : 0); + if (fg >= 0xf0 && fg <= 0xff) { + return hostColors[fg - 0xf0]; + } + if (faIsProtected(currentFA & 0xFF)) { + return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_WHITE] : hostColors[HOST_COLOR_BLUE]; + } + return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_RED] : hostColors[HOST_COLOR_GREEN]; + } + + private Color getBackgroundForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa) { + int bg = ea.bg != 0 ? (ea.bg & 0xFF) + : (currentFieldEa != null && currentFieldEa.bg != 0 ? (currentFieldEa.bg & 0xFF) : 0); + if (bg >= 0xf0 && bg <= 0xff) { + int idx = bg - 0xf0; + // Neutral black (0xf0) and black (0xf8) should use window bgColor + // to avoid visible seams between field bg and window bg + if (idx == HOST_COLOR_NEUTRAL_BLACK || idx == HOST_COLOR_BLACK) { + return bgColor; + } + return hostColors[idx]; + } + return bgColor; + } + + private Color getMonoColor(byte fa) { + if (faIsProtected(fa & 0xFF)) { + return faIsHigh(fa & 0xFF) ? monoProtectedHigh : monoProtected; + } + return faIsHigh(fa & 0xFF) ? monoIntensify : monoNormal; + } + + public void dispose() { + if (blinkTimer != null) + blinkTimer.stop(); + } +} diff --git a/lib3270j/build.gradle b/lib3270j/build.gradle new file mode 100644 index 0000000..164eaac --- /dev/null +++ b/lib3270j/build.gradle @@ -0,0 +1,13 @@ +plugins { + id 'java-library' +} + +dependencies { +} + +jar { + manifest { + attributes 'Implementation-Title': 'lib3270j', + 'Implementation-Version': archiveVersion + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/ConnectionConfig.java b/lib3270j/src/main/java/org/lib3270j/ConnectionConfig.java new file mode 100644 index 0000000..303709f --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/ConnectionConfig.java @@ -0,0 +1,67 @@ +package org.lib3270j; + +/** + * Configuration for a 3270 terminal connection. + */ +public class ConnectionConfig { + + private String host; + private int port = 23; + private TerminalModel model = TerminalModel.IBM_3279_4; + private String luName = null; + private boolean extendedDataStream = true; + private boolean useTls = false; + private int connectTimeoutMs = 15000; + private int nopIntervalSeconds = 0; + private String terminalName = null; // override terminal type string + + public ConnectionConfig() {} + + public ConnectionConfig(String host, int port) { + this.host = host; + this.port = port; + } + + public ConnectionConfig(String host, int port, TerminalModel model) { + this.host = host; + this.port = port; + this.model = model; + } + + public String getHost() { return host; } + public void setHost(String host) { this.host = host; } + + public int getPort() { return port; } + public void setPort(int port) { this.port = port; } + + public TerminalModel getModel() { return model; } + public void setModel(TerminalModel model) { this.model = model; } + + public String getLuName() { return luName; } + public void setLuName(String luName) { this.luName = luName; } + + public boolean isExtendedDataStream() { return extendedDataStream; } + public void setExtendedDataStream(boolean ext) { this.extendedDataStream = ext; } + + public boolean isUseTls() { return useTls; } + public void setUseTls(boolean useTls) { this.useTls = useTls; } + + public int getConnectTimeoutMs() { return connectTimeoutMs; } + public void setConnectTimeoutMs(int ms) { this.connectTimeoutMs = ms; } + + public int getNopIntervalSeconds() { return nopIntervalSeconds; } + public void setNopIntervalSeconds(int s) { this.nopIntervalSeconds = s; } + + public String getTerminalName() { return terminalName; } + public void setTerminalName(String name) { this.terminalName = name; } + + /** + * Get the effective terminal type string to send during negotiation. + */ + public String getEffectiveTerminalType() { + if (terminalName != null) { + return terminalName; + } + return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType(); + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/ConnectionState.java b/lib3270j/src/main/java/org/lib3270j/ConnectionState.java new file mode 100644 index 0000000..d8c1199 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/ConnectionState.java @@ -0,0 +1,62 @@ +package org.lib3270j; + +/** + * Connection state machine states. + * Mirrors the cstate enum from globals.h in x3270. + */ +public enum ConnectionState { + NOT_CONNECTED, // No socket, unknown mode + RECONNECTING, // Delay before automatic reconnect + RESOLVING, // Resolving hostname + TCP_PENDING, // Socket connection pending + TLS_PENDING, // TLS negotiation pending + PROXY_PENDING, // Proxy negotiation pending + TELNET_PENDING, // Telnet negotiation pending + CONNECTED_NVT, // Connected in NVT line mode + CONNECTED_NVT_CHAR, // Connected in NVT character-at-a-time mode + CONNECTED_3270, // Connected in RFC 1576 TN3270 mode + CONNECTED_UNBOUND, // Connected in TN3270E mode, unbound + CONNECTED_E_NVT, // Connected in TN3270E NVT mode + CONNECTED_SSCP, // Connected in TN3270E SSCP-LU mode + CONNECTED_TN3270E; // Connected in TN3270E 3270 mode + + /** True if any kind of connection exists (even half-connected). */ + public boolean isConnected() { + return this.ordinal() > NOT_CONNECTED.ordinal(); + } + + /** True if in a half-connected state (resolving through telnet pending). */ + public boolean isHalfConnected() { + return this.ordinal() >= RESOLVING.ordinal() && this.ordinal() < CONNECTED_NVT.ordinal(); + } + + /** True if fully connected (past TCP pending). */ + public boolean isFullyConnected() { + return this.ordinal() > TCP_PENDING.ordinal(); + } + + /** True if in NVT mode (any flavor). */ + public boolean isNvt() { + return this == CONNECTED_NVT || this == CONNECTED_NVT_CHAR || this == CONNECTED_E_NVT; + } + + /** True if in 3270 mode (any flavor). */ + public boolean is3270() { + return this == CONNECTED_3270 || this == CONNECTED_TN3270E || this == CONNECTED_SSCP; + } + + /** True if in SSCP-LU mode. */ + public boolean isSscp() { + return this == CONNECTED_SSCP; + } + + /** True if in TN3270E mode (any submode). */ + public boolean isTn3270e() { + return this.ordinal() >= CONNECTED_UNBOUND.ordinal(); + } + + /** True if in a full data session (NVT or 3270). */ + public boolean isFullSession() { + return isNvt() || is3270(); + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/DiagnosticClient.java b/lib3270j/src/main/java/org/lib3270j/DiagnosticClient.java new file mode 100644 index 0000000..618b9d9 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/DiagnosticClient.java @@ -0,0 +1,174 @@ +package org.lib3270j; + +import org.lib3270j.screen.ScreenBuffer; +import org.lib3270j.screen.ExtendedAttribute; +import org.lib3270j.listener.*; + +import java.util.logging.*; + +/** + * Diagnostic: login as guest1/guest and trace the Welcome → MOTD transition. + */ +public class DiagnosticClient { + + static volatile int screenUpdateCount = 0; + static Telnet3270Client client; + + public static void main(String[] args) throws Exception { + Logger rootLogger = Logger.getLogger("org.lib3270j"); + rootLogger.setLevel(Level.ALL); + Handler handler = new ConsoleHandler(); + handler.setLevel(Level.ALL); + handler.setFormatter(new SimpleFormatter()); + rootLogger.addHandler(handler); + Logger.getLogger("").setLevel(Level.WARNING); + + String host = args.length >= 1 ? args[0] : "192.168.0.30"; + int port = args.length >= 2 ? Integer.parseInt(args[1]) : 3270; + + ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_2); + client = new Telnet3270Client(config); + + client.addConnectionListener(new ConnectionListener() { + @Override + public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) { + System.out.println(">>> STATE: " + oldState + " -> " + newState); + } + @Override + public void onConnectionError(String message) { + System.out.println(">>> ERROR: " + message); + } + @Override + public void onTN3270ENegotiated(String deviceType, String deviceName) { + System.out.println(">>> TN3270E: type=" + deviceType + " name=" + deviceName); + } + }); + + client.addScreenUpdateListener(new ScreenUpdateListener() { + @Override + public void onScreenUpdated() { + screenUpdateCount++; + System.out.println("\n>>> SCREEN UPDATE #" + screenUpdateCount); + dumpScreen(); + client.getInputProcessor().setKeyboardLocked(false); + } + @Override + public void onSoundAlarm() { + System.out.println(">>> ALARM"); + } + @Override + public void onScreenSizeChanged(int rows, int cols) { + System.out.println(">>> SCREEN SIZE: " + rows + "x" + cols); + } + }); + + System.out.println("=== Connecting to " + host + ":" + port + " ==="); + client.connect(); + + // Wait for login screen + waitForUpdates(5000); + + // Type guest1 at login field + System.out.println("\n=== Typing 'guest1' ==="); + typeAndWait("guest1", 2000); + + // Press Enter to submit login + System.out.println("\n=== Pressing Enter (submit login) ==="); + client.sendEnter(); + waitForUpdates(3000); + + // Check if we need to enter password + System.out.println("\n=== Typing password 'guest' ==="); + typeAndWait("guest", 2000); + client.sendEnter(); + waitForUpdates(5000); + + // Now we should see Welcome/allocations screen + // Wait for *** prompt + Thread.sleep(3000); + System.out.println("\n=== Current screen (should be Welcome/allocations with ***) ==="); + dumpScreen(); + + // Press Enter at *** to continue — THIS is where the screen should clear + System.out.println("\n=== Pressing Enter at *** (Welcome → MOTD transition) ==="); + client.sendEnter(); + waitForUpdates(5000); + + // Wait for more updates + Thread.sleep(3000); + System.out.println("\n=== After *** Enter — screen should have been cleared for MOTD ==="); + dumpScreen(); + + // If there's another *** prompt, press Enter again + System.out.println("\n=== Pressing Enter again ==="); + client.sendEnter(); + waitForUpdates(5000); + Thread.sleep(2000); + dumpScreen(); + + // One more Enter + System.out.println("\n=== Pressing Enter one more time ==="); + client.sendEnter(); + waitForUpdates(5000); + Thread.sleep(2000); + dumpScreen(); + + // Let it sit for any more data + Thread.sleep(3000); + + System.out.println("\n=== Final screen ==="); + dumpScreen(); + + client.disconnect(); + System.out.println(">>> Disconnected"); + } + + static void typeAndWait(String text, int waitMs) throws InterruptedException { + for (char c : text.toCharArray()) { + client.typeCharacter(c); + Thread.sleep(30); + } + Thread.sleep(waitMs); + } + + static void waitForUpdates(int timeoutMs) throws InterruptedException { + int start = screenUpdateCount; + long deadline = System.currentTimeMillis() + timeoutMs; + while (screenUpdateCount == start && System.currentTimeMillis() < deadline) { + Thread.sleep(100); + } + if (screenUpdateCount == start) { + System.out.println(">>> (no screen update within " + timeoutMs + "ms)"); + } + Thread.sleep(500); + } + + static void dumpScreen() { + ScreenBuffer sb = client.getScreenBuffer(); + int rows = sb.getRows(); + int cols = sb.getCols(); + System.out.println("--- Screen " + rows + "x" + cols + + " cursor=" + sb.getCursorAddress() + + " formatted=" + sb.isFormatted() + + " state=" + client.getConnectionState() + " ---"); + for (int r = 0; r < rows; r++) { + StringBuilder line = new StringBuilder(); + boolean hasContent = false; + for (int c = 0; c < cols; c++) { + ExtendedAttribute ea = sb.getCell(r * cols + c); + if (ea.isFieldAttribute()) { + line.append('|'); + } else if (ea.ucs4 > 0x20 && ea.ucs4 != 0xFF) { + line.append(ea.ucs4); + hasContent = true; + } else { + line.append(' '); + } + } + if (hasContent) { + System.out.println(String.format("%02d: %s", r, line.toString().stripTrailing())); + } + } + System.out.println("---"); + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java b/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java new file mode 100644 index 0000000..0d1dc31 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java @@ -0,0 +1,192 @@ +package org.lib3270j; + +import org.lib3270j.charset.EbcdicTranslator; +import org.lib3270j.datastream.DataStreamProcessor; +import org.lib3270j.input.InputProcessor; +import org.lib3270j.listener.ConnectionListener; +import org.lib3270j.listener.ScreenUpdateListener; +import org.lib3270j.screen.ScreenBuffer; +import org.lib3270j.telnet.TelnetConnection; +import org.lib3270j.telnet.TelnetFSM; + +import java.io.IOException; +import java.util.logging.Logger; + +/** + * Main API entry point for lib3270j. + * + * Provides a high-level interface for connecting to a TN3270 host, + * managing the screen buffer, and handling user input. + * + * Usage: + *
+ *   ConnectionConfig config = new ConnectionConfig("hostname", 23, TerminalModel.IBM_3279_4);
+ *   Telnet3270Client client = new Telnet3270Client(config);
+ *   client.addConnectionListener(myListener);
+ *   client.addScreenUpdateListener(myScreenListener);
+ *   client.connect();
+ *   // ... interact with screen ...
+ *   client.disconnect();
+ * 
+ */ +public class Telnet3270Client { + + private static final Logger log = Logger.getLogger(Telnet3270Client.class.getName()); + + private final ConnectionConfig config; + private final EbcdicTranslator translator; + private final ScreenBuffer screenBuffer; + private final DataStreamProcessor dsProcessor; + private final TelnetFSM fsm; + private final InputProcessor inputProcessor; + private TelnetConnection connection; + + public Telnet3270Client(ConnectionConfig config) { + this.config = config; + this.translator = new EbcdicTranslator(); + this.screenBuffer = new ScreenBuffer(config.getModel(), translator); + this.dsProcessor = new DataStreamProcessor(screenBuffer, translator); + this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor); + this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm); + + // Wire up the output sender: DSProcessor -> FSM -> TelnetConnection + dsProcessor.setOutputSender(fsm::send3270Data); + } + + /** + * Connect to the configured host. + * This method blocks until the TCP connection is established, + * then returns while telnet/TN3270E negotiation continues asynchronously. + */ + public void connect() throws IOException { + log.info("Connecting to " + config.getHost() + ":" + config.getPort() + + " model=" + config.getModel()); + + connection = new TelnetConnection(config, fsm); + fsm.setConnection(connection); + + // Establish TCP connection + connection.connect(); + + // Notify FSM that TCP is connected — begins telnet negotiation + fsm.onConnected(); + } + + /** + * Disconnect from the host. + */ + public void disconnect() { + if (connection != null) { + connection.disconnect(); + connection = null; + } + fsm.onDisconnect(); + } + + /** + * Check if connected (any state past TCP pending). + */ + public boolean isConnected() { + return connection != null && connection.isConnected(); + } + + // ========== Listener management ========== + + public void addConnectionListener(ConnectionListener l) { + fsm.addConnectionListener(l); + } + + public void addScreenUpdateListener(ScreenUpdateListener l) { + fsm.addScreenUpdateListener(l); + dsProcessor.addScreenUpdateListener(l); + } + + // ========== Screen access ========== + + /** Get the screen buffer for rendering. */ + public ScreenBuffer getScreenBuffer() { return screenBuffer; } + + /** Get the EBCDIC translator. */ + public EbcdicTranslator getTranslator() { return translator; } + + /** Get the current connection state. */ + public ConnectionState getConnectionState() { return fsm.getConnectionState(); } + + /** Get the input processor for keyboard operations. */ + public InputProcessor getInputProcessor() { return inputProcessor; } + + /** Get the connection config. */ + public ConnectionConfig getConfig() { return config; } + + // ========== Convenience input methods ========== + + /** Type a character at the cursor position. */ + public void typeCharacter(char ch) { inputProcessor.typeCharacter(ch); } + + /** Type a string at the cursor position. */ + public void typeString(String s) { + for (char ch : s.toCharArray()) { + inputProcessor.typeCharacter(ch); + } + } + + /** Send Enter key. */ + public void sendEnter() { + inputProcessor.sendAid(org.lib3270j.protocol.DS3270Constants.AID_ENTER); + } + + /** Send a PF key (1-24). */ + public void sendPF(int number) { + int aid; + switch (number) { + case 1: aid = 0xf1; break; case 2: aid = 0xf2; break; + case 3: aid = 0xf3; break; case 4: aid = 0xf4; break; + case 5: aid = 0xf5; break; case 6: aid = 0xf6; break; + case 7: aid = 0xf7; break; case 8: aid = 0xf8; break; + case 9: aid = 0xf9; break; case 10: aid = 0x7a; break; + case 11: aid = 0x7b; break; case 12: aid = 0x7c; break; + case 13: aid = 0xc1; break; case 14: aid = 0xc2; break; + case 15: aid = 0xc3; break; case 16: aid = 0xc4; break; + case 17: aid = 0xc5; break; case 18: aid = 0xc6; break; + case 19: aid = 0xc7; break; case 20: aid = 0xc8; break; + case 21: aid = 0xc9; break; case 22: aid = 0x4a; break; + case 23: aid = 0x4b; break; case 24: aid = 0x4c; break; + default: return; + } + inputProcessor.sendAid(aid); + } + + /** Send a PA key (1-3). */ + public void sendPA(int number) { + int aid; + switch (number) { + case 1: aid = 0x6c; break; + case 2: aid = 0x6e; break; + case 3: aid = 0x6b; break; + default: return; + } + inputProcessor.sendAid(aid); + } + + /** Send Clear key. */ + public void sendClear() { + inputProcessor.sendAid(org.lib3270j.protocol.DS3270Constants.AID_CLEAR); + } + + /** Move cursor up. */ + public void cursorUp() { inputProcessor.cursorUp(); } + /** Move cursor down. */ + public void cursorDown() { inputProcessor.cursorDown(); } + /** Move cursor left. */ + public void cursorLeft() { inputProcessor.cursorLeft(); } + /** Move cursor right. */ + public void cursorRight() { inputProcessor.cursorRight(); } + /** Move cursor to home position. */ + public void cursorHome() { inputProcessor.cursorHome(); } + /** Tab to next unprotected field. */ + public void tab() { inputProcessor.tab(); } + /** Back-tab to previous unprotected field. */ + public void backTab() { inputProcessor.backTab(); } + /** Reset (unlock keyboard). */ + public void reset() { inputProcessor.reset(); } +} diff --git a/lib3270j/src/main/java/org/lib3270j/TerminalModel.java b/lib3270j/src/main/java/org/lib3270j/TerminalModel.java new file mode 100644 index 0000000..a2fe139 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/TerminalModel.java @@ -0,0 +1,75 @@ +package org.lib3270j; + +import static org.lib3270j.protocol.DS3270Constants.*; + +/** + * Terminal model definitions for IBM 3278 and 3279 terminals. + * Models 2-5 are supported, each with default (24x80) and alternate screen sizes. + */ +public enum TerminalModel { + IBM_3278_2(2, false, MODEL_2_ROWS, MODEL_2_COLS, MODEL_2_ROWS, MODEL_2_COLS), + IBM_3278_3(3, false, MODEL_2_ROWS, MODEL_2_COLS, MODEL_3_ROWS, MODEL_3_COLS), + IBM_3278_4(4, false, MODEL_2_ROWS, MODEL_2_COLS, MODEL_4_ROWS, MODEL_4_COLS), + IBM_3278_5(5, false, MODEL_2_ROWS, MODEL_2_COLS, MODEL_5_ROWS, MODEL_5_COLS), + IBM_3279_2(2, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_2_ROWS, MODEL_2_COLS), + IBM_3279_3(3, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_3_ROWS, MODEL_3_COLS), + IBM_3279_4(4, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_4_ROWS, MODEL_4_COLS), + IBM_3279_5(5, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_5_ROWS, MODEL_5_COLS); + + private final int modelNumber; + private final boolean color; + private final int defaultRows; + private final int defaultCols; + private final int alternateRows; + private final int alternateCols; + + TerminalModel(int modelNumber, boolean color, + int defaultRows, int defaultCols, + int alternateRows, int alternateCols) { + this.modelNumber = modelNumber; + this.color = color; + this.defaultRows = defaultRows; + this.defaultCols = defaultCols; + this.alternateRows = alternateRows; + this.alternateCols = alternateCols; + } + + public int getModelNumber() { return modelNumber; } + public boolean isColor() { return color; } + public int getDefaultRows() { return defaultRows; } + public int getDefaultCols() { return defaultCols; } + public int getAlternateRows() { return alternateRows; } + public int getAlternateCols() { return alternateCols; } + + /** + * Returns the terminal type string for TN3270E negotiation. + * e.g., "IBM-3279-4-E" for a color model 4 with extended data stream. + */ + public String getTerminalType() { + return String.format("IBM-327%c-%d-E", color ? '9' : '8', modelNumber); + } + + /** + * Returns the base terminal type without "-E" suffix (for non-extended mode). + */ + public String getBaseTerminalType() { + return String.format("IBM-327%c-%d", color ? '9' : '8', modelNumber); + } + + /** + * Look up a model by number and color mode. + */ + public static TerminalModel forModel(int number, boolean isColor) { + for (TerminalModel m : values()) { + if (m.modelNumber == number && m.color == isColor) { + return m; + } + } + throw new IllegalArgumentException("Unknown model: " + number + " color=" + isColor); + } + + @Override + public String toString() { + return getTerminalType(); + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/charset/EbcdicTranslator.java b/lib3270j/src/main/java/org/lib3270j/charset/EbcdicTranslator.java new file mode 100644 index 0000000..862d0cb --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/charset/EbcdicTranslator.java @@ -0,0 +1,128 @@ +package org.lib3270j.charset; + +/** + * EBCDIC ↔ Unicode translator. + * Default: Code Page 037 (US/Canada EBCDIC). + */ +public class EbcdicTranslator { + + /** + * EBCDIC Code Page 037 to Unicode mapping. + * Index is the EBCDIC byte value (0x00-0xFF), value is the Unicode codepoint. + */ + private static final int[] CP037_TO_UNICODE = { + // 00-0F + 0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F, + 0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, + // 10-1F + 0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087, + 0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F, + // 20-2F + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B, + 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007, + // 30-3F + 0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004, + 0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A, + // 40-4F (space, accent chars, punctuation) + 0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5, + 0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x007C, + // 50-5F + 0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF, + 0x00EC, 0x00DF, 0x0021, 0x0024, 0x002A, 0x0029, 0x003B, 0x00AC, + // 60-6F + 0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5, + 0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F, + // 70-7F + 0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF, + 0x00CC, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022, + // 80-8F (lowercase a-i) + 0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, + 0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1, + // 90-9F (lowercase j-r) + 0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070, + 0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4, + // A0-AF (lowercase s-z) + 0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078, + 0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE, + // B0-BF + 0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC, + 0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7, + // C0-CF (uppercase A-I) + 0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, + 0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5, + // D0-DF (uppercase J-R) + 0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050, + 0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF, + // E0-EF (uppercase S-Z) + 0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058, + 0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5, + // F0-FF (digits 0-9) + 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, + 0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F, + }; + + /** + * Unicode to EBCDIC Code Page 037 mapping (for basic Latin + Latin-1). + * Index is the Unicode codepoint (0x00-0xFF), value is the EBCDIC byte (-1 if unmappable). + */ + private static final int[] UNICODE_TO_CP037 = new int[256]; + + static { + // Build reverse mapping + java.util.Arrays.fill(UNICODE_TO_CP037, -1); + for (int i = 0; i < 256; i++) { + int unicode = CP037_TO_UNICODE[i]; + if (unicode < 256) { + UNICODE_TO_CP037[unicode] = i; + } + } + } + + /** + * Translate EBCDIC byte to Unicode character. + */ + public char ebcdicToUnicode(int ebc) { + return (char) CP037_TO_UNICODE[ebc & 0xFF]; + } + + /** + * Translate Unicode character to EBCDIC byte. + * Returns -1 if the character cannot be mapped. + */ + public int unicodeToEbcdic(char unicode) { + if (unicode < 256) { + return UNICODE_TO_CP037[unicode]; + } + return -1; + } + + /** + * Translate Unicode character to EBCDIC, returning EBCDIC space (0x40) if unmappable. + */ + public byte unicodeToEbcdicSafe(char unicode) { + int ebc = unicodeToEbcdic(unicode); + return (byte) (ebc >= 0 ? ebc : 0x40); + } + + /** + * Translate a byte array from EBCDIC to a Unicode string. + */ + public String ebcdicToString(byte[] ebcdic, int offset, int length) { + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + sb.append(ebcdicToUnicode(ebcdic[offset + i] & 0xFF)); + } + return sb.toString(); + } + + /** + * Translate a Unicode string to EBCDIC byte array. + */ + public byte[] stringToEbcdic(String s) { + byte[] result = new byte[s.length()]; + for (int i = 0; i < s.length(); i++) { + result[i] = unicodeToEbcdicSafe(s.charAt(i)); + } + return result; + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java b/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java new file mode 100644 index 0000000..eac671a --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java @@ -0,0 +1,813 @@ +package org.lib3270j.datastream; + +import org.lib3270j.screen.ScreenBuffer; +import org.lib3270j.screen.ExtendedAttribute; +import org.lib3270j.charset.EbcdicTranslator; +import org.lib3270j.listener.ScreenUpdateListener; +import static org.lib3270j.protocol.DS3270Constants.*; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.logging.Logger; + +/** + * Processes inbound 3270 data stream records. + * Handles all 3270 commands: Write, Erase/Write, Read Buffer, WSF, etc. + * Equivalent to process_ds() and ctlr_write() in ctlr.c. + */ +public class DataStreamProcessor { + + private static final Logger log = Logger.getLogger(DataStreamProcessor.class.getName()); + + private final ScreenBuffer screen; + private final EbcdicTranslator translator; + private final QueryReplyBuilder qrBuilder; + private final List screenListeners = new CopyOnWriteArrayList<>(); + + // Output buffer for responses (read buffer, query replies, etc.) + private byte[] outputBuffer; + private int outputPos; + + // Callback for sending data back to host + private OutputSender outputSender; + + /** Functional interface for sending output back through the telnet stack. */ + @FunctionalInterface + public interface OutputSender { + void send3270Data(byte[] data); + } + + public DataStreamProcessor(ScreenBuffer screen, EbcdicTranslator translator) { + this.screen = screen; + this.translator = translator; + this.qrBuilder = new QueryReplyBuilder(screen); + this.outputBuffer = new byte[32768]; + this.outputPos = 0; + } + + public void setOutputSender(OutputSender sender) { + this.outputSender = sender; + } + + public void addScreenUpdateListener(ScreenUpdateListener l) { + screenListeners.add(l); + } + + /** + * Process a 3270 data stream record. + * + * @param data Raw data buffer + * @param offset Offset of the first byte (command byte) + * @param length Number of bytes + * @param keyboardRestore Whether to restore keyboard after processing + */ + public void processRecord(byte[] data, int offset, int length, boolean keyboardRestore) { + if (length == 0) + return; + + int cmd = data[offset] & 0xFF; + log.info(">>> CMD: " + commandName(cmd) + " (0x" + String.format("%02x", cmd) + ") " + length + " bytes"); + + switch (cmd) { + case CMD_W: + case SNA_CMD_W: + processWrite(data, offset, length, false); + break; + case CMD_EW: + case SNA_CMD_EW: + log.info(">>> ERASE/WRITE: clearing screen (default size)"); { + int oldRows = screen.getRows(); + int oldCols = screen.getCols(); + screen.erase(false); + processWrite(data, offset, length, true); + if (oldRows != screen.getRows() || oldCols != screen.getCols()) { + notifyScreenSizeChanged(); + } + } + break; + case CMD_EWA: + case SNA_CMD_EWA: + log.info(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)"); { + int oldRows = screen.getRows(); + int oldCols = screen.getCols(); + screen.erase(true); + processWrite(data, offset, length, true); + if (oldRows != screen.getRows() || oldCols != screen.getCols()) { + notifyScreenSizeChanged(); + } + } + break; + case CMD_RB: + case SNA_CMD_RB: + processReadBuffer(); + break; + case CMD_RM: + case SNA_CMD_RM: + processReadModified(false); + break; + case CMD_RMA: + case SNA_CMD_RMA: + processReadModified(true); + break; + case CMD_EAU: + case SNA_CMD_EAU: + log.info(">>> EAU: erasing all unprotected fields"); + screen.eraseAllUnprotected(); + break; + case CMD_WSF: + case SNA_CMD_WSF: + processWriteStructuredField(data, offset, length); + break; + case CMD_NOP: + log.info(">>> NOP command"); + break; + default: + log.warning(">>> UNKNOWN 3270 command: " + String.format("0x%02x", cmd)); + break; + } + + // Translate EBCDIC to Unicode for display + screen.translateToUnicode(); + screen.markAllChanged(); + + // Debug: dump non-empty screen lines + if (cmd == SNA_CMD_EWA || cmd == CMD_EWA || cmd == SNA_CMD_EW || cmd == CMD_EW) { + int r = screen.getRows(); + int c = screen.getCols(); + StringBuilder dump = new StringBuilder(); + int linesShown = 0; + for (int row = 0; row < r && linesShown < 5; row++) { + StringBuilder line = new StringBuilder(); + boolean hasContent = false; + for (int col = 0; col < c; col++) { + int addr = row * c + col; + ExtendedAttribute ea = screen.getCell(addr); + byte fa = screen.getFieldAttributeAt(addr); + + if ((fa & org.lib3270j.protocol.DS3270Constants.FA_MASK) == org.lib3270j.protocol.DS3270Constants.FA_INT_ZERO_NSEL) { + if (ea.ucs4 > 0x20 && ea.ucs4 != 0xFF) { + line.append('*'); + hasContent = true; + } else { + line.append(' '); + } + } else if (ea.ucs4 > 0x20 && ea.ucs4 != 0xFF) { + line.append(ea.ucs4); + hasContent = true; + } else { + line.append(' '); + } + } + if (hasContent) { + dump.append(String.format(" %02d: %s\n", row, line.toString().stripTrailing())); + linesShown++; + } + } + if (dump.length() > 0) { + log.info("Screen content after " + commandName(cmd) + ":\n" + dump.toString()); + } + } + } + + /** + * Process SSCP-LU data (TN3270E SSCP-LU mode). + * Data is character data placed starting at cursor position. + * Unlike 3270 mode, SSCP-LU data accumulates — we do NOT clear the screen. + */ + public void processSscpLuData(byte[] data, int offset, int length) { + int size = screen.getRows() * screen.getCols(); + int baddr = screen.getCursorAddress(); + int cols = screen.getCols(); + + for (int i = offset; i < offset + length; i++) { + int c = data[i] & 0xFF; + switch (c) { + case 0x15: // NL (new line) — move to column 0 of next row + baddr = ((baddr / cols) + 1) * cols; + if (baddr >= size) + baddr = 0; + break; + case 0x0d: // CR (carriage return) — move to column 0 of current row + baddr = (baddr / cols) * cols; + break; + case 0x0c: // FF (form feed) — clear screen and home cursor + screen.clear(); + baddr = 0; + break; + case 0x00: // NULL — skip + break; + default: + ExtendedAttribute ea = screen.getCell(baddr); + ea.ec = (byte) c; + ea.ucs4 = translator.ebcdicToUnicode(c); + baddr = (baddr + 1) % size; + break; + } + } + screen.setCursorAddress(baddr); + screen.translateToUnicode(); + screen.markAllChanged(); + } + + // ========== Write processing ========== + + private void processWrite(byte[] data, int offset, int length, boolean eraseFirst) { + if (length < 2) + return; + + // WCC is the second byte + int wcc = data[offset + 1] & 0xFF; + boolean alarm = wccSoundAlarm(wcc); + boolean kbdRestore = wccKeyboardRestore(wcc); + boolean resetMdt = wccResetMDT(wcc); + + log.fine("WCC: " + String.format("0x%02x", wcc) + + " reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt); + + if (resetMdt) { + resetAllMDT(); + } + + // WCC reset bit — reset partition characteristics + if (wccReset(wcc)) { + // Reset all character attributes to defaults + log.fine("WCC reset: clearing default attributes"); + } + + if (eraseFirst) { + screen.clear(); + log.fine("Cleared screen for Erase/Write"); + } + + // Process orders and data starting at byte 2 + int pos = offset + 2; + int end = offset + length; + int baddr = screen.getBufferAddress(); + 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; + + while (pos < end) { + int b = data[pos] & 0xFF; + + switch (b) { + case ORDER_SBA: { // Set Buffer Address + if (pos + 2 >= end) { + pos = end; + break; + } + baddr = decodeAddress(data[pos + 1] & 0xFF, data[pos + 2] & 0xFF); + if (baddr >= size) + baddr = baddr % size; + screen.setBufferAddress(baddr); + log.finest("SBA " + baddr + " (row=" + (baddr / screen.getCols()) + " col=" + + (baddr % screen.getCols()) + ")"); + pos += 3; + lastWasOrder = true; + break; + } + + case ORDER_SF: { // Start Field + if (pos + 1 >= end) { + pos = end; + break; + } + int fa = data[pos + 1] & 0xFF; + ExtendedAttribute ea = screen.getCell(baddr); + ea.clear(); + ea.fa = (byte) (fa & FA_MASK); + // FA position is a display position that shows as blank + ea.ec = 0; + ea.ucs4 = ' '; + currentFg = 0; + currentBg = 0; + currentGr = 0; + currentCs = 0; + screen.setFormatted(true); + baddr = (baddr + 1) % size; + screen.setBufferAddress(baddr); + log.finest("SF fa=" + String.format("0x%02x", fa)); + pos += 2; + lastWasOrder = true; + break; + } + + case ORDER_SFE: { // Start Field Extended + if (pos + 1 >= end) { + pos = end; + break; + } + int nPairs = data[pos + 1] & 0xFF; + if (pos + 2 + nPairs * 2 > end) { + pos = end; + break; + } + + ExtendedAttribute ea = screen.getCell(baddr); + ea.clear(); + ea.ec = 0; + ea.ucs4 = ' '; + currentFg = 0; + currentBg = 0; + currentGr = 0; + currentCs = 0; + + for (int i = 0; i < nPairs; i++) { + int attrType = data[pos + 2 + i * 2] & 0xFF; + int attrValue = data[pos + 2 + i * 2 + 1] & 0xFF; + applyExtendedAttribute(ea, attrType, attrValue); + } + if (ea.fa == 0) { + // If no 3270 FA was among the pairs, set default + ea.fa = (byte) (FA_PRINTABLE); + } + screen.setFormatted(true); + baddr = (baddr + 1) % size; + screen.setBufferAddress(baddr); + pos += 2 + nPairs * 2; + lastWasOrder = true; + break; + } + + case ORDER_SA: { // Set Attribute + if (pos + 2 >= end) { + pos = end; + break; + } + int attrType = data[pos + 1] & 0xFF; + int attrValue = data[pos + 2] & 0xFF; + switch (attrType) { + case XA_FOREGROUND: + currentFg = (byte) attrValue; + break; + case XA_BACKGROUND: + currentBg = (byte) attrValue; + break; + case XA_HIGHLIGHTING: + if (attrValue == XAH_DEFAULT) + currentGr = 0; + else if (attrValue == XAH_BLINK) + currentGr = GR_BLINK; + else if (attrValue == XAH_REVERSE) + currentGr = GR_REVERSE; + else if (attrValue == XAH_UNDERSCORE) + currentGr = GR_UNDERLINE; + else if (attrValue == XAH_INTENSIFY) + currentGr = GR_INTENSIFY; + else if (attrValue == XAH_NORMAL) + currentGr = 0; + break; + case XA_CHARSET: + currentCs = (byte) attrValue; + break; + } + pos += 3; + lastWasOrder = true; + break; + } + + case ORDER_MF: { // Modify Field + if (pos + 1 >= end) { + pos = end; + break; + } + int nPairs = data[pos + 1] & 0xFF; + if (pos + 2 + nPairs * 2 > end) { + pos = end; + break; + } + + // Find the field attribute at or before current position + int faAddr = screen.findFieldAttribute(baddr); + if (faAddr >= 0) { + ExtendedAttribute ea = screen.getCell(faAddr); + for (int i = 0; i < nPairs; i++) { + int attrType = data[pos + 2 + i * 2] & 0xFF; + int attrValue = data[pos + 2 + i * 2 + 1] & 0xFF; + applyExtendedAttribute(ea, attrType, attrValue); + } + } + pos += 2 + nPairs * 2; + lastWasOrder = true; + break; + } + + case ORDER_IC: { // Insert Cursor + screen.setCursorAddress(baddr); + log.finest("IC at " + baddr); + pos += 1; + lastWasOrder = true; + break; + } + + case ORDER_PT: { // Program Tab + // Skip to next unprotected field + baddr = screen.findNextUnprotected(baddr); + screen.setBufferAddress(baddr); + pos += 1; + lastWasOrder = true; + break; + } + + case ORDER_RA: { // Repeat to Address + if (pos + 3 >= end) { + pos = end; + break; + } + int toAddr = decodeAddress(data[pos + 1] & 0xFF, data[pos + 2] & 0xFF); + int fillChar = data[pos + 3] & 0xFF; + if (toAddr >= size) + toAddr = toAddr % size; + + // Handle GE (graphic escape) prefix + byte fillCs = currentCs; + if (pos + 4 < end && fillChar == ORDER_GE) { + fillChar = data[pos + 4] & 0xFF; + fillCs = CS_GE; + pos += 5; + } else { + pos += 4; + } + + // Fill from current position to target + while (baddr != toAddr) { + ExtendedAttribute ea = screen.getCell(baddr); + ea.fa = 0; // Destroy previous field attribute if any + ea.ec = (byte) fillChar; + ea.fg = currentFg; + ea.bg = currentBg; + ea.gr = (byte) currentGr; + ea.cs = fillCs; + baddr = (baddr + 1) % size; + } + screen.setBufferAddress(baddr); + lastWasOrder = true; + break; + } + + case ORDER_EUA: { // Erase Unprotected to Address + if (pos + 2 >= end) { + pos = end; + break; + } + int toAddr = decodeAddress(data[pos + 1] & 0xFF, data[pos + 2] & 0xFF); + if (toAddr >= size) + toAddr = toAddr % size; + + while (baddr != toAddr) { + ExtendedAttribute ea = screen.getCell(baddr); + if (!ea.isFieldAttribute()) { + int faAddr = screen.findFieldAttribute(baddr); + byte faVal = faAddr >= 0 ? screen.getCell(faAddr).fa : 0; + if (!faIsProtected(faVal & 0xFF)) { + ea.ec = 0; + ea.ucs4 = 0; + } + } + baddr = (baddr + 1) % size; + } + screen.setBufferAddress(baddr); + pos += 3; + lastWasOrder = true; + break; + } + + case ORDER_GE: { // Graphic Escape + if (pos + 1 >= end) { + pos = end; + break; + } + int geChar = data[pos + 1] & 0xFF; + ExtendedAttribute ea = screen.getCell(baddr); + ea.fa = 0; // Destroy previous field attribute if any + ea.ec = (byte) geChar; + ea.cs = CS_GE; + ea.fg = currentFg; + ea.bg = currentBg; + ea.gr = (byte) currentGr; + baddr = (baddr + 1) % size; + screen.setBufferAddress(baddr); + pos += 2; + lastWasOrder = true; + break; + } + + default: { + // Regular data byte (EBCDIC character) + // Format control codes display as nulls/blanks per 3270 spec + boolean isFCOrder = (b == FCORDER_NULL || b == FCORDER_FF || + b == FCORDER_CR || b == FCORDER_NL || b == FCORDER_EM || + b == FCORDER_DUP || b == FCORDER_FM || b == FCORDER_SUB || + b == FCORDER_EO); + ExtendedAttribute ea = screen.getCell(baddr); + ea.fa = 0; // Destroy previous field attribute if any + + if (isFCOrder) { + ea.ec = 0; // Display as null/blank + } else { + ea.ec = (byte) b; + } + ea.fg = currentFg; + ea.bg = currentBg; + ea.gr = (byte) currentGr; + ea.cs = currentCs; + + baddr = (baddr + 1) % size; + screen.setBufferAddress(baddr); + pos += 1; + lastWasOrder = false; + break; + } + } + } + + if (alarm) { + for (ScreenUpdateListener l : screenListeners) { + l.onSoundAlarm(); + } + } + + screen.updateFormatted(); + } + + private void applyExtendedAttribute(ExtendedAttribute ea, int type, int value) { + switch (type) { + case XA_3270: + ea.fa = (byte) (value & FA_MASK); + break; + case XA_FOREGROUND: + ea.fg = (byte) value; + break; + case XA_BACKGROUND: + ea.bg = (byte) value; + break; + case XA_HIGHLIGHTING: + if (value == XAH_DEFAULT || value == XAH_NORMAL) + ea.gr = 0; + else if (value == XAH_BLINK) + ea.gr = GR_BLINK; + else if (value == XAH_REVERSE) + ea.gr = GR_REVERSE; + else if (value == XAH_UNDERSCORE) + ea.gr = GR_UNDERLINE; + else if (value == XAH_INTENSIFY) + ea.gr = GR_INTENSIFY; + break; + case XA_CHARSET: + ea.cs = (byte) value; + break; + case XA_VALIDATION: + case XA_OUTLINING: + case XA_INPUT_CONTROL: + // Acknowledged but not visually rendered yet + break; + } + } + + private void resetAllMDT() { + int size = screen.getRows() * screen.getCols(); + for (int i = 0; i < size; i++) { + ExtendedAttribute ea = screen.getCell(i); + if (ea.isFieldAttribute()) { + ea.fa = (byte) (ea.fa & ~FA_MODIFY); + } + } + } + + // ========== Read Buffer ========== + + private void processReadBuffer() { + outputPos = 0; + + int size = screen.getRows() * screen.getCols(); + + // AID byte + outputWrite(AID_NO); + // Cursor address + byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols()); + outputWrite(caddr[0] & 0xFF); + outputWrite(caddr[1] & 0xFF); + + // Buffer contents + for (int i = 0; i < size; i++) { + ExtendedAttribute ea = screen.getCell(i); + if (ea.isFieldAttribute()) { + outputWrite(ORDER_SF); + outputWrite(ea.fa & 0xFF); + } else { + outputWrite(ea.ec & 0xFF); + } + } + + sendOutput(); + } + + // ========== Read Modified ========== + + private void processReadModified(boolean all) { + outputPos = 0; + + int aid = AID_NO; // Last AID + int size = screen.getRows() * screen.getCols(); + + // AID byte + outputWrite(aid); + + // Cursor address + byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols()); + outputWrite(caddr[0] & 0xFF); + outputWrite(caddr[1] & 0xFF); + + if (!screen.isFormatted()) { + // Unformatted: send everything + if (all) { + for (int i = 0; i < size; i++) { + outputWrite(screen.getCell(i).ec & 0xFF); + } + } + } else { + // Formatted: send modified fields + for (int i = 0; i < size; i++) { + ExtendedAttribute ea = screen.getCell(i); + if (ea.isFieldAttribute() && (all || faIsModified(ea.fa & 0xFF))) { + int fieldStart = (i + 1) % size; + // Send SBA for field start + outputWrite(ORDER_SBA); + byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols()); + outputWrite(addr[0] & 0xFF); + outputWrite(addr[1] & 0xFF); + + // Send field contents until next FA + int pos = fieldStart; + while (pos < size && !screen.getCell(pos).isFieldAttribute()) { + outputWrite(screen.getCell(pos).ec & 0xFF); + pos = (pos + 1) % size; + if (pos == fieldStart) + break; + } + } + } + } + + sendOutput(); + } + + // ========== Write Structured Field ========== + + private void processWriteStructuredField(byte[] data, int offset, int length) { + int pos = offset + 1; // Skip WSF command byte + int end = offset + length; + + while (pos < end) { + // Field length (2 bytes) + if (pos + 2 > end) + break; + int fieldLen = ((data[pos] & 0xFF) << 8) | (data[pos + 1] & 0xFF); + if (fieldLen == 0) + fieldLen = end - pos; + if (fieldLen < 3 || pos + fieldLen > end) + break; + + int sfId = data[pos + 2] & 0xFF; + log.fine("SF id=" + String.format("0x%02x", sfId) + " len=" + fieldLen); + + switch (sfId) { + case SF_READ_PART: + processSFReadPartition(data, pos, fieldLen); + break; + case SF_ERASE_RESET: + if (fieldLen >= 4) { + boolean alt = (data[pos + 3] & 0xFF) == SF_ER_ALT; + screen.erase(alt); + notifyScreenSizeChanged(); + } + break; + case SF_SET_REPLY_MODE: + if (fieldLen >= 5) { + screen.setReplyMode((byte) (data[pos + 4] & 0xFF)); + } + break; + case SF_CREATE_PART: + // Acknowledged — we use implicit partition + break; + case SF_OUTBOUND_DS: + if (fieldLen > 5) { + // Outbound DS contains another 3270 command + processRecord(data, pos + 4, fieldLen - 4, false); + } + break; + default: + log.fine("Unknown SF id: " + String.format("0x%02x", sfId)); + break; + } + + pos += fieldLen; + } + } + + private void processSFReadPartition(byte[] data, int offset, int fieldLen) { + if (fieldLen < 5) + return; + + int partition = data[offset + 3] & 0xFF; + int type = data[offset + 4] & 0xFF; + + // Log the incoming ReadPartition request + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < fieldLen && i < 32; i++) { + sb.append(String.format("%02x ", data[offset + i] & 0xFF)); + } + log.info("ReadPartition raw: " + sb.toString().trim() + + " partition=" + String.format("0x%02x", partition) + + " type=" + String.format("0x%02x", type)); + + switch (type) { + case SF_RP_QUERY: + log.info("ReadPartition Query — sending all query replies"); + sendAllQueryReplies(); + break; + case SF_RP_QLIST: + if (fieldLen >= 6) { + int listType = data[offset + 5] & 0xFF; + log.info("ReadPartition QueryList type=" + String.format("0x%02x", listType)); + if (listType == SF_RPQ_ALL || listType == SF_RPQ_EQUIV) { + sendAllQueryReplies(); + } else if (listType == SF_RPQ_LIST) { + // Send only requested query replies + byte[] requestedCodes = new byte[fieldLen - 6]; + System.arraycopy(data, offset + 6, requestedCodes, 0, requestedCodes.length); + StringBuilder reqSb = new StringBuilder(); + for (byte c : requestedCodes) { + reqSb.append(String.format("%02x ", c & 0xFF)); + } + log.info("Requested QR codes: " + reqSb.toString().trim()); + sendRequestedQueryReplies(requestedCodes); + } + } else { + sendAllQueryReplies(); + } + break; + case SNA_CMD_RMA: + processReadModified(true); + break; + case SNA_CMD_RB: + processReadBuffer(); + break; + case SNA_CMD_RM: + processReadModified(false); + break; + default: + log.fine("Unknown ReadPartition type: " + String.format("0x%02x", type)); + break; + } + } + + private void sendAllQueryReplies() { + byte[] qr = qrBuilder.buildAllQueryReplies(screen.getMaxCols(), screen.getMaxRows(), + screen.getMaxCols() * screen.getMaxRows()); + // Hex dump the query reply for debugging + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < qr.length; i++) { + sb.append(String.format("%02x ", qr[i] & 0xFF)); + if ((i + 1) % 32 == 0) + sb.append("\n "); + } + log.info("Query reply (" + qr.length + " bytes):\n " + sb.toString().trim()); + if (outputSender != null) { + outputSender.send3270Data(qr); + } + } + + private void sendRequestedQueryReplies(byte[] codes) { + // For now, send all — refinement can come later + sendAllQueryReplies(); + } + + // ========== Output helpers ========== + + private void outputWrite(int b) { + if (outputPos >= outputBuffer.length) { + byte[] newBuf = new byte[outputBuffer.length * 2]; + System.arraycopy(outputBuffer, 0, newBuf, 0, outputPos); + outputBuffer = newBuf; + } + outputBuffer[outputPos++] = (byte) b; + } + + private void sendOutput() { + if (outputSender != null && outputPos > 0) { + byte[] data = new byte[outputPos]; + System.arraycopy(outputBuffer, 0, data, 0, outputPos); + outputSender.send3270Data(data); + } + outputPos = 0; + } + + private void notifyScreenSizeChanged() { + for (ScreenUpdateListener l : screenListeners) { + l.onScreenSizeChanged(screen.getRows(), screen.getCols()); + } + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java b/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java new file mode 100644 index 0000000..4498b93 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java @@ -0,0 +1,215 @@ +package org.lib3270j.datastream; + +import java.io.ByteArrayOutputStream; +import java.util.logging.Logger; + +import org.lib3270j.screen.ScreenBuffer; +import static org.lib3270j.protocol.DS3270Constants.*; + +/** + * Builds Query Reply structured fields in response to host Read Partition queries. + * Equivalent to the do_qr_* functions in sf.c. + */ +public class QueryReplyBuilder { + + private static final Logger log = Logger.getLogger(QueryReplyBuilder.class.getName()); + + // Canned values from 3279-2 (matching sf.c) + private static final int SW_3279_2 = 0x09; + private static final int SH_3279_2 = 0x0c; + private static final int Xr_3279_2 = 0x000a02e5; + private static final int Yr_3279_2 = 0x0002006f; + + private final ScreenBuffer screen; + + // Supported query reply codes (must match what we send in summary) + private static final int[] SUPPORTED_QR = { + QR_SUMMARY, // 0x80 — summary must list itself + QR_USABLE_AREA, // 0x81 + QR_ALPHA_PART, // 0x84 + QR_CHARSETS, // 0x85 + QR_COLOR, // 0x86 + QR_HIGHLIGHTING, // 0x87 + QR_REPLY_MODES, // 0x88 + QR_IMP_PART, // 0xa6 + }; + + public QueryReplyBuilder(ScreenBuffer screen) { + this.screen = screen; + } + + /** + * Build all query replies as a single AID_SF + structured field response. + */ + public byte[] buildAllQueryReplies(int maxCols, int maxRows, int bufferSize) { + ByteArrayOutputStream out = new ByteArrayOutputStream(512); + + // AID byte for structured field + out.write(AID_SF); + + // Summary + appendQueryReply(out, QR_SUMMARY, buildSummary()); + + // Usable Area + appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize)); + + // Alpha Partitions + appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows)); + + // Character Sets + appendQueryReply(out, QR_CHARSETS, buildCharsets()); + + // Color + appendQueryReply(out, QR_COLOR, buildColor()); + + // Highlighting + appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); + + // Reply Modes + appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); + + // Implicit Partition + appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); + + log.info("Built " + out.size() + " bytes of query replies"); + return out.toByteArray(); + } + + private void appendQueryReply(ByteArrayOutputStream out, int code, byte[] data) { + // Length includes the 2-byte length field + SFID_QREPLY + code + data + int len = 4 + data.length; + out.write((len >> 8) & 0xFF); + out.write(len & 0xFF); + out.write(SFID_QREPLY); + out.write(code); + out.write(data, 0, data.length); + } + + private byte[] buildSummary() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + for (int code : SUPPORTED_QR) { + out.write(code); + } + return out.toByteArray(); + } + + private byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) { + ByteArrayOutputStream out = new ByteArrayOutputStream(19); + out.write(0x01); // 12/14-bit addressing + out.write(0x00); // no special character features + out.write((maxCols >> 8) & 0xFF); // usable width high + out.write(maxCols & 0xFF); // usable width low + out.write((maxRows >> 8) & 0xFF); // usable height high + out.write(maxRows & 0xFF); // usable height low + out.write(0x01); // units (mm) + // Xr (4 bytes) - canned from 3279-2 + out.write((Xr_3279_2 >> 24) & 0xFF); + out.write((Xr_3279_2 >> 16) & 0xFF); + out.write((Xr_3279_2 >> 8) & 0xFF); + out.write(Xr_3279_2 & 0xFF); + // Yr (4 bytes) - canned from 3279-2 + out.write((Yr_3279_2 >> 24) & 0xFF); + out.write((Yr_3279_2 >> 16) & 0xFF); + out.write((Yr_3279_2 >> 8) & 0xFF); + out.write(Yr_3279_2 & 0xFF); + out.write(SW_3279_2); // AW + out.write(SH_3279_2); // AH + int buf = maxCols * maxRows; + out.write((buf >> 8) & 0xFF); // buffer size high + out.write(buf & 0xFF); // buffer size low + return out.toByteArray(); + } + + private byte[] buildAlphaPartitions(int maxRows) { + int bufSize = screen.getMaxCols() * screen.getMaxRows(); + ByteArrayOutputStream out = new ByteArrayOutputStream(4); + out.write(0x00); // max partitions (1 partition) + out.write((bufSize >> 8) & 0xFF); // total partition storage high + out.write(bufSize & 0xFF); // total partition storage low + out.write(0x00); // no special features + return out.toByteArray(); + } + + private byte[] buildCharsets() { + ByteArrayOutputStream out = new ByteArrayOutputStream(32); + out.write(0x82); // flags: GE, CGCSGID present + out.write(0x00); // more flags + out.write(SW_3279_2); // SDW - default char width + out.write(SH_3279_2); // SDH - default char height + out.write(0x00); // Load PS format types supported: none + out.write(0x00); // Load PS device type (high) + out.write(0x00); // Load PS device type (low) + out.write(0x00); // reserved + out.write(0x07); // DL = 7 bytes per descriptor (non-DBCS) + // Descriptor 1 (SET 0): default character set + out.write(0x00); // SET 0 + out.write(0x10); // FLAGS: non-loadable, single-plane, single-byte, no compare + out.write(0x00); // LCID 0 + out.write(0x02); // CGCSGID (4 bytes) = 0x02b90025 (CGEN|CSET) + out.write(0xb9); + out.write(0x00); + out.write(0x25); + // Descriptor 2 (SET 1): APL/GE character set + out.write(0x01); // SET 1 + out.write(0x00); // FLAGS: non-loadable, single-plane, single-byte, no compare + out.write(0xf1); // LCID 0xf1 + out.write(0x03); // CGCSGID: 3179-style APL2 = 0x03c30136 + out.write(0xc3); + out.write(0x01); + out.write(0x36); + return out.toByteArray(); + } + + private byte[] buildColor() { + ByteArrayOutputStream out = new ByteArrayOutputStream(36); + int colorMax = 16; + out.write(0x00); // no options + out.write(colorMax); // number of colors + out.write(0x00); // default color pair: attribute + out.write(0xf0 + HOST_COLOR_GREEN); // default color: green + for (int i = 0xf1; i < 0xf1 + colorMax - 1; i++) { + out.write(i); // color attribute value + out.write(i); // maps to itself (color mode) + } + return out.toByteArray(); + } + + private byte[] buildHighlighting() { + ByteArrayOutputStream out = new ByteArrayOutputStream(11); + out.write(5); // 5 pairs + out.write(XAH_DEFAULT); out.write(XAH_NORMAL); + out.write(XAH_BLINK); out.write(XAH_BLINK); + out.write(XAH_REVERSE); out.write(XAH_REVERSE); + out.write(XAH_UNDERSCORE); out.write(XAH_UNDERSCORE); + out.write(XAH_INTENSIFY); out.write(XAH_INTENSIFY); + return out.toByteArray(); + } + + private byte[] buildReplyModes() { + return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR }; + } + + private byte[] buildImplicitPartition(int maxCols, int maxRows) { + ByteArrayOutputStream out = new ByteArrayOutputStream(22); + // Implicit partition sizes, 2 self-defining parameters + + // SDP 1: Default screen size + out.write(0x00); // flags + out.write(0x00); // reserved + out.write(0x0b); // SDP length + out.write(0x01); // SDP type: implicit partition sizes + out.write(0x00); // reserved + // Default + out.write((MODEL_2_COLS >> 8) & 0xFF); + out.write(MODEL_2_COLS & 0xFF); + out.write((MODEL_2_ROWS >> 8) & 0xFF); + out.write(MODEL_2_ROWS & 0xFF); + // Alternate + out.write((maxCols >> 8) & 0xFF); + out.write(maxCols & 0xFF); + out.write((maxRows >> 8) & 0xFF); + out.write(maxRows & 0xFF); + + return out.toByteArray(); + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java b/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java new file mode 100644 index 0000000..99d9ea6 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java @@ -0,0 +1,322 @@ +package org.lib3270j.input; + +import org.lib3270j.screen.ScreenBuffer; +import org.lib3270j.screen.ExtendedAttribute; +import org.lib3270j.charset.EbcdicTranslator; +import org.lib3270j.telnet.TelnetFSM; +import static org.lib3270j.protocol.DS3270Constants.*; + +import java.io.ByteArrayOutputStream; +import java.util.logging.Logger; + +/** + * Handles keyboard input and generates 3270 outbound data streams. + * Manages cursor positioning, character entry, and AID key handling. + */ +public class InputProcessor { + + private static final Logger log = Logger.getLogger(InputProcessor.class.getName()); + + private final ScreenBuffer screen; + private final EbcdicTranslator translator; + private final TelnetFSM fsm; + private int lastAid = AID_NO; + private boolean keyboardLocked; + private boolean insertMode; + + public InputProcessor(ScreenBuffer screen, EbcdicTranslator translator, TelnetFSM fsm) { + this.screen = screen; + this.translator = translator; + this.fsm = fsm; + } + + public boolean isKeyboardLocked() { return keyboardLocked; } + public void setKeyboardLocked(boolean locked) { this.keyboardLocked = locked; } + public boolean isInsertMode() { return insertMode; } + public void setInsertMode(boolean insert) { this.insertMode = insert; } + + /** + * Enter a character at the current cursor position. + */ + public void typeCharacter(char ch) { + if (keyboardLocked) return; + + int baddr = screen.getCursorAddress(); + int size = screen.getRows() * screen.getCols(); + + // Check if cursor is at a field attribute or in a protected field + ExtendedAttribute ea = screen.getCell(baddr); + if (ea.isFieldAttribute()) { + // Move to next position + baddr = (baddr + 1) % size; + ea = screen.getCell(baddr); + } + + byte faVal = screen.getFieldAttributeAt(baddr); + if (faIsProtected(faVal & 0xFF)) { + // Protected field — can't type here + return; + } + + // Translate character to EBCDIC + int ebc = translator.unicodeToEbcdic(ch); + if (ebc < 0) return; + + if (insertMode) { + // Insert mode: shift characters right from cursor to end of field + // Find end of field + int endAddr = baddr; + while (!screen.getCell(screen.incrementAddress(endAddr)).isFieldAttribute()) { + endAddr = screen.incrementAddress(endAddr); + if (endAddr == baddr) break; // wrapped around (unformatted) + } + // Check if last position is non-null (field overflow) + if (screen.getCell(endAddr).ec != 0) { + // Field overflow — can't insert + return; + } + // Shift right from endAddr-1 down to baddr + int dst = endAddr; + while (dst != baddr) { + int src = screen.decrementAddress(dst); + screen.getCell(dst).ec = screen.getCell(src).ec; + screen.getCell(dst).ucs4 = screen.getCell(src).ucs4; + dst = src; + } + } + + // Write character — preserve existing fg/bg/gr/cs attributes + // so the character inherits the field's color scheme + ea = screen.getCell(baddr); + ea.ec = (byte) ebc; + ea.ucs4 = ch; + + // Set MDT on field attribute + int faAddr = screen.findFieldAttribute(baddr); + if (faAddr >= 0) { + ExtendedAttribute faEa = screen.getCell(faAddr); + faEa.fa = (byte) (faEa.fa | FA_MODIFY); + } + + // Advance cursor + baddr = (baddr + 1) % size; + // Skip over field attributes + while (screen.getCell(baddr).isFieldAttribute()) { + baddr = (baddr + 1) % size; + } + screen.setCursorAddress(baddr); + screen.markAllChanged(); + } + + /** + * Send an AID key (Enter, PF1-24, PA1-3, Clear). + */ + public void sendAid(int aidCode) { + if (keyboardLocked && aidCode != AID_CLEAR) return; + + lastAid = aidCode; + keyboardLocked = true; + + if (aidCode == AID_CLEAR) { + screen.clear(); + screen.markAllChanged(); + // Send just the AID + byte[] data = new byte[] { (byte) aidCode }; + sendAidResponse(data); + return; + } + + if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) { + // PA keys: send AID + cursor address only (no modified data) + byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols()); + byte[] data = new byte[] { (byte) aidCode, caddr[0], caddr[1] }; + sendAidResponse(data); + return; + } + + // Enter, PF keys: send AID + cursor address + modified field data + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(aidCode); + + byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols()); + out.write(caddr[0] & 0xFF); + out.write(caddr[1] & 0xFF); + + if (screen.isFormatted()) { + // Send modified fields with SBA + // Per 3270 Data Stream Architecture: strip trailing NULLs (0x00) from field data + int size = screen.getRows() * screen.getCols(); + for (int i = 0; i < size; i++) { + ExtendedAttribute ea = screen.getCell(i); + if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) { + if (faIsProtected(ea.fa & 0xFF)) continue; + + int fieldStart = (i + 1) % size; + + // First, collect field data and find last non-null byte + ByteArrayOutputStream fieldData = new ByteArrayOutputStream(); + int pos = fieldStart; + int lastNonNull = -1; + int fieldLen = 0; + while (!screen.getCell(pos).isFieldAttribute()) { + int b = screen.getCell(pos).ec & 0xFF; + fieldData.write(b); + if (b != 0x00) { + lastNonNull = fieldLen; + } + fieldLen++; + pos = (pos + 1) % size; + if (pos == fieldStart) break; + } + + // Only send if there's actual data (strip trailing nulls) + if (lastNonNull >= 0) { + out.write(ORDER_SBA); + byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols()); + out.write(addr[0] & 0xFF); + out.write(addr[1] & 0xFF); + + // Write only up to the last non-null byte + byte[] allData = fieldData.toByteArray(); + out.write(allData, 0, lastNonNull + 1); + } + } + } + } else { + // Unformatted screen: send all data + int size = screen.getRows() * screen.getCols(); + for (int i = 0; i < size; i++) { + out.write(screen.getCell(i).ec & 0xFF); + } + } + + sendAidResponse(out.toByteArray()); + } + + private void sendAidResponse(byte[] data) { + if (fsm.getConnectionState().isSscp()) { + fsm.sendSscpLuData(data); + } else { + fsm.send3270Data(data); + } + } + + // ========== Cursor movement ========== + + public void cursorUp() { + int addr = screen.getCursorAddress(); + addr -= screen.getCols(); + if (addr < 0) addr += screen.getRows() * screen.getCols(); + screen.setCursorAddress(addr); + } + + public void cursorDown() { + int addr = screen.getCursorAddress(); + addr += screen.getCols(); + if (addr >= screen.getRows() * screen.getCols()) addr -= screen.getRows() * screen.getCols(); + screen.setCursorAddress(addr); + } + + public void cursorLeft() { + int addr = screen.getCursorAddress(); + addr = screen.decrementAddress(addr); + screen.setCursorAddress(addr); + } + + public void cursorRight() { + int addr = screen.getCursorAddress(); + addr = screen.incrementAddress(addr); + screen.setCursorAddress(addr); + } + + public void cursorHome() { + if (screen.isFormatted()) { + screen.setCursorAddress(screen.findNextUnprotected(0)); + } else { + screen.setCursorAddress(0); + } + } + + public void tab() { + int addr = screen.findNextUnprotected(screen.getCursorAddress()); + screen.setCursorAddress(addr); + } + + public void backTab() { + // Find previous unprotected field + int addr = screen.getCursorAddress(); + int size = screen.getRows() * screen.getCols(); + int start = screen.decrementAddress(addr); + addr = start; + do { + addr = screen.decrementAddress(addr); + if (screen.getCell(addr).isFieldAttribute()) { + if (!faIsProtected(screen.getCell(addr).fa & 0xFF)) { + screen.setCursorAddress(screen.incrementAddress(addr)); + return; + } + } + } while (addr != start); + } + + public void eraseEof() { + int addr = screen.getCursorAddress(); + int size = screen.getRows() * screen.getCols(); + byte faVal = screen.getFieldAttributeAt(addr); + if (faIsProtected(faVal & 0xFF)) return; + + // Erase from cursor to end of field + while (!screen.getCell(addr).isFieldAttribute()) { + ExtendedAttribute ea = screen.getCell(addr); + ea.ec = 0; + ea.ucs4 = 0; + addr = screen.incrementAddress(addr); + } + + // Set MDT + int faAddr = screen.findFieldAttribute(screen.getCursorAddress()); + if (faAddr >= 0) { + screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY); + } + screen.markAllChanged(); + } + + public void deleteChar() { + int addr = screen.getCursorAddress(); + byte faVal = screen.getFieldAttributeAt(addr); + if (faIsProtected(faVal & 0xFF)) return; + + // Shift characters left within the field + int shiftAddr = addr; + int size = screen.getRows() * screen.getCols(); + while (true) { + int next = screen.incrementAddress(shiftAddr); + if (screen.getCell(next).isFieldAttribute()) { + screen.getCell(shiftAddr).ec = 0; + screen.getCell(shiftAddr).ucs4 = 0; + break; + } + screen.getCell(shiftAddr).ec = screen.getCell(next).ec; + screen.getCell(shiftAddr).ucs4 = screen.getCell(next).ucs4; + shiftAddr = next; + } + + int faAddr = screen.findFieldAttribute(addr); + if (faAddr >= 0) { + screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY); + } + screen.markAllChanged(); + } + + public void backspace() { + if (screen.getCursorAddress() == 0) return; + cursorLeft(); + deleteChar(); + } + + /** Reset (unlock keyboard, cancel insert mode). */ + public void reset() { + keyboardLocked = false; + insertMode = false; + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/listener/ConnectionListener.java b/lib3270j/src/main/java/org/lib3270j/listener/ConnectionListener.java new file mode 100644 index 0000000..7b3eacb --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/listener/ConnectionListener.java @@ -0,0 +1,17 @@ +package org.lib3270j.listener; + +import org.lib3270j.ConnectionState; + +/** + * Listener for connection state changes and errors. + */ +public interface ConnectionListener { + /** Called when the connection state changes. */ + void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState); + + /** Called when a connection error occurs. */ + void onConnectionError(String message); + + /** Called when TN3270E negotiation completes. */ + default void onTN3270ENegotiated(String deviceType, String deviceName) {} +} diff --git a/lib3270j/src/main/java/org/lib3270j/listener/ScreenUpdateListener.java b/lib3270j/src/main/java/org/lib3270j/listener/ScreenUpdateListener.java new file mode 100644 index 0000000..6aaa4b3 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/listener/ScreenUpdateListener.java @@ -0,0 +1,18 @@ +package org.lib3270j.listener; + +/** + * Listener for screen buffer changes. + */ +public interface ScreenUpdateListener { + /** Called when the screen buffer has been updated. */ + void onScreenUpdated(); + + /** Called when the cursor position changes. */ + default void onCursorMoved(int oldAddress, int newAddress) {} + + /** Called when the host sends a sound alarm (WCC bit). */ + default void onSoundAlarm() {} + + /** Called when the screen size changes (erase/write vs erase/write alternate). */ + default void onScreenSizeChanged(int rows, int cols) {} +} diff --git a/lib3270j/src/main/java/org/lib3270j/protocol/DS3270Constants.java b/lib3270j/src/main/java/org/lib3270j/protocol/DS3270Constants.java new file mode 100644 index 0000000..7ba9565 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/protocol/DS3270Constants.java @@ -0,0 +1,376 @@ +package org.lib3270j.protocol; + +/** + * 3270 Data Stream protocol constants. + * Derived from 3270ds.h in x3270. + */ +public final class DS3270Constants { + + private DS3270Constants() {} + + // ========== 3270 Commands ========== + public static final int CMD_W = 0x01; // Write + public static final int CMD_RB = 0x02; // Read Buffer + public static final int CMD_NOP = 0x03; // No-Op + public static final int CMD_EW = 0x05; // Erase/Write + public static final int CMD_RM = 0x06; // Read Modified + public static final int CMD_EWA = 0x0d; // Erase/Write Alternate + public static final int CMD_RMA = 0x0e; // Read Modified All + public static final int CMD_EAU = 0x0f; // Erase All Unprotected + public static final int CMD_WSF = 0x11; // Write Structured Field + + // SNA 3270 Commands + public static final int SNA_CMD_RMA = 0x6e; // Read Modified All + public static final int SNA_CMD_EAU = 0x6f; // Erase All Unprotected + public static final int SNA_CMD_EWA = 0x7e; // Erase/Write Alternate + public static final int SNA_CMD_W = 0xf1; // Write + public static final int SNA_CMD_RB = 0xf2; // Read Buffer + public static final int SNA_CMD_WSF = 0xf3; // Write Structured Field + public static final int SNA_CMD_EW = 0xf5; // Erase/Write + public static final int SNA_CMD_RM = 0xf6; // Read Modified + + // ========== 3270 Orders ========== + public static final int ORDER_PT = 0x05; // Program Tab + public static final int ORDER_GE = 0x08; // Graphic Escape + public static final int ORDER_SBA = 0x11; // Set Buffer Address + public static final int ORDER_EUA = 0x12; // Erase Unprotected to Address + public static final int ORDER_IC = 0x13; // Insert Cursor + public static final int ORDER_SF = 0x1d; // Start Field + public static final int ORDER_SA = 0x28; // Set Attribute + public static final int ORDER_SFE = 0x29; // Start Field Extended + public static final int ORDER_YALE = 0x2b; // Yale sub command + public static final int ORDER_MF = 0x2c; // Modify Field + public static final int ORDER_RA = 0x3c; // Repeat to Address + + // Format control orders + public static final int FCORDER_NULL = 0x00; + public static final int FCORDER_FF = 0x0c; // Form feed + public static final int FCORDER_CR = 0x0d; // Carriage return + public static final int FCORDER_SO = 0x0e; // Shift out (DBCS start) + public static final int FCORDER_SI = 0x0f; // Shift in (DBCS end) + public static final int FCORDER_NL = 0x15; // New line + public static final int FCORDER_EM = 0x19; // End of medium + public static final int FCORDER_LF = 0x25; // Line feed + public static final int FCORDER_DUP = 0x1c; // Duplicate + public static final int FCORDER_FM = 0x1e; // Field mark + public static final int FCORDER_SUB = 0x3f; // Substitute + public static final int FCORDER_EO = 0xff; // Eight ones + + // ========== Field Attributes ========== + public static final int FA_PRINTABLE = 0xc0; + public static final int FA_PROTECT = 0x20; // Protected (1) / Unprotected (0) + public static final int FA_NUMERIC = 0x10; // Numeric (1) / Alphanumeric (0) + public static final int FA_INTENSITY = 0x0c; // Display/selector pen mask + public static final int FA_INT_NORM_NSEL = 0x00; // Normal, non-detect + public static final int FA_INT_NORM_SEL = 0x04; // Normal, detectable + public static final int FA_INT_HIGH_SEL = 0x08; // Intensified, detectable + public static final int FA_INT_ZERO_NSEL = 0x0c; // Non-display, non-detect + public static final int FA_RESERVED = 0x02; + public static final int FA_MODIFY = 0x01; // Modified + + public static final int FA_MASK = FA_PRINTABLE | FA_PROTECT | FA_NUMERIC | FA_INTENSITY | FA_MODIFY; + + public static boolean faIsModified(int fa) { return (fa & FA_MODIFY) != 0; } + public static boolean faIsNumeric(int fa) { return (fa & FA_NUMERIC) != 0; } + public static boolean faIsProtected(int fa) { return (fa & FA_PROTECT) != 0; } + public static boolean faIsSkip(int fa) { return (fa & FA_PROTECT) != 0 && (fa & FA_NUMERIC) != 0; } + public static boolean faIsZero(int fa) { return (fa & FA_INTENSITY) == FA_INT_ZERO_NSEL; } + public static boolean faIsHigh(int fa) { return (fa & FA_INTENSITY) == FA_INT_HIGH_SEL; } + public static boolean faIsNormal(int fa) { + return (fa & FA_INTENSITY) == FA_INT_NORM_NSEL || (fa & FA_INTENSITY) == FA_INT_NORM_SEL; + } + public static boolean faIsSelectable(int fa) { + return (fa & FA_INTENSITY) == FA_INT_NORM_SEL || (fa & FA_INTENSITY) == FA_INT_HIGH_SEL; + } + + // ========== Extended Attributes ========== + public static final int XA_ALL = 0x00; + public static final int XA_3270 = 0xc0; + public static final int XA_VALIDATION = 0xc1; + public static final int XA_OUTLINING = 0xc2; + public static final int XA_HIGHLIGHTING = 0x41; + public static final int XA_FOREGROUND = 0x42; + public static final int XA_CHARSET = 0x43; + public static final int XA_BACKGROUND = 0x45; + public static final int XA_TRANSPARENCY = 0x46; + public static final int XA_INPUT_CONTROL = 0xfe; + + // Highlighting values + public static final int XAH_DEFAULT = 0x00; + public static final int XAH_NORMAL = 0xf0; + public static final int XAH_BLINK = 0xf1; + public static final int XAH_REVERSE = 0xf2; + public static final int XAH_UNDERSCORE = 0xf4; + public static final int XAH_INTENSIFY = 0xf8; + + // Default color + public static final int XAC_DEFAULT = 0x00; + + // Outlining values + public static final int XAO_UNDERLINE = 0x01; + public static final int XAO_RIGHT = 0x02; + public static final int XAO_OVERLINE = 0x04; + public static final int XAO_LEFT = 0x08; + + // Validation values + public static final int XAV_FILL = 0x04; + public static final int XAV_ENTRY = 0x02; + public static final int XAV_TRIGGER = 0x01; + + // Transparency values + public static final int XAT_DEFAULT = 0x00; + public static final int XAT_OR = 0xf0; + public static final int XAT_XOR = 0xf1; + public static final int XAT_OPAQUE = 0xff; + + // Input control + public static final int XAI_DISABLED = 0x00; + public static final int XAI_ENABLED = 0x01; + + // ========== WCC (Write Control Character) ========== + public static final int WCC_RESET_BIT = 0x40; + public static final int WCC_START_PRINTER_BIT = 0x08; + public static final int WCC_SOUND_ALARM_BIT = 0x04; + public static final int WCC_KEYBOARD_RESTORE_BIT = 0x02; + public static final int WCC_RESET_MDT_BIT = 0x01; + + public static boolean wccReset(int wcc) { return (wcc & WCC_RESET_BIT) != 0; } + public static boolean wccStartPrinter(int wcc) { return (wcc & WCC_START_PRINTER_BIT) != 0; } + public static boolean wccSoundAlarm(int wcc) { return (wcc & WCC_SOUND_ALARM_BIT) != 0; } + public static boolean wccKeyboardRestore(int wcc) { return (wcc & WCC_KEYBOARD_RESTORE_BIT) != 0; } + public static boolean wccResetMDT(int wcc) { return (wcc & WCC_RESET_MDT_BIT) != 0; } + + // ========== AID (Attention Identifier) ========== + public static final int AID_NO = 0x60; + public static final int AID_QREPLY = 0x61; + public static final int AID_ENTER = 0x7d; + public static final int AID_PF1 = 0xf1; + public static final int AID_PF2 = 0xf2; + public static final int AID_PF3 = 0xf3; + public static final int AID_PF4 = 0xf4; + public static final int AID_PF5 = 0xf5; + public static final int AID_PF6 = 0xf6; + public static final int AID_PF7 = 0xf7; + public static final int AID_PF8 = 0xf8; + public static final int AID_PF9 = 0xf9; + public static final int AID_PF10 = 0x7a; + public static final int AID_PF11 = 0x7b; + public static final int AID_PF12 = 0x7c; + public static final int AID_PF13 = 0xc1; + public static final int AID_PF14 = 0xc2; + public static final int AID_PF15 = 0xc3; + public static final int AID_PF16 = 0xc4; + public static final int AID_PF17 = 0xc5; + public static final int AID_PF18 = 0xc6; + public static final int AID_PF19 = 0xc7; + public static final int AID_PF20 = 0xc8; + public static final int AID_PF21 = 0xc9; + public static final int AID_PF22 = 0x4a; + public static final int AID_PF23 = 0x4b; + public static final int AID_PF24 = 0x4c; + public static final int AID_OICR = 0xe6; + public static final int AID_MSR_MHS = 0xe7; + public static final int AID_SELECT = 0x7e; + public static final int AID_PA1 = 0x6c; + public static final int AID_PA2 = 0x6e; + public static final int AID_PA3 = 0x6b; + public static final int AID_CLEAR = 0x6d; + public static final int AID_SYSREQ = 0xf0; + public static final int AID_SF = 0x88; + + public static final int SFID_QREPLY = 0x81; + + // ========== Structured Field IDs ========== + public static final int SF_READ_PART = 0x01; + public static final int SF_RP_QUERY = 0x02; + public static final int SF_RP_QLIST = 0x03; + public static final int SF_RPQ_LIST = 0x00; + public static final int SF_RPQ_EQUIV = 0x40; + public static final int SF_RPQ_ALL = 0x80; + public static final int SF_ERASE_RESET = 0x03; + public static final int SF_ER_DEFAULT = 0x00; + public static final int SF_ER_ALT = 0x80; + public static final int SF_SET_REPLY_MODE = 0x09; + public static final int SF_SRM_FIELD = 0x00; + public static final int SF_SRM_XFIELD = 0x01; + public static final int SF_SRM_CHAR = 0x02; + public static final int SF_CREATE_PART = 0x0c; + public static final int SF_OUTBOUND_DS = 0x40; + public static final int SF_TRANSFER_DATA = 0xd0; + + // ========== Query Reply codes ========== + public static final int QR_SUMMARY = 0x80; + public static final int QR_USABLE_AREA = 0x81; + public static final int QR_IMAGE = 0x82; + public static final int QR_TEXT_PART = 0x83; + public static final int QR_ALPHA_PART = 0x84; + public static final int QR_CHARSETS = 0x85; + public static final int QR_COLOR = 0x86; + public static final int QR_HIGHLIGHTING = 0x87; + public static final int QR_REPLY_MODES = 0x88; + public static final int QR_DBCS_ASIA = 0x91; + public static final int QR_DDM = 0x95; + public static final int QR_RPQNAMES = 0xa1; + public static final int QR_IMP_PART = 0xa6; + public static final int QR_NULL = 0xff; + + // ========== Screen model sizes ========== + public static final int MODEL_2_ROWS = 24; + public static final int MODEL_2_COLS = 80; + public static final int MODEL_3_ROWS = 32; + public static final int MODEL_3_COLS = 80; + public static final int MODEL_4_ROWS = 43; + public static final int MODEL_4_COLS = 80; + public static final int MODEL_5_ROWS = 27; + public static final int MODEL_5_COLS = 132; + + public static final int MAX_ROWS_COLS = 0x3fff; + + // ========== Host colors ========== + public static final int HOST_COLOR_NEUTRAL_BLACK = 0; + public static final int HOST_COLOR_BLUE = 1; + public static final int HOST_COLOR_RED = 2; + public static final int HOST_COLOR_PINK = 3; + public static final int HOST_COLOR_GREEN = 4; + public static final int HOST_COLOR_TURQUOISE = 5; + public static final int HOST_COLOR_YELLOW = 6; + public static final int HOST_COLOR_NEUTRAL_WHITE = 7; + public static final int HOST_COLOR_BLACK = 8; + public static final int HOST_COLOR_DEEP_BLUE = 9; + public static final int HOST_COLOR_ORANGE = 10; + public static final int HOST_COLOR_PURPLE = 11; + public static final int HOST_COLOR_PALE_GREEN = 12; + public static final int HOST_COLOR_PALE_TURQUOISE = 13; + public static final int HOST_COLOR_GREY = 14; + public static final int HOST_COLOR_WHITE = 15; + + // ========== Graphics rendition bits ========== + public static final int GR_BLINK = 0x01; + public static final int GR_REVERSE = 0x02; + public static final int GR_UNDERLINE = 0x04; + public static final int GR_INTENSIFY = 0x08; + + // ========== Character set codes ========== + public static final int CS_MASK = 0x03; + public static final int CS_BASE = 0x00; + public static final int CS_APL = 0x01; + public static final int CS_LINEDRAW = 0x02; + public static final int CS_DBCS = 0x03; + public static final int CS_GE = 0x04; + + // ========== BIND definitions ========== + public static final int BIND_RU = 0x31; + public static final int BIND_OFF_MAXRU_SEC = 10; + public static final int BIND_OFF_MAXRU_PRI = 11; + public static final int BIND_OFF_RD = 20; + public static final int BIND_OFF_CD = 21; + public static final int BIND_OFF_RA = 22; + public static final int BIND_OFF_CA = 23; + public static final int BIND_OFF_SSIZE = 24; + public static final int BIND_OFF_PLU_NAME_LEN = 27; + public static final int BIND_PLU_NAME_MAX = 8; + public static final int BIND_OFF_PLU_NAME = 28; + + // BIND dimension flags + public static final int BIND_DIMS_PRESENT = 0x1; + public static final int BIND_DIMS_ALT = 0x2; + public static final int BIND_DIMS_VALID = 0x4; + + // ========== EBCDIC common characters ========== + public static final int EBC_NULL = 0x00; + public static final int EBC_SPACE = 0x40; + public static final int EBC_AMPERSAND = 0x50; + public static final int EBC_MINUS = 0x60; + public static final int EBC_PERIOD = 0x4b; + public static final int EBC_COMMA = 0x6b; + public static final int EBC_DUP = 0x1c; + public static final int EBC_FM = 0x1e; + public static final int EBC_FF = 0x0c; + public static final int EBC_CR = 0x0d; + public static final int EBC_NL = 0x15; + public static final int EBC_EM = 0x19; + public static final int EBC_SUB = 0x3f; + public static final int EBC_EO = 0xff; + + /** + * 6-bit code table for 12-bit buffer address encoding. + */ + public static final int[] CODE_TABLE = { + 0x40, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, + 0xC8, 0xC9, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, + 0x50, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, + 0xD8, 0xD9, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, + 0x60, 0x61, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, + }; + + /** + * Decode a 2-byte buffer address. + * Handles both 12-bit and 14-bit addressing. + */ + public static int decodeAddress(int b1, int b2) { + if ((b1 & 0xC0) == 0x00) { + // 14-bit format + return ((b1 & 0x3F) << 8) | b2; + } else { + // 12-bit format + return ((b1 & 0x3F) << 6) | (b2 & 0x3F); + } + } + + /** + * Encode a buffer address into a 2-byte array. + * Uses 14-bit format if screen > 4096 positions, otherwise 12-bit. + */ + public static byte[] encodeAddress(int addr, int rows, int cols) { + byte[] result = new byte[2]; + if (rows * cols > 0x1000) { + // 14-bit format + result[0] = (byte) ((addr >> 8) & 0x3F); + result[1] = (byte) (addr & 0xFF); + } else { + // 12-bit format + result[0] = (byte) CODE_TABLE[(addr >> 6) & 0x3F]; + result[1] = (byte) CODE_TABLE[addr & 0x3F]; + } + return result; + } + + /** Check if a byte is a 3270 order. */ + public static boolean isOrder(int b) { + switch (b) { + case ORDER_PT: + case ORDER_GE: + case ORDER_SBA: + case ORDER_EUA: + case ORDER_IC: + case ORDER_SF: + case ORDER_SA: + case ORDER_SFE: + case ORDER_YALE: + case ORDER_MF: + case ORDER_RA: + return true; + default: + return false; + } + } + + /** Get human-readable command name. */ + public static String commandName(int cmd) { + switch (cmd) { + case CMD_W: case SNA_CMD_W: return "Write"; + case CMD_EW: case SNA_CMD_EW: return "EraseWrite"; + case CMD_EWA: case SNA_CMD_EWA: return "EraseWriteAlternate"; + case CMD_RB: case SNA_CMD_RB: return "ReadBuffer"; + case CMD_RM: case SNA_CMD_RM: return "ReadModified"; + case CMD_RMA: case SNA_CMD_RMA: return "ReadModifiedAll"; + case CMD_EAU: case SNA_CMD_EAU: return "EraseAllUnprotected"; + case CMD_WSF: case SNA_CMD_WSF: return "WriteStructuredField"; + case CMD_NOP: return "NoOp"; + default: return String.format("Unknown(0x%02x)", cmd); + } + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/protocol/TN3270EConstants.java b/lib3270j/src/main/java/org/lib3270j/protocol/TN3270EConstants.java new file mode 100644 index 0000000..78ea609 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/protocol/TN3270EConstants.java @@ -0,0 +1,142 @@ +package org.lib3270j.protocol; + +/** + * TN3270E protocol constants per RFC 2355. + * Derived from tn3270e.h in x3270. + */ +public final class TN3270EConstants { + + private TN3270EConstants() {} + + // Negotiation operations + public static final int OP_ASSOCIATE = 0; + public static final int OP_CONNECT = 1; + public static final int OP_DEVICE_TYPE = 2; + public static final int OP_FUNCTIONS = 3; + public static final int OP_IS = 4; + public static final int OP_REASON = 5; + public static final int OP_REJECT = 6; + public static final int OP_REQUEST = 7; + public static final int OP_SEND = 8; + + // Reason codes + public static final int REASON_CONN_PARTNER = 0; + public static final int REASON_DEVICE_IN_USE = 1; + public static final int REASON_INV_ASSOCIATE = 2; + public static final int REASON_INV_DEVICE_NAME = 3; + public static final int REASON_INV_DEVICE_TYPE = 4; + public static final int REASON_TYPE_NAME_ERROR = 5; + public static final int REASON_UNKNOWN_ERROR = 6; + public static final int REASON_UNSUPPORTED_REQ = 7; + + // Function names + public static final int FUNC_BIND_IMAGE = 0; + public static final int FUNC_DATA_STREAM_CTL = 1; + public static final int FUNC_RESPONSES = 2; + public static final int FUNC_SCS_CTL_CODES = 3; + public static final int FUNC_SYSREQ = 4; + public static final int FUNC_CONTENTION_RESOLUTION = 5; + public static final int FUNC_SNA_SENSE = 6; + + // Data type names + public static final int DT_3270_DATA = 0x00; + public static final int DT_SCS_DATA = 0x01; + public static final int DT_RESPONSE = 0x02; + public static final int DT_BIND_IMAGE = 0x03; + public static final int DT_UNBIND = 0x04; + public static final int DT_NVT_DATA = 0x05; + public static final int DT_REQUEST = 0x06; + public static final int DT_SSCP_LU_DATA = 0x07; + public static final int DT_PRINT_EOJ = 0x08; + public static final int DT_BID = 0x09; + + // Request flags + public static final int RQF_ERR_COND_CLEARED = 0x00; + public static final int RQF_SEND_DATA = 0x01; + public static final int RQF_KEYBOARD_RESTORE = 0x02; + public static final int RQF_SIGNAL = 0x04; + + // Response flags (header) + public static final int RSF_NO_RESPONSE = 0x00; + public static final int RSF_ERROR_RESPONSE = 0x01; + public static final int RSF_ALWAYS_RESPONSE = 0x02; + + // Response flags (trailer) + public static final int RSF_POSITIVE_RESPONSE = 0x00; + public static final int RSF_NEGATIVE_RESPONSE = 0x01; + public static final int RSF_SNA_SENSE = 0x02; + + // Positive response data + public static final int POS_DEVICE_END = 0x00; + + // Negative response data + public static final int NEG_COMMAND_REJECT = 0x00; + public static final int NEG_INTERVENTION_REQUIRED = 0x01; + public static final int NEG_OPERATION_CHECK = 0x02; + public static final int NEG_COMPONENT_DISCONNECTED = 0x03; + + // TN3270E header size + public static final int EH_SIZE = 5; + + // UNBIND types + public static final int UNBIND_NORMAL = 0x01; + public static final int UNBIND_BIND_FORTHCOMING = 0x02; + public static final int UNBIND_VR_INOPERATIVE = 0x07; + public static final int UNBIND_RX_INOPERATIVE = 0x08; + public static final int UNBIND_HRESET = 0x09; + public static final int UNBIND_SSCP_GONE = 0x0a; + public static final int UNBIND_VR_DEACTIVATED = 0x0b; + public static final int UNBIND_LU_FAILURE_PERM = 0x0c; + public static final int UNBIND_LU_FAILURE_TEMP = 0x0e; + public static final int UNBIND_CLEANUP = 0x0f; + public static final int UNBIND_BAD_SENSE = 0xfe; + + // Name lookups for tracing + + private static final String[] REASON_NAMES = { + "CONN-PARTNER", "DEVICE-IN-USE", "INV-ASSOCIATE", "INV-NAME", + "INV-DEVICE-TYPE", "TYPE-NAME-ERROR", "UNKNOWN-ERROR", "UNSUPPORTED-REQ" + }; + + private static final String[] FUNCTION_NAMES = { + "BIND-IMAGE", "DATA-STREAM-CTL", "RESPONSES", "SCS-CTL-CODES", + "SYSREQ", "CONTENTION-RESOLUTION", "SNA-SENSE" + }; + + private static final String[] DATA_TYPE_NAMES = { + "3270-DATA", "SCS-DATA", "RESPONSE", "BIND-IMAGE", "UNBIND", + "NVT-DATA", "REQUEST", "SSCP-LU-DATA", "PRINT-EOJ", "BID" + }; + + private static final String[] HRSP_FLAG_NAMES = { + "NO-RESPONSE", "ERROR-RESPONSE", "ALWAYS-RESPONSE" + }; + + public static String reasonName(int code) { + return code >= 0 && code < REASON_NAMES.length ? REASON_NAMES[code] : "??"; + } + + public static String functionName(int code) { + return code >= 0 && code < FUNCTION_NAMES.length ? FUNCTION_NAMES[code] : "??"; + } + + public static String dataTypeName(int code) { + return code >= 0 && code < DATA_TYPE_NAMES.length ? DATA_TYPE_NAMES[code] : "??"; + } + + public static String responseHeaderFlagName(int code) { + return code >= 0 && code < HRSP_FLAG_NAMES.length ? HRSP_FLAG_NAMES[code] : "??"; + } + + /** Format a function set as a human-readable string. */ + public static String functionNames(boolean[] funcs) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < funcs.length && i <= FUNC_SNA_SENSE; i++) { + if (funcs[i]) { + if (sb.length() > 0) sb.append(", "); + sb.append(functionName(i)); + } + } + return sb.toString(); + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/protocol/TelnetConstants.java b/lib3270j/src/main/java/org/lib3270j/protocol/TelnetConstants.java new file mode 100644 index 0000000..476f7e6 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/protocol/TelnetConstants.java @@ -0,0 +1,117 @@ +package org.lib3270j.protocol; + +/** + * Telnet protocol constants from RFC 854 and extensions. + * Derived from arpa_telnet.h in x3270. + */ +public final class TelnetConstants { + + private TelnetConstants() {} + + // Telnet commands + public static final int IAC = 255; // Interpret As Command + public static final int DONT = 254; // You are not to use option + public static final int DO = 253; // Please, you use option + public static final int WONT = 252; // I won't use option + public static final int WILL = 251; // I will use option + public static final int SB = 250; // Interpret as subnegotiation + public static final int GA = 249; // You may reverse the line + public static final int EL = 248; // Erase the current line + public static final int EC = 247; // Erase the current character + public static final int AYT = 246; // Are you there + public static final int AO = 245; // Abort output + public static final int IP = 244; // Interrupt process + public static final int BREAK = 243; // Break + public static final int DM = 242; // Data mark + public static final int NOP = 241; // No operation + public static final int SE = 240; // End sub negotiation + public static final int EOR = 239; // End of record + public static final int SUSP = 237; // Suspend process + public static final int xEOF = 236; // End of file + + // Telnet options + public static final int TELOPT_BINARY = 0; // 8-bit data path + public static final int TELOPT_ECHO = 1; // Echo + public static final int TELOPT_RCP = 2; // Prepare to reconnect + public static final int TELOPT_SGA = 3; // Suppress go ahead + public static final int TELOPT_NAMS = 4; // Approximate message size + public static final int TELOPT_STATUS = 5; // Give status + public static final int TELOPT_TM = 6; // Timing mark + public static final int TELOPT_TTYPE = 24; // Terminal type + public static final int TELOPT_EOR = 25; // End of record + public static final int TELOPT_NAWS = 31; // Window size + public static final int TELOPT_TSPEED = 32; // Terminal speed + public static final int TELOPT_LFLOW = 33; // Remote flow control + public static final int TELOPT_LINEMODE = 34; // Linemode option + public static final int TELOPT_XDISPLOC = 35; // X Display Location + public static final int TELOPT_OLD_ENVIRON = 36; // Old environment variables + public static final int TELOPT_AUTHENTICATION = 37; // Authenticate + public static final int TELOPT_ENCRYPT = 38; // Encryption option + public static final int TELOPT_NEW_ENVIRON = 39; // New environment variables + public static final int TELOPT_TN3270E = 40; // Extended 3270 regime + public static final int TELOPT_STARTTLS = 46; // Start TLS + public static final int TELOPT_EXOPL = 255; // Extended options list + + // Sub-option qualifiers + public static final int TELQUAL_IS = 0; // Option is... + public static final int TELQUAL_SEND = 1; // Send option + public static final int TELQUAL_INFO = 2; // Info + + // New-environ sub-option objects + public static final int TELOBJ_VAR = 0; + public static final int TELOBJ_VALUE = 1; + public static final int TELOBJ_ESC = 2; + public static final int TELOBJ_USERVAR = 3; + + // STARTTLS sub-option + public static final int TLS_FOLLOWS = 1; + + // Standard ports + public static final int TELNET_PORT = 23; + public static final int TELNETS_PORT = 992; + + /** Telnet option name lookup for tracing. */ + public static String optionName(int opt) { + switch (opt) { + case TELOPT_BINARY: return "BINARY"; + case TELOPT_ECHO: return "ECHO"; + case TELOPT_SGA: return "SGA"; + case TELOPT_TTYPE: return "TTYPE"; + case TELOPT_EOR: return "EOR"; + case TELOPT_NAWS: return "NAWS"; + case TELOPT_NEW_ENVIRON: return "NEW-ENVIRON"; + case TELOPT_TN3270E: return "TN3270E"; + case TELOPT_STARTTLS: return "STARTTLS"; + case TELOPT_TSPEED: return "TSPEED"; + case TELOPT_LFLOW: return "LFLOW"; + case TELOPT_LINEMODE: return "LINEMODE"; + case TELOPT_XDISPLOC: return "XDISPLOC"; + case TELOPT_OLD_ENVIRON: return "OLD-ENVIRON"; + default: return "OPT-" + opt; + } + } + + /** Telnet command name lookup for tracing. */ + public static String commandName(int cmd) { + switch (cmd) { + case IAC: return "IAC"; + case DONT: return "DONT"; + case DO: return "DO"; + case WONT: return "WONT"; + case WILL: return "WILL"; + case SB: return "SB"; + case GA: return "GA"; + case EL: return "EL"; + case EC: return "EC"; + case AYT: return "AYT"; + case AO: return "AO"; + case IP: return "IP"; + case BREAK: return "BRK"; + case DM: return "DMARK"; + case NOP: return "NOP"; + case SE: return "SE"; + case EOR: return "EOR"; + default: return "CMD-" + cmd; + } + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/screen/ExtendedAttribute.java b/lib3270j/src/main/java/org/lib3270j/screen/ExtendedAttribute.java new file mode 100644 index 0000000..aaed191 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/screen/ExtendedAttribute.java @@ -0,0 +1,77 @@ +package org.lib3270j.screen; + +/** + * Extended attribute structure for a single screen buffer position. + * Mirrors struct ea from globals.h in x3270. + */ +public class ExtendedAttribute { + + /** EBCDIC character code at this position. */ + public byte ec; + + /** Field attribute byte (non-zero if this position IS a field attribute). */ + public byte fa; + + /** Foreground color (0x00 for default, or 0xf0-0xff for explicit). */ + public byte fg; + + /** Background color (0x00 for default, or 0xf0-0xff for explicit). */ + public byte bg; + + /** + * Graphics rendition bits. + * GR_BLINK=0x01, GR_REVERSE=0x02, GR_UNDERLINE=0x04, GR_INTENSIFY=0x08 + */ + public byte gr; + + /** Character set (CS_BASE=0, CS_APL=1, CS_LINEDRAW=2, CS_DBCS=3; CS_GE=0x04 flag). */ + public byte cs; + + /** Input control (DBCS). */ + public byte ic; + + /** DBCS state. */ + public byte db; + + /** + * Unicode character for display (set by translation from ec, or directly in NVT mode). + */ + public char ucs4; + + /** Clear all attributes. */ + public void clear() { + ec = 0; + fa = 0; + fg = 0; + bg = 0; + gr = 0; + cs = 0; + ic = 0; + db = 0; + ucs4 = 0; + } + + /** Copy all values from another ExtendedAttribute. */ + public void copyFrom(ExtendedAttribute other) { + this.ec = other.ec; + this.fa = other.fa; + this.fg = other.fg; + this.bg = other.bg; + this.gr = other.gr; + this.cs = other.cs; + this.ic = other.ic; + this.db = other.db; + this.ucs4 = other.ucs4; + } + + /** Check if this position is a field attribute. */ + public boolean isFieldAttribute() { + return fa != 0; + } + + @Override + public String toString() { + return String.format("EA[ec=%02x fa=%02x fg=%02x bg=%02x gr=%02x cs=%02x u=%04x]", + ec & 0xFF, fa & 0xFF, fg & 0xFF, bg & 0xFF, gr & 0xFF, cs & 0xFF, (int) ucs4); + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java b/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java new file mode 100644 index 0000000..e1cec28 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java @@ -0,0 +1,329 @@ +package org.lib3270j.screen; + +import org.lib3270j.TerminalModel; +import org.lib3270j.charset.EbcdicTranslator; +import static org.lib3270j.protocol.DS3270Constants.*; + +/** + * The 3270 screen buffer. + * Manages the display buffer, cursor, field attributes, and screen dimensions. + * Equivalent to ea_buf[] / aea_buf[] and related state in ctlr.c. + */ +public class ScreenBuffer { + + private ExtendedAttribute[] buffer; // Main screen buffer + private ExtendedAttribute[] altBuffer; // Alternate screen buffer + private final ExtendedAttribute defaultFA; // Default field attribute (ea_buf[-1]) + + private int maxRows, maxCols; // Maximum (alternate) dimensions + private int defRows, defCols; // Default dimensions (24x80) + private int altRows, altCols; // Alternate dimensions + private int rows, cols; // Current dimensions + private int cursorAddress; + private int bufferAddress; + private boolean screenAlt; // Using alternate screen? + private boolean formatted; // Screen has at least one field attribute? + private byte replyMode = SF_SRM_FIELD; + + // Change tracking + private boolean screenChanged; + private int firstChanged = -1; + private int lastChanged = -1; + + // Default attribute values + private byte defaultFg = 0x00; + private byte defaultBg = 0x00; + private byte defaultGr = 0x00; + private byte defaultCs = 0x00; + private byte defaultIc = 0x00; + + private final EbcdicTranslator translator; + + public ScreenBuffer(TerminalModel model, EbcdicTranslator translator) { + this.translator = translator; + this.defRows = MODEL_2_ROWS; + this.defCols = MODEL_2_COLS; + this.altRows = model.getAlternateRows(); + this.altCols = model.getAlternateCols(); + this.maxRows = altRows; + this.maxCols = altCols; + this.rows = defRows; + this.cols = defCols; + + // Default field attribute (like ea_buf[-1]) + defaultFA = new ExtendedAttribute(); + defaultFA.fa = (byte) (FA_PRINTABLE | FA_MODIFY); + defaultFA.ic = 1; + + allocateBuffers(); + } + + private void allocateBuffers() { + int size = maxRows * maxCols; + buffer = new ExtendedAttribute[size]; + altBuffer = new ExtendedAttribute[size]; + for (int i = 0; i < size; i++) { + buffer[i] = new ExtendedAttribute(); + altBuffer[i] = new ExtendedAttribute(); + } + cursorAddress = 0; + bufferAddress = 0; + } + + /** Get the current screen buffer. */ + public ExtendedAttribute[] getBuffer() { return buffer; } + + /** Get a cell at the given buffer address. */ + public ExtendedAttribute getCell(int addr) { + if (addr < 0 || addr >= rows * cols) return defaultFA; + return buffer[addr]; + } + + // ========== Dimension accessors ========== + public int getRows() { return rows; } + public int getCols() { return cols; } + public int getMaxRows() { return maxRows; } + public int getMaxCols() { return maxCols; } + public int getDefRows() { return defRows; } + public int getDefCols() { return defCols; } + public int getAltRows() { return altRows; } + public int getAltCols() { return altCols; } + public boolean isScreenAlt() { return screenAlt; } + + /** Update alternate dimensions from BIND image. Re-allocates buffers if needed. */ + public void setAlternateDimensions(int newAltRows, int newAltCols) { + if (newAltRows == altRows && newAltCols == altCols) return; + this.altRows = newAltRows; + this.altCols = newAltCols; + // maxRows/maxCols should be the larger of alt vs current max + if (newAltRows > maxRows || newAltCols > maxCols) { + this.maxRows = Math.max(maxRows, newAltRows); + this.maxCols = Math.max(maxCols, newAltCols); + allocateBuffers(); + } + } + + // ========== Cursor ========== + public int getCursorAddress() { return cursorAddress; } + public void setCursorAddress(int addr) { this.cursorAddress = addr; } + public int getCursorRow() { return cursorAddress / cols; } + public int getCursorCol() { return cursorAddress % cols; } + + public int getBufferAddress() { return bufferAddress; } + public void setBufferAddress(int addr) { this.bufferAddress = addr; } + + public byte getReplyMode() { return replyMode; } + public void setReplyMode(byte mode) { this.replyMode = mode; } + + // ========== Screen erase ========== + + /** + * Perform an erase, optionally using the alternate screen size. + */ + public void erase(boolean alt) { + clear(); + int newRows = alt ? altRows : defRows; + int newCols = alt ? altCols : defCols; + if (alt == screenAlt && rows == newRows && cols == newCols) { + return; + } + rows = newRows; + cols = newCols; + screenAlt = alt; + } + + /** Clear the entire buffer. */ + public void clear() { + for (ExtendedAttribute ea : buffer) { + ea.clear(); + } + cursorAddress = 0; + bufferAddress = 0; + formatted = false; + screenChanged = true; + + defaultFg = 0x00; + defaultBg = 0x00; + defaultGr = 0x00; + defaultCs = 0x00; + defaultIc = 0x00; + replyMode = SF_SRM_FIELD; + } + + /** + * Erase all unprotected fields. + */ + public void eraseAllUnprotected() { + int size = rows * cols; + boolean inUnprotected = false; + + for (int i = 0; i < size; i++) { + ExtendedAttribute ea = buffer[i]; + if (ea.isFieldAttribute()) { + if (!faIsProtected(ea.fa & 0xFF)) { + inUnprotected = true; + // Clear modified bit + ea.fa = (byte) (ea.fa & ~FA_MODIFY); + } else { + inUnprotected = false; + } + } else if (inUnprotected) { + ea.ec = 0; + ea.ucs4 = 0; + ea.cs = 0; + ea.fg = 0; + ea.bg = 0; + ea.gr = 0; + ea.ic = 0; + } + } + + // Move cursor to first unprotected field + cursorAddress = findNextUnprotected(0); + screenChanged = true; + } + + // ========== Field attribute navigation ========== + + /** + * Find the field attribute for a given buffer address. + * Returns -1 if screen is not formatted. + */ + public int findFieldAttribute(int baddr) { + if (!formatted) return -1; + int size = rows * cols; + int start = baddr; + do { + if (buffer[baddr].isFieldAttribute()) { + return baddr; + } + baddr = (baddr > 0) ? baddr - 1 : size - 1; + } while (baddr != start); + return -1; + } + + /** + * Get the field attribute byte for a given position. + */ + public byte getFieldAttributeAt(int baddr) { + int fa_addr = findFieldAttribute(baddr); + if (fa_addr < 0) return defaultFA.fa; + return buffer[fa_addr].fa; + } + + /** + * Find the next unprotected field after the given address. + * Returns 0 if none found. + */ + public int findNextUnprotected(int baddr) { + int size = rows * cols; + int start = baddr; + do { + int next = (baddr + 1) % size; + if (buffer[baddr].isFieldAttribute() + && !faIsProtected(buffer[baddr].fa & 0xFF) + && !buffer[next].isFieldAttribute()) { + return next; + } + baddr = next; + } while (baddr != start); + return 0; + } + + /** Refresh the formatted flag by scanning for any field attributes. */ + public void updateFormatted() { + formatted = false; + int size = rows * cols; + for (int i = 0; i < size; i++) { + if (buffer[i].isFieldAttribute()) { + formatted = true; + return; + } + } + } + + public boolean isFormatted() { return formatted; } + public void setFormatted(boolean f) { this.formatted = f; } + + // ========== Buffer address arithmetic ========== + + /** Increment buffer address (wrapping). */ + public int incrementAddress(int addr) { + return (addr + 1) % (rows * cols); + } + + /** Decrement buffer address (wrapping). */ + public int decrementAddress(int addr) { + return (addr > 0) ? addr - 1 : (rows * cols) - 1; + } + + /** Convert buffer address to row. */ + public int addressToRow(int addr) { return addr / cols; } + + /** Convert buffer address to column. */ + public int addressToCol(int addr) { return addr % cols; } + + /** Convert row/col to buffer address. */ + public int rowColToAddress(int row, int col) { return row * cols + col; } + + // ========== Change tracking ========== + + public boolean isScreenChanged() { return screenChanged; } + public void clearChanged() { screenChanged = false; firstChanged = -1; lastChanged = -1; } + public void markAllChanged() { screenChanged = true; } + + // ========== Setters for model reconfiguration ========== + + public void setDimensions(int maxRows, int maxCols, int defRows, int defCols, + int altRows, int altCols) { + this.maxRows = maxRows; + this.maxCols = maxCols; + this.defRows = defRows; + this.defCols = defCols; + this.altRows = altRows; + this.altCols = altCols; + this.rows = defRows; + this.cols = defCols; + allocateBuffers(); + } + + public void translateToUnicode() { + int size = rows * cols; + for (int i = 0; i < size; i++) { + ExtendedAttribute ea = buffer[i]; + if (!ea.isFieldAttribute()) { + if (ea.ec == 0) { + ea.ucs4 = 0; + } else if (ea.cs == 1 || ea.cs == 0x04) { // CS_APL or CS_GE + ea.ucs4 = getAplGraphic(ea.ec & 0xFF); + } else { + ea.ucs4 = translator.ebcdicToUnicode(ea.ec & 0xFF); + } + } + } + } + + private char getAplGraphic(int ec) { + switch (ec) { + case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─' + case 0x85: return '\u2502'; // Vertical Line 'e' -> '│' + case 0xC5: return '\u250C'; // Top Left 'E' -> '┌' + case 0xD5: return '\u2510'; // Top Right 'N' -> '┐' + case 0xC4: return '\u2514'; // Bottom Left 'D' -> '└' + case 0xD4: return '\u2518'; // Bottom Right 'M' -> '┘' + case 0xC6: return '\u251C'; // T-Junction Left 'F' -> '├' + case 0xD6: return '\u2524'; // T-Junction Right 'O' -> '┤' + case 0xC7: return '\u252C'; // T-Junction Top 'G' -> '┬' + case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴' + case 0xCB: return '\u253C'; // Cross -> '┼' + default: return translator.ebcdicToUnicode(ec); + } + } + + /** + * Get the default field attribute. + */ + public ExtendedAttribute getDefaultFieldAttribute() { + return defaultFA; + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/telnet/TelnetConnection.java b/lib3270j/src/main/java/org/lib3270j/telnet/TelnetConnection.java new file mode 100644 index 0000000..32b96ff --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/telnet/TelnetConnection.java @@ -0,0 +1,151 @@ +package org.lib3270j.telnet; + +import org.lib3270j.ConnectionState; +import org.lib3270j.ConnectionConfig; +import org.lib3270j.Telnet3270Client; + +import java.io.*; +import java.net.*; +import java.util.logging.Logger; +import java.util.logging.Level; + +import static org.lib3270j.protocol.TelnetConstants.*; + +/** + * Manages the raw TCP socket connection to a TN3270 host. + * Handles connect/disconnect, raw byte I/O, and spawns a reader thread. + */ +public class TelnetConnection { + + private static final Logger log = Logger.getLogger(TelnetConnection.class.getName()); + private static final int READ_BUFFER_SIZE = 32768; + + private Socket socket; + private InputStream inputStream; + private OutputStream outputStream; + private Thread readerThread; + private volatile boolean running; + private final TelnetFSM fsm; + private final ConnectionConfig config; + + public TelnetConnection(ConnectionConfig config, TelnetFSM fsm) { + this.config = config; + this.fsm = fsm; + } + + /** + * Connect to the host. Blocks until connection is established or fails. + */ + public void connect() throws IOException { + log.info("Connecting to " + config.getHost() + ":" + config.getPort()); + socket = new Socket(); + socket.setKeepAlive(true); + socket.setOOBInline(true); + socket.setTcpNoDelay(true); + socket.connect(new InetSocketAddress(config.getHost(), config.getPort()), + config.getConnectTimeoutMs()); + + inputStream = new BufferedInputStream(socket.getInputStream(), READ_BUFFER_SIZE); + outputStream = new BufferedOutputStream(socket.getOutputStream()); + + log.info("Connected to " + socket.getRemoteSocketAddress()); + + running = true; + readerThread = new Thread(this::readLoop, "TN3270-Reader"); + readerThread.setDaemon(true); + readerThread.start(); + } + + /** + * Send raw bytes to the host. + */ + public synchronized void sendRaw(byte[] data) throws IOException { + sendRaw(data, 0, data.length); + } + + /** + * Send raw bytes to the host with offset and length. + */ + public synchronized void sendRaw(byte[] data, int offset, int length) throws IOException { + if (outputStream == null) return; + outputStream.write(data, offset, length); + outputStream.flush(); + if (log.isLoggable(Level.FINE)) { + log.fine("SENT " + length + " bytes: " + formatHex(data, offset, length)); + } + } + + /** + * Disconnect from the host. + */ + public void disconnect() { + running = false; + try { + if (socket != null && !socket.isClosed()) { + socket.shutdownInput(); + socket.shutdownOutput(); + socket.close(); + } + } catch (IOException e) { + log.log(Level.FINE, "Error during disconnect", e); + } + socket = null; + inputStream = null; + outputStream = null; + log.info("Disconnected"); + } + + /** + * Check if the socket is connected. + */ + public boolean isConnected() { + return socket != null && socket.isConnected() && !socket.isClosed(); + } + + /** + * Main reader loop. Reads from socket and feeds bytes to the telnet FSM. + */ + private void readLoop() { + byte[] buf = new byte[READ_BUFFER_SIZE]; + try { + while (running && isConnected()) { + int n = inputStream.read(buf); + if (n < 0) { + log.info("Host disconnected (EOF)"); + fsm.onDisconnect(); + break; + } + if (n > 0) { + if (log.isLoggable(Level.FINE)) { + log.fine("RCVD " + n + " bytes: " + formatHex(buf, 0, n)); + } + for (int i = 0; i < n; i++) { + fsm.feedByte(buf[i] & 0xFF); + } + fsm.endOfNetworkData(); + } + } + } catch (SocketException e) { + if (running) { + log.info("Socket closed: " + e.getMessage()); + fsm.onDisconnect(); + } + } catch (IOException e) { + if (running) { + log.log(Level.WARNING, "Read error", e); + fsm.onError("Read error: " + e.getMessage()); + } + } + } + + /** Format bytes as hex string for logging. */ + static String formatHex(byte[] data, int offset, int length) { + StringBuilder sb = new StringBuilder(length * 3); + for (int i = 0; i < length && i < 128; i++) { + if (i > 0) sb.append(' '); + sb.append(String.format("%02x", data[offset + i] & 0xFF)); + } + if (length > 128) sb.append("..."); + return sb.toString(); + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/telnet/TelnetFSM.java b/lib3270j/src/main/java/org/lib3270j/telnet/TelnetFSM.java new file mode 100644 index 0000000..9a16fbc --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/telnet/TelnetFSM.java @@ -0,0 +1,954 @@ +package org.lib3270j.telnet; + +import org.lib3270j.*; +import org.lib3270j.datastream.DataStreamProcessor; +import org.lib3270j.protocol.*; +import org.lib3270j.screen.ScreenBuffer; +import org.lib3270j.listener.*; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.logging.Logger; +import java.util.logging.Level; + +import static org.lib3270j.protocol.TelnetConstants.*; +import static org.lib3270j.protocol.TN3270EConstants.*; + +/** + * Telnet finite state machine. + * Processes incoming bytes one at a time, handling telnet commands, + * option negotiation, sub-negotiation, and 3270 data stream framing. + * + * Eight states matching telnet.c: + * TNS_DATA, TNS_IAC, TNS_WILL, TNS_WONT, TNS_DO, TNS_DONT, TNS_SB, TNS_SB_IAC + */ +public class TelnetFSM { + + private static final Logger log = Logger.getLogger(TelnetFSM.class.getName()); + + // Telnet FSM states + private static final int TNS_DATA = 0; + private static final int TNS_IAC = 1; + private static final int TNS_WILL = 2; + private static final int TNS_WONT = 3; + private static final int TNS_DO = 4; + private static final int TNS_DONT = 5; + private static final int TNS_SB = 6; + private static final int TNS_SB_IAC = 7; + + private int state = TNS_DATA; + + // Option state tracking + private final boolean[] myOpts = new boolean[256]; // options we have enabled + private final boolean[] hisOpts = new boolean[256]; // options the host has enabled + + // 3270 input buffer (accumulated between telnet framing) + private final ByteArrayOutputStream ibuf = new ByteArrayOutputStream(32768); + + // Sub-negotiation buffer + private final ByteArrayOutputStream sbbuf = new ByteArrayOutputStream(4096); + + // TN3270E state + private boolean tn3270eNegotiated; + private TN3270ESubmode tn3270eSubmode = TN3270ESubmode.UNBOUND; + private boolean tn3270eBound; + private final boolean[] eFuncs = new boolean[8]; // Negotiated TN3270E functions + private int eXmitSeq; + private int responseRequired = RSF_NO_RESPONSE; + private boolean deferredWillTtype; + + // Connection references + private TelnetConnection connection; + private final ConnectionConfig config; + private final ScreenBuffer screenBuffer; + private final DataStreamProcessor dsProcessor; + private volatile ConnectionState connectionState = ConnectionState.NOT_CONNECTED; + + // Listeners + private final List connectionListeners = new CopyOnWriteArrayList<>(); + private final List screenListeners = new CopyOnWriteArrayList<>(); + + // Connected LU info + private String connectedLu; + private String connectedType; + + enum TN3270ESubmode { UNBOUND, E_3270, E_NVT, E_SSCP } + + public TelnetFSM(ConnectionConfig config, ScreenBuffer screenBuffer, DataStreamProcessor dsProcessor) { + this.config = config; + this.screenBuffer = screenBuffer; + this.dsProcessor = dsProcessor; + } + + public void setConnection(TelnetConnection connection) { + this.connection = connection; + } + + public void addConnectionListener(ConnectionListener l) { connectionListeners.add(l); } + public void addScreenUpdateListener(ScreenUpdateListener l) { screenListeners.add(l); } + + public ConnectionState getConnectionState() { return connectionState; } + public boolean[] getMyOpts() { return myOpts; } + public boolean[] getHisOpts() { return hisOpts; } + + /** + * Called when the TCP connection is established. + * Initialize telnet state and prepare for negotiation. + */ + public void onConnected() { + state = TNS_DATA; + java.util.Arrays.fill(myOpts, false); + java.util.Arrays.fill(hisOpts, false); + java.util.Arrays.fill(eFuncs, false); + tn3270eNegotiated = false; + tn3270eSubmode = TN3270ESubmode.UNBOUND; + tn3270eBound = false; + eXmitSeq = 0; + deferredWillTtype = false; + ibuf.reset(); + sbbuf.reset(); + + // Initial TN3270E function requests + eFuncs[FUNC_BIND_IMAGE] = true; + eFuncs[FUNC_RESPONSES] = true; + eFuncs[FUNC_SYSREQ] = true; + + changeState(ConnectionState.TELNET_PENDING); + } + + /** + * Feed a single byte from the network into the FSM. + */ + public void feedByte(int c) { + switch (state) { + case TNS_DATA: + processData(c); + break; + case TNS_IAC: + processIAC(c); + break; + case TNS_WILL: + processWill(c); + state = TNS_DATA; + break; + case TNS_WONT: + processWont(c); + state = TNS_DATA; + break; + case TNS_DO: + processDo(c); + state = TNS_DATA; + break; + case TNS_DONT: + processDont(c); + state = TNS_DATA; + break; + case TNS_SB: + processSB(c); + break; + case TNS_SB_IAC: + processSBIAC(c); + break; + } + } + + /** Called at the end of a network read batch. */ + public void endOfNetworkData() { + // Opportunity to flush any pending NVT data + } + + // ========== TNS_DATA ========== + + private void processData(int c) { + if (c == IAC) { + state = TNS_IAC; + return; + } + + if (connectionState == ConnectionState.TELNET_PENDING) { + // Got data before any telnet commands — assume NVT mode + changeState(ConnectionState.CONNECTED_NVT); + } + + // Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states) + if (connectionState.is3270() || connectionState.isTn3270e()) { + ibuf.write(c); + } + // NVT data would go to NVT processor (not implemented in initial version) + } + + // ========== TNS_IAC ========== + + private void processIAC(int c) { + switch (c) { + case IAC: // Escaped IAC — literal 0xFF + ibuf.write(c); + state = TNS_DATA; + break; + case EOR: // End of record — process accumulated 3270 data + log.fine("RCVD EOR"); + if (connectionState.is3270() || connectionState.isTn3270e()) { + processEndOfRecord(); + } + ibuf.reset(); + state = TNS_DATA; + break; + case WILL: state = TNS_WILL; break; + case WONT: state = TNS_WONT; break; + case DO: state = TNS_DO; break; + case DONT: state = TNS_DONT; break; + case SB: + sbbuf.reset(); + state = TNS_SB; + break; + case GA: + log.fine("RCVD GA"); + state = TNS_DATA; + break; + case NOP: + log.fine("RCVD NOP"); + state = TNS_DATA; + break; + default: + log.fine("RCVD IAC " + TelnetConstants.commandName(c)); + state = TNS_DATA; + break; + } + } + + // ========== TNS_WILL (host sends WILL option) ========== + + private void processWill(int opt) { + log.info("RCVD WILL " + TelnetConstants.optionName(opt)); + + switch (opt) { + case TELOPT_SGA: + case TELOPT_BINARY: + case TELOPT_EOR: + case TELOPT_ECHO: + if (!hisOpts[opt]) { + hisOpts[opt] = true; + sendCommand(DO, opt); + } + break; + + case TELOPT_TN3270E: + if (!hisOpts[opt]) { + hisOpts[opt] = true; + sendCommand(DO, opt); + } + break; + + default: + sendCommand(DONT, opt); + break; + } + + checkIn3270(); + } + + // ========== TNS_WONT ========== + + private void processWont(int opt) { + log.info("RCVD WONT " + TelnetConstants.optionName(opt)); + if (hisOpts[opt]) { + hisOpts[opt] = false; + sendCommand(DONT, opt); + } + checkIn3270(); + } + + // ========== TNS_DO (host requests we enable option) ========== + + private void processDo(int opt) { + log.info("RCVD DO " + TelnetConstants.optionName(opt)); + + switch (opt) { + case TELOPT_BINARY: + case TELOPT_EOR: + case TELOPT_SGA: + if (!myOpts[opt]) { + myOpts[opt] = true; + sendCommand(WILL, opt); + } + break; + + case TELOPT_TTYPE: + if (!myOpts[opt]) { + myOpts[opt] = true; + if (hisOpts[TELOPT_TN3270E]) { + // Defer TTYPE response until TN3270E negotiation completes + deferredWillTtype = true; + } else { + sendCommand(WILL, opt); + } + } + break; + + case TELOPT_TN3270E: + if (!myOpts[opt]) { + myOpts[opt] = true; + sendCommand(WILL, opt); + // Start TN3270E sub-negotiation: send device type request + sendTN3270EDeviceTypeRequest(); + } + break; + + case TELOPT_NAWS: + if (!myOpts[opt]) { + myOpts[opt] = true; + sendCommand(WILL, opt); + } + sendNaws(); + break; + + case TELOPT_NEW_ENVIRON: + if (!myOpts[opt]) { + myOpts[opt] = true; + sendCommand(WILL, opt); + } + break; + + default: + sendCommand(WONT, opt); + break; + } + + checkIn3270(); + } + + // ========== TNS_DONT ========== + + private void processDont(int opt) { + log.info("RCVD DONT " + TelnetConstants.optionName(opt)); + if (myOpts[opt]) { + myOpts[opt] = false; + sendCommand(WONT, opt); + } + checkIn3270(); + } + + // ========== TNS_SB (accumulating sub-negotiation data) ========== + + private void processSB(int c) { + if (c == IAC) { + state = TNS_SB_IAC; + } else { + sbbuf.write(c); + } + } + + // ========== TNS_SB_IAC (IAC seen during sub-negotiation) ========== + + private void processSBIAC(int c) { + if (c == SE) { + // Sub-negotiation complete + processSubNegotiation(sbbuf.toByteArray()); + state = TNS_DATA; + } else if (c == IAC) { + // Escaped IAC within sub-negotiation + sbbuf.write(IAC); + state = TNS_SB; + } else { + // Shouldn't happen, but recover + log.warning("Unexpected byte " + c + " after IAC in SB"); + state = TNS_DATA; + } + } + + // ========== Sub-negotiation processing ========== + + private void processSubNegotiation(byte[] data) { + if (data.length < 1) return; + + int opt = data[0] & 0xFF; + log.info("RCVD SB " + TelnetConstants.optionName(opt) + " (" + data.length + " bytes)"); + + switch (opt) { + case TELOPT_TTYPE: + handleTTypeSB(data); + break; + case TELOPT_TN3270E: + handleTN3270ESB(data); + break; + case TELOPT_NEW_ENVIRON: + handleNewEnvironSB(data); + break; + default: + log.info("Ignoring SB for option " + opt); + break; + } + } + + // ========== TTYPE sub-negotiation ========== + + private void handleTTypeSB(byte[] data) { + if (data.length >= 2 && data[1] == TELQUAL_SEND) { + // Host asks for terminal type + String termType = config.getEffectiveTerminalType(); + log.info("RCVD SB TTYPE SEND - Responding with: " + termType); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(IAC); + out.write(SB); + out.write(TELOPT_TTYPE); + out.write(TELQUAL_IS); + for (char ch : termType.toCharArray()) { + out.write((byte) ch); + } + out.write(IAC); + out.write(SE); + sendBytes(out.toByteArray()); + log.info("SENT SB TTYPE IS " + termType + " SE"); + } + } + + // ========== NEW_ENVIRON sub-negotiation ========== + + private void handleNewEnvironSB(byte[] data) { + if (data.length >= 2 && data[1] == TELQUAL_SEND) { + log.info("RCVD SB NEW-ENVIRON SEND - Responding with empty IS"); + byte[] response = { (byte) IAC, (byte) SB, (byte) TELOPT_NEW_ENVIRON, + (byte) TELQUAL_IS, (byte) IAC, (byte) SE }; + sendBytes(response); + log.info("SENT SB NEW-ENVIRON IS SE"); + } + } + + // ========== TN3270E sub-negotiation ========== + + private void handleTN3270ESB(byte[] data) { + if (data.length < 2) return; + + int op = data[1] & 0xFF; + log.info("TN3270E SB op=" + op + " (" + tn3270eOpName(op) + ")"); + + switch (op) { + case OP_SEND: + // Host asks us to send device-type request + sendTN3270EDeviceTypeRequest(); + break; + + case OP_DEVICE_TYPE: + handleTN3270EDeviceType(data); + break; + + case OP_FUNCTIONS: + handleTN3270EFunctions(data); + break; + + case OP_IS: + // Could be device-type IS or functions IS, check context + if (data.length >= 3 && data[2] == OP_DEVICE_TYPE) { + handleTN3270EDeviceType(data); + } else if (data.length >= 3 && data[2] == OP_FUNCTIONS) { + handleTN3270EFunctions(data); + } + break; + + default: + log.info("Unhandled TN3270E op: " + op); + break; + } + } + + private void sendTN3270EDeviceTypeRequest() { + String termType = config.getEffectiveTerminalType(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(IAC); + out.write(SB); + out.write(TELOPT_TN3270E); + out.write(OP_DEVICE_TYPE); + out.write(OP_REQUEST); + for (char ch : termType.toCharArray()) { + out.write((byte) ch); + } + // Add LU name if specified + if (config.getLuName() != null && !config.getLuName().isEmpty()) { + out.write(OP_CONNECT); + for (char ch : config.getLuName().toCharArray()) { + out.write((byte) ch); + } + } + out.write(IAC); + out.write(SE); + sendBytes(out.toByteArray()); + log.info("SENT SB TN3270E DEVICE-TYPE REQUEST " + termType + + (config.getLuName() != null ? " CONNECT " + config.getLuName() : "") + " SE"); + } + + private void handleTN3270EDeviceType(byte[] data) { + // Parse: TN3270E DEVICE-TYPE IS CONNECT + // or: TN3270E DEVICE-TYPE REJECT REASON + int pos = 2; // Skip TN3270E and DEVICE-TYPE + + if (pos < data.length && (data[pos] & 0xFF) == OP_IS) { + pos++; + } + + if (pos < data.length && (data[pos] & 0xFF) == OP_REJECT) { + // Rejection + pos++; + int reason = -1; + if (pos < data.length && (data[pos] & 0xFF) == OP_REASON) { + pos++; + if (pos < data.length) { + reason = data[pos] & 0xFF; + } + } + log.warning("TN3270E device-type rejected: " + TN3270EConstants.reasonName(reason)); + + // Fall back to plain TN3270 + myOpts[TELOPT_TN3270E] = false; + hisOpts[TELOPT_TN3270E] = false; + + // Send deferred WILL TTYPE if needed + if (deferredWillTtype) { + sendCommand(WILL, TELOPT_TTYPE); + deferredWillTtype = false; + } + return; + } + + // Parse device type name + StringBuilder deviceType = new StringBuilder(); + while (pos < data.length && (data[pos] & 0xFF) != OP_CONNECT) { + deviceType.append((char) (data[pos] & 0xFF)); + pos++; + } + + // Parse device name (after CONNECT) + String deviceName = null; + if (pos < data.length && (data[pos] & 0xFF) == OP_CONNECT) { + pos++; + StringBuilder name = new StringBuilder(); + while (pos < data.length) { + name.append((char) (data[pos] & 0xFF)); + pos++; + } + deviceName = name.toString(); + } + + connectedType = deviceType.toString(); + connectedLu = deviceName; + log.info("TN3270E device-type IS " + connectedType + + (connectedLu != null ? " CONNECT " + connectedLu : "")); + + // Now send functions request + sendTN3270EFunctionsRequest(); + } + + private void sendTN3270EFunctionsRequest() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(IAC); + out.write(SB); + out.write(TELOPT_TN3270E); + out.write(OP_FUNCTIONS); + out.write(OP_REQUEST); + + StringBuilder funcNames = new StringBuilder(); + for (int i = 0; i < eFuncs.length; i++) { + if (eFuncs[i]) { + out.write(i); + if (funcNames.length() > 0) funcNames.append(" "); + funcNames.append(TN3270EConstants.functionName(i)); + } + } + + out.write(IAC); + out.write(SE); + sendBytes(out.toByteArray()); + log.info("SENT SB TN3270E FUNCTIONS REQUEST " + funcNames + " SE"); + } + + private void handleTN3270EFunctions(byte[] data) { + // Parse: TN3270E FUNCTIONS IS [func...] + int pos = 2; // Skip TN3270E, FUNCTIONS + + if (pos < data.length && (data[pos] & 0xFF) == OP_IS) { + pos++; // Skip IS + } + + // The remaining bytes are the agreed-upon functions + java.util.Arrays.fill(eFuncs, false); + StringBuilder funcNames = new StringBuilder(); + while (pos < data.length) { + int func = data[pos] & 0xFF; + if (func <= FUNC_SNA_SENSE) { + eFuncs[func] = true; + if (funcNames.length() > 0) funcNames.append(" "); + funcNames.append(TN3270EConstants.functionName(func)); + } + pos++; + } + + tn3270eNegotiated = true; + log.info("TN3270E functions IS: " + funcNames); + log.info("TN3270E negotiation complete"); + + // Move to CONNECTED_UNBOUND or CONNECTED_SSCP + changeState(ConnectionState.CONNECTED_UNBOUND); + + // Notify listeners + for (ConnectionListener l : connectionListeners) { + l.onTN3270ENegotiated(connectedType, connectedLu); + } + } + + // ========== End of Record processing ========== + + private void processEndOfRecord() { + byte[] data = ibuf.toByteArray(); + if (data.length == 0) return; + + if (tn3270eNegotiated) { + // TN3270E mode: data starts with 5-byte header + processTN3270ERecord(data); + } else { + // Plain TN3270 mode: data is raw 3270 data stream + dsProcessor.processRecord(data, 0, data.length, true); + notifyScreenUpdate(); + } + } + + private void processTN3270ERecord(byte[] data) { + if (data.length < EH_SIZE) { + log.warning("TN3270E record too short: " + data.length); + return; + } + + int dataType = data[0] & 0xFF; + int requestFlag = data[1] & 0xFF; + int responseFlag = data[2] & 0xFF; + int seqNumber = ((data[3] & 0xFF) << 8) | (data[4] & 0xFF); + + log.fine("TN3270E header: type=" + TN3270EConstants.dataTypeName(dataType) + + " rqf=" + requestFlag + " rsf=" + responseFlag + " seq=" + seqNumber); + + responseRequired = responseFlag; + + switch (dataType) { + case DT_3270_DATA: + if (data.length > EH_SIZE) { + // Transition to 3270 mode + if (connectionState == ConnectionState.CONNECTED_UNBOUND || + connectionState == ConnectionState.CONNECTED_SSCP) { + // Clear screen on transition to 3270 mode from unbound/SSCP + // This ensures old SSCP-LU data or stale content doesn't persist + screenBuffer.erase(false); + changeState(ConnectionState.CONNECTED_TN3270E); + tn3270eSubmode = TN3270ESubmode.E_3270; + } + dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, true); + notifyScreenUpdate(); + } + // Send positive response if required + if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { + sendTN3270EPositiveResponse(seqNumber); + } + break; + + case DT_SSCP_LU_DATA: + if (connectionState == ConnectionState.CONNECTED_UNBOUND) { + // Clear screen on first SSCP-LU transition to remove stale data + screenBuffer.clear(); + changeState(ConnectionState.CONNECTED_SSCP); + tn3270eSubmode = TN3270ESubmode.E_SSCP; + } + if (data.length > EH_SIZE) { + dsProcessor.processSscpLuData(data, EH_SIZE, data.length - EH_SIZE); + notifyScreenUpdate(); + } + break; + + case DT_BIND_IMAGE: + tn3270eBound = true; + // Parse BIND image for screen dimensions (SNA BIND format) + { + int bindLen = data.length - EH_SIZE; + StringBuilder bindHex = new StringBuilder(); + for (int bi = EH_SIZE; bi < data.length && bi < EH_SIZE + 40; bi++) { + bindHex.append(String.format("%02x ", data[bi] & 0xFF)); + } + log.info("Received BIND image (" + bindLen + " bytes)" + + " responseFlag=" + responseFlag + " raw: " + bindHex.toString().trim()); + + // SNA BIND RU offsets (from 3270ds.h): + // Byte 20 = RD (default rows), Byte 21 = CD (default cols) + // Byte 22 = RA (alternate rows), Byte 23 = CA (alternate cols) + // Byte 24 = SSIZE (screen size indicator) + final int BIND_OFF_RD = 20, BIND_OFF_CD = 21; + final int BIND_OFF_RA = 22, BIND_OFF_CA = 23; + final int BIND_OFF_SSIZE = 24; + + if (bindLen > BIND_OFF_SSIZE) { + int ssize = data[EH_SIZE + BIND_OFF_SSIZE] & 0xFF; + int bindRd = data[EH_SIZE + BIND_OFF_RD] & 0xFF; + int bindCd = data[EH_SIZE + BIND_OFF_CD] & 0xFF; + int bindRa, bindCa; + + switch (ssize) { + case 0x00: case 0x02: + // Default model 2 dimensions for both default and alt + bindRd = 24; bindCd = 80; + bindRa = 24; bindCa = 80; + break; + case 0x03: + // Default = 24x80, alternate = configured model max + bindRd = 24; bindCd = 80; + bindRa = screenBuffer.getMaxRows(); + bindCa = screenBuffer.getMaxCols(); + break; + case 0x7e: + // Both default and alternate = specified values + bindRa = bindRd; bindCa = bindCd; + break; + case 0x7f: + // Default and alternate are both specified separately + bindRa = data[EH_SIZE + BIND_OFF_RA] & 0xFF; + bindCa = data[EH_SIZE + BIND_OFF_CA] & 0xFF; + break; + default: + // Unknown SSIZE - use model defaults + bindRa = screenBuffer.getMaxRows(); + bindCa = screenBuffer.getMaxCols(); + break; + } + log.info("BIND SSIZE=0x" + String.format("%02x", ssize) + + " default=" + bindRd + "x" + bindCd + + " alt=" + bindRa + "x" + bindCa); + + // Apply dimensions — constrain to model max + int maxR = screenBuffer.getMaxRows(); + int maxC = screenBuffer.getMaxCols(); + if (bindRa > 0 && bindCa > 0 && bindRa <= maxR && bindCa <= maxC) { + screenBuffer.setAlternateDimensions(bindRa, bindCa); + } + } + } + // Clear and reset screen for new session + screenBuffer.erase(false); + changeState(ConnectionState.CONNECTED_TN3270E); + tn3270eSubmode = TN3270ESubmode.E_3270; + notifyScreenUpdate(); + // Send positive response if required (critical for ISPF NEWAPPL) + if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { + sendTN3270EPositiveResponse(seqNumber); + } + break; + + case DT_UNBIND: + log.info("Received UNBIND responseFlag=" + responseFlag); + tn3270eBound = false; + // Restore alternate dimensions to configured model max (per x3270) + screenBuffer.setAlternateDimensions( + screenBuffer.getMaxRows(), screenBuffer.getMaxCols()); + // Clear screen on UNBIND — essential for ISPF NEWAPPL transitions + screenBuffer.clear(); + // Send positive response BEFORE changing state (host expects it) + if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { + sendTN3270EPositiveResponse(seqNumber); + } + changeState(ConnectionState.CONNECTED_UNBOUND); + tn3270eSubmode = TN3270ESubmode.UNBOUND; + notifyScreenUpdate(); + break; + + case DT_NVT_DATA: + // NVT data in TN3270E mode + changeState(ConnectionState.CONNECTED_E_NVT); + tn3270eSubmode = TN3270ESubmode.E_NVT; + break; + + case DT_RESPONSE: + log.fine("Received response, seq=" + seqNumber); + break; + + default: + log.info("Unhandled TN3270E data type: " + dataType); + break; + } + } + + private void sendTN3270EPositiveResponse(int seqNumber) { + byte[] resp = new byte[EH_SIZE + 1]; + resp[0] = (byte) DT_RESPONSE; + resp[1] = 0; + resp[2] = (byte) RSF_POSITIVE_RESPONSE; + resp[3] = (byte) ((seqNumber >> 8) & 0xFF); + resp[4] = (byte) (seqNumber & 0xFF); + resp[5] = (byte) POS_DEVICE_END; + + sendRecord(resp); + } + + // ========== Check if we should transition to 3270 mode ========== + + private void checkIn3270() { + if (connectionState != ConnectionState.TELNET_PENDING) return; + + // For TN3270E, we wait for TN3270E negotiation to complete + if (myOpts[TELOPT_TN3270E] && hisOpts[TELOPT_TN3270E]) { + return; // TN3270E in progress + } + + // For plain TN3270: need BINARY and EOR in both directions + if (myOpts[TELOPT_BINARY] && hisOpts[TELOPT_BINARY] && + myOpts[TELOPT_EOR] && hisOpts[TELOPT_EOR]) { + log.info("Transitioning to plain TN3270 mode"); + changeState(ConnectionState.CONNECTED_3270); + } + } + + // ========== Sending helpers ========== + + private void sendCommand(int cmd, int opt) { + byte[] msg = { (byte) IAC, (byte) cmd, (byte) opt }; + sendBytes(msg); + log.info("SENT " + TelnetConstants.commandName(cmd) + " " + TelnetConstants.optionName(opt)); + } + + private void sendNaws() { + int cols = screenBuffer.getMaxCols(); + int rows = screenBuffer.getMaxRows(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(IAC); + out.write(SB); + out.write(TELOPT_NAWS); + writeNawsValue(out, cols); + writeNawsValue(out, rows); + out.write(IAC); + out.write(SE); + sendBytes(out.toByteArray()); + log.info("SENT SB NAWS " + cols + " " + rows + " SE"); + } + + private void writeNawsValue(ByteArrayOutputStream out, int value) { + int hi = (value >> 8) & 0xFF; + int lo = value & 0xFF; + out.write(hi); + if (hi == IAC) out.write(IAC); + out.write(lo); + if (lo == IAC) out.write(IAC); + } + + /** + * Send a 3270 record (with EOR framing, and TN3270E header if applicable). + */ + public void sendRecord(byte[] data) { + ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + 10); + + // Escape any IAC bytes in the payload + for (byte b : data) { + out.write(b & 0xFF); + if ((b & 0xFF) == IAC) { + out.write(IAC); + } + } + + out.write(IAC); + out.write(EOR); + sendBytes(out.toByteArray()); + } + + /** + * Send a 3270 data record, with TN3270E header if in TN3270E mode. + */ + public void send3270Data(byte[] data) { + if (tn3270eNegotiated) { + ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + EH_SIZE); + // TN3270E header + out.write(DT_3270_DATA); // data type + out.write(0); // request flag + out.write(0); // response flag + out.write((eXmitSeq >> 8) & 0xFF); // seq high + out.write(eXmitSeq & 0xFF); // seq low + eXmitSeq = (eXmitSeq + 1) & 0xFFFF; + // 3270 data + for (byte b : data) { + out.write(b & 0xFF); + } + sendRecord(out.toByteArray()); + } else { + sendRecord(data); + } + } + + /** + * Send SSCP-LU data (in TN3270E SSCP-LU mode). + */ + public void sendSscpLuData(byte[] data) { + if (tn3270eNegotiated) { + ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + EH_SIZE); + out.write(DT_SSCP_LU_DATA); + out.write(0); + out.write(0); + out.write((eXmitSeq >> 8) & 0xFF); + out.write(eXmitSeq & 0xFF); + eXmitSeq = (eXmitSeq + 1) & 0xFFFF; + for (byte b : data) { + out.write(b & 0xFF); + } + sendRecord(out.toByteArray()); + } + } + + private void sendBytes(byte[] data) { + try { + connection.sendRaw(data); + } catch (IOException e) { + log.log(Level.WARNING, "Send error", e); + onError("Send error: " + e.getMessage()); + } + } + + // ========== State management ========== + + private void changeState(ConnectionState newState) { + ConnectionState old = this.connectionState; + this.connectionState = newState; + log.info("State: " + old + " -> " + newState); + for (ConnectionListener l : connectionListeners) { + l.onConnectionStateChanged(old, newState); + } + } + + public void onDisconnect() { + changeState(ConnectionState.NOT_CONNECTED); + } + + public void onError(String message) { + log.warning("Error: " + message); + for (ConnectionListener l : connectionListeners) { + l.onConnectionError(message); + } + } + + private void notifyScreenUpdate() { + for (ScreenUpdateListener l : screenListeners) { + l.onScreenUpdated(); + } + } + + public boolean isTn3270eNegotiated() { return tn3270eNegotiated; } + public String getConnectedLu() { return connectedLu; } + public String getConnectedType() { return connectedType; } + + private static String tn3270eOpName(int op) { + switch (op) { + case OP_ASSOCIATE: return "ASSOCIATE"; + case OP_CONNECT: return "CONNECT"; + case OP_DEVICE_TYPE: return "DEVICE-TYPE"; + case OP_FUNCTIONS: return "FUNCTIONS"; + case OP_IS: return "IS"; + case OP_REASON: return "REASON"; + case OP_REJECT: return "REJECT"; + case OP_REQUEST: return "REQUEST"; + case OP_SEND: return "SEND"; + default: return "OP-" + op; + } + } +} diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..e877d9f --- /dev/null +++ b/run.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Launch j3270 - Java TN3270 Terminal Emulator +# Usage: ./run.sh [host] [port] [model] +# Example: ./run.sh mainframe.example.com 23 4 + +JAVA_DIR="$(cd "$(dirname "$0")" && pwd)" +BUILD_DIR="$JAVA_DIR/build" + +# Always rebuild to pick up changes +echo "Building lib3270j..." +rm -rf "$BUILD_DIR" +mkdir -p "$BUILD_DIR/lib3270j" "$BUILD_DIR/j3270" + +# Compile libraries +find "$JAVA_DIR/lib3270j/src" -name "*.java" -print0 | xargs -0 javac -d "$BUILD_DIR/lib3270j" + +echo "Building j3270..." +find "$JAVA_DIR/j3270/src" -name "*.java" -print0 | xargs -0 javac -cp "$BUILD_DIR/lib3270j" -d "$BUILD_DIR/j3270" + +echo "Build complete." + +# --------------------------------------------------------- +# NEW: Package the shareable JAR +# --------------------------------------------------------- +echo "Packaging j3270.jar..." + +# 1. Create the Manifest file indicating the entry point +# Note: The manifest file must end with a new line or carriage return +echo "Main-Class: org.pubvm.j3270.J3270App" > "$BUILD_DIR/MANIFEST.MF" +echo "" >> "$BUILD_DIR/MANIFEST.MF" + +# 2. Build the JAR using both compiled directories +jar cvfm "$BUILD_DIR/j3270.jar" "$BUILD_DIR/MANIFEST.MF" \ + -C "$BUILD_DIR/lib3270j" . \ + -C "$BUILD_DIR/j3270" . + +echo "Executable JAR created at: $BUILD_DIR/j3270.jar" + +# --------------------------------------------------------- +# Run the newly packaged JAR +# --------------------------------------------------------- +exec java -Dapple.laf.useScreenMenuBar=true \ + -Dapple.awt.application.name=j3270 \ + -jar "$BUILD_DIR/j3270.jar" "$@" \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..27331d1 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,4 @@ +rootProject.name = 'j3270-project' + +include 'lib3270j' +include 'j3270'