This commit is contained in:
2026-08-28 12:38:56 -04:00
parent ff5dfda343
commit 79258c0c44
10 changed files with 1219 additions and 0 deletions
@@ -39,6 +39,8 @@ public class Telnet3270Client {
private final DataStreamProcessor dsProcessor;
private final TelnetFSM fsm;
private final InputProcessor inputProcessor;
private final haus.nightmare.lib3270j.ecl.ECLPS ps;
private final haus.nightmare.lib3270j.ecl.ECLOIA oia;
private TelnetConnection connection;
public Telnet3270Client(ConnectionConfig config) {
@@ -49,6 +51,8 @@ public class Telnet3270Client {
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
this.ps = new haus.nightmare.lib3270j.ecl.ECLPS(screenBuffer, inputProcessor, translator);
this.oia = new haus.nightmare.lib3270j.ecl.ECLOIA(screenBuffer, inputProcessor, fsm);
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
dsProcessor.setOutputSender(fsm::send3270Data);
@@ -125,6 +129,18 @@ public class Telnet3270Client {
/** Get the connection config. */
public ConnectionConfig getConfig() { return config; }
/** Get the ECL Presentation Space API. */
public haus.nightmare.lib3270j.ecl.ECLPS getPS() { return ps; }
/** Get the ECL Operator Information Area API. */
public haus.nightmare.lib3270j.ecl.ECLOIA getOIA() { return oia; }
/** Get the list of all fields currently on screen. */
public haus.nightmare.lib3270j.ecl.ECLFieldList getFieldList() { return ps.getFieldList(); }
/** Send IBM ECL bracketed mnemonic keystrokes (e.g. "USER[tab]PASS[enter]"). */
public void sendKeys(String keys) { inputProcessor.sendKeys(keys); }
/** Set a custom or interactive TLS certificate verifier callback. */
public void setTlsCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) {
config.setCertificateVerifier(verifier);
@@ -0,0 +1,52 @@
package haus.nightmare.lib3270j.ecl;
/**
* Common constants for IBM Host On-Demand Emulator Class Library (ECL) emulation.
*/
public interface ECLConstants {
// Presentation space logical planes
int PLANE_TEXT = 1; // Unicode/EBCDIC characters
int PLANE_COLOR = 2; // Color attributes
int PLANE_HILITE = 3; // Extended highlighting
int PLANE_EXTENDED = 4; // Extended character sets / outlining
int PLANE_FIELD = 5; // Field attribute definition bytes
int PLANE_DBCS = 6; // Double-byte character plane
// Color definitions (IBM 3279 standard 16-color palette)
char COLOR_NEUTRAL_BLACK = 0;
char COLOR_BLUE = 1;
char COLOR_RED = 2;
char COLOR_PINK = 3;
char COLOR_GREEN = 4;
char COLOR_TURQUOISE = 5;
char COLOR_YELLOW = 6;
char COLOR_NEUTRAL_WHITE = 7;
char COLOR_BLACK = 8;
char COLOR_DEEP_BLUE = 9;
char COLOR_ORANGE = 10;
char COLOR_PURPLE = 11;
char COLOR_PALE_GREEN = 12;
char COLOR_PALE_TURQUOISE= 13;
char COLOR_GREY = 14;
char COLOR_WHITE = 15;
// Highlighting attributes
char HILITE_DEFAULT = 0x00;
char HILITE_BLINK = 0xF1;
char HILITE_REVERSE = 0xF2;
char HILITE_UNDERSCORE = 0xF4;
// Search directions
int SEARCH_FORWARD = 1;
int SEARCH_BACKWARD = 2;
// OIA Input Inhibited Reason Codes
int INHIBIT_NOT_INHIBITED = 0;
int INHIBIT_SYSTEM_LOCK = 1; // X SYSTEM (Waiting for host response)
int INHIBIT_NUMERIC_ONLY = 2; // X NUMERIC (Non-numeric in numeric field)
int INHIBIT_PROTECTED_FIELD = 3; // X PROT (Attempted write into protected field)
int INHIBIT_OVERFLOW = 4; // X > (Insert mode buffer overflow)
int INHIBIT_COMM_CHECK = 5; // X COMM (Communication or socket check)
int INHIBIT_OPERATOR_DUE = 6; // X OP (Operator Intervention Due)
}
@@ -0,0 +1,148 @@
package haus.nightmare.lib3270j.ecl;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/**
* Represents a discrete 3270 field within an IBM Host On-Demand Presentation Space.
* Provides programmatic inspection and modification of field text and attributes.
*/
public class ECLField {
private final ECLPS ps;
private final int startPos; // Position of field attribute byte
private final int dataStart; // First data position (startPos + 1 wrapped)
private final int endPos; // Last data position inclusive
private final int length; // Usable data character length
private final byte attribute; // Field attribute byte (FA)
public ECLField(ECLPS ps, int startPos, int dataStart, int endPos, int length, byte attribute) {
this.ps = ps;
this.startPos = startPos;
this.dataStart = dataStart;
this.endPos = endPos;
this.length = length;
this.attribute = attribute;
}
/** Buffer address of the field attribute character. */
public int getStart() { return startPos; }
/** First buffer address of the field data (start + 1). */
public int getDataStart() { return dataStart; }
/** Last buffer address of the field data inclusive. */
public int getEnd() { return endPos; }
/** Number of data characters in the field. */
public int getLength() { return length; }
public int getStartRow() {
int cols = ps.getCols();
return cols > 0 ? startPos / cols : 0;
}
public int getStartCol() {
int cols = ps.getCols();
return cols > 0 ? startPos % cols : 0;
}
public int getEndRow() {
int cols = ps.getCols();
return cols > 0 ? endPos / cols : 0;
}
public int getEndCol() {
int cols = ps.getCols();
return cols > 0 ? endPos % cols : 0;
}
public boolean isModified() {
return faIsModified(attribute & 0xFF);
}
public boolean isProtected() {
return faIsProtected(attribute & 0xFF);
}
public boolean isNumeric() {
return faIsNumeric(attribute & 0xFF);
}
public boolean isHighIntensity() {
return faIsHigh(attribute & 0xFF);
}
public boolean isHidden() {
return faIsZero(attribute & 0xFF);
}
public boolean isDisplay() {
return !isHidden();
}
public boolean isPenSelectable() {
return faIsSelectable(attribute & 0xFF);
}
public short getAttribute() {
return (short) (attribute & 0xFF);
}
/**
* Get the text contents of this field as a String.
*/
public String getText() {
if (length <= 0) return "";
return ps.getString(dataStart, length);
}
/**
* Set the text contents of this field.
*/
public void setText(String text) {
if (isProtected() || length <= 0) return;
ps.setText(text, dataStart);
}
/**
* Get selector light pen type.
* Returns '?' for selectable, '>' for selected, ' ' for space/unselectable.
*/
public char getSelectorPenType() {
if (length <= 0) return ' ';
String t = getText();
if (!t.isEmpty()) {
char first = t.charAt(0);
if (first == '?' || first == '>') return first;
}
return ' ';
}
/**
* Actuate lightpen selection on this field ('?' -> '>').
*/
public void selectField() {
if (isProtected() || length <= 0) return;
String t = getText();
if (!t.isEmpty() && t.charAt(0) == '?') {
setText(">" + (t.length() > 1 ? t.substring(1) : ""));
}
}
/**
* Deselect lightpen selection on this field ('>' -> '?').
*/
public void deSelectField() {
if (isProtected() || length <= 0) return;
String t = getText();
if (!t.isEmpty() && t.charAt(0) == '>') {
setText("?" + (t.length() > 1 ? t.substring(1) : ""));
}
}
@Override
public String toString() {
return String.format("ECLField[start=%d, end=%d, len=%d, prot=%b, mod=%b, text=\"%s\"]",
startPos, endPos, length, isProtected(), isModified(), getText());
}
}
@@ -0,0 +1,152 @@
package haus.nightmare.lib3270j.ecl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
/**
* Manages the collection of all 3270 fields currently present in the Presentation Space.
* Conforms to IBM ECL ECLFieldList specification.
*/
public class ECLFieldList {
private final ECLPS ps;
private final ScreenBuffer screen;
private final List<ECLField> fields = new ArrayList<>();
public ECLFieldList(ECLPS ps, ScreenBuffer screen) {
this.ps = ps;
this.screen = screen;
refresh();
}
/**
* Refresh the field list by scanning the presentation space buffer.
*/
public synchronized void refresh() {
fields.clear();
if (screen == null || !screen.isFormatted()) {
return;
}
int rows = screen.getRows();
int cols = screen.getCols();
int size = rows * cols;
if (size <= 0) return;
// Collect all field attribute positions
List<Integer> faPositions = new ArrayList<>();
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute()) {
faPositions.add(i);
}
}
if (faPositions.isEmpty()) {
return;
}
int numFields = faPositions.size();
for (int i = 0; i < numFields; i++) {
int startPos = faPositions.get(i);
int nextFaPos = faPositions.get((i + 1) % numFields);
int dataStart = (startPos + 1) % size;
int endPos = (nextFaPos == 0) ? size - 1 : nextFaPos - 1;
int len;
if (nextFaPos > startPos) {
len = nextFaPos - startPos - 1;
} else {
len = (size - startPos - 1) + nextFaPos;
}
byte faVal = screen.getCell(startPos).fa;
fields.add(new ECLField(ps, startPos, dataStart, endPos, len, faVal));
}
}
public synchronized int getFieldCount() {
return fields.size();
}
public synchronized List<ECLField> getFields() {
return Collections.unmodifiableList(new ArrayList<>(fields));
}
public synchronized ECLField getFirstField() {
if (fields.isEmpty()) return null;
return fields.get(0);
}
public synchronized ECLField getNextField(ECLField prev) {
if (prev == null || fields.isEmpty()) return getFirstField();
int idx = fields.indexOf(prev);
if (idx >= 0 && idx + 1 < fields.size()) {
return fields.get(idx + 1);
}
return null;
}
/**
* Find the field that contains the specified buffer position.
*/
public synchronized ECLField findField(int pos) {
if (fields.isEmpty() || screen == null) return null;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return null;
pos = ((pos % size) + size) % size;
for (ECLField f : fields) {
int start = f.getStart();
int end = f.getEnd();
if (start <= end) {
if (pos >= start && pos <= end) return f;
} else {
// Wrapped field
if (pos >= start || pos <= end) return f;
}
}
return null;
}
/**
* Find the field at the specified 0-indexed row and column.
*/
public ECLField findField(int row, int col) {
if (screen == null) return null;
int cols = screen.getCols();
return findField(row * cols + col);
}
/**
* Find field containing the given text string.
*/
public synchronized ECLField findField(String text, int startPos) {
if (text == null || text.isEmpty() || fields.isEmpty()) return null;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return null;
startPos = ((startPos % size) + size) % size;
// Find starting index in field list
int startIdx = 0;
for (int i = 0; i < fields.size(); i++) {
if (fields.get(i).getStart() >= startPos) {
startIdx = i;
break;
}
}
for (int i = startIdx; i < fields.size(); i++) {
ECLField f = fields.get(i);
if (f.getText().contains(text)) {
return f;
}
}
return null;
}
}
@@ -0,0 +1,143 @@
package haus.nightmare.lib3270j.ecl;
import java.util.ArrayList;
import java.util.List;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.telnet.TelnetFSM;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/**
* Operator Information Area (OIA) status engine matching IBM Host On-Demand ECL specification.
* Provides synchronization primitives (waitForInput, waitForSysAvailable) and status inspection.
*/
public class ECLOIA implements ECLConstants {
private final ScreenBuffer screen;
private final InputProcessor inputProcessor;
private final TelnetFSM fsm;
private final List<ECLOIANotify> listeners = new ArrayList<>();
public interface ECLOIANotify {
void onOIAChanged(ECLOIA oia);
}
public ECLOIA(ScreenBuffer screen, InputProcessor inputProcessor, TelnetFSM fsm) {
this.screen = screen;
this.inputProcessor = inputProcessor;
this.fsm = fsm;
if (inputProcessor != null) {
inputProcessor.setLockStateListener(locked -> notifyOIAChanged());
}
}
public synchronized void registerOIAEvent(ECLOIANotify listener) {
if (listener != null && !listeners.contains(listener)) {
listeners.add(listener);
}
}
public synchronized void unregisterOIAEvent(ECLOIANotify listener) {
listeners.remove(listener);
}
private synchronized void notifyOIAChanged() {
for (ECLOIANotify l : listeners) {
try {
l.onOIAChanged(this);
} catch (Exception ignored) {}
}
}
public boolean isInsertMode() {
return inputProcessor != null && inputProcessor.isInsertMode();
}
public boolean isNumeric() {
if (screen == null || !screen.isFormatted()) return false;
byte fa = screen.getFieldAttributeAt(screen.getCursorAddress());
return faIsNumeric(fa & 0xFF);
}
public boolean isAlphanumeric() {
return !isNumeric();
}
public boolean isMessageWaiting() {
return false;
}
public boolean isCommError() {
return fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isConnected();
}
/**
* Get the current Input Inhibited code.
* Returns one of INHIBIT_* constants from ECLConstants.
*/
public int getInputInhibited() {
if (isCommError()) {
return INHIBIT_COMM_CHECK;
}
if (inputProcessor != null && inputProcessor.isKeyboardLocked()) {
return INHIBIT_SYSTEM_LOCK;
}
return INHIBIT_NOT_INHIBITED;
}
/**
* Block until the keyboard becomes unlocked and ready for user input, or timeout expires.
* @return true if keyboard unlocked, false if timeout occurred.
*/
public boolean waitForInput(long timeoutMs) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
if (getInputInhibited() == INHIBIT_NOT_INHIBITED) {
return true;
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return getInputInhibited() == INHIBIT_NOT_INHIBITED;
}
/**
* Block until the host system is available.
*/
public boolean waitForSysAvailable(long timeoutMs) {
return waitForInput(timeoutMs);
}
/**
* Block until application is available.
*/
public boolean waitForAppAvailable(long timeoutMs) {
return waitForInput(timeoutMs);
}
/**
* Block until any OIA transition occurs.
*/
public boolean waitForTransition(long timeoutMs) {
int initialInhibit = getInputInhibited();
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
if (getInputInhibited() != initialInhibit) {
return true;
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return getInputInhibited() != initialInhibit;
}
}
@@ -0,0 +1,237 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/**
* Presentation Space (ECLPS) implementation conforming to IBM Host On-Demand ECL specification.
* Provides multi-plane presentation buffer access, string searches, formatted field navigation,
* and automated keystroke streaming.
*/
public class ECLPS implements ECLConstants {
private final ScreenBuffer screen;
private final InputProcessor inputProcessor;
private final EbcdicTranslator translator;
private final ECLFieldList fieldList;
public ECLPS(ScreenBuffer screen, InputProcessor inputProcessor, EbcdicTranslator translator) {
this.screen = screen;
this.inputProcessor = inputProcessor;
this.translator = translator;
this.fieldList = new ECLFieldList(this, screen);
}
public ScreenBuffer getScreenBuffer() { return screen; }
public InputProcessor getInputProcessor() { return inputProcessor; }
public EbcdicTranslator getTranslator() { return translator; }
public ECLFieldList getFieldList() {
fieldList.refresh();
return fieldList;
}
public int getSize() { return screen.getRows() * screen.getCols(); }
public int getRows() { return screen.getRows(); }
public int getCols() { return screen.getCols(); }
public int getCursorPos(){ return screen.getCursorAddress(); }
public int getCursorRow(){ return screen.getCursorRow(); }
public int getCursorCol(){ return screen.getCursorCol(); }
public void setCursorPos(int pos) {
screen.setCursorAddress(pos);
}
public void setCursorPos(int row, int col) {
int pos = screen.rowColToAddress(row, col);
screen.setCursorAddress(pos);
}
/**
* Copy presentation data from a specific plane into a destination buffer.
*/
public synchronized int getPlane(int planeType, char[] destBuffer, int start, int length) {
if (destBuffer == null || screen == null || length <= 0) return 0;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return 0;
start = ((start % size) + size) % size;
int copyLen = Math.min(length, destBuffer.length);
for (int i = 0; i < copyLen; i++) {
int addr = (start + i) % size;
ExtendedAttribute ea = screen.getCell(addr);
switch (planeType) {
case PLANE_TEXT:
if (ea.isFieldAttribute()) {
destBuffer[i] = ' ';
} else if (ea.ucs4 != 0) {
destBuffer[i] = (char) ea.ucs4;
} else if (ea.ec != 0) {
destBuffer[i] = translator.ebcdicToUnicode(ea.ec & 0xFF);
} else {
destBuffer[i] = ' ';
}
break;
case PLANE_COLOR:
destBuffer[i] = (char) (ea.fg & 0xFF);
break;
case PLANE_HILITE:
destBuffer[i] = (char) (ea.gr & 0xFF);
break;
case PLANE_EXTENDED:
destBuffer[i] = (char) (ea.cs & 0xFF);
break;
case PLANE_FIELD:
destBuffer[i] = ea.isFieldAttribute() ? (char) (ea.fa & 0xFF) : 0;
break;
default:
destBuffer[i] = ' ';
break;
}
}
return copyLen;
}
/**
* Get a string of characters from the presentation space starting at address pos.
*/
public synchronized String getString(int pos, int length) {
if (length <= 0) return "";
char[] buf = new char[length];
getPlane(PLANE_TEXT, buf, pos, length);
return new String(buf);
}
/**
* Get a string of characters from the presentation space starting at row/column.
*/
public String getString(int row, int col, int length) {
int pos = screen.rowColToAddress(row, col);
return getString(pos, length);
}
/**
* Insert text directly into unprotected fields in the presentation space starting at pos.
*/
public synchronized void setText(String text, int pos) {
if (text == null || text.isEmpty() || screen == null) return;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
pos = ((pos % size) + size) % size;
setCursorPos(pos);
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
inputProcessor.typeCharacter(ch);
}
}
/**
* Insert text starting at row and column.
*/
public void setText(String text, int row, int col) {
int pos = screen.rowColToAddress(row, col);
setText(text, pos);
}
/**
* Search for a string in the presentation space.
* Returns 1-based or 0-based position, or -1 if not found.
*/
public synchronized int searchString(String target, int startRow, int startCol, int dir, boolean ignoreCase) {
if (target == null || target.isEmpty() || screen == null) return -1;
int rows = screen.getRows();
int cols = screen.getCols();
int size = rows * cols;
if (size <= 0) return -1;
int startPos = (startRow * cols + startCol) % size;
char[] fullScreen = new char[size];
getPlane(PLANE_TEXT, fullScreen, 0, size);
String screenText = new String(fullScreen);
if (ignoreCase) {
screenText = screenText.toLowerCase();
target = target.toLowerCase();
}
int targetLen = target.length();
if (dir == SEARCH_FORWARD) {
// Forward search with wrapping
for (int i = 0; i < size; i++) {
int pos = (startPos + i) % size;
if (matchesAt(screenText, target, pos, size)) {
return pos;
}
}
} else {
// Backward search with wrapping
for (int i = 0; i < size; i++) {
int pos = (startPos - i + size) % size;
if (matchesAt(screenText, target, pos, size)) {
return pos;
}
}
}
return -1;
}
private boolean matchesAt(String screenText, String target, int pos, int size) {
int len = target.length();
for (int j = 0; j < len; j++) {
int charPos = (pos + j) % size;
if (screenText.charAt(charPos) != target.charAt(j)) {
return false;
}
}
return true;
}
/**
* Paste text with line wrapping across unprotected fields.
*/
public synchronized int pasteLineWrap(String text, int startPos, int endCol, boolean wordWrap) {
if (text == null || text.isEmpty() || screen == null) return 0;
int cols = screen.getCols();
int rows = screen.getRows();
int size = rows * cols;
if (size <= 0) return 0;
setCursorPos(startPos);
int charsPasted = 0;
String[] lines = text.split("\r?\n");
for (int l = 0; l < lines.length; l++) {
String line = lines[l];
for (int i = 0; i < line.length(); i++) {
int curPos = screen.getCursorAddress();
int curCol = curPos % cols;
if (endCol > 0 && curCol >= endCol) {
// Advance to next row
int nextRow = (curPos / cols + 1) % rows;
setCursorPos(nextRow * cols);
}
inputProcessor.typeCharacter(line.charAt(i));
charsPasted++;
}
if (l < lines.length - 1) {
// Newline key between lines
inputProcessor.newline();
}
}
return charsPasted;
}
/**
* Send standard IBM ECL mnemonic keystrokes.
*/
public void sendKeys(String keys) {
if (inputProcessor != null) {
inputProcessor.sendKeys(keys);
}
}
}
@@ -863,6 +863,204 @@ public class InputProcessor {
return fieldLen;
}
/**
* Parse and execute IBM ECL standard mnemonic keystrokes (e.g. "USER[tab]PASS[enter]").
*/
public void sendKeys(String keys) {
if (keys == null || keys.isEmpty()) return;
int len = keys.length();
int i = 0;
while (i < len) {
char c = keys.charAt(i);
if (c == '[') {
int end = keys.indexOf(']', i);
if (end > i) {
String token = keys.substring(i + 1, end).trim().toLowerCase();
executeMnemonicToken(token);
i = end + 1;
continue;
}
}
typeCharacter(c);
i++;
}
}
private void executeMnemonicToken(String token) {
switch (token) {
case "enter":
case "return":
sendAid(AID_ENTER);
break;
case "clear":
sendAid(AID_CLEAR);
break;
case "tab":
tab();
break;
case "backtab":
case "btab":
backTab();
break;
case "newline":
case "nl":
newline();
break;
case "home":
cursorHome();
break;
case "up":
case "curup":
cursorUp();
break;
case "down":
case "curdown":
cursorDown();
break;
case "left":
case "curleft":
cursorLeft();
break;
case "right":
case "curright":
cursorRight();
break;
case "eraseeof":
case "erase_eof":
eraseEof();
break;
case "eraseinp":
case "erase_input":
eraseInput();
break;
case "dup":
dup();
break;
case "fm":
case "fieldmark":
fieldMark();
break;
case "attn":
attn();
break;
case "sysreq":
sysReq();
break;
case "reset":
reset();
break;
case "insert":
setInsertMode(!isInsertMode());
break;
case "delete":
deleteChar();
break;
case "backspace":
case "bs":
backspace();
break;
case "wordtab":
processWordTab(true);
break;
case "wordbacktab":
processWordTab(false);
break;
case "deleteword":
processDeleteWord();
break;
default:
if (token.startsWith("pf")) {
try {
int pfNum = Integer.parseInt(token.substring(2));
if (pfNum >= 1 && pfNum <= 24) {
sendAid(AID_PF1 + (pfNum - 1));
}
} catch (NumberFormatException ignored) {}
} else if (token.startsWith("pa")) {
try {
int paNum = Integer.parseInt(token.substring(2));
if (paNum >= 1 && paNum <= 3) {
sendAid(AID_PA1 + (paNum - 1));
}
} catch (NumberFormatException ignored) {}
}
break;
}
}
/**
* Jump cursor to next or previous word boundary.
*/
public void processWordTab(boolean forward) {
if (keyboardLocked || screen == null) return;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
int cur = screen.getCursorAddress();
if (forward) {
// Find next whitespace then next non-whitespace
int pos = screen.incrementAddress(cur);
int count = 0;
while (count < size && !isWhitespaceOrNull(pos)) {
pos = screen.incrementAddress(pos);
count++;
}
while (count < size && isWhitespaceOrNull(pos)) {
pos = screen.incrementAddress(pos);
count++;
}
screen.setCursorAddress(pos);
} else {
// Find previous non-whitespace after whitespace
int pos = screen.decrementAddress(cur);
int count = 0;
while (count < size && isWhitespaceOrNull(pos)) {
pos = screen.decrementAddress(pos);
count++;
}
while (count < size && !isWhitespaceOrNull(screen.decrementAddress(pos))) {
pos = screen.decrementAddress(pos);
count++;
}
screen.setCursorAddress(pos);
}
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
/**
* Delete the word starting at the cursor position.
*/
public void processDeleteWord() {
if (keyboardLocked || screen == null) return;
int cur = screen.getCursorAddress();
if (screen.isFormatted()) {
byte fa = screen.getFieldAttributeAt(cur);
if (faIsProtected(fa & 0xFF)) return;
}
int count = 0;
int size = screen.getRows() * screen.getCols();
while (count < size && !isWhitespaceOrNull(screen.getCursorAddress())) {
deleteChar();
count++;
}
// Also delete trailing spaces
while (count < size && isWhitespaceOrNull(screen.getCursorAddress()) && screen.getCell(screen.getCursorAddress()).ec != 0) {
deleteChar();
count++;
}
}
private boolean isWhitespaceOrNull(int pos) {
if (screen == null) return true;
ExtendedAttribute ea = screen.getCell(pos);
if (ea.isFieldAttribute()) return true;
int ec = ea.ec & 0xFF;
return ec == 0 || ec == 0x40; // Null or EBCDIC Space
}
/**
* Send a structured field response directly.
* Used by DFT mode file transfer.
@@ -0,0 +1,97 @@
package haus.nightmare.lib3270j.ecl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class ECLFieldListTest {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
private ECLPS ps;
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
ps = new ECLPS(screen, input, translator);
}
@Test
public void testFieldListDetection() {
// Create 2 fields: pos 0 (unprotected, 9 chars), pos 10 (protected, 19 chars), pos 30 (high intensity)
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.setFieldAttribute(30, (byte) (FA_PRINTABLE | FA_INT_HIGH_SEL));
ECLFieldList list = ps.getFieldList();
assertEquals(3, list.getFieldCount());
ECLField f1 = list.getFirstField();
assertNotNull(f1);
assertEquals(0, f1.getStart());
assertEquals(1, f1.getDataStart());
assertEquals(9, f1.getEnd());
assertEquals(9, f1.getLength());
assertFalse(f1.isProtected());
ECLField f2 = list.getNextField(f1);
assertNotNull(f2);
assertEquals(10, f2.getStart());
assertEquals(11, f2.getDataStart());
assertEquals(29, f2.getEnd());
assertEquals(19, f2.getLength());
assertTrue(f2.isProtected());
ECLField f3 = list.getNextField(f2);
assertNotNull(f3);
assertEquals(30, f3.getStart());
assertTrue(f3.isHighIntensity());
assertTrue(f3.isPenSelectable());
assertNull(list.getNextField(f3));
}
@Test
public void testFindFieldByPositionAndRowCol() {
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(80, (byte) (FA_PRINTABLE | FA_PROTECT));
ECLFieldList list = ps.getFieldList();
ECLField found1 = list.findField(40);
assertNotNull(found1);
assertEquals(0, found1.getStart());
ECLField found2 = list.findField(1, 10); // Row 1, Col 10 -> addr 90
assertNotNull(found2);
assertEquals(80, found2.getStart());
assertTrue(found2.isProtected());
}
@Test
public void testLightpenSelectionOnField() {
screen.setFieldAttribute(0, (byte) (FA_PRINTABLE | FA_INT_HIGH_SEL));
screen.setCell(1, translator.unicodeToEbcdic('?'));
screen.setCell(2, translator.unicodeToEbcdic('O'));
screen.setCell(3, translator.unicodeToEbcdic('K'));
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.translateToUnicode();
ECLField field = ps.getFieldList().getFirstField();
assertEquals('?', field.getSelectorPenType());
field.selectField();
assertEquals('>', field.getSelectorPenType());
field.deSelectField();
assertEquals('?', field.getSelectorPenType());
}
}
@@ -0,0 +1,59 @@
package haus.nightmare.lib3270j.ecl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.util.concurrent.atomic.AtomicBoolean;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.ecl.ECLConstants.*;
public class ECLOIATest {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
private ECLOIA oia;
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
oia = new ECLOIA(screen, input, null);
}
@Test
public void testInhibitStateTransitions() {
assertEquals(INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
assertTrue(oia.waitForInput(100));
input.setKeyboardLocked(true);
assertEquals(INHIBIT_SYSTEM_LOCK, oia.getInputInhibited());
assertFalse(oia.waitForInput(50));
input.setKeyboardLocked(false);
assertEquals(INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
assertTrue(oia.waitForInput(100));
}
@Test
public void testOIAEventListener() {
AtomicBoolean triggered = new AtomicBoolean(false);
oia.registerOIAEvent(changedOia -> triggered.set(true));
input.setKeyboardLocked(true);
assertTrue(triggered.get());
}
@Test
public void testInsertModeState() {
assertFalse(oia.isInsertMode());
input.setInsertMode(true);
assertTrue(oia.isInsertMode());
}
}
@@ -0,0 +1,117 @@
package haus.nightmare.lib3270j.ecl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.telnet.TelnetFSM;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class ECLPSTest implements ECLConstants {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
private ECLPS ps;
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
ps = new ECLPS(screen, input, translator);
}
@Test
public void testGeometry() {
assertEquals(24, ps.getRows());
assertEquals(80, ps.getCols());
assertEquals(24 * 80, ps.getSize());
assertEquals(0, ps.getCursorPos());
ps.setCursorPos(10, 20);
assertEquals(10 * 80 + 20, ps.getCursorPos());
assertEquals(10, ps.getCursorRow());
assertEquals(20, ps.getCursorCol());
}
@Test
public void testPlaneTextExtractionAndSearch() {
// Set some text in buffer
screen.setCell(0, 0xC8); // 'H'
screen.setCell(1, 0xC5); // 'E'
screen.setCell(2, 0xD3); // 'L'
screen.setCell(3, 0xD3); // 'L'
screen.setCell(4, 0xD6); // 'O'
screen.translateToUnicode();
assertEquals("HELLO", ps.getString(0, 5));
// Search string forward
int pos = ps.searchString("ELL", 0, 0, SEARCH_FORWARD, false);
assertEquals(1, pos);
// Search string case-insensitive
int posLower = ps.searchString("hello", 0, 0, SEARCH_FORWARD, true);
assertEquals(0, posLower);
// Search not found
int notFound = ps.searchString("WORLD", 0, 0, SEARCH_FORWARD, false);
assertEquals(-1, notFound);
}
@Test
public void testMultiPlaneReading() {
screen.setFieldAttribute(0, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.setCell(1, 0xC1); // 'A'
screen.getCell(1).fg = COLOR_RED;
screen.getCell(1).gr = (byte) HILITE_BLINK;
char[] textPlane = new char[2];
char[] colorPlane = new char[2];
char[] hilitePlane = new char[2];
char[] fieldPlane = new char[2];
ps.getPlane(PLANE_TEXT, textPlane, 0, 2);
ps.getPlane(PLANE_COLOR, colorPlane, 0, 2);
ps.getPlane(PLANE_HILITE, hilitePlane, 0, 2);
ps.getPlane(PLANE_FIELD, fieldPlane, 0, 2);
assertEquals(' ', textPlane[0]); // FA is space in text plane
assertEquals('A', textPlane[1]);
assertEquals(0, colorPlane[0]);
assertEquals(COLOR_RED, colorPlane[1]);
assertEquals(0, hilitePlane[0]);
assertEquals((char) (HILITE_BLINK & 0xFF), hilitePlane[1]);
assertTrue((fieldPlane[0] & 0xFF) != 0);
assertEquals(0, fieldPlane[1]);
}
@Test
public void testSendKeysMnemonics() {
// Create an unprotected field at pos 0
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setCursorAddress(1);
ps.sendKeys("IBM[tab]");
assertEquals('I', screen.getCell(1).ucs4);
assertEquals('B', screen.getCell(2).ucs4);
assertEquals('M', screen.getCell(3).ucs4);
}
@Test
public void testPasteLineWrap() {
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setCursorAddress(1);
int count = ps.pasteLineWrap("ABC\nDEF", 1, 80, false);
assertEquals(6, count);
}
}