IBM-DYNAMIC tweaking
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m25s

This commit is contained in:
2026-09-04 20:58:19 -04:00
parent cbb310b889
commit 74e30d1ead
18 changed files with 1374 additions and 86 deletions
@@ -434,7 +434,7 @@ public class ConnectionConfig {
return terminalName;
}
if (isDynamicModel()) {
return extendedDataStream ? "IBM-DYNAMIC-E" : "IBM-DYNAMIC";
return "IBM-DYNAMIC";
}
return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType();
}
@@ -462,8 +462,23 @@ public class Telnet3270Client {
public void processFieldMark() { inputProcessor.processFieldMark(); }
/** Toggle Insert Mode. */
public void toggleInsert() { inputProcessor.setInsertMode(!inputProcessor.isInsertMode()); }
public void processToggleInsert() { inputProcessor.processToggleInsert(); }
public boolean isInsertMode() { return inputProcessor != null && inputProcessor.isInsertMode(); }
public void toggleInsert() { if (inputProcessor != null) inputProcessor.setInsertMode(!inputProcessor.isInsertMode()); }
public void processToggleInsert() { if (inputProcessor != null) inputProcessor.processToggleInsert(); }
/** Document Mode (Entry Assist) operations. */
public boolean isDocMode() { return screenBuffer != null && screenBuffer.isEntryAssistDOCmode(); }
public void setDocMode(boolean b) { if (screenBuffer != null) screenBuffer.setEntryAssistDOCmode(b); if (oia != null) oia.notifyOIAChanged(); }
public void toggleDocMode() { setDocMode(!isDocMode()); }
public boolean isWordWrap() { return screenBuffer != null && screenBuffer.isEntryAssistWordWrap(); }
public void setWordWrap(boolean b) { if (screenBuffer != null) screenBuffer.setEntryAssistWordWrap(b); if (oia != null) oia.notifyOIAChanged(); }
public void toggleWordWrap() { setWordWrap(!isWordWrap()); }
/** APL Keyboard Mode operations. */
public boolean isAplMode() { return inputProcessor != null && inputProcessor.isAplKeyboardMode(); }
public void setAplMode(boolean b) { if (inputProcessor != null) inputProcessor.setAplKeyboardMode(b); }
public void toggleAplMode() { if (inputProcessor != null) inputProcessor.toggleAplKeyboardMode(); }
/** Move word left. */
public void processWordLeft() { inputProcessor.processWordLeft(); }
@@ -46,11 +46,11 @@ public enum TerminalModel {
/**
* Returns the terminal type string for TN3270E negotiation.
* e.g., "IBM-3279-4-E" for a color model 4 with extended data stream,
* or "IBM-DYNAMIC-E" for dynamic model.
* or "IBM-DYNAMIC" for dynamic model.
*/
public String getTerminalType() {
if (modelNumber == 0) {
return "IBM-DYNAMIC-E";
return "IBM-DYNAMIC";
}
return String.format("IBM-327%c-%d-E", color ? '9' : '8', modelNumber);
}
@@ -178,7 +178,7 @@ public class ECLOIA implements ECLConstants {
}
}
private synchronized void notifyOIAChanged() {
public synchronized void notifyOIAChanged() {
ECLOIAEvent event = new ECLOIAEvent(this, ECLOIAEvent.OIA_UPDATE, getInputInhibited(),
getAlphanumericType(), isInsertMode(), getStatusString());
for (haus.nightmare.lib3270j.ecl.ECLOIANotify l : listeners) {
@@ -484,6 +484,7 @@ public class ECLOIA implements ECLConstants {
if (isNumeric()) s |= STATE_NUMFIELD;
if (screen != null && screen.isEntryAssistDOCmode()) s |= STATE_DOC_MODE;
if (screen != null && screen.isEntryAssistWordWrap()) s |= STATE_WORDWRAP;
if (isApl()) s |= STATE_APL;
if (isXSystem()) s |= STATE_SYS_LOCK;
if (isXComm()) s |= STATE_COMM_CHECK;
return s;
@@ -494,6 +495,21 @@ public class ECLOIA implements ECLConstants {
public int getStatusFlags() { return GetStatusFlags(); }
public long getStatusFlagsEx() { return GetStatusFlagsEx(); }
public boolean isApl() {
return inputProcessor != null && inputProcessor.isAplKeyboardMode();
}
public boolean IsApl() { return isApl(); }
public boolean isDocMode() {
return screen != null && screen.isEntryAssistDOCmode();
}
public boolean IsDocMode() { return isDocMode(); }
public boolean isWordWrap() {
return screen != null && screen.isEntryAssistWordWrap();
}
public boolean IsWordWrap() { return isWordWrap(); }
public synchronized void setBitmaskState(long flag, boolean on) {
this.previousState = this.state;
if (on) {
@@ -118,7 +118,91 @@ public class InputProcessor {
}
}
public boolean isInsertMode() { return insertMode; }
public void setInsertMode(boolean insert) { this.insertMode = insert; }
public void setInsertMode(boolean insert) {
this.insertMode = insert;
if (oia != null) {
oia.notifyOIAChanged();
}
}
@FunctionalInterface
public interface BellListener {
void onBell();
}
private BellListener bellListener;
private boolean bellEnabled = false;
private int bellColumn = 74; // 0-based index for column 75
private boolean insertOffOnAid = true;
private boolean aplKeyboardMode = false;
private boolean numericFieldLock = true;
private boolean autoSkipEnabled = true;
public void setBellListener(BellListener listener) { this.bellListener = listener; }
public BellListener getBellListener() { return bellListener; }
public boolean isBellEnabled() { return bellEnabled; }
public void setBellEnabled(boolean enabled) { this.bellEnabled = enabled; }
public int getBellColumn() { return bellColumn; }
public void setBellColumn(int col) { this.bellColumn = col; }
public boolean isInsertOffOnAid() { return insertOffOnAid; }
public void setInsertOffOnAid(boolean val) { this.insertOffOnAid = val; }
public boolean isAplKeyboardMode() { return aplKeyboardMode; }
public void setAplKeyboardMode(boolean enabled) {
this.aplKeyboardMode = enabled;
if (oia != null) {
oia.notifyOIAChanged();
}
}
public void toggleAplKeyboardMode() {
setAplKeyboardMode(!aplKeyboardMode);
}
public boolean isNumericFieldLock() { return numericFieldLock; }
public void setNumericFieldLock(boolean lock) { this.numericFieldLock = lock; }
public boolean isAutoSkipEnabled() { return autoSkipEnabled; }
public void setAutoSkipEnabled(boolean enabled) { this.autoSkipEnabled = enabled; }
/**
* Map ASCII key character to IBM 3270 APL / Graphic Escape code point.
*/
private int getAplCodeForChar(char ch) {
char upper = Character.toUpperCase(ch);
switch (upper) {
case 'A': return 0x81; // Alpha
case 'B': return 0x82; // ⊥ Up tack / decode
case 'C': return 0x83; // ∩ Intersection
case 'D': return 0x84; // ⌊ Floor
case 'E': return 0x85; // │ Vertical line
case 'F': return 0x87; // ∇ Del / Grad
case 'G': return 0x88; // ∆ Delta
case 'H': return 0x89; // Iota
case 'I': return 0x8A; // → Right arrow
case 'J': return 0x8B; // ⍞ Quote Quad
case 'K': return 0x8C; // ≤ Less than or equal
case 'L': return 0xAD; // [ Bracket left
case 'M': return 0x8E; // × Multiply
case 'N': return 0x8F; // ÷ Divide
case 'O': return 0x90; // ⍟ Circle Star
case 'P': return 0x91; // ⌹ Domino / Quad divide
case 'Q': return 0x92; // Down tack / encode
case 'R': return 0x95; // Rho
case 'S': return 0x94; // ⌈ Ceiling
case 'T': return 0x98; // ⍷ Epsilon underbar
case 'U': return 0x93; // Union
case 'V': return 0x97; // ≠ Not equal
case 'W': return 0x96; // ⍵ Omega
case 'X': return 0xAC; // ⍉ Transpose
case 'Y': return 0xA8; // ↑ Up arrow / Take
case 'Z': return 0xA9; // ↓ Down arrow / Drop
case '-': return 0xA2; // ─ Horizontal line
case '|': return 0x85; // │ Vertical line
case '+': return 0xCB; // ┼ Cross
default: return -1;
}
}
/**
* Enter a character at the current cursor position.
@@ -142,8 +226,8 @@ public class InputProcessor {
int baddr = screen.getCursorAddress();
baddr = ((baddr % size) + size) % size;
// Entry Assist DOC mode / Word Wrap handling
if ((screen.isEntryAssistDOCmode() || screen.isEntryAssistWordWrap()) && !isNvtMode()) {
// Entry Assist Word Wrap handling
if (screen.isEntryAssistWordWrap() && !isNvtMode()) {
int curCol = baddr % screen.getCols();
int endCol = screen.getEntryAssistEndColumn();
int startCol = screen.getEntryAssistStartColumn();
@@ -178,7 +262,7 @@ public class InputProcessor {
}
// Numeric-only field check: digits 0-9, minus (-), period (.), space ( ), DUP, FM
if (faIsNumeric(faVal & 0xFF)) {
if (numericFieldLock && faIsNumeric(faVal & 0xFF)) {
boolean isValidNumeric = (ch >= '0' && ch <= '9') || ch == '-' || ch == '.' || ch == ' '
|| ch == '*' || ch == ';' || ch == (char) FCORDER_DUP || ch == (char) FCORDER_FM;
if (!isValidNumeric) {
@@ -191,8 +275,20 @@ public class InputProcessor {
}
}
// Translate character to EBCDIC
int ebc = translator.unicodeToEbcdic(ch);
// Translate character to EBCDIC (or APL Graphic Escape if in APL Keyboard Mode)
boolean isAplChar = false;
int ebc = -1;
if (aplKeyboardMode) {
int aplCode = getAplCodeForChar(ch);
if (aplCode >= 0) {
ebc = aplCode;
ch = translator.mapAPL(aplCode);
isAplChar = true;
}
}
if (ebc < 0) {
ebc = translator.unicodeToEbcdic(ch);
}
if (ebc < 0) return;
if (insertMode) {
@@ -229,6 +325,9 @@ public class InputProcessor {
ExtendedAttribute ea = screen.getCell(baddr);
ea.ec = (byte) ebc;
ea.ucs4 = ch;
if (isAplChar) {
ea.cs = ExtendedAttribute.CS_GE;
}
// Set MDT on field attribute
if (screen.isFormatted()) {
@@ -244,7 +343,7 @@ public class InputProcessor {
int nextAddr = screen.incrementAddress(baddr);
if (screen.getCell(nextAddr).isFieldAttribute()) {
byte nextFa = screen.getCell(nextAddr).fa;
if (faIsSkip(nextFa & 0xFF)) {
if (autoSkipEnabled && faIsSkip(nextFa & 0xFF)) {
// Auto-skip field (Protected + Numeric): jump to next unprotected field
int skipTarget = screen.findNextUnprotected(nextAddr);
screen.setCursorAddress(skipTarget);
@@ -264,6 +363,28 @@ public class InputProcessor {
screen.setCursorAddress((baddr + 1) % size);
}
// Entry Assist DOC mode: if we typed into or past the right margin, advance to startCol on next row
if (screen != null && screen.isEntryAssistDOCmode() && !screen.isEntryAssistWordWrap() && !isNvtMode()) {
int typedCol = baddr % screen.getCols();
if (typedCol >= screen.getEntryAssistEndColumn()) {
int curRow = baddr / screen.getCols();
int nextRow = (curRow + 1) % screen.getRows();
int targetAddr = nextRow * screen.getCols() + screen.getEntryAssistStartColumn();
if (screen.isFormatted()) {
targetAddr = screen.findNextUnprotected(targetAddr - 1);
}
screen.setCursorAddress(targetAddr);
}
}
// Audible End-of-Line Warning Signal
if (bellEnabled && screen != null) {
int curCol = screen.getCursorCol();
if (curCol == bellColumn && bellListener != null) {
bellListener.onBell();
}
}
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
@@ -523,6 +644,9 @@ public class InputProcessor {
}
if (aidCode == AID_CLEAR) {
if (insertOffOnAid && insertMode) {
setInsertMode(false);
}
screen.clear();
screen.markAllChanged();
if (graphicsPlane != null) {
@@ -538,6 +662,10 @@ public class InputProcessor {
return;
}
if (insertOffOnAid && insertMode) {
setInsertMode(false);
}
lastAid = aidCode;
setKeyboardLocked(true);
@@ -792,6 +920,10 @@ public class InputProcessor {
}
public void tab() {
if (screen != null && screen.isEntryAssistDOCmode()) {
screen.processWordTab(true);
return;
}
int addr = screen.findNextUnprotected(screen.getCursorAddress());
screen.setCursorAddress(addr);
screen.updateDisplaySnapshot();
@@ -806,6 +938,10 @@ public class InputProcessor {
}
public void backTab() {
if (screen != null && screen.isEntryAssistDOCmode()) {
screen.processWordTab(false);
return;
}
if (!screen.isFormatted()) return;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
@@ -1546,6 +1682,22 @@ public class InputProcessor {
case "lightpen":
processLightPen();
break;
case "docmode":
if (screen != null) {
screen.setEntryAssistDOCmode(!screen.isEntryAssistDOCmode());
if (oia != null) oia.notifyOIAChanged();
}
break;
case "wordwrap":
if (screen != null) {
screen.setEntryAssistWordWrap(!screen.isEntryAssistWordWrap());
if (oia != null) oia.notifyOIAChanged();
}
break;
case "apl":
case "aplmode":
toggleAplKeyboardMode();
break;
default:
if (token.startsWith("pf")) {
try {
@@ -1071,12 +1071,16 @@ public class ScreenBuffer {
public void setEntryAssistTabStops(int[] stops) { this.tabStops = stops; }
public void SetEntryAssistTabStops(int[] stops) { setEntryAssistTabStops(stops); }
public void setLeftMargin(int n) { setEntryAssistStartColumn(n); }
public void setRightMargin(int n) { setEntryAssistEndColumn(n); }
public void setWordTabPositions(int[] stops) { setEntryAssistTabStops(stops); }
/**
* Perform Entry Assist word wrap if typing near/past end margin.
* Moves any partial word typed on the current line to the beginning of the next line (docStartCol).
*/
public synchronized boolean handleWordWrap(int curAddr, char typedChar) {
if (!docMode && !wordWrap) return false;
if (!wordWrap) return false;
int size = rows * cols;
if (size <= 0) return false;
curAddr = ((curAddr % size) + size) % size;
@@ -75,9 +75,6 @@ public class TelnetFSM {
return list;
}
if (config.isDynamicModel()) {
if (config.isExtendedDataStream()) {
list.add("IBM-DYNAMIC-E");
}
list.add("IBM-DYNAMIC");
list.add("IBM-3279-4-E");
list.add("IBM-3279-4");
@@ -0,0 +1,215 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.concurrent.atomic.AtomicInteger;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class EntryAssistFullModeTest {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor inputProcessor;
private ECLOIA oia;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator); // 24x80
inputProcessor = new InputProcessor(screen, translator, null);
oia = new ECLOIA(screen, inputProcessor, null);
inputProcessor.setOIA(oia);
}
@Test
public void testDocModeCursorAdvanceWithoutWordWrap() {
// Document Mode ON, Word Wrap OFF
screen.setEntryAssistDOCmode(true);
screen.setEntryAssistWordWrap(false);
screen.setLeftMargin(0); // Col 1 (0-based 0)
screen.setRightMargin(72); // Col 73 (0-based 72)
assertTrue(screen.isEntryAssistDOCmode());
assertFalse(screen.isEntryAssistWordWrap());
assertEquals(0, screen.getEntryAssistStartColumn());
assertEquals(72, screen.getEntryAssistEndColumn());
// Setup open cursor position
screen.setCursorPosition(0, 72); // Row 0, Col 72 (at right margin)
inputProcessor.typeCharacter('X');
// Should advance to next row at left margin (Row 1, Col 0)
assertEquals(1, screen.getCursorRow());
assertEquals(0, screen.getCursorCol());
assertEquals('X', (char) screen.getCell(72).ucs4);
}
@Test
public void testWordWrapTransfersPartialWord() {
// Document Mode ON, Word Wrap ON
screen.setEntryAssistDOCmode(true);
screen.setEntryAssistWordWrap(true);
screen.setLeftMargin(5); // Start col = 5
screen.setRightMargin(20); // End col = 20
// Type "HELLO " starting at col 10
screen.setCursorPosition(0, 10);
for (char c : "HELLO ".toCharArray()) {
inputProcessor.typeCharacter(c);
}
// Now cursor is at col 16. Type "TEST" which crosses col 20
screen.setCursorPosition(0, 18);
inputProcessor.typeCharacter('W');
inputProcessor.typeCharacter('O');
inputProcessor.typeCharacter('R');
inputProcessor.typeCharacter('D'); // At col 21, past end col 20
// Word wrap should have moved "WORD" to next line starting at left margin 5
assertEquals(1, screen.getCursorRow());
assertTrue(screen.getCursorCol() >= 5);
}
@Test
public void testDocModeTabStopsNavigation() {
screen.setEntryAssistDOCmode(true);
screen.setLeftMargin(0);
screen.setRightMargin(79);
screen.setEntryAssistTabStops(new int[]{0, 10, 20, 30});
screen.setCursorPosition(0, 0);
// Word tab forward
screen.processWordTab(true);
assertEquals(10, screen.getCursorCol());
assertEquals(0, screen.getCursorRow());
screen.processWordTab(true);
assertEquals(20, screen.getCursorCol());
// Word back tab
screen.processWordTab(false);
assertEquals(10, screen.getCursorCol());
screen.processWordTab(false);
assertEquals(0, screen.getCursorCol());
}
@Test
public void testAudibleEndOfLineBell() {
inputProcessor.setBellEnabled(true);
inputProcessor.setBellColumn(74);
AtomicInteger bellCount = new AtomicInteger(0);
inputProcessor.setBellListener(bellCount::incrementAndGet);
screen.setCursorPosition(0, 70);
inputProcessor.typeCharacter('A'); // 71
inputProcessor.typeCharacter('B'); // 72
inputProcessor.typeCharacter('C'); // 73
assertEquals(0, bellCount.get());
inputProcessor.typeCharacter('D'); // 74 -> triggers bell
assertEquals(1, bellCount.get());
inputProcessor.typeCharacter('E'); // 75 -> does not retrigger on same line
assertEquals(1, bellCount.get());
// Move to next line at col 73 and type across bell column
screen.setCursorPosition(1, 73);
inputProcessor.typeCharacter('Z'); // advances to 74 -> triggers bell on next line
assertEquals(2, bellCount.get());
}
@Test
public void testInsertModeResetOnAid() {
inputProcessor.setInsertOffOnAid(true);
inputProcessor.setInsertMode(true);
assertTrue(inputProcessor.isInsertMode());
// Send AID_ENTER
inputProcessor.sendAid(AID_ENTER);
assertFalse(inputProcessor.isInsertMode(), "Insert mode should be reset after AID key");
// When insertOffOnAid is disabled
inputProcessor.setInsertOffOnAid(false);
inputProcessor.setInsertMode(true);
inputProcessor.sendAid(AID_ENTER);
assertTrue(inputProcessor.isInsertMode(), "Insert mode should be preserved when insertOffOnAid is false");
}
@Test
public void testAplKeyboardModeAndOia() {
inputProcessor.setAplKeyboardMode(true);
assertTrue(inputProcessor.isAplKeyboardMode());
// OIA status flag includes STATE_APL
long statusEx = oia.GetStatusFlagsEx();
assertEquals(ECLOIA.STATE_APL, statusEx & ECLOIA.STATE_APL);
assertTrue(oia.isApl());
// Test APL translation for key 'a' (alpha -> 0x41 with CS_GE)
screen.setCursorPosition(0, 0);
inputProcessor.typeCharacter('a');
var cell = screen.getCell(0);
assertEquals(CS_GE, cell.cs, "APL character must be stored with CS_GE (Graphic Escape) charset");
// Toggle APL mode off
inputProcessor.toggleAplKeyboardMode();
assertFalse(inputProcessor.isAplKeyboardMode());
assertFalse(oia.isApl());
assertEquals(0, oia.GetStatusFlagsEx() & ECLOIA.STATE_APL);
}
@Test
public void testNumericFieldLockSetting() {
// Field: pos 0 is numeric FA, pos 1..5 unprotected, pos 6 protect FA
screen.setFieldAttribute(0, (byte) FA_NUMERIC);
screen.setFieldAttribute(6, (byte) FA_PROTECT);
screen.setCursorAddress(1);
// Case 1: NumericFieldLock is true
inputProcessor.setNumericFieldLock(true);
inputProcessor.typeCharacter('X'); // invalid numeric character
assertTrue(inputProcessor.isKeyboardLocked(), "Keyboard should be locked on non-numeric char");
assertEquals(ECLConstants.INHIBIT_NUMERIC_ONLY, oia.getInputInhibited());
// Reset keyboard
inputProcessor.reset();
assertFalse(inputProcessor.isKeyboardLocked());
// Case 2: NumericFieldLock is false
inputProcessor.setNumericFieldLock(false);
screen.setCursorAddress(1);
inputProcessor.typeCharacter('X');
assertFalse(inputProcessor.isKeyboardLocked(), "Keyboard should NOT be locked when NumericFieldLock is false");
}
@Test
public void testOiaModeIndicators() {
screen.setEntryAssistDOCmode(true);
screen.setEntryAssistWordWrap(true);
inputProcessor.setAplKeyboardMode(true);
inputProcessor.setInsertMode(true);
long flags = oia.GetStatusFlagsEx();
assertTrue((flags & ECLOIA.STATE_DOC_MODE) != 0);
assertTrue((flags & ECLOIA.STATE_WORDWRAP) != 0);
assertTrue((flags & ECLOIA.STATE_APL) != 0);
assertTrue((flags & ECLOIA.STATE_INSERT) != 0);
assertTrue(oia.isDocMode());
assertTrue(oia.isWordWrap());
assertTrue(oia.isApl());
assertTrue(oia.isInsertMode());
}
}
@@ -76,8 +76,9 @@ public class DynamicScreenBufferTest {
assertTrue(model.isDynamic());
assertEquals(0, model.getModelNumber());
assertTrue(model.isColor());
assertEquals("IBM-DYNAMIC-E", model.getTerminalType());
assertEquals("IBM-DYNAMIC", model.getTerminalType());
assertEquals("IBM-DYNAMIC", model.getBaseTerminalType());
assertEquals("IBM-DYNAMIC", model.toString());
ScreenBuffer buffer = new ScreenBuffer(model, translator);
assertEquals(24, buffer.getDefRows());
@@ -65,12 +65,14 @@ public class DynamicTelnetFSMTest {
// Host requests TTYPE: IAC SB TTYPE SEND IAC SE
feedBytes(255, 250, 24, 1, 255, 240);
// Verify sent response is IBM-DYNAMIC-E
// Verify sent response is IBM-DYNAMIC
assertFalse(connection.sentData.isEmpty());
byte[] lastSent = connection.sentData.get(connection.sentData.size() - 1);
String s = new String(lastSent);
assertTrue(s.contains("IBM-DYNAMIC-E") || s.contains("IBM-DYNAMIC"),
assertTrue(s.contains("IBM-DYNAMIC"),
"Expected terminal type negotiation to send IBM-DYNAMIC, got: " + s);
assertFalse(s.contains("IBM-DYNAMIC-E"),
"Terminal type negotiation should not contain IBM-DYNAMIC-E, got: " + s);
}
@Test